perf(nuxt): emit simpler functional ui templates (w/o lodash) (#27091)

This commit is contained in:
Daniel Roe 2024-05-07 22:45:03 +01:00 committed by GitHub
parent a4d0958727
commit 68f4b193be
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 43 additions and 40 deletions

View File

@ -8,11 +8,11 @@
"license": "MIT", "license": "MIT",
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "pnpm --filter @nuxt/ui-templates prepack && pnpm --filter './packages/[^u]**' prepack", "build": "pnpm --filter './packages/**' prepack",
"build:stub": "pnpm dev:prepare", "build:stub": "pnpm dev:prepare",
"cleanup": "rimraf 'packages/**/node_modules' 'playground/node_modules' 'node_modules'", "cleanup": "rimraf 'packages/**/node_modules' 'playground/node_modules' 'node_modules'",
"dev": "pnpm play", "dev": "pnpm play",
"dev:prepare": "pnpm --filter @nuxt/ui-templates prepack && pnpm --filter './packages/[^u]**' prepack --stub", "dev:prepare": "pnpm --filter './packages/**' prepack --stub",
"lint": "eslint . --cache", "lint": "eslint . --cache",
"lint:fix": "eslint . --cache --fix", "lint:fix": "eslint . --cache --fix",
"lint:docs": "markdownlint ./docs && case-police 'docs/**/*.md' *.md", "lint:docs": "markdownlint ./docs && case-police 'docs/**/*.md' *.md",
@ -28,7 +28,7 @@
"test:fixtures:webpack": "TEST_BUILDER=webpack pnpm test:fixtures", "test:fixtures:webpack": "TEST_BUILDER=webpack pnpm test:fixtures",
"test:runtime": "vitest -c vitest.nuxt.config.ts", "test:runtime": "vitest -c vitest.nuxt.config.ts",
"test:types": "pnpm --filter './test/fixtures/**' test:types", "test:types": "pnpm --filter './test/fixtures/**' test:types",
"test:unit": "JITI_CACHE=0 vitest run packages/", "test:unit": "vitest run packages/",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"typecheck:docs": "DOCS_TYPECHECK=true pnpm nuxi prepare && nuxt-content-twoslash verify --content-dir docs" "typecheck:docs": "DOCS_TYPECHECK=true pnpm nuxi prepare && nuxt-content-twoslash verify --content-dir docs"
}, },

View File

@ -1 +0,0 @@
../../../../../ui-templates/dist/templates/error-500.d.ts

View File

@ -1 +0,0 @@
../../../../../ui-templates/dist/templates/error-500.js

View File

@ -0,0 +1 @@
../../../../../ui-templates/dist/templates/error-500.ts

View File

@ -1 +0,0 @@
../../../../../ui-templates/dist/templates/error-dev.d.ts

View File

@ -1 +0,0 @@
../../../../../ui-templates/dist/templates/error-dev.js

View File

@ -0,0 +1 @@
../../../../../ui-templates/dist/templates/error-dev.ts

View File

@ -1,9 +1,9 @@
import { fileURLToPath } from 'node:url'
import { readFileSync, rmdirSync, unlinkSync, writeFileSync } from 'node:fs' import { readFileSync, rmdirSync, unlinkSync, writeFileSync } from 'node:fs'
import { basename, dirname, join, resolve } from 'pathe' import { basename, dirname, join, resolve } from 'pathe'
import type { Plugin } from 'vite' import type { Plugin } from 'vite'
// @ts-expect-error https://github.com/GoogleChromeLabs/critters/pull/151 // @ts-expect-error https://github.com/GoogleChromeLabs/critters/pull/151
import Critters from 'critters' import Critters from 'critters'
import { template } from 'lodash-es'
import { genObjectFromRawEntries } from 'knitwork' import { genObjectFromRawEntries } from 'knitwork'
import htmlMinifier from 'html-minifier' import htmlMinifier from 'html-minifier'
import { globby } from 'globby' import { globby } from 'globby'
@ -11,18 +11,20 @@ import { camelCase } from 'scule'
import genericMessages from '../templates/messages.json' import genericMessages from '../templates/messages.json'
const r = (...path: string[]) => resolve(join(__dirname, '..', ...path)) const r = (path: string) => fileURLToPath(new URL(join('..', path), import.meta.url))
const replaceAll = (input: string, search: string | RegExp, replace: string) => input.split(search).join(replace) const replaceAll = (input: string, search: string | RegExp, replace: string) => input.split(search).join(replace)
export const RenderPlugin = () => { export const RenderPlugin = () => {
let outputDir: string
return <Plugin> { return <Plugin> {
name: 'render', name: 'render',
configResolved (config) {
outputDir = r(config.build.outDir)
},
enforce: 'post', enforce: 'post',
async writeBundle () { async writeBundle () {
const distDir = r('dist') const critters = new Critters({ path: outputDir })
const critters = new Critters({ path: distDir }) const htmlFiles = await globby(resolve(outputDir, 'templates/**/*.html'), { absolute: true })
const htmlFiles = await globby(r('dist/templates/**/*.html'))
const templateExports = [] const templateExports = []
@ -50,7 +52,7 @@ export const RenderPlugin = () => {
.filter(src => src?.match(/\.svg$/)) .filter(src => src?.match(/\.svg$/))
for (const src of svgSources) { for (const src of svgSources) {
const svg = readFileSync(r('dist', src), 'utf-8') const svg = readFileSync(join(outputDir, src), 'utf-8')
const base64Source = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}` const base64Source = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`
html = replaceAll(html, src, base64Source) html = replaceAll(html, src, base64Source)
} }
@ -60,7 +62,7 @@ export const RenderPlugin = () => {
.filter(([_block, src]) => src?.match(/^\/.*\.js$/)) .filter(([_block, src]) => src?.match(/^\/.*\.js$/))
for (const [scriptBlock, src] of scriptSources) { for (const [scriptBlock, src] of scriptSources) {
let contents = readFileSync(r('dist', src), 'utf-8') let contents = readFileSync(join(outputDir, src), 'utf-8')
contents = replaceAll(contents, '/* empty css */', '').trim() contents = replaceAll(contents, '/* empty css */', '').trim()
html = html.replace(scriptBlock, contents.length ? `<script>${contents}</script>` : '') html = html.replace(scriptBlock, contents.length ? `<script>${contents}</script>` : '')
} }
@ -77,11 +79,22 @@ export const RenderPlugin = () => {
const messages = JSON.parse(readFileSync(r(`templates/${templateName}/messages.json`), 'utf-8')) const messages = JSON.parse(readFileSync(r(`templates/${templateName}/messages.json`), 'utf-8'))
// Serialize into a js function // Serialize into a js function
const jsCode = [ const chunks = html.split(/\{{2,3}\s*[^{}]+\s*\}{2,3}/g).map(chunk => JSON.stringify(chunk))
let templateString = chunks.shift()
for (const expression of html.matchAll(/\{{2,3}(\s*[^{}]+\s*)\}{2,3}/g)) {
templateString += ` + (${expression[1].trim()}) + ${chunks.shift()}`
}
if (chunks.length > 0) {
templateString += ' + ' + chunks.join(' + ')
}
const functionalCode = [
`export type DefaultMessages = Record<${Object.keys({ ...genericMessages, ...messages }).map(a => `"${a}"`).join(' | ') || 'string'}, string | boolean | number >`,
`const _messages = ${JSON.stringify({ ...genericMessages, ...messages })}`, `const _messages = ${JSON.stringify({ ...genericMessages, ...messages })}`,
`const _render = ${template(html, { variable: '__var__', interpolate: /{{{?([\s\S]+?)}?}}/g }).toString().replace('__var__', '{ messages }')}`, 'export const template = (messages: Partial<DefaultMessages>) => {',
'const _template = (messages) => _render({ messages: { ..._messages, ...messages } })', ' messages = { ..._messages, ...messages }',
].join('\n').trim() ` return ${templateString}`,
'}',
].join('\n')
const templateContent = html const templateContent = html
.match(/<body.*?>([\s\S]*)<\/body>/)?.[0] .match(/<body.*?>([\s\S]*)<\/body>/)?.[0]
@ -146,20 +159,13 @@ export const RenderPlugin = () => {
}) })
// Write new template // Write new template
writeFileSync(fileName.replace('/index.html', '.js'), `${jsCode}\nexport const template = _template`) writeFileSync(fileName.replace('/index.html', '.ts'), functionalCode)
writeFileSync(fileName.replace('/index.html', '.vue'), vueCode) writeFileSync(fileName.replace('/index.html', '.vue'), vueCode)
writeFileSync(fileName.replace('/index.html', '.d.ts'), `${types}`)
// Remove original html file // Remove original html file
unlinkSync(fileName) unlinkSync(fileName)
rmdirSync(dirname(fileName)) rmdirSync(dirname(fileName))
} }
// Write an index file with named exports for each template
const contents = templateExports.map(exp => `export { template as ${exp.exportName} } from './templates/${exp.templateName}.js'`).join('\n')
writeFileSync(r('dist/index.js'), contents, 'utf8')
writeFileSync(r('dist/index.d.ts'), replaceAll(contents, /\.js/g, ''), 'utf8')
}, },
} }
} }

View File

@ -14,13 +14,12 @@
"dev": "vite", "dev": "vite",
"lint": "eslint --ext .ts,.js .", "lint": "eslint --ext .ts,.js .",
"optimize-assets": "npx svgo public/assets/**/*.svg", "optimize-assets": "npx svgo public/assets/**/*.svg",
"prepack": "pnpm build", "postinstall": "pnpm build",
"prerender": "pnpm build && jiti ./lib/prerender", "prerender": "pnpm build && jiti ./lib/prerender",
"test": "pnpm lint && pnpm build" "test": "pnpm lint && pnpm build"
}, },
"devDependencies": { "devDependencies": {
"@types/html-minifier": "4.0.5", "@types/html-minifier": "4.0.5",
"@types/lodash-es": "4.17.12",
"@unocss/reset": "0.59.4", "@unocss/reset": "0.59.4",
"critters": "0.0.22", "critters": "0.0.22",
"execa": "8.0.1", "execa": "8.0.1",
@ -28,7 +27,6 @@
"html-minifier": "4.0.0", "html-minifier": "4.0.0",
"jiti": "1.21.0", "jiti": "1.21.0",
"knitwork": "1.1.0", "knitwork": "1.1.0",
"lodash-es": "4.17.21",
"pathe": "1.1.2", "pathe": "1.1.2",
"prettier": "3.2.5", "prettier": "3.2.5",
"scule": "1.3.0", "scule": "1.3.0",

View File

@ -1,17 +1,22 @@
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { readFileSync } from 'node:fs' import { readFileSync } from 'node:fs'
import { beforeAll, describe, expect, it } from 'vitest' import { rm } from 'node:fs/promises'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { execaCommand } from 'execa' import { execaCommand } from 'execa'
import { format } from 'prettier' import { format } from 'prettier'
const distDir = fileURLToPath(new URL('../dist/templates', import.meta.url)) const distDir = fileURLToPath(new URL('../node_modules/.temp/dist/templates', import.meta.url))
describe('template', () => { describe('template', () => {
beforeAll(async () => { beforeAll(async () => {
await execaCommand('pnpm build', { await execaCommand('pnpm build', {
cwd: fileURLToPath(new URL('..', import.meta.url)), cwd: fileURLToPath(new URL('..', import.meta.url)),
env: {
OUTPUT_DIR: './node_modules/.temp/dist',
},
}) })
}) })
afterAll(() => rm(distDir, { force: true, recursive: true }))
function formatCss (css: string) { function formatCss (css: string) {
return format(css, { return format(css, {

View File

@ -1,4 +1,5 @@
import { resolve } from 'node:path' import { fileURLToPath } from 'node:url'
import { join } from 'node:path'
import { readdirSync } from 'node:fs' import { readdirSync } from 'node:fs'
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
@ -7,10 +8,11 @@ import UnoCSS from 'unocss/vite'
import { DevRenderingPlugin } from './lib/dev' import { DevRenderingPlugin } from './lib/dev'
import { RenderPlugin } from './lib/render' import { RenderPlugin } from './lib/render'
const r = (...path: string[]) => resolve(__dirname, ...path) const r = (...path: string[]) => fileURLToPath(new URL(join(...path), import.meta.url))
export default defineConfig({ export default defineConfig({
build: { build: {
outDir: process.env.OUTPUT_DIR || 'dist',
rollupOptions: { rollupOptions: {
input: { input: {
...Object.fromEntries( ...Object.fromEntries(

View File

@ -557,9 +557,6 @@ importers:
'@types/html-minifier': '@types/html-minifier':
specifier: 4.0.5 specifier: 4.0.5
version: 4.0.5 version: 4.0.5
'@types/lodash-es':
specifier: 4.17.12
version: 4.17.12
'@unocss/reset': '@unocss/reset':
specifier: 0.59.4 specifier: 0.59.4
version: 0.59.4 version: 0.59.4
@ -581,9 +578,6 @@ importers:
knitwork: knitwork:
specifier: 1.1.0 specifier: 1.1.0
version: 1.1.0 version: 1.1.0
lodash-es:
specifier: 4.17.21
version: 4.17.21
pathe: pathe:
specifier: 1.1.2 specifier: 1.1.2
version: 1.1.2 version: 1.1.2