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

fix(nuxt)!: use parser to generate page metadata #8536

Merged
merged 7 commits into from Nov 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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.5",
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.