fix: sanitize import filenames in generated imports (#2216)

This commit is contained in:
Daniel Roe 2022-02-07 13:45:47 +00:00 committed by GitHub
parent 614e87e9f0
commit 29171bd105
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 118 additions and 94 deletions

View File

@ -38,6 +38,7 @@
"globby": "^13.1.1", "globby": "^13.1.1",
"h3": "^0.3.9", "h3": "^0.3.9",
"hash-sum": "^2.0.0", "hash-sum": "^2.0.0",
"knitwork": "^0.1.0",
"magic-string": "^0.25.7", "magic-string": "^0.25.7",
"mlly": "^0.4.1", "mlly": "^0.4.1",
"murmurhash-es": "^0.1.1", "murmurhash-es": "^0.1.1",

View File

@ -2,6 +2,7 @@ import hash from 'hash-sum'
import { resolve } from 'pathe' import { resolve } from 'pathe'
import type { Nuxt, NuxtApp } from '@nuxt/schema' import type { Nuxt, NuxtApp } from '@nuxt/schema'
import { genImport, genObjectFromRawEntries } from 'knitwork'
type TemplateContext = { type TemplateContext = {
nuxt: Nuxt; nuxt: Nuxt;
@ -24,10 +25,8 @@ export const middlewareTemplate = {
id: m.name || m.src.replace(/[\\/]/g, '/').replace(/\.(js|ts)$/, '') id: m.name || m.src.replace(/[\\/]/g, '/').replace(/\.(js|ts)$/, '')
} }
}) })
return `${_middleware.map(m => `import $${hash(m.id)} from '${m.filePath}'`).join('\n')} return `${_middleware.map(m => genImport(m.filePath, `$${hash(m.id)}`)).join('\n')}
const middleware = { const middleware = ${genObjectFromRawEntries(_middleware.map(m => [m.id, `$${hash(m.id)}`]))}
${_middleware.map(m => ` ['${m.id}']: $${hash(m.id)}`).join(',\n')}
}
export default middleware` export default middleware`
} }
} }
@ -49,14 +48,12 @@ export const storeTemplate = {
return `import Vue from 'vue' return `import Vue from 'vue'
import Vuex from 'vuex' import Vuex from 'vuex'
${_storeModules.map(s => `import * as $${hash(s.id)} from '${s.filePath}'`).join('\n')} ${_storeModules.map(s => genImport(s.filePath, { name: '*', as: `$${hash(s.id)}` })).join('\n')}
Vue.use(Vuex) Vue.use(Vuex)
const VUEX_PROPERTIES = ['state', 'getters', 'actions', 'mutations'] const VUEX_PROPERTIES = ['state', 'getters', 'actions', 'mutations']
const storeModules = { const storeModules = ${genObjectFromRawEntries(_storeModules.map(m => [m.id, `$${hash(m.id)}`]))}
${_storeModules.map(m => ` ['${m.id}']: $${hash(m.id)}`).join(',\n')}
}
export function createStore() { export function createStore() {
let store = normalizeRoot(storeModules.root || {}) let store = normalizeRoot(storeModules.root || {})

View File

@ -20,6 +20,7 @@
"globby": "^13.1.1", "globby": "^13.1.1",
"hash-sum": "^2.0.0", "hash-sum": "^2.0.0",
"jiti": "^1.12.15", "jiti": "^1.12.15",
"knitwork": "^0.1.0",
"lodash.template": "^4.5.0", "lodash.template": "^4.5.0",
"mlly": "^0.4.1", "mlly": "^0.4.1",
"pathe": "^0.2.0", "pathe": "^0.2.0",

View File

@ -1,5 +1,6 @@
import { pascalCase, kebabCase } from 'scule' import { pascalCase, kebabCase } from 'scule'
import type { ComponentsDir, Component } from '@nuxt/schema' import type { ComponentsDir, Component } from '@nuxt/schema'
import { genDynamicImport } from 'knitwork'
import { useNuxt } from './context' import { useNuxt } from './context'
import { assertNuxtCompatibility } from './compatibility' import { assertNuxtCompatibility } from './compatibility'
@ -43,8 +44,8 @@ export async function addComponent (opts: AddComponentOptions) {
shortPath: opts.filePath, shortPath: opts.filePath,
async: false, async: false,
level: 0, level: 0,
asyncImport: `() => import('${opts.filePath}').then(r => r['${opts.export || 'default'}'])`, asyncImport: `${genDynamicImport(opts.filePath)}.then(r => r['${opts.export || 'default'}'])`,
import: `require('${opts.filePath}')['${opts.export || 'default'}']`, import: `require(${JSON.stringify(opts.filePath)})['${opts.export || 'default'}']`,
...opts ...opts
} }

View File

@ -3,6 +3,7 @@ import lodashTemplate from 'lodash.template'
import hash from 'hash-sum' import hash from 'hash-sum'
import { camelCase } from 'scule' import { camelCase } from 'scule'
import { basename, extname } from 'pathe' import { basename, extname } from 'pathe'
import { genDynamicImport, genImport } from 'knitwork'
import type { NuxtTemplate } from '@nuxt/schema' import type { NuxtTemplate } from '@nuxt/schema'
@ -23,7 +24,7 @@ export async function compileTemplate (template: NuxtTemplate, ctx: any) {
throw new Error('Invalid template: ' + JSON.stringify(template)) throw new Error('Invalid template: ' + JSON.stringify(template))
} }
const serialize = (data: any) => JSON.stringify(data, null, 2).replace(/"{(.+)}"/g, '$1') const serialize = (data: any) => JSON.stringify(data, null, 2).replace(/"{(.+)}"(?=,?$)/gm, r => JSON.parse(r).replace(/^{(.*)}$/, '$1'))
const importName = (src: string) => `${camelCase(basename(src, extname(src))).replace(/[^a-zA-Z?\d\s:]/g, '')}_${hash(src)}` const importName = (src: string) => `${camelCase(basename(src, extname(src))).replace(/[^a-zA-Z?\d\s:]/g, '')}_${hash(src)}`
@ -33,9 +34,9 @@ const importSources = (sources: string | string[], { lazy = false } = {}) => {
} }
return sources.map((src) => { return sources.map((src) => {
if (lazy) { if (lazy) {
return `const ${importName(src)} = () => import('${src}' /* webpackChunkName: '${src}' */)` return `const ${importName(src)} = ${genDynamicImport(src, { comment: `webpackChunkName: ${JSON.stringify(src)}` })}`
} }
return `import ${importName(src)} from '${src}'` return genImport(src, importName(src))
}).join('\n') }).join('\n')
} }

View File

@ -48,6 +48,7 @@
"http-proxy": "^1.18.1", "http-proxy": "^1.18.1",
"is-primitive": "^3.0.1", "is-primitive": "^3.0.1",
"jiti": "^1.12.15", "jiti": "^1.12.15",
"knitwork": "^0.1.0",
"listhen": "^0.2.6", "listhen": "^0.2.6",
"mime": "^3.0.0", "mime": "^3.0.0",
"mlly": "^0.4.1", "mlly": "^0.4.1",

View File

@ -2,6 +2,7 @@ import { relative, resolve, join } from 'pathe'
import consola from 'consola' import consola from 'consola'
import * as rollup from 'rollup' import * as rollup from 'rollup'
import fse from 'fs-extra' import fse from 'fs-extra'
import { genDynamicImport } from 'knitwork'
import { printFSTree } from './utils/tree' import { printFSTree } from './utils/tree'
import { getRollupConfig } from './rollup/config' import { getRollupConfig } from './rollup/config'
import { hl, prettyPath, serializeTemplate, writeFile, isDirectory, replaceAll } from './utils' import { hl, prettyPath, serializeTemplate, writeFile, isDirectory, replaceAll } from './utils'
@ -73,7 +74,7 @@ export async function writeTypes (nitroContext: NitroContext) {
if (typeof mw.handle !== 'string') { continue } if (typeof mw.handle !== 'string') { continue }
const relativePath = relative(nitroContext._nuxt.buildDir, mw.handle).replace(/\.[a-z]+$/, '') const relativePath = relative(nitroContext._nuxt.buildDir, mw.handle).replace(/\.[a-z]+$/, '')
routeTypes[mw.route] = routeTypes[mw.route] || [] routeTypes[mw.route] = routeTypes[mw.route] || []
routeTypes[mw.route].push(`Awaited<ReturnType<typeof import('${relativePath}').default>>`) routeTypes[mw.route].push(`Awaited<ReturnType<typeof ${genDynamicImport(relativePath, { wrapper: false })}.default>>`)
} }
const lines = [ const lines = [

View File

@ -2,6 +2,7 @@ import { existsSync, promises as fsp } from 'fs'
import { resolve } from 'pathe' import { resolve } from 'pathe'
import consola from 'consola' import consola from 'consola'
import { joinURL } from 'ufo' import { joinURL } from 'ufo'
import { genString } from 'knitwork'
import { extendPreset, prettyPath } from '../utils' import { extendPreset, prettyPath } from '../utils'
import { NitroPreset, NitroContext, NitroInput } from '../context' import { NitroPreset, NitroContext, NitroInput } from '../context'
import { worker } from './worker' import { worker } from './worker'
@ -13,7 +14,7 @@ export const browser: NitroPreset = extendPreset(worker, (input: NitroInput) =>
const script = `<script> const script = `<script>
if ('serviceWorker' in navigator) { if ('serviceWorker' in navigator) {
window.addEventListener('load', function () { window.addEventListener('load', function () {
navigator.serviceWorker.register('${joinURL(baseURL, 'sw.js')}'); navigator.serviceWorker.register(${genString(joinURL(baseURL, 'sw.js'))});
}); });
} }
</script>` </script>`
@ -27,7 +28,7 @@ if ('serviceWorker' in navigator) {
<link rel="prefetch" href="${joinURL(baseURL, '_server/index.mjs')}"> <link rel="prefetch" href="${joinURL(baseURL, '_server/index.mjs')}">
<script> <script>
async function register () { async function register () {
const registration = await navigator.serviceWorker.register('${joinURL(baseURL, 'sw.js')}') const registration = await navigator.serviceWorker.register(${genString(joinURL(baseURL, 'sw.js'))})
await navigator.serviceWorker.ready await navigator.serviceWorker.ready
registration.active.addEventListener('statechange', (event) => { registration.active.addEventListener('statechange', (event) => {
if (event.target.state === 'activated') { if (event.target.state === 'activated') {
@ -64,7 +65,7 @@ if ('serviceWorker' in navigator) {
tmpl.contents = tmpl.contents.replace('</body>', script + '</body>') tmpl.contents = tmpl.contents.replace('</body>', script + '</body>')
}, },
async 'nitro:compiled' ({ output }: NitroContext) { async 'nitro:compiled' ({ output }: NitroContext) {
await fsp.writeFile(resolve(output.publicDir, 'sw.js'), `self.importScripts('${joinURL(baseURL, '_server/index.mjs')}');`, 'utf8') await fsp.writeFile(resolve(output.publicDir, 'sw.js'), `self.importScripts(${genString(joinURL(baseURL, '_server/index.mjs'))});`, 'utf8')
// Temp fix // Temp fix
if (!existsSync(resolve(output.publicDir, 'index.html'))) { if (!existsSync(resolve(output.publicDir, 'index.html'))) {

View File

@ -18,6 +18,7 @@ import devalue from '@nuxt/devalue'
import type { Preset } from 'unenv' import type { Preset } from 'unenv'
import { sanitizeFilePath } from 'mlly' import { sanitizeFilePath } from 'mlly'
import { genImport } from 'knitwork'
import { NitroContext } from '../context' import { NitroContext } from '../context'
import { resolvePath } from '../utils' import { resolvePath } from '../utils'
import { pkgDir } from '../dirs' import { pkgDir } from '../dirs'
@ -215,7 +216,7 @@ export const getRollupConfig = (nitroContext: NitroContext) => {
// Polyfill // Polyfill
rollupConfig.plugins.push(virtual({ rollupConfig.plugins.push(virtual({
'#polyfill': env.polyfill.map(p => `import '${p}';`).join('\n') '#polyfill': env.polyfill.map(p => genImport(p)).join('\n')
})) }))
// https://github.com/rollup/plugins/tree/master/packages/alias // https://github.com/rollup/plugins/tree/master/packages/alias

View File

@ -4,6 +4,7 @@ import createEtag from 'etag'
import mime from 'mime' import mime from 'mime'
import { resolve } from 'pathe' import { resolve } from 'pathe'
import { globby } from 'globby' import { globby } from 'globby'
import { genDynamicImport, genObjectFromRawEntries } from 'knitwork'
import virtual from './virtual' import virtual from './virtual'
export interface AssetOptions { export interface AssetOptions {
@ -81,9 +82,12 @@ function normalizeKey (key) {
function getAssetProd (assets: Record<string, Asset>) { function getAssetProd (assets: Record<string, Asset>) {
return ` return `
const _assets = {\n${Object.entries(assets).map(([id, asset]) => const _assets = ${genObjectFromRawEntries(
` ['${normalizeKey(id)}']: {\n import: () => import('${asset.fsPath}').then(r => r.default || r),\n meta: ${JSON.stringify(asset.meta)}\n }` Object.entries(assets).map(([id, asset]) => [normalizeKey(id), {
).join(',\n')}\n} import: genDynamicImport(asset.fsPath, { interopDefault: true }),
meta: asset.meta
}])
)}
${normalizeKey.toString()} ${normalizeKey.toString()}

View File

@ -2,6 +2,7 @@ import { pathToFileURL } from 'url'
import { resolve } from 'pathe' import { resolve } from 'pathe'
import { globby } from 'globby' import { globby } from 'globby'
import type { Plugin } from 'rollup' import type { Plugin } from 'rollup'
import { genDynamicImport, genObjectFromRawEntries, genImport } from 'knitwork'
import { serializeImportName } from '../../utils' import { serializeImportName } from '../../utils'
const PLUGIN_NAME = 'dynamic-require' const PLUGIN_NAME = 'dynamic-require'
@ -36,7 +37,7 @@ export function dynamicRequire ({ dir, ignore, inline }: Options): Plugin {
name: PLUGIN_NAME, name: PLUGIN_NAME,
transform (code: string, _id: string) { transform (code: string, _id: string) {
return { return {
code: code.replace(DYNAMIC_REQUIRE_RE, `import('${HELPER_DYNAMIC}').then(r => r.default || r).then(dynamicRequire => dynamicRequire($1)).then`), code: code.replace(DYNAMIC_REQUIRE_RE, `${genDynamicImport(HELPER_DYNAMIC, { wrapper: false, interopDefault: true })}.then(dynamicRequire => dynamicRequire($1)).then`),
map: null map: null
} }
}, },
@ -89,10 +90,8 @@ async function getWebpackChunkMeta (src: string) {
} }
function TMPL_INLINE ({ chunks }: TemplateContext) { function TMPL_INLINE ({ chunks }: TemplateContext) {
return `${chunks.map(i => `import * as ${i.name} from '${i.src}'`).join('\n')} return `${chunks.map(i => genImport(i.src, { name: '*', as: i.name })).join('\n')}
const dynamicChunks = { const dynamicChunks = ${genObjectFromRawEntries(chunks.map(i => [i.id, i.name]))};
${chunks.map(i => ` ['${i.id}']: ${i.name}`).join(',\n')}
};
export default function dynamicRequire(id) { export default function dynamicRequire(id) {
return Promise.resolve(dynamicChunks[id]); return Promise.resolve(dynamicChunks[id]);
@ -101,9 +100,7 @@ export default function dynamicRequire(id) {
function TMPL_LAZY ({ chunks }: TemplateContext) { function TMPL_LAZY ({ chunks }: TemplateContext) {
return ` return `
const dynamicChunks = { const dynamicChunks = ${genObjectFromRawEntries(chunks.map(i => [i.id, genDynamicImport(i.src)]))};
${chunks.map(i => ` ['${i.id}']: () => import('${i.src}')`).join(',\n')}
};
export default function dynamicRequire(id) { export default function dynamicRequire(id) {
return dynamicChunks[id](); return dynamicChunks[id]();

View File

@ -3,6 +3,7 @@ import { relative } from 'pathe'
import table from 'table' import table from 'table'
import isPrimitive from 'is-primitive' import isPrimitive from 'is-primitive'
import { isDebug } from 'std-env' import { isDebug } from 'std-env'
import { genArrayFromRaw, genDynamicImport, genImport } from 'knitwork'
import type { ServerMiddleware } from '../../server/middleware' import type { ServerMiddleware } from '../../server/middleware'
import virtual from './virtual' import virtual from './virtual'
@ -35,15 +36,18 @@ export function middleware (getMiddleware: () => ServerMiddleware[]) {
const lazyImports = unique(middleware.filter(m => m.lazy !== false && !imports.includes(m.handle)).map(m => m.handle)) const lazyImports = unique(middleware.filter(m => m.lazy !== false && !imports.includes(m.handle)).map(m => m.handle))
return ` return `
${imports.map(handle => `import ${getImportId(handle)} from '${handle}';`).join('\n')} ${imports.map(handle => `${genImport(handle, getImportId(handle))};`).join('\n')}
${lazyImports.map(handle => `const ${getImportId(handle)} = () => import('${handle}');`).join('\n')} ${lazyImports.map(handle => `const ${getImportId(handle)} = ${genDynamicImport(handle)};`).join('\n')}
const middleware = [ const middleware = ${genArrayFromRaw(middleware.map(m => ({
${middleware.map(m => `{ route: '${m.route}', handle: ${getImportId(m.handle)}, lazy: ${m.lazy || true}, promisify: ${m.promisify !== undefined ? m.promisify : true} }`).join(',\n')} route: JSON.stringify(m.route),
]; handle: getImportId(m.handle),
lazy: m.lazy || true,
promisify: m.promisify !== undefined ? m.promisify : true
})))};
export default middleware export default middleware
` `
} }
} }

View File

@ -1,4 +1,5 @@
import virtual from '@rollup/plugin-virtual' import virtual from '@rollup/plugin-virtual'
import { genImport, genString } from 'knitwork'
import { serializeImportName } from '../../utils' import { serializeImportName } from '../../utils'
export interface StorageOptions { export interface StorageOptions {
@ -35,13 +36,13 @@ export function storage (opts: StorageOptions) {
import { createStorage } from 'unstorage' import { createStorage } from 'unstorage'
import { assets } from '#assets' import { assets } from '#assets'
${driverImports.map(i => `import ${serializeImportName(i)} from '${i}'`).join('\n')} ${driverImports.map(i => genImport(i, serializeImportName(i))).join('\n')}
export const storage = createStorage({}) export const storage = createStorage({})
storage.mount('/assets', assets) storage.mount('/assets', assets)
${mounts.map(m => `storage.mount('${m.path}', ${serializeImportName(m.driver)}(${JSON.stringify(m.opts)}))`).join('\n')} ${mounts.map(m => `storage.mount(${genString(m.path)}, ${serializeImportName(m.driver)}(${JSON.stringify(m.opts)}))`).join('\n')}
` `
}) })
} }

View File

@ -39,6 +39,7 @@
"hash-sum": "^2.0.0", "hash-sum": "^2.0.0",
"hookable": "^5.1.1", "hookable": "^5.1.1",
"ignore": "^5.2.0", "ignore": "^5.2.0",
"knitwork": "^0.1.0",
"mlly": "^0.4.1", "mlly": "^0.4.1",
"murmurhash-es": "^0.1.1", "murmurhash-es": "^0.1.1",
"nuxi": "3.0.0", "nuxi": "3.0.0",

View File

@ -1,6 +1,7 @@
import { addVitePlugin, addWebpackPlugin, defineNuxtModule, addTemplate, resolveAlias, addPluginTemplate, useNuxt } from '@nuxt/kit' import { addVitePlugin, addWebpackPlugin, defineNuxtModule, addTemplate, resolveAlias, addPluginTemplate, useNuxt } from '@nuxt/kit'
import type { AutoImportsOptions } from '@nuxt/schema' import type { AutoImportsOptions } from '@nuxt/schema'
import { isAbsolute, join, relative, resolve, normalize } from 'pathe' import { isAbsolute, join, relative, resolve, normalize } from 'pathe'
import { genDynamicImport } from 'knitwork'
import { TransformPlugin } from './transform' import { TransformPlugin } from './transform'
import { Nuxt3AutoImports } from './imports' import { Nuxt3AutoImports } from './imports'
import { scanForComposables } from './composables' import { scanForComposables } from './composables'
@ -132,7 +133,7 @@ function generateDts (ctx: AutoImportContext) {
write: true, write: true,
getContents: () => `// Generated by auto imports getContents: () => `// Generated by auto imports
declare global { declare global {
${ctx.autoImports.map(i => ` const ${i.as}: typeof import('${r(i.from)}')['${i.name}']`).join('\n')} ${ctx.autoImports.map(i => ` const ${i.as}: typeof ${genDynamicImport(r(i.from), { wrapper: false })}['${i.name}']`).join('\n')}
} }
export {} export {}

View File

@ -1,4 +1,5 @@
import type { AutoImport } from '@nuxt/schema' import type { AutoImport } from '@nuxt/schema'
import { genExport, genImport, genString } from 'knitwork'
export function toImportModuleMap (autoImports: AutoImport[], isCJS = false) { export function toImportModuleMap (autoImports: AutoImport[], isCJS = false) {
const aliasKeyword = isCJS ? ' : ' : ' as ' const aliasKeyword = isCJS ? ' : ' : ' as '
@ -20,11 +21,11 @@ export function toImports (autoImports: AutoImport[], isCJS = false) {
const map = toImportModuleMap(autoImports, isCJS) const map = toImportModuleMap(autoImports, isCJS)
if (isCJS) { if (isCJS) {
return Object.entries(map) return Object.entries(map)
.map(([name, imports]) => `const { ${Array.from(imports).join(', ')} } = require('${name}');`) .map(([name, imports]) => `const { ${Array.from(imports).join(', ')} } = require(${genString(name)});`)
.join('\n') .join('\n')
} else { } else {
return Object.entries(map) return Object.entries(map)
.map(([name, imports]) => `import { ${Array.from(imports).join(', ')} } from '${name}';`) .map(([name, imports]) => genImport(name, Array.from(imports)))
.join('\n') .join('\n')
} }
} }
@ -32,7 +33,7 @@ export function toImports (autoImports: AutoImport[], isCJS = false) {
export function toExports (autoImports: AutoImport[]) { export function toExports (autoImports: AutoImport[]) {
const map = toImportModuleMap(autoImports, false) const map = toImportModuleMap(autoImports, false)
return Object.entries(map) return Object.entries(map)
.map(([name, imports]) => `export { ${Array.from(imports).join(', ')} } from '${name}';`) .map(([name, imports]) => genExport(name, Array.from(imports)))
.join('\n') .join('\n')
} }

View File

@ -1,6 +1,7 @@
import { createUnplugin } from 'unplugin' import { createUnplugin } from 'unplugin'
import { parseQuery, parseURL } from 'ufo' import { parseQuery, parseURL } from 'ufo'
import { Component } from '@nuxt/schema' import { Component } from '@nuxt/schema'
import { genImport } from 'knitwork'
interface LoaderOptions { interface LoaderOptions {
getComponents(): Component[] getComponents(): Component[]
@ -36,7 +37,7 @@ function transform (content: string, components: Component[]) {
if (component) { if (component) {
const identifier = map.get(component) || `__nuxt_component_${num++}` const identifier = map.get(component) || `__nuxt_component_${num++}`
map.set(component, identifier) map.set(component, identifier)
imports += `import ${identifier} from "${component.filePath}";` imports += genImport(component.filePath, identifier)
return ` ${identifier}` return ` ${identifier}`
} }
// no matched // no matched

View File

@ -1,6 +1,7 @@
import { isAbsolute, join, relative } from 'pathe' import { isAbsolute, join, relative } from 'pathe'
import type { Component } from '@nuxt/schema' import type { Component } from '@nuxt/schema'
import { genDynamicImport, genObjectFromRawEntries } from 'knitwork'
export type ComponentsTemplateOptions = { export type ComponentsTemplateOptions = {
buildDir?: string buildDir?: string
@ -27,14 +28,12 @@ export const componentsTemplate = {
getContents ({ options }: { options: ComponentsTemplateOptions }) { getContents ({ options }: { options: ComponentsTemplateOptions }) {
return `import { defineAsyncComponent } from 'vue' return `import { defineAsyncComponent } from 'vue'
const components = { const components = ${genObjectFromRawEntries(options.components.filter(c => c.global !== false).map((c) => {
${options.components.filter(c => c.global !== false).map((c) => {
const exp = c.export === 'default' ? 'c.default || c' : `c['${c.export}']` const exp = c.export === 'default' ? 'c.default || c' : `c['${c.export}']`
const magicComments = createImportMagicComments(c) const comment = createImportMagicComments(c)
return ` '${c.pascalName}': defineAsyncComponent(() => import('${c.filePath}' /* ${magicComments} */).then(c => ${exp}))` return [c.pascalName, `defineAsyncComponent(${genDynamicImport(c.filePath, { comment })}.then(c => ${exp}))`]
}).join(',\n')} }))}
}
export default function (nuxtApp) { export default function (nuxtApp) {
for (const name in components) { for (const name in components) {
@ -51,7 +50,7 @@ export const componentsTypeTemplate = {
getContents: ({ options }: { options: ComponentsTemplateOptions }) => `// Generated by components discovery getContents: ({ options }: { options: ComponentsTemplateOptions }) => `// Generated by components discovery
declare module 'vue' { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
${options.components.map(c => ` '${c.pascalName}': typeof import('${isAbsolute(c.filePath) ? relative(join(options.buildDir, 'types'), c.filePath) : c.filePath}')['${c.export}']`).join(',\n')} ${options.components.map(c => ` '${c.pascalName}': typeof ${genDynamicImport(isAbsolute(c.filePath) ? relative(join(options.buildDir, 'types'), c.filePath) : c.filePath, { wrapper: false })}['${c.export}']`).join(',\n')}
} }
} }
export {} export {}

View File

@ -1,5 +1,6 @@
import { templateUtils } from '@nuxt/kit' import { templateUtils } from '@nuxt/kit'
import type { Nuxt, NuxtApp } from '@nuxt/schema' import type { Nuxt, NuxtApp } from '@nuxt/schema'
import { genArrayFromRaw, genDynamicImport, genExport, genImport } from 'knitwork'
import { isAbsolute, join, relative } from 'pathe' import { isAbsolute, join, relative } from 'pathe'
import escapeRE from 'escape-string-regexp' import escapeRE from 'escape-string-regexp'
@ -24,23 +25,17 @@ export const vueShim = {
// TODO: Use an alias // TODO: Use an alias
export const appComponentTemplate = { export const appComponentTemplate = {
filename: 'app-component.mjs', filename: 'app-component.mjs',
getContents (ctx: TemplateContext) { getContents: (ctx: TemplateContext) => genExport(ctx.app.mainComponent, ['default'])
return `export { default } from '${ctx.app.mainComponent}'`
}
} }
// TODO: Use an alias // TODO: Use an alias
export const rootComponentTemplate = { export const rootComponentTemplate = {
filename: 'root-component.mjs', filename: 'root-component.mjs',
getContents (ctx: TemplateContext) { getContents: (ctx: TemplateContext) => genExport(ctx.app.rootComponent, ['default'])
return `export { default } from '${ctx.app.rootComponent}'`
}
} }
export const cssTemplate = { export const cssTemplate = {
filename: 'css.mjs', filename: 'css.mjs',
getContents (ctx: TemplateContext) { getContents: (ctx: TemplateContext) => ctx.nuxt.options.css.map(i => genImport(i.src || i)).join('\n')
return ctx.nuxt.options.css.map(i => `import '${i.src || i}';`).join('\n')
}
} }
export const clientPluginTemplate = { export const clientPluginTemplate = {
@ -49,9 +44,7 @@ export const clientPluginTemplate = {
const clientPlugins = ctx.app.plugins.filter(p => !p.mode || p.mode !== 'server') const clientPlugins = ctx.app.plugins.filter(p => !p.mode || p.mode !== 'server')
return [ return [
templateUtils.importSources(clientPlugins.map(p => p.src)), templateUtils.importSources(clientPlugins.map(p => p.src)),
'export default [', `export default ${genArrayFromRaw(clientPlugins.map(p => templateUtils.importName(p.src)))}`
clientPlugins.map(p => templateUtils.importName(p.src)).join(',\n '),
']'
].join('\n') ].join('\n')
} }
} }
@ -63,10 +56,10 @@ export const serverPluginTemplate = {
return [ return [
"import preload from '#app/plugins/preload.server'", "import preload from '#app/plugins/preload.server'",
templateUtils.importSources(serverPlugins.map(p => p.src)), templateUtils.importSources(serverPlugins.map(p => p.src)),
'export default [', `export default ${genArrayFromRaw([
' preload,', 'preload',
serverPlugins.map(p => templateUtils.importName(p.src)).join(',\n '), ...serverPlugins.map(p => templateUtils.importName(p.src))
']' ])}`
].join('\n') ].join('\n')
} }
} }
@ -103,7 +96,7 @@ type Decorate<T extends Record<string, any>> = { [K in keyof T as K extends stri
type InjectionType<A extends Plugin> = A extends Plugin<infer T> ? Decorate<T> : unknown type InjectionType<A extends Plugin> = A extends Plugin<infer T> ? Decorate<T> : unknown
type NuxtAppInjections = \n ${tsImports.map(p => `InjectionType<typeof import('${p}').default>`).join(' &\n ')} type NuxtAppInjections = \n ${tsImports.map(p => `InjectionType<typeof ${genDynamicImport(p, { wrapper: false })}.default>`).join(' &\n ')}
declare module '#app' { declare module '#app' {
interface NuxtApp extends NuxtAppInjections { } interface NuxtApp extends NuxtAppInjections { }

View File

@ -1,6 +1,7 @@
import { existsSync } from 'fs' import { existsSync } from 'fs'
import { defineNuxtModule, addTemplate, addPlugin, templateUtils, addVitePlugin, addWebpackPlugin } from '@nuxt/kit' import { defineNuxtModule, addTemplate, addPlugin, addVitePlugin, addWebpackPlugin } from '@nuxt/kit'
import { resolve } from 'pathe' import { resolve } from 'pathe'
import { genDynamicImport, genString, genArrayFromRaw, genImport, genObjectFromRawEntries } from 'knitwork'
import escapeRE from 'escape-string-regexp' import escapeRE from 'escape-string-regexp'
import { distDir } from '../dirs' import { distDir } from '../dirs'
import { resolveLayouts, resolvePagesRoutes, normalizeRoutes, resolveMiddleware, getImportName } from './utils' import { resolveLayouts, resolvePagesRoutes, normalizeRoutes, resolveMiddleware, getImportName } from './utils'
@ -80,8 +81,8 @@ export default defineNuxtModule({
async getContents () { async getContents () {
const pages = await resolvePagesRoutes(nuxt) const pages = await resolvePagesRoutes(nuxt)
await nuxt.callHook('pages:extend', pages) await nuxt.callHook('pages:extend', pages)
const { routes: serializedRoutes, imports } = normalizeRoutes(pages) const { routes, imports } = normalizeRoutes(pages)
return [...imports, `export default ${templateUtils.serialize(serializedRoutes)}`].join('\n') return [...imports, `export default ${routes}`].join('\n')
} }
}) })
@ -92,11 +93,11 @@ export default defineNuxtModule({
const middleware = await resolveMiddleware() const middleware = await resolveMiddleware()
const globalMiddleware = middleware.filter(mw => mw.global) const globalMiddleware = middleware.filter(mw => mw.global)
const namedMiddleware = middleware.filter(mw => !mw.global) const namedMiddleware = middleware.filter(mw => !mw.global)
const namedMiddlewareObject = Object.fromEntries(namedMiddleware.map(mw => [mw.name, `{() => import('${mw.path}')}`])) const namedMiddlewareObject = genObjectFromRawEntries(namedMiddleware.map(mw => [mw.name, genDynamicImport(mw.path)]))
return [ return [
...globalMiddleware.map(mw => `import ${getImportName(mw.name)} from '${mw.path}'`), ...globalMiddleware.map(mw => genImport(mw.path, getImportName(mw.name))),
`export const globalMiddleware = [${globalMiddleware.map(mw => getImportName(mw.name)).join(', ')}]`, `export const globalMiddleware = ${genArrayFromRaw(globalMiddleware.map(mw => getImportName(mw.name)))}`,
`export const namedMiddleware = ${templateUtils.serialize(namedMiddlewareObject)}` `export const namedMiddleware = ${namedMiddlewareObject}`
].join('\n') ].join('\n')
} }
}) })
@ -109,8 +110,8 @@ export default defineNuxtModule({
const namedMiddleware = middleware.filter(mw => !mw.global) const namedMiddleware = middleware.filter(mw => !mw.global)
return [ return [
'import type { NavigationGuard } from \'vue-router\'', 'import type { NavigationGuard } from \'vue-router\'',
`export type MiddlewareKey = ${namedMiddleware.map(mw => `"${mw.name}"`).join(' | ') || 'string'}`, `export type MiddlewareKey = ${namedMiddleware.map(mw => genString(mw.name)).join(' | ') || 'string'}`,
`declare module '${composablesFile}' {`, `declare module ${genString(composablesFile)} {`,
' interface PageMeta {', ' interface PageMeta {',
' middleware?: MiddlewareKey | NavigationGuard | Array<MiddlewareKey | NavigationGuard>', ' middleware?: MiddlewareKey | NavigationGuard | Array<MiddlewareKey | NavigationGuard>',
' }', ' }',
@ -126,8 +127,8 @@ export default defineNuxtModule({
const layouts = await resolveLayouts(nuxt) const layouts = await resolveLayouts(nuxt)
return [ return [
'import { ComputedRef, Ref } from \'vue\'', 'import { ComputedRef, Ref } from \'vue\'',
`export type LayoutKey = ${layouts.map(layout => `"${layout.name}"`).join(' | ') || 'string'}`, `export type LayoutKey = ${layouts.map(layout => genString(layout.name)).join(' | ') || 'string'}`,
`declare module '${composablesFile}' {`, `declare module ${genString(composablesFile)} {`,
' interface PageMeta {', ' interface PageMeta {',
' layout?: false | LayoutKey | Ref<LayoutKey> | ComputedRef<LayoutKey>', ' layout?: false | LayoutKey | Ref<LayoutKey> | ComputedRef<LayoutKey>',
' }', ' }',
@ -141,12 +142,12 @@ export default defineNuxtModule({
filename: 'layouts.mjs', filename: 'layouts.mjs',
async getContents () { async getContents () {
const layouts = await resolveLayouts(nuxt) const layouts = await resolveLayouts(nuxt)
const layoutsObject = Object.fromEntries(layouts.map(({ name, file }) => { const layoutsObject = genObjectFromRawEntries(layouts.map(({ name, file }) => {
return [name, `{defineAsyncComponent({ suspensible: false, loader: () => import('${file}') })}`] return [name, `defineAsyncComponent({ suspensible: false, loader: ${genDynamicImport(file)} })`]
})) }))
return [ return [
'import { defineAsyncComponent } from \'vue\'', 'import { defineAsyncComponent } from \'vue\'',
`export default ${templateUtils.serialize(layoutsObject)}` `export default ${layoutsObject}`
].join('\n') ].join('\n')
} }
}) })

View File

@ -3,6 +3,7 @@ import { encodePath } from 'ufo'
import type { Nuxt, NuxtMiddleware, NuxtPage } from '@nuxt/schema' import type { Nuxt, NuxtMiddleware, NuxtPage } from '@nuxt/schema'
import { resolveFiles, useNuxt } from '@nuxt/kit' import { resolveFiles, useNuxt } from '@nuxt/kit'
import { kebabCase, pascalCase } from 'scule' import { kebabCase, pascalCase } from 'scule'
import { genImport, genDynamicImport, genArrayFromRaw } from 'knitwork'
import escapeRE from 'escape-string-regexp' import escapeRE from 'escape-string-regexp'
enum SegmentParserState { enum SegmentParserState {
@ -225,20 +226,20 @@ export async function resolveLayouts (nuxt: Nuxt) {
return layouts return layouts
} }
export function normalizeRoutes (routes: NuxtPage[], metaImports: Set<string> = new Set()): { imports: Set<string>, routes: NuxtPage[]} { export function normalizeRoutes (routes: NuxtPage[], metaImports: Set<string> = new Set()): { imports: Set<string>, routes: string } {
return { return {
imports: metaImports, imports: metaImports,
routes: routes.map((route) => { routes: genArrayFromRaw(routes.map((route) => {
const file = normalize(route.file) const file = normalize(route.file)
const metaImportName = getImportName(file) + 'Meta' const metaImportName = getImportName(file) + 'Meta'
metaImports.add(`import { meta as ${metaImportName} } from '${file}?macro=true'`) metaImports.add(genImport(`${file}?macro=true`, [{ name: 'meta', as: metaImportName }]))
return { return {
...route, ...Object.fromEntries(Object.entries(route).map(([key, value]) => [key, JSON.stringify(value)])),
children: route.children ? normalizeRoutes(route.children, metaImports).routes : [], children: route.children ? normalizeRoutes(route.children, metaImports).routes : [],
meta: route.meta || `{${metaImportName}}` as any, meta: route.meta ? JSON.stringify(route.meta) : metaImportName,
component: `{() => import('${file}')}` component: genDynamicImport(file)
} }
}) }))
} }
} }

View File

@ -28,8 +28,8 @@ describe('auto-imports:transform', () => {
const transform = (code: string) => transformPlugin.transform.call({ error: null, warn: null }, code, '') const transform = (code: string) => transformPlugin.transform.call({ error: null, warn: null }, code, '')
it('should correct inject', async () => { it('should correct inject', async () => {
expect(await transform('const a = ref(0)')).to.equal('import { ref } from \'vue\';const a = ref(0)') expect(await transform('const a = ref(0)')).to.equal('import { ref } from "vue";const a = ref(0)')
expect(await transform('import { computed as ref } from "foo"; const a = ref(0)')).to.include('import { computed } from \'bar\';') expect(await transform('import { computed as ref } from "foo"; const a = ref(0)')).to.include('import { computed } from "bar";')
}) })
it('should ignore existing imported', async () => { it('should ignore existing imported', async () => {
@ -43,7 +43,7 @@ describe('auto-imports:transform', () => {
it('should ignore comments', async () => { it('should ignore comments', async () => {
const result = await transform('// import { computed } from "foo"\n;const a = computed(0)') const result = await transform('// import { computed } from "foo"\n;const a = computed(0)')
expect(result).to.equal('import { computed } from \'bar\';// import { computed } from "foo"\n;const a = computed(0)') expect(result).to.equal('import { computed } from "bar";// import { computed } from "foo"\n;const a = computed(0)')
}) })
it('should exclude files from transform', () => { it('should exclude files from transform', () => {

View File

@ -29,6 +29,7 @@
"escape-string-regexp": "^5.0.0", "escape-string-regexp": "^5.0.0",
"externality": "^0.1.6", "externality": "^0.1.6",
"fs-extra": "^10.0.0", "fs-extra": "^10.0.0",
"knitwork": "^0.1.0",
"magic-string": "^0.25.7", "magic-string": "^0.25.7",
"mlly": "^0.4.1", "mlly": "^0.4.1",
"p-debounce": "^4.0.0", "p-debounce": "^4.0.0",

View File

@ -4,6 +4,7 @@ import { builtinModules } from 'module'
import { resolve } from 'pathe' import { resolve } from 'pathe'
import * as vite from 'vite' import * as vite from 'vite'
import { ExternalsOptions, isExternal as _isExternal, ExternalsDefaults } from 'externality' import { ExternalsOptions, isExternal as _isExternal, ExternalsDefaults } from 'externality'
import { genDynamicImport, genObjectFromRawEntries } from 'knitwork'
import { hashId, uniq } from './utils' import { hashId, uniq } from './utils'
export interface TransformChunk { export interface TransformChunk {
@ -78,7 +79,7 @@ async function transformRequest (opts: TransformOptions, id: string) {
? withoutVersionQuery ? withoutVersionQuery
: pathToFileURL(withoutVersionQuery) : pathToFileURL(withoutVersionQuery)
return { return {
code: `(global, exports, importMeta, ssrImport, ssrDynamicImport, ssrExportAll) => import('${path}').then(r => { exports.default = r.default; ssrExportAll(r) }).catch(e => { console.error(e); throw new Error('[vite dev] Error loading external "${id}".') })`, code: `(global, exports, importMeta, ssrImport, ssrDynamicImport, ssrExportAll) => ${genDynamicImport(path, { wrapper: false })}.then(r => { exports.default = r.default; ssrExportAll(r) }).catch(e => { console.error(e); throw new Error(${JSON.stringify(`[vite dev] Error loading external "${id}".`)}) })`,
deps: [], deps: [],
dynamicDeps: [] dynamicDeps: []
} }
@ -131,8 +132,9 @@ export async function bundleRequest (opts: TransformOptions, entryURL: string) {
const ${hashId(chunk.id)} = ${chunk.code} const ${hashId(chunk.id)} = ${chunk.code}
`).join('\n') `).join('\n')
const manifestCode = 'const __modules__ = {\n' + const manifestCode = `const __modules__ = ${
chunks.map(chunk => ` '${chunk.id}': ${hashId(chunk.id)}`).join(',\n') + '\n}' genObjectFromRawEntries(chunks.map(chunk => [chunk.id, hashId(chunk.id)]))
}`
// https://github.com/vitejs/vite/blob/main/packages/vite/src/node/ssr/ssrModuleLoader.ts // https://github.com/vitejs/vite/blob/main/packages/vite/src/node/ssr/ssrModuleLoader.ts
const ssrModuleLoader = ` const ssrModuleLoader = `
@ -213,7 +215,7 @@ async function __instantiateModule__(url, urlStack) {
chunksCode, chunksCode,
manifestCode, manifestCode,
ssrModuleLoader, ssrModuleLoader,
`export default await __ssrLoadModule__('${entryURL}')` `export default await __ssrLoadModule__(${JSON.stringify(entryURL)})`
].join('\n\n') ].join('\n\n')
return { return {

View File

@ -2589,6 +2589,7 @@ __metadata:
globby: ^13.1.1 globby: ^13.1.1
h3: ^0.3.9 h3: ^0.3.9
hash-sum: ^2.0.0 hash-sum: ^2.0.0
knitwork: ^0.1.0
magic-string: ^0.25.7 magic-string: ^0.25.7
mlly: ^0.4.1 mlly: ^0.4.1
murmurhash-es: ^0.1.1 murmurhash-es: ^0.1.1
@ -2900,6 +2901,7 @@ __metadata:
globby: ^13.1.1 globby: ^13.1.1
hash-sum: ^2.0.0 hash-sum: ^2.0.0
jiti: ^1.12.15 jiti: ^1.12.15
knitwork: ^0.1.0
lodash.template: ^4.5.0 lodash.template: ^4.5.0
mlly: ^0.4.1 mlly: ^0.4.1
pathe: ^0.2.0 pathe: ^0.2.0
@ -2993,6 +2995,7 @@ __metadata:
http-proxy: ^1.18.1 http-proxy: ^1.18.1
is-primitive: ^3.0.1 is-primitive: ^3.0.1
jiti: ^1.12.15 jiti: ^1.12.15
knitwork: ^0.1.0
listhen: ^0.2.6 listhen: ^0.2.6
mime: ^3.0.0 mime: ^3.0.0
mlly: ^0.4.1 mlly: ^0.4.1
@ -3265,6 +3268,7 @@ __metadata:
escape-string-regexp: ^5.0.0 escape-string-regexp: ^5.0.0
externality: ^0.1.6 externality: ^0.1.6
fs-extra: ^10.0.0 fs-extra: ^10.0.0
knitwork: ^0.1.0
magic-string: ^0.25.7 magic-string: ^0.25.7
mlly: ^0.4.1 mlly: ^0.4.1
p-debounce: ^4.0.0 p-debounce: ^4.0.0
@ -12956,6 +12960,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"knitwork@npm:^0.1.0":
version: 0.1.0
resolution: "knitwork@npm:0.1.0"
checksum: 36782ee8fcfb78a18684ff28ab6e829381d45a02cb8eb9efa192b3c521d9f6ff6d1ba3fbc28bdae471a9d251821ea99018a1f3e224e7b3cb62f72bbdb3a4cbd6
languageName: node
linkType: hard
"kolorist@npm:^1.5.0": "kolorist@npm:^1.5.0":
version: 1.5.1 version: 1.5.1
resolution: "kolorist@npm:1.5.1" resolution: "kolorist@npm:1.5.1"
@ -14979,6 +14990,7 @@ __metadata:
hash-sum: ^2.0.0 hash-sum: ^2.0.0
hookable: ^5.1.1 hookable: ^5.1.1
ignore: ^5.2.0 ignore: ^5.2.0
knitwork: ^0.1.0
mlly: ^0.4.1 mlly: ^0.4.1
murmurhash-es: ^0.1.1 murmurhash-es: ^0.1.1
nuxi: 3.0.0 nuxi: 3.0.0