Skip to content

Commit

Permalink
Merge pull request #199 from AthennaIO/develop
Browse files Browse the repository at this point in the history
chore(npm): update dependencies
  • Loading branch information
jlenon7 authored Dec 31, 2024
2 parents 116edaf + 403f100 commit d1f96d7
Show file tree
Hide file tree
Showing 5 changed files with 360 additions and 11 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>",
Expand Down
51 changes: 51 additions & 0 deletions src/providers/HttpServerProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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<ServerImpl>('Athenna/Core/HttpServer')

Expand Down
305 changes: 305 additions & 0 deletions src/vite/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
/**
* @athenna/http
*
* (c) João Lenon <[email protected]>
*
* 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<T>(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<string, string | boolean>) {
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')}</${
element.tag
}>`
}
}
}

/**
* 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!
}
}
Loading

0 comments on commit d1f96d7

Please sign in to comment.