From 403f10087f8f734c26fb51ec409582a7fdf30bf3 Mon Sep 17 00:00:00 2001 From: jlenon7 Date: Tue, 31 Dec 2024 19:59:07 -0300 Subject: [PATCH] chore(npm): update dependencies --- package-lock.json | 4 +- package.json | 2 +- src/providers/HttpServerProvider.ts | 51 +++++ src/vite/index.ts | 305 ++++++++++++++++++++++++++++ src/vite/plugin.ts | 9 +- 5 files changed, 360 insertions(+), 11 deletions(-) create mode 100644 src/vite/index.ts diff --git a/package-lock.json b/package-lock.json index c3147a6..ebcabca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@athenna/http", - "version": "5.12.0", + "version": "5.13.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@athenna/http", - "version": "5.12.0", + "version": "5.13.0", "license": "MIT", "devDependencies": { "@athenna/artisan": "^5.3.0", diff --git a/package.json b/package.json index 5fc64c9..f3ce050 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@athenna/http", - "version": "5.12.0", + "version": "5.13.0", "description": "The Athenna Http server. Built on top of fastify.", "license": "MIT", "author": "João Lenon ", diff --git a/src/providers/HttpServerProvider.ts b/src/providers/HttpServerProvider.ts index 660fb29..0fc0d28 100644 --- a/src/providers/HttpServerProvider.ts +++ b/src/providers/HttpServerProvider.ts @@ -7,6 +7,9 @@ * file that was distributed with this source code. */ +import { View } from '@athenna/view' +import { Vite } from '#src/vite/index' +import { EdgeError } from 'edge-error' import { ServiceProvider } from '@athenna/ioc' import { ServerImpl } from '#src/server/ServerImpl' @@ -18,6 +21,54 @@ export class HttpServerProvider extends ServiceProvider { ) } + public boot() { + View.edge.global('vite', new Vite()) + View.edge.registerTag({ + tagName: 'vite', + seekable: true, + block: false, + compile(parser, buffer, token) { + /** + * Ensure an argument is defined + */ + if (!token.properties.jsArg.trim()) { + throw new EdgeError( + 'Missing entrypoint name', + 'E_RUNTIME_EXCEPTION', + { + filename: token.filename, + line: token.loc.start.line, + col: token.loc.start.col + } + ) + } + + const parsed = parser.utils.transformAst( + parser.utils.generateAST( + token.properties.jsArg, + token.loc, + token.filename + ), + token.filename, + parser + ) + + const entrypoints = parser.utils.stringify(parsed) + const methodCall = + parsed.type === 'SequenceExpression' + ? `generateEntryPointsTags${entrypoints}` + : `generateEntryPointsTags(${entrypoints})` + + buffer.outputExpression( + `(await state.vite.${methodCall}).join('\\n')`, + token.filename, + token.loc.start.line, + false + ) + } + }) + } + public async shutdown() { const Server = this.container.use('Athenna/Core/HttpServer') diff --git a/src/vite/index.ts b/src/vite/index.ts new file mode 100644 index 0000000..1edf2e4 --- /dev/null +++ b/src/vite/index.ts @@ -0,0 +1,305 @@ +/** + * @athenna/http + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { File, Path } from '@athenna/common' +import type { Manifest, ModuleNode } from 'vite' + +const styleFileRegex = /\.(css|less|sass|scss|styl|stylus|pcss|postcss)($|\?)/ + +export class Vite { + /** + * We cache the manifest file content in production + * to avoid reading the file multiple times. + */ + public manifestCache?: Manifest + + /** + * Verify if vite is running in development mode. + */ + public get isViteRunning() { + return process.argv.includes('--vite') + } + + /** + * Reads the file contents as JSON. + */ + public readFileAsJSON(filePath: string) { + return new File(filePath).getContentAsJsonSync() + } + + /** + * Returns a new array with unique items by the given key + */ + public uniqueBy(array: T[], key: keyof T): T[] { + const seen = new Set() + + return array.filter(item => { + const k = item[key] + return seen.has(k) ? false : seen.add(k) + }) + } + + /** + * Convert Record of attributes to a valid HTML string. + */ + public makeAttributes(attributes: Record) { + return Object.keys(attributes) + .map(key => { + const value = attributes[key] + + if (value === true) { + return key + } + + if (!value) { + return null + } + + return `${key}="${value}"` + }) + .filter(attr => attr !== null) + .join(' ') + } + + /** + * Generates a JSON element with a custom toString implementation. + */ + public generateElement(element: any) { + return { + ...element, + toString() { + const attributes = `${this.makeAttributes(element.attributes)}` + if (element.tag === 'link') { + return `<${element.tag} ${attributes}/>` + } + + return `<${element.tag} ${attributes}>${element.children.join('\n')}` + } + } + } + + /** + * Returns the script needed for the HMR working with Vite. + */ + public getViteHmrScript() { + return this.generateElement({ + tag: 'script', + attributes: { + type: 'module', + src: '/@vite/client' + }, + children: [] + }) + } + + /** + * Check if the given path is a CSS path. + */ + public isCssPath(path: string) { + return path.match(styleFileRegex) !== null + } + + /** + * If the module is a style module. + */ + public isStyleModule(mod: ModuleNode) { + if ( + this.isCssPath(mod.url) || + (mod.id && /\?vue&type=style/.test(mod.id)) + ) { + return true + } + + return false + } + + /** + * Create a style tag for the given path + */ + public makeStyleTag(url: string, attributes?: any) { + return this.generateElement({ + tag: 'link', + attributes: { rel: 'stylesheet', href: url, ...attributes } + }) + } + + /** + * Create a script tag for the given path + */ + public makeScriptTag(url: string, attributes?: any) { + return this.generateElement({ + tag: 'script', + attributes: { type: 'module', src: url, ...attributes }, + children: [] + }) + } + + /** + * Generate a HTML tag for the given asset + */ + public generateTag(asset: string, attributes?: any) { + let url = '' + + if (this.isViteRunning) { + url = `/${asset}` + } else { + url = asset + } + + if (this.isCssPath(asset)) { + return this.makeStyleTag(url, attributes) + } + + return this.makeScriptTag(url, attributes) + } + + /** + * Get a chunk from the manifest file for a given file name + */ + public chunk(manifest: Manifest, entrypoint: string) { + const chunk = manifest[entrypoint] + + if (!chunk) { + throw new Error(`Cannot find "${entrypoint}" chunk in the manifest file`) + } + + return chunk + } + + /** + * Get a list of chunks for a given filename + */ + public chunksByFile(manifest: Manifest, file: string) { + return Object.entries(manifest) + .filter(([, chunk]) => chunk.file === file) + .map(([_, chunk]) => chunk) + } + + /** + * Generate preload tag for a given url + */ + public makePreloadTagForUrl(url: string) { + const attributes = this.isCssPath(url) + ? { rel: 'preload', as: 'style', href: url } + : { rel: 'modulepreload', href: url } + + return this.generateElement({ tag: 'link', attributes }) + } + + /** + * Generate style and script tags for the given entrypoints + * Also adds the @vite/client script + */ + public async generateEntryPointsTagsForDevMode( + entryPoints: string[], + attributes?: any + ) { + const tags = entryPoints.map(entrypoint => + this.generateTag(entrypoint, attributes) + ) + + const viteHmr = this.getViteHmrScript() + const result = [viteHmr, tags] + + return result.sort(tag => (tag.tag === 'link' ? -1 : 1)) + } + + /** + * Generate style and script tags for the given entrypoints + * using the manifest file + */ + public generateEntryPointsTagsWithManifest( + entryPoints: string[], + attributes?: any + ) { + const manifest = this.manifest() + const tags: { path: string; tag: any }[] = [] + const preloads: Array<{ path: string }> = [] + + for (const entryPoint of entryPoints) { + const chunk = this.chunk(manifest, entryPoint) + preloads.push({ path: chunk.file }) + tags.push({ + path: chunk.file, + tag: this.generateTag(chunk.file, { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + integrity: chunk.integrity + }) + }) + + for (const css of chunk.css || []) { + preloads.push({ path: css }) + tags.push({ path: css, tag: this.generateTag(css) }) + } + + for (const importNode of chunk.imports || []) { + preloads.push({ path: manifest[importNode].file }) + + for (const css of manifest[importNode].css || []) { + const subChunk = this.chunksByFile(manifest, css) + + preloads.push({ path: css }) + tags.push({ + path: css, + tag: this.generateTag(css, { + ...attributes, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + integrity: subChunk[0]?.integrity + }) + }) + } + } + } + + const preloadsElements = this.uniqueBy(preloads, 'path') + .sort(preload => (this.isCssPath(preload.path) ? -1 : 1)) + .map(preload => this.makePreloadTagForUrl(preload.path)) + + return preloadsElements.concat(tags.map(({ tag }) => tag)) + } + + /** + * Generate tags for the entry points + */ + public async generateEntryPointsTags( + entryPoints: string[] | string, + attributes?: any + ) { + entryPoints = Array.isArray(entryPoints) ? entryPoints : [entryPoints] + + if (this.isViteRunning) { + return this.generateEntryPointsTagsForDevMode(entryPoints, attributes) + } + + return this.generateEntryPointsTagsWithManifest(entryPoints, attributes) + } + + /** + * Returns the manifest file contents + * + * @throws Will throw an exception when running in dev + */ + public manifest(): Manifest { + if (this.isViteRunning) { + throw new Error('Cannot read the manifest file when running in dev mode') + } + + if (!this.manifestCache) { + this.manifestCache = this.readFileAsJSON( + Path.public('assets/.vite/manifest.json') + ) + } + + return this.manifestCache! + } +} diff --git a/src/vite/plugin.ts b/src/vite/plugin.ts index 79ed080..9fcd579 100644 --- a/src/vite/plugin.ts +++ b/src/vite/plugin.ts @@ -18,14 +18,7 @@ export function athenna(options: PluginOptions): PluginOption[] { { assetsUrl: '/assets', buildDirectory: 'public/assets', - reload: ['./src/resources/views/**/*.edge'], - css: { - preprocessorOptions: { - scss: { - api: 'modern' - } - } - } + reload: ['./src/resources/views/**/*.edge'] }, options )