|
1 | 1 | import { createUnplugin } from 'unplugin'
|
2 |
| -import type { AnyNode, VariableDeclarator, ExportDefaultDeclaration, Property } from 'acorn' |
3 |
| -import { extname } from 'pathe' |
4 |
| -import { parse } from 'acorn' |
5 |
| -import { transform } from 'esbuild' |
6 |
| - |
7 |
| -const SCRIPT_RE = /<script[^>]*>([\s\S]*)<\/script>/ |
8 |
| - |
9 |
| -export default () => createUnplugin(() => { |
10 |
| - return { |
11 |
| - name: 'nuxt-scripts:check-scripts', |
12 |
| - enforce: 'pre', |
13 |
| - async transform(code, id) { |
14 |
| - if (!code.includes('useScript')) // all integrations should start with useScript* |
15 |
| - return |
16 |
| - |
17 |
| - const extName = extname(id) |
18 |
| - if (extName === '.vue') { |
19 |
| - const scriptAst = await extractScriptContentAst(code) |
20 |
| - if (scriptAst) { |
21 |
| - analyzeNodes(id, scriptAst) |
22 |
| - } |
23 |
| - } |
24 |
| - else if (extName === '.ts' || extName === '.js') { |
25 |
| - if (!code.includes('defineComponent')) return |
26 |
| - |
27 |
| - let result = code |
28 |
| - |
29 |
| - if (extName === '.ts') { |
30 |
| - result = (await transform(code, { loader: 'ts' })).code |
31 |
| - } |
32 |
| - |
33 |
| - const setupFunction = extractSetupFunction(result) |
34 |
| - |
35 |
| - if (setupFunction) { |
36 |
| - analyzeNodes(id, setupFunction) |
37 |
| - } |
38 |
| - } |
39 |
| - |
40 |
| - return undefined |
41 |
| - }, |
42 |
| - } |
43 |
| -}) |
44 |
| - |
45 |
| -function analyzeNodes(id: string, nodes: AnyNode[]) { |
46 |
| - let name: string | undefined |
47 |
| - |
48 |
| - for (const node of nodes) { |
49 |
| - if (name) { |
50 |
| - if (isAwaitingLoad(name, node)) { |
51 |
| - throw new Error('Awaiting load should not be used at top level of a composable or <script>') |
52 |
| - } |
53 |
| - } |
54 |
| - else { |
55 |
| - if (node.type === 'VariableDeclaration') { |
56 |
| - name = findScriptVar(node.declarations[0]) |
57 |
| - } |
58 |
| - } |
59 |
| - } |
60 |
| -} |
61 |
| - |
62 |
| -function findScriptVar(scriptDeclaration: VariableDeclarator) { |
63 |
| - if (scriptDeclaration.id.type === 'ObjectPattern') { |
64 |
| - for (const property of scriptDeclaration.id.properties) { |
65 |
| - if (property.type === 'Property' && property.key.type === 'Identifier' && property.key.name === '$script' && property.value.type === 'Identifier') { |
66 |
| - return property.value.name |
67 |
| - } |
68 |
| - } |
69 |
| - } |
70 |
| - else if (scriptDeclaration.id.type === 'Identifier') { |
71 |
| - return scriptDeclaration.id.name |
72 |
| - } |
73 |
| -} |
74 |
| - |
75 |
| -function isAwaitingLoad(name: string, node: AnyNode) { |
76 |
| - if (node.type === 'ExpressionStatement' && node.expression.type === 'AwaitExpression') { |
77 |
| - const arg = node.expression.argument |
78 |
| - if (arg.type === 'CallExpression') { |
79 |
| - const callee = arg.callee |
80 |
| - if (callee.type === 'MemberExpression' && callee.object.type === 'Identifier' && callee.object.name === name) { |
81 |
| - // $script or alias is used |
82 |
| - if (callee.property.type === 'Identifier' && callee.property.name === 'load') { |
83 |
| - return true |
| 2 | +import { type Node, walk } from 'estree-walker' |
| 3 | +import type { AssignmentExpression, CallExpression, ObjectPattern, ArrowFunctionExpression, Identifier, MemberExpression } from 'estree' |
| 4 | +import { isVue } from './util' |
| 5 | + |
| 6 | +export function NuxtScriptsCheckScripts() { |
| 7 | + return createUnplugin(() => { |
| 8 | + return { |
| 9 | + name: 'nuxt-scripts:check-scripts', |
| 10 | + transformInclude(id) { |
| 11 | + return isVue(id, { type: ['script'] }) |
| 12 | + }, |
| 13 | + |
| 14 | + async transform(code) { |
| 15 | + if (!code.includes('useScript')) // all integrations should start with useScript* |
| 16 | + return |
| 17 | + |
| 18 | + const ast = this.parse(code) |
| 19 | + let nameNode: Node | undefined |
| 20 | + let errorNode: Node | undefined |
| 21 | + walk(ast as Node, { |
| 22 | + enter(_node) { |
| 23 | + if (_node.type === 'VariableDeclaration' && _node.declarations?.[0]?.id?.type === 'ObjectPattern') { |
| 24 | + const objPattern = _node.declarations[0]?.id as ObjectPattern |
| 25 | + for (const property of objPattern.properties) { |
| 26 | + if (property.type === 'Property' && property.key.type === 'Identifier' && property.key.name === '$script' && property.value.type === 'Identifier') { |
| 27 | + nameNode = _node |
| 28 | + } |
| 29 | + } |
| 30 | + } |
| 31 | + if (nameNode) { |
| 32 | + let sequence = _node.type === 'SequenceExpression' ? _node : null |
| 33 | + let assignmentExpression |
| 34 | + if (_node.type === 'VariableDeclaration') { |
| 35 | + if (_node.declarations[0]?.init?.type === 'SequenceExpression') { |
| 36 | + sequence = _node.declarations[0]?.init |
| 37 | + assignmentExpression = _node.declarations[0]?.init?.expressions?.[0] |
| 38 | + } |
| 39 | + } |
| 40 | + if (sequence && !assignmentExpression) { |
| 41 | + assignmentExpression = (sequence.expressions[0]?.type === 'AssignmentExpression' ? sequence.expressions[0] : null) |
| 42 | + } |
| 43 | + if (assignmentExpression) { |
| 44 | + // check right call expression is calling $script |
| 45 | + const right = (assignmentExpression as AssignmentExpression)?.right as CallExpression |
| 46 | + // @ts-expect-error untyped |
| 47 | + if (right.callee?.name === '_withAsyncContext') { |
| 48 | + if (((right.arguments[0] as ArrowFunctionExpression)?.body as Identifier)?.name === '$script' |
| 49 | + || ((((right.arguments[0] as ArrowFunctionExpression)?.body as CallExpression)?.callee as MemberExpression)?.object as Identifier)?.name === '$script') { |
| 50 | + errorNode = nameNode |
| 51 | + } |
| 52 | + } |
| 53 | + } |
| 54 | + } |
| 55 | + }, |
| 56 | + }) |
| 57 | + if (errorNode) { |
| 58 | + return this.error(new Error('You can\'t use a top-level await on $script as it will never resolve.')) |
84 | 59 | }
|
85 |
| - } |
| 60 | + }, |
86 | 61 | }
|
87 |
| - } |
88 |
| -} |
89 |
| - |
90 |
| -async function extractScriptContentAst(code: string): Promise<AnyNode[] | undefined> { |
91 |
| - const scriptCode = code.match(SCRIPT_RE) |
92 |
| - return scriptCode |
93 |
| - ? parse((await transform(scriptCode[1], { loader: 'ts' })).code, { |
94 |
| - ecmaVersion: 'latest', |
95 |
| - sourceType: 'module', |
96 |
| - }).body |
97 |
| - : undefined |
98 |
| -} |
99 |
| - |
100 |
| -function extractSetupFunction(code: string): AnyNode[] | undefined { |
101 |
| - const ast = parse(code, { |
102 |
| - ecmaVersion: 'latest', |
103 |
| - sourceType: 'module', |
104 | 62 | })
|
105 |
| - |
106 |
| - const defaultExport = ast.body.find((node): node is ExportDefaultDeclaration => node.type === 'ExportDefaultDeclaration') |
107 |
| - |
108 |
| - if (defaultExport && defaultExport.declaration.type === 'CallExpression' && defaultExport.declaration.callee.type === 'Identifier' && defaultExport.declaration.callee.name === 'defineComponent') { |
109 |
| - const arg = defaultExport.declaration.arguments[0] |
110 |
| - if (arg && arg.type === 'ObjectExpression') { |
111 |
| - const setupProperty = arg.properties.find((prop): prop is Property => prop.type === 'Property' && prop.key.type === 'Identifier' && prop.key.name === 'setup') |
112 |
| - if (setupProperty) { |
113 |
| - const setupValue = setupProperty.value |
114 |
| - if (setupValue.type === 'FunctionExpression') { |
115 |
| - return setupValue.body.body |
116 |
| - } |
117 |
| - } |
118 |
| - } |
119 |
| - } |
120 | 63 | }
|
0 commit comments