Skip to content
This repository has been archived by the owner on Apr 6, 2023. It is now read-only.

Commit

Permalink
fix(nuxt)!: use parser to generate page metadata (#8536)
Browse files Browse the repository at this point in the history
  • Loading branch information
danielroe committed Nov 2, 2022
1 parent f485c14 commit 491d02f
Show file tree
Hide file tree
Showing 9 changed files with 185 additions and 153 deletions.
1 change: 1 addition & 0 deletions packages/nuxt/package.json
Expand Up @@ -50,6 +50,7 @@
"defu": "^6.1.0",
"destr": "^1.2.0",
"escape-string-regexp": "^5.0.0",
"estree-walker": "^3.0.1",
"fs-extra": "^10.1.0",
"globby": "^13.1.2",
"h3": "^0.8.6",
Expand Down
133 changes: 0 additions & 133 deletions packages/nuxt/src/pages/macros.ts

This file was deleted.

18 changes: 10 additions & 8 deletions packages/nuxt/src/pages/module.ts
Expand Up @@ -7,7 +7,7 @@ import type { NuxtApp, NuxtPage } from '@nuxt/schema'
import { joinURL } from 'ufo'
import { distDir } from '../dirs'
import { resolvePagesRoutes, normalizeRoutes } from './utils'
import { TransformMacroPlugin, TransformMacroPluginOptions } from './macros'
import { PageMetaPlugin, PageMetaPluginOptions } from './page-meta'

export default defineNuxtModule({
meta: {
Expand Down Expand Up @@ -64,7 +64,9 @@ export default defineNuxtModule({

const pathPattern = new RegExp(`(^|\\/)(${dirs.map(escapeRE).join('|')})/`)
if (event !== 'change' && path.match(pathPattern)) {
await updateTemplates()
await updateTemplates({
filter: template => template.filename === 'routes.mjs'
})
}
})

Expand Down Expand Up @@ -114,15 +116,15 @@ export default defineNuxtModule({
})

// Extract macros from pages
const macroOptions: TransformMacroPluginOptions = {
const pageMetaOptions: PageMetaPluginOptions = {
dev: nuxt.options.dev,
sourcemap: nuxt.options.sourcemap.server || nuxt.options.sourcemap.client,
macros: {
definePageMeta: 'meta'
}
dirs: nuxt.options._layers.map(
layer => resolve(layer.config.srcDir, layer.config.dir?.pages || 'pages')
)
}
addVitePlugin(TransformMacroPlugin.vite(macroOptions))
addWebpackPlugin(TransformMacroPlugin.webpack(macroOptions))
addVitePlugin(PageMetaPlugin.vite(pageMetaOptions))
addWebpackPlugin(PageMetaPlugin.webpack(pageMetaOptions))

// Add router plugin
addPlugin(resolve(runtimeDir, 'router'))
Expand Down
160 changes: 160 additions & 0 deletions packages/nuxt/src/pages/page-meta.ts
@@ -0,0 +1,160 @@
import { pathToFileURL } from 'node:url'
import { createUnplugin } from 'unplugin'
import { parseQuery, parseURL, stringifyQuery } from 'ufo'
import { findStaticImports, findExports, StaticImport, parseStaticImport } from 'mlly'
import type { CallExpression, Expression } from 'estree'
import { walk } from 'estree-walker'
import MagicString from 'magic-string'
import { isAbsolute, normalize } from 'pathe'

export interface PageMetaPluginOptions {
dirs: Array<string | RegExp>
dev?: boolean
sourcemap?: boolean
}

export const PageMetaPlugin = createUnplugin((options: PageMetaPluginOptions) => {
return {
name: 'nuxt:pages-macros-transform',
enforce: 'post',
transformInclude (id) {
const query = parseMacroQuery(id)
id = normalize(id)

const isPagesDir = options.dirs.some(dir => typeof dir === 'string' ? id.startsWith(dir) : dir.test(id))
if (!isPagesDir && !query.macro) { return false }

const { pathname } = parseURL(decodeURIComponent(pathToFileURL(id).href))
return /\.(m?[jt]sx?|vue)/.test(pathname)
},
transform (code, id) {
const query = parseMacroQuery(id)
if (query.type && query.type !== 'script') { return }

const s = new MagicString(code)
function result () {
if (s.hasChanged()) {
return {
code: s.toString(),
map: options.sourcemap
? s.generateMap({ source: id, includeContent: true })
: undefined
}
}
}

const hasMacro = code.match(/\bdefinePageMeta\s*\(\s*/)

// Remove any references to the macro from our pages
if (!query.macro) {
if (hasMacro) {
walk(this.parse(code, {
sourceType: 'module',
ecmaVersion: 'latest'
}), {
enter (_node) {
if (_node.type !== 'CallExpression' || (_node as CallExpression).callee.type !== 'Identifier') { return }
const node = _node as CallExpression & { start: number, end: number }
const name = 'name' in node.callee && node.callee.name
if (name === 'definePageMeta') {
s.overwrite(node.start, node.end, 'false && {}')
}
}
})
}
return result()
}

const imports = findStaticImports(code)

// [vite] Re-export any script imports
const scriptImport = imports.find(i => parseMacroQuery(i.specifier).type === 'script')
if (scriptImport) {
const specifier = rewriteQuery(scriptImport.specifier)
s.overwrite(0, code.length, `export { default } from ${JSON.stringify(specifier)}`)
return result()
}

// [webpack] Re-export any exports from script blocks in the components
const currentExports = findExports(code)
for (const match of currentExports) {
if (match.type !== 'default' || !match.specifier) {
continue
}

const specifier = rewriteQuery(match.specifier)
s.overwrite(0, code.length, `export { default } from ${JSON.stringify(specifier)}`)
return result()
}

if (!hasMacro && !code.includes('export { default }') && !code.includes('__nuxt_page_meta')) {
s.overwrite(0, code.length, 'export default {}')
return result()
}

const importMap = new Map<string, StaticImport>()
for (const i of imports) {
const parsed = parseStaticImport(i)
for (const name of [
parsed.defaultImport,
...Object.keys(parsed.namedImports || {}),
parsed.namespacedImport
].filter(Boolean) as string[]) {
importMap.set(name, i)
}
}

walk(this.parse(code, {
sourceType: 'module',
ecmaVersion: 'latest'
}), {
enter (_node) {
if (_node.type !== 'CallExpression' || (_node as CallExpression).callee.type !== 'Identifier') { return }
const node = _node as CallExpression & { start: number, end: number }
const name = 'name' in node.callee && node.callee.name
if (name !== 'definePageMeta') { return }

const meta = node.arguments[0] as Expression & { start: number, end: number }

let contents = `const __nuxt_page_meta = ${code!.slice(meta.start, meta.end) || '{}'}\nexport default __nuxt_page_meta`

walk(meta, {
enter (_node) {
if (_node.type === 'CallExpression') {
const node = _node as CallExpression & { start: number, end: number }
const name = 'name' in node.callee && node.callee.name
if (name && importMap.has(name)) {
contents = importMap.get(name)!.code + '\n' + contents
}
}
}
})

s.overwrite(0, code.length, contents)
}
})

if (!s.hasChanged() && !code.includes('__nuxt_page_meta')) {
s.overwrite(0, code.length, 'export default {}')
}

return result()
}
}
})

// https://github.com/vuejs/vue-loader/pull/1911
// https://github.com/vitejs/vite/issues/8473
function rewriteQuery (id: string) {
const query = stringifyQuery({ macro: 'true', ...parseMacroQuery(id) })
return id.replace(/\?.+$/, '?' + query)
}

function parseMacroQuery (id: string) {
const { search } = parseURL(decodeURIComponent(isAbsolute(id) ? pathToFileURL(id).href : id).replace(/\?macro=true$/, ''))
const query = parseQuery(search)
if (id.includes('?macro=true')) {
return { macro: 'true', ...query }
}
return query
}
2 changes: 1 addition & 1 deletion packages/nuxt/src/pages/utils.ts
Expand Up @@ -231,7 +231,7 @@ export function normalizeRoutes (routes: NuxtPage[], metaImports: Set<string> =
routes: genArrayFromRaw(routes.map((route) => {
const file = normalize(route.file)
const metaImportName = genSafeVariableName(file) + 'Meta'
metaImports.add(genImport(`${file}?macro=true`, [{ name: 'meta', as: metaImportName }]))
metaImports.add(genImport(`${file}?macro=true`, [{ name: 'default', as: metaImportName }]))

let aliasCode = `${metaImportName}?.alias || []`
if (Array.isArray(route.alias) && route.alias.length) {
Expand Down
2 changes: 1 addition & 1 deletion packages/vite/src/plugins/composable-keys.ts
Expand Up @@ -24,7 +24,7 @@ export const composableKeysPlugin = createUnplugin((options: ComposableKeysOptio
enforce: 'post',
transformInclude (id) {
const { pathname, search } = parseURL(decodeURIComponent(pathToFileURL(id).href))
return !pathname.match(/node_modules\/nuxt3?\//) && pathname.match(/\.(m?[jt]sx?|vue)/) && parseQuery(search).type !== 'style'
return !pathname.match(/node_modules\/nuxt3?\//) && pathname.match(/\.(m?[jt]sx?|vue)/) && parseQuery(search).type !== 'style' && !parseQuery(search).macro
},
transform (code, id) {
if (!KEYED_FUNCTIONS_RE.test(code)) { return }
Expand Down
2 changes: 2 additions & 0 deletions pnpm-lock.yaml

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

0 comments on commit 491d02f

Please sign in to comment.