2022-04-15 15:19:05 +00:00
|
|
|
import { pathToFileURL } from 'node:url'
|
2022-01-17 18:27:23 +00:00
|
|
|
import { createUnplugin } from 'unplugin'
|
|
|
|
import { parseQuery, parseURL, withQuery } from 'ufo'
|
|
|
|
import { findStaticImports, findExports } from 'mlly'
|
2022-03-03 10:01:14 +00:00
|
|
|
import MagicString from 'magic-string'
|
2022-07-21 10:44:33 +00:00
|
|
|
import { isAbsolute } from 'pathe'
|
2022-01-17 18:27:23 +00:00
|
|
|
|
|
|
|
export interface TransformMacroPluginOptions {
|
|
|
|
macros: Record<string, string>
|
2022-01-19 18:07:54 +00:00
|
|
|
dev?: boolean
|
2022-04-22 15:35:42 +00:00
|
|
|
sourcemap?: boolean
|
2022-01-17 18:27:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export const TransformMacroPlugin = createUnplugin((options: TransformMacroPluginOptions) => {
|
|
|
|
return {
|
2022-02-10 09:29:49 +00:00
|
|
|
name: 'nuxt:pages-macros-transform',
|
2022-01-17 18:27:23 +00:00
|
|
|
enforce: 'post',
|
|
|
|
transformInclude (id) {
|
2022-03-16 10:52:05 +00:00
|
|
|
if (!id || id.startsWith('\x00')) { return }
|
2022-03-16 13:41:37 +00:00
|
|
|
const { pathname, search } = parseURL(decodeURIComponent(pathToFileURL(id).href))
|
2022-02-11 08:59:52 +00:00
|
|
|
return pathname.endsWith('.vue') || !!parseQuery(search).macro
|
2022-01-17 18:27:23 +00:00
|
|
|
},
|
|
|
|
transform (code, id) {
|
2022-03-03 10:01:14 +00:00
|
|
|
const s = new MagicString(code)
|
2022-03-16 13:41:37 +00:00
|
|
|
const { search } = parseURL(decodeURIComponent(pathToFileURL(id).href))
|
2022-01-17 18:27:23 +00:00
|
|
|
|
2022-03-03 10:01:14 +00:00
|
|
|
function result () {
|
|
|
|
if (s.hasChanged()) {
|
2022-04-22 15:35:42 +00:00
|
|
|
return { code: s.toString(), map: options.sourcemap && s.generateMap({ source: id, includeContent: true }) }
|
2022-03-03 10:01:14 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-17 18:27:23 +00:00
|
|
|
// Tree-shake out any runtime references to the macro.
|
|
|
|
// We do this first as it applies to all files, not just those with the query
|
|
|
|
for (const macro in options.macros) {
|
2022-02-28 19:21:03 +00:00
|
|
|
const match = code.match(new RegExp(`\\b${macro}\\s*\\(\\s*`))
|
|
|
|
if (match?.[0]) {
|
|
|
|
s.overwrite(match.index, match.index + match[0].length, `/*#__PURE__*/ false && ${match[0]}`)
|
2022-01-17 18:27:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-11 08:59:52 +00:00
|
|
|
if (!parseQuery(search).macro) {
|
2022-03-03 10:01:14 +00:00
|
|
|
return result()
|
2022-02-11 08:59:52 +00:00
|
|
|
}
|
2022-01-17 18:27:23 +00:00
|
|
|
|
|
|
|
// [webpack] Re-export any imports from script blocks in the components
|
|
|
|
// with workaround for vue-loader bug: https://github.com/vuejs/vue-loader/pull/1911
|
|
|
|
const scriptImport = findStaticImports(code).find(i => parseQuery(i.specifier.replace('?macro=true', '')).type === 'script')
|
|
|
|
if (scriptImport) {
|
2022-06-12 21:46:31 +00:00
|
|
|
// https://github.com/vuejs/vue-loader/pull/1911
|
|
|
|
// https://github.com/vitejs/vite/issues/8473
|
2022-07-21 10:44:33 +00:00
|
|
|
const url = isAbsolute(scriptImport.specifier) ? pathToFileURL(scriptImport.specifier).href : scriptImport.specifier
|
|
|
|
const parsed = parseURL(decodeURIComponent(url).replace('?macro=true', ''))
|
2022-06-12 21:46:31 +00:00
|
|
|
const specifier = withQuery(parsed.pathname, { macro: 'true', ...parseQuery(parsed.search) })
|
2022-02-28 19:21:03 +00:00
|
|
|
s.overwrite(0, code.length, `export { meta } from "${specifier}"`)
|
2022-03-03 10:01:14 +00:00
|
|
|
return result()
|
2022-01-17 18:27:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const currentExports = findExports(code)
|
|
|
|
for (const match of currentExports) {
|
2022-02-28 19:21:03 +00:00
|
|
|
if (match.type !== 'default') {
|
|
|
|
continue
|
|
|
|
}
|
2022-01-17 18:27:23 +00:00
|
|
|
if (match.specifier && match._type === 'named') {
|
|
|
|
// [webpack] Export named exports rather than the default (component)
|
2022-02-28 19:21:03 +00:00
|
|
|
s.overwrite(match.start, match.end, `export {${Object.values(options.macros).join(', ')}} from "${match.specifier}"`)
|
2022-03-03 10:01:14 +00:00
|
|
|
return result()
|
2022-01-19 18:07:54 +00:00
|
|
|
} else if (!options.dev) {
|
2022-01-17 18:27:23 +00:00
|
|
|
// ensure we tree-shake any _other_ default exports out of the macro script
|
2022-02-28 19:21:03 +00:00
|
|
|
s.overwrite(match.start, match.end, '/*#__PURE__*/ false &&')
|
|
|
|
s.append('\nexport default {}')
|
2022-01-17 18:27:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const macro in options.macros) {
|
|
|
|
// Skip already-processed macros
|
2022-02-28 19:21:03 +00:00
|
|
|
if (currentExports.some(e => e.name === options.macros[macro])) {
|
|
|
|
continue
|
|
|
|
}
|
2022-01-17 18:27:23 +00:00
|
|
|
|
|
|
|
const { 0: match, index = 0 } = code.match(new RegExp(`\\b${macro}\\s*\\(\\s*`)) || {} as RegExpMatchArray
|
|
|
|
const macroContent = match ? extractObject(code.slice(index + match.length)) : 'undefined'
|
|
|
|
|
2022-02-28 19:21:03 +00:00
|
|
|
s.append(`\nexport const ${options.macros[macro]} = ${macroContent}`)
|
2022-01-17 18:27:23 +00:00
|
|
|
}
|
|
|
|
|
2022-03-03 10:01:14 +00:00
|
|
|
return result()
|
2022-01-17 18:27:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
const starts = {
|
|
|
|
'{': '}',
|
|
|
|
'[': ']',
|
|
|
|
'(': ')',
|
|
|
|
'<': '>',
|
|
|
|
'"': '"',
|
|
|
|
"'": "'"
|
|
|
|
}
|
|
|
|
|
|
|
|
function extractObject (code: string) {
|
|
|
|
// Strip comments
|
|
|
|
code = code.replace(/^\s*\/\/.*$/gm, '')
|
|
|
|
|
|
|
|
const stack = []
|
|
|
|
let result = ''
|
|
|
|
do {
|
|
|
|
if (stack[0] === code[0] && result.slice(-1) !== '\\') {
|
|
|
|
stack.shift()
|
|
|
|
} else if (code[0] in starts) {
|
|
|
|
stack.unshift(starts[code[0]])
|
|
|
|
}
|
|
|
|
result += code[0]
|
|
|
|
code = code.slice(1)
|
|
|
|
} while (stack.length && code.length)
|
|
|
|
return result
|
|
|
|
}
|