2022-07-07 16:26:04 +00:00
|
|
|
import { pathToFileURL } from 'node:url'
|
|
|
|
import { createUnplugin } from 'unplugin'
|
|
|
|
import { isAbsolute, relative } from 'pathe'
|
2023-01-20 16:17:31 +00:00
|
|
|
import type { Node } from 'estree-walker'
|
2022-07-07 16:26:04 +00:00
|
|
|
import { walk } from 'estree-walker'
|
|
|
|
import MagicString from 'magic-string'
|
|
|
|
import { hash } from 'ohash'
|
2023-06-05 19:15:12 +00:00
|
|
|
import type { CallExpression, Pattern } from 'estree'
|
2022-09-08 08:55:30 +00:00
|
|
|
import { parseQuery, parseURL } from 'ufo'
|
2023-03-07 21:06:15 +00:00
|
|
|
import escapeRE from 'escape-string-regexp'
|
2023-02-03 11:55:58 +00:00
|
|
|
import { findStaticImports, parseStaticImport } from 'mlly'
|
2023-06-05 19:15:12 +00:00
|
|
|
import { matchWithStringOrRegex } from '../utils'
|
2022-07-07 16:26:04 +00:00
|
|
|
|
2023-06-27 09:38:40 +00:00
|
|
|
interface ComposableKeysOptions {
|
2022-08-15 13:40:06 +00:00
|
|
|
sourcemap: boolean
|
|
|
|
rootDir: string
|
2023-06-05 19:15:12 +00:00
|
|
|
composables: Array<{ name: string, source?: string | RegExp, argumentLength: number }>
|
2022-07-07 16:26:04 +00:00
|
|
|
}
|
|
|
|
|
2022-09-19 09:34:42 +00:00
|
|
|
const stringTypes = ['Literal', 'TemplateLiteral']
|
2023-10-12 14:17:38 +00:00
|
|
|
const NUXT_LIB_RE = /node_modules\/(nuxt|nuxt3|nuxt-nightly)\//
|
2023-05-22 20:25:42 +00:00
|
|
|
const SUPPORTED_EXT_RE = /\.(m?[jt]sx?|vue)/
|
2022-07-07 16:26:04 +00:00
|
|
|
|
2022-08-15 13:40:06 +00:00
|
|
|
export const composableKeysPlugin = createUnplugin((options: ComposableKeysOptions) => {
|
2023-03-07 21:06:15 +00:00
|
|
|
const composableMeta = Object.fromEntries(options.composables.map(({ name, ...meta }) => [name, meta]))
|
|
|
|
|
|
|
|
const maxLength = Math.max(...options.composables.map(({ argumentLength }) => argumentLength))
|
|
|
|
const keyedFunctions = new Set(options.composables.map(({ name }) => name))
|
|
|
|
const KEYED_FUNCTIONS_RE = new RegExp(`\\b(${[...keyedFunctions].map(f => escapeRE(f)).join('|')})\\b`)
|
|
|
|
|
2022-07-07 16:26:04 +00:00
|
|
|
return {
|
|
|
|
name: 'nuxt:composable-keys',
|
|
|
|
enforce: 'post',
|
2022-09-19 09:34:42 +00:00
|
|
|
transformInclude (id) {
|
2022-09-08 08:55:30 +00:00
|
|
|
const { pathname, search } = parseURL(decodeURIComponent(pathToFileURL(id).href))
|
2023-05-22 20:25:42 +00:00
|
|
|
return !NUXT_LIB_RE.test(pathname) && SUPPORTED_EXT_RE.test(pathname) && parseQuery(search).type !== 'style' && !parseQuery(search).macro
|
2022-09-19 09:34:42 +00:00
|
|
|
},
|
|
|
|
transform (code, id) {
|
2022-07-07 16:26:04 +00:00
|
|
|
if (!KEYED_FUNCTIONS_RE.test(code)) { return }
|
2022-11-16 00:02:13 +00:00
|
|
|
const { 0: script = code, index: codeIndex = 0 } = code.match(/(?<=<script[^>]*>)[\S\s.]*?(?=<\/script>)/) || { index: 0, 0: code }
|
2022-07-07 16:26:04 +00:00
|
|
|
const s = new MagicString(code)
|
|
|
|
// https://github.com/unjs/unplugin/issues/90
|
2023-02-03 11:55:58 +00:00
|
|
|
let imports: Set<string> | undefined
|
2022-07-29 09:40:04 +00:00
|
|
|
let count = 0
|
2022-07-07 16:26:04 +00:00
|
|
|
const relativeID = isAbsolute(id) ? relative(options.rootDir, id) : id
|
2023-06-05 19:15:12 +00:00
|
|
|
const { pathname: relativePathname } = parseURL(relativeID)
|
|
|
|
|
|
|
|
const ast = this.parse(script, {
|
2022-07-07 16:26:04 +00:00
|
|
|
sourceType: 'module',
|
|
|
|
ecmaVersion: 'latest'
|
2023-06-05 19:15:12 +00:00
|
|
|
}) as Node
|
|
|
|
|
|
|
|
// To handle variables hoisting we need a pre-pass to collect variable and function declarations with scope info.
|
|
|
|
let scopeTracker = new ScopeTracker()
|
|
|
|
const varCollector = new ScopedVarsCollector()
|
|
|
|
walk(ast, {
|
|
|
|
enter (_node) {
|
|
|
|
if (_node.type === 'BlockStatement') {
|
|
|
|
scopeTracker.enterScope()
|
|
|
|
varCollector.refresh(scopeTracker.curScopeKey)
|
|
|
|
} else if (_node.type === 'FunctionDeclaration' && _node.id) {
|
|
|
|
varCollector.addVar(_node.id.name)
|
|
|
|
} else if (_node.type === 'VariableDeclarator') {
|
|
|
|
varCollector.collect(_node.id)
|
|
|
|
}
|
|
|
|
},
|
|
|
|
leave (_node) {
|
|
|
|
if (_node.type === 'BlockStatement') {
|
|
|
|
scopeTracker.leaveScope()
|
|
|
|
varCollector.refresh(scopeTracker.curScopeKey)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
scopeTracker = new ScopeTracker()
|
|
|
|
walk(ast, {
|
2022-08-15 13:40:06 +00:00
|
|
|
enter (_node) {
|
2023-06-05 19:15:12 +00:00
|
|
|
if (_node.type === 'BlockStatement') {
|
|
|
|
scopeTracker.enterScope()
|
|
|
|
}
|
2022-08-15 13:40:06 +00:00
|
|
|
if (_node.type !== 'CallExpression' || (_node as CallExpression).callee.type !== 'Identifier') { return }
|
|
|
|
const node: CallExpression = _node as CallExpression
|
2022-09-19 09:34:42 +00:00
|
|
|
const name = 'name' in node.callee && node.callee.name
|
2023-03-07 21:06:15 +00:00
|
|
|
if (!name || !keyedFunctions.has(name) || node.arguments.length >= maxLength) { return }
|
2022-09-19 09:34:42 +00:00
|
|
|
|
2023-06-05 19:15:12 +00:00
|
|
|
imports = imports || detectImportNames(script, composableMeta)
|
2023-02-03 11:55:58 +00:00
|
|
|
if (imports.has(name)) { return }
|
|
|
|
|
2023-03-07 21:06:15 +00:00
|
|
|
const meta = composableMeta[name]
|
|
|
|
|
2023-06-05 19:15:12 +00:00
|
|
|
if (varCollector.hasVar(scopeTracker.curScopeKey, name)) {
|
|
|
|
let skip = true
|
|
|
|
if (meta.source) {
|
|
|
|
skip = !matchWithStringOrRegex(relativePathname, meta.source)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (skip) { return }
|
|
|
|
}
|
|
|
|
|
2023-03-07 21:06:15 +00:00
|
|
|
if (node.arguments.length >= meta.argumentLength) { return }
|
|
|
|
|
2022-09-19 09:34:42 +00:00
|
|
|
switch (name) {
|
|
|
|
case 'useState':
|
2023-03-07 21:06:15 +00:00
|
|
|
if (stringTypes.includes(node.arguments[0]?.type)) { return }
|
2022-09-19 09:34:42 +00:00
|
|
|
break
|
|
|
|
|
|
|
|
case 'useFetch':
|
|
|
|
case 'useLazyFetch':
|
2023-03-07 21:06:15 +00:00
|
|
|
if (stringTypes.includes(node.arguments[1]?.type)) { return }
|
2022-09-19 09:34:42 +00:00
|
|
|
break
|
|
|
|
|
|
|
|
case 'useAsyncData':
|
|
|
|
case 'useLazyAsyncData':
|
2023-03-07 21:06:15 +00:00
|
|
|
if (stringTypes.includes(node.arguments[0]?.type) || stringTypes.includes(node.arguments[node.arguments.length - 1]?.type)) { return }
|
2022-09-19 09:34:42 +00:00
|
|
|
break
|
2022-07-07 16:26:04 +00:00
|
|
|
}
|
2022-09-19 09:34:42 +00:00
|
|
|
|
2022-11-02 10:15:33 +00:00
|
|
|
// TODO: Optimize me (https://github.com/nuxt/framework/pull/8529)
|
|
|
|
const endsWithComma = code.slice(codeIndex + (node as any).start, codeIndex + (node as any).end - 1).trim().endsWith(',')
|
|
|
|
|
2022-09-19 09:34:42 +00:00
|
|
|
s.appendLeft(
|
|
|
|
codeIndex + (node as any).end - 1,
|
2022-11-02 10:15:33 +00:00
|
|
|
(node.arguments.length && !endsWithComma ? ', ' : '') + "'$" + hash(`${relativeID}-${++count}`) + "'"
|
2022-09-19 09:34:42 +00:00
|
|
|
)
|
2023-06-05 19:15:12 +00:00
|
|
|
},
|
|
|
|
leave (_node) {
|
|
|
|
if (_node.type === 'BlockStatement') {
|
|
|
|
scopeTracker.leaveScope()
|
|
|
|
}
|
2022-07-07 16:26:04 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
if (s.hasChanged()) {
|
|
|
|
return {
|
|
|
|
code: s.toString(),
|
2022-08-15 13:40:06 +00:00
|
|
|
map: options.sourcemap
|
2023-04-14 17:21:08 +00:00
|
|
|
? s.generateMap({ hires: true })
|
2022-08-15 13:40:06 +00:00
|
|
|
: undefined
|
2022-07-07 16:26:04 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
2023-02-03 11:55:58 +00:00
|
|
|
|
2023-08-17 13:35:28 +00:00
|
|
|
/*
|
|
|
|
* track scopes with unique keys. for example
|
|
|
|
* ```js
|
|
|
|
* // root scope, marked as ''
|
|
|
|
* function a () { // '0'
|
|
|
|
* function b () {} // '0-0'
|
|
|
|
* function c () {} // '0-1'
|
|
|
|
* }
|
|
|
|
* function d () {} // '1'
|
|
|
|
* // ''
|
|
|
|
* ```
|
|
|
|
* */
|
2023-06-05 19:15:12 +00:00
|
|
|
class ScopeTracker {
|
2023-08-17 13:35:28 +00:00
|
|
|
// the top of the stack is not a part of current key, it is used for next level
|
2023-06-05 19:15:12 +00:00
|
|
|
scopeIndexStack: number[]
|
|
|
|
curScopeKey: string
|
|
|
|
|
|
|
|
constructor () {
|
|
|
|
this.scopeIndexStack = [0]
|
2023-08-17 13:35:28 +00:00
|
|
|
this.curScopeKey = ''
|
2023-06-05 19:15:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
getKey () {
|
|
|
|
return this.scopeIndexStack.slice(0, -1).join('-')
|
|
|
|
}
|
|
|
|
|
|
|
|
enterScope () {
|
|
|
|
this.scopeIndexStack.push(0)
|
|
|
|
this.curScopeKey = this.getKey()
|
|
|
|
}
|
|
|
|
|
|
|
|
leaveScope () {
|
|
|
|
this.scopeIndexStack.pop()
|
|
|
|
this.curScopeKey = this.getKey()
|
|
|
|
this.scopeIndexStack[this.scopeIndexStack.length - 1]++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class ScopedVarsCollector {
|
|
|
|
curScopeKey: string
|
|
|
|
all: Map<string, Set<string>>
|
|
|
|
|
|
|
|
constructor () {
|
|
|
|
this.all = new Map()
|
2023-08-17 13:35:28 +00:00
|
|
|
this.curScopeKey = ''
|
2023-06-05 19:15:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
refresh (scopeKey: string) {
|
|
|
|
this.curScopeKey = scopeKey
|
|
|
|
}
|
|
|
|
|
|
|
|
addVar (name: string) {
|
|
|
|
let vars = this.all.get(this.curScopeKey)
|
|
|
|
if (!vars) {
|
|
|
|
vars = new Set()
|
|
|
|
this.all.set(this.curScopeKey, vars)
|
|
|
|
}
|
|
|
|
vars.add(name)
|
|
|
|
}
|
|
|
|
|
|
|
|
hasVar (scopeKey: string, name: string) {
|
|
|
|
const indices = scopeKey.split('-').map(Number)
|
2023-08-17 13:35:28 +00:00
|
|
|
for (let i = indices.length; i >= 0; i--) {
|
2023-06-05 19:15:12 +00:00
|
|
|
if (this.all.get(indices.slice(0, i).join('-'))?.has(name)) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
collect (n: Pattern) {
|
|
|
|
const t = n.type
|
|
|
|
if (t === 'Identifier') {
|
|
|
|
this.addVar(n.name)
|
|
|
|
} else if (t === 'RestElement') {
|
|
|
|
this.collect(n.argument)
|
|
|
|
} else if (t === 'AssignmentPattern') {
|
|
|
|
this.collect(n.left)
|
|
|
|
} else if (t === 'ArrayPattern') {
|
|
|
|
n.elements.forEach(e => e && this.collect(e))
|
|
|
|
} else if (t === 'ObjectPattern') {
|
|
|
|
n.properties.forEach((p) => {
|
|
|
|
if (p.type === 'RestElement') {
|
|
|
|
this.collect(p)
|
|
|
|
} else {
|
|
|
|
this.collect(p.value)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-03 11:55:58 +00:00
|
|
|
const NUXT_IMPORT_RE = /nuxt|#app|#imports/
|
|
|
|
|
2023-07-05 09:35:45 +00:00
|
|
|
export function detectImportNames (code: string, composableMeta: Record<string, { source?: string | RegExp }>) {
|
2023-02-03 11:55:58 +00:00
|
|
|
const imports = findStaticImports(code)
|
|
|
|
const names = new Set<string>()
|
|
|
|
for (const i of imports) {
|
|
|
|
if (NUXT_IMPORT_RE.test(i.specifier)) { continue }
|
2023-06-05 19:15:12 +00:00
|
|
|
|
|
|
|
function addName (name: string) {
|
|
|
|
const source = composableMeta[name]?.source
|
|
|
|
if (source && matchWithStringOrRegex(i.specifier, source)) {
|
|
|
|
return
|
|
|
|
}
|
2023-07-05 09:35:45 +00:00
|
|
|
names.add(name)
|
2023-06-05 19:15:12 +00:00
|
|
|
}
|
|
|
|
|
2023-02-03 11:55:58 +00:00
|
|
|
const { namedImports, defaultImport, namespacedImport } = parseStaticImport(i)
|
|
|
|
for (const name in namedImports || {}) {
|
2023-06-05 19:15:12 +00:00
|
|
|
addName(namedImports![name])
|
2023-02-03 11:55:58 +00:00
|
|
|
}
|
|
|
|
if (defaultImport) {
|
2023-06-05 19:15:12 +00:00
|
|
|
addName(defaultImport)
|
2023-02-03 11:55:58 +00:00
|
|
|
}
|
|
|
|
if (namespacedImport) {
|
2023-06-05 19:15:12 +00:00
|
|
|
addName(namespacedImport)
|
2023-02-03 11:55:58 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return names
|
|
|
|
}
|