nexa-compiler 0.7.1 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/codegen/render.d.ts +2 -1
- package/dist/codegen/render.js +6 -5
- package/dist/codegen/sourcemap.d.ts +1 -1
- package/dist/codegen/sourcemap.js +60 -22
- package/dist/codegen/template.js +8 -2
- package/dist/codegen/validate.d.ts +1 -0
- package/dist/codegen/validate.js +138 -0
- package/dist/parse/errors.d.ts +48 -0
- package/dist/parse/errors.js +94 -0
- package/dist/parse/template.js +99 -27
- package/dist/transform/template.js +15 -1
- package/package.json +1 -1
package/dist/codegen/render.d.ts
CHANGED
|
@@ -10,11 +10,12 @@ export declare function generateComponentCode(sfc: {
|
|
|
10
10
|
setup?: boolean;
|
|
11
11
|
scoped: boolean;
|
|
12
12
|
hmrId?: string;
|
|
13
|
-
}): string;
|
|
13
|
+
}, filename?: string): string;
|
|
14
14
|
export declare function generateComponentCodeWithSourceMap(sfc: {
|
|
15
15
|
template: string | null;
|
|
16
16
|
script: string | null;
|
|
17
17
|
style: string | null;
|
|
18
|
+
setup?: boolean;
|
|
18
19
|
scoped: boolean;
|
|
19
20
|
hmrId?: string;
|
|
20
21
|
}, options?: GenerateOptions): {
|
package/dist/codegen/render.js
CHANGED
|
@@ -2,7 +2,8 @@ import { transformStyle } from '../transform/style.js';
|
|
|
2
2
|
import { generateSourceMap } from './sourcemap.js';
|
|
3
3
|
import { parseDefineProps, parseDefineEmits, stripMacroLines, extractBindings, extractImportBindings } from './macros.js';
|
|
4
4
|
import { generateRenderCode } from './template.js';
|
|
5
|
-
|
|
5
|
+
import { validateTemplateBindings } from './validate.js';
|
|
6
|
+
export function generateComponentCode(sfc, filename) {
|
|
6
7
|
const lines = [];
|
|
7
8
|
const scriptContent = sfc.script || '';
|
|
8
9
|
const importRegex = /import\s+[\s\S]*?\s+from\s+['"][^'"]+['"]|import\s+['"][^'"]+['"]/g;
|
|
@@ -163,6 +164,8 @@ export function generateComponentCode(sfc) {
|
|
|
163
164
|
lines.push('');
|
|
164
165
|
lines.push(`const _sfc_main = ${componentDefinition}`);
|
|
165
166
|
if (sfc.template) {
|
|
167
|
+
const renderBindings = [...new Set([...allBindings, ...propsKeys])];
|
|
168
|
+
validateTemplateBindings(sfc.template, renderBindings, filename);
|
|
166
169
|
lines.push('// Injected render function');
|
|
167
170
|
lines.push('_sfc_main.render = function(ctx) {');
|
|
168
171
|
const usedBuiltIns = ['Fragment'];
|
|
@@ -170,7 +173,6 @@ export function generateComponentCode(sfc) {
|
|
|
170
173
|
usedBuiltIns.push('Teleport');
|
|
171
174
|
if (sfc.template.includes('<Transition'))
|
|
172
175
|
usedBuiltIns.push('Transition');
|
|
173
|
-
const renderBindings = [...new Set([...allBindings, ...propsKeys])];
|
|
174
176
|
const componentBindings = renderBindings.map(b => /^[A-Z]/.test(b) ? `${b}: _ntc_${b}` : b);
|
|
175
177
|
const allRenderBindings = [...componentBindings, ...usedBuiltIns.map(b => `${b}: _ntc_${b}`)];
|
|
176
178
|
if (allRenderBindings.length > 0) {
|
|
@@ -195,11 +197,10 @@ export function generateComponentCode(sfc) {
|
|
|
195
197
|
return lines.join('\n');
|
|
196
198
|
}
|
|
197
199
|
export function generateComponentCodeWithSourceMap(sfc, options = {}) {
|
|
198
|
-
const code = generateComponentCode(sfc);
|
|
200
|
+
const code = generateComponentCode(sfc, options.filename);
|
|
199
201
|
if (!options.sourceMap || !options.filename || !options.source) {
|
|
200
202
|
return { code, map: null };
|
|
201
203
|
}
|
|
202
|
-
const
|
|
203
|
-
const map = generateSourceMap(options.filename, code, options.source, scriptLines);
|
|
204
|
+
const map = generateSourceMap(options.filename, code, options.source);
|
|
204
205
|
return { code, map };
|
|
205
206
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare function generateSourceMap(filename: string, code: string, source: string
|
|
1
|
+
export declare function generateSourceMap(filename: string, code: string, source: string): string | null;
|
|
@@ -17,7 +17,7 @@ function encodeVLQ(value) {
|
|
|
17
17
|
}
|
|
18
18
|
return result;
|
|
19
19
|
}
|
|
20
|
-
function encodeMappings(generatedLines,
|
|
20
|
+
function encodeMappings(generatedLines, lineMap) {
|
|
21
21
|
let result = '';
|
|
22
22
|
let lastGenCol = 0;
|
|
23
23
|
let lastSrcIdx = 0;
|
|
@@ -26,36 +26,74 @@ function encodeMappings(generatedLines, scriptLines, _templateLineStart) {
|
|
|
26
26
|
for (let genLine = 0; genLine < generatedLines; genLine++) {
|
|
27
27
|
if (genLine > 0)
|
|
28
28
|
result += ';';
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
lastGenCol = genCol;
|
|
43
|
-
lastSrcIdx = srcIdx;
|
|
44
|
-
lastSrcLine = srcLine;
|
|
45
|
-
lastSrcCol = srcCol;
|
|
46
|
-
}
|
|
29
|
+
const srcLine = lineMap[genLine];
|
|
30
|
+
const dGenCol = 0 - lastGenCol;
|
|
31
|
+
const dSrcIdx = 0 - lastSrcIdx;
|
|
32
|
+
const dSrcLine = srcLine - lastSrcLine;
|
|
33
|
+
const dSrcCol = 0 - lastSrcCol;
|
|
34
|
+
result += encodeVLQ(dGenCol);
|
|
35
|
+
result += encodeVLQ(dSrcIdx);
|
|
36
|
+
result += encodeVLQ(dSrcLine);
|
|
37
|
+
result += encodeVLQ(dSrcCol);
|
|
38
|
+
lastGenCol = 0;
|
|
39
|
+
lastSrcIdx = 0;
|
|
40
|
+
lastSrcLine = srcLine;
|
|
41
|
+
lastSrcCol = 0;
|
|
47
42
|
}
|
|
48
43
|
return result;
|
|
49
44
|
}
|
|
50
|
-
export function generateSourceMap(filename, code, source
|
|
51
|
-
|
|
45
|
+
export function generateSourceMap(filename, code, source) {
|
|
46
|
+
if (!code || !source)
|
|
47
|
+
return null;
|
|
48
|
+
const genLines = code.split('\n');
|
|
49
|
+
const srcLines = source.split('\n');
|
|
50
|
+
const generatedLines = genLines.length;
|
|
51
|
+
const templateOpen = srcLines.findIndex(l => l.trim().startsWith('<template'));
|
|
52
|
+
const templateClose = srcLines.findIndex(l => l.trim().startsWith('</template'));
|
|
53
|
+
const templateContentStart = templateOpen >= 0 ? templateOpen + 1 : 0;
|
|
54
|
+
const templateContentEnd = templateClose >= 0 ? templateClose - 1 : srcLines.length - 1;
|
|
55
|
+
const templateContentCount = Math.max(1, templateContentEnd - templateContentStart + 1);
|
|
56
|
+
const renderFuncLine = genLines.findIndex(l => /_sfc_main\.render\s*=/.test(l));
|
|
57
|
+
const lineMap = [];
|
|
58
|
+
if (renderFuncLine < 0) {
|
|
59
|
+
for (let i = 0; i < generatedLines; i++) {
|
|
60
|
+
lineMap.push(Math.min(i, srcLines.length - 1));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
let renderBodyStart = renderFuncLine + 1;
|
|
65
|
+
let renderDepth = 0;
|
|
66
|
+
let renderBodyEnd = genLines.length;
|
|
67
|
+
if (genLines[renderFuncLine].includes('{')) {
|
|
68
|
+
renderDepth = (genLines[renderFuncLine].match(/\{/g) || []).length;
|
|
69
|
+
renderDepth -= (genLines[renderFuncLine].match(/\}/g) || []).length;
|
|
70
|
+
}
|
|
71
|
+
for (let i = renderFuncLine + 1; i < genLines.length; i++) {
|
|
72
|
+
const opens = (genLines[i].match(/\{/g) || []).length;
|
|
73
|
+
const closes = (genLines[i].match(/\}/g) || []).length;
|
|
74
|
+
renderDepth += opens - closes;
|
|
75
|
+
if (renderDepth <= 0) {
|
|
76
|
+
renderBodyEnd = i;
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
for (let genLine = 0; genLine < generatedLines; genLine++) {
|
|
81
|
+
if (genLine >= renderBodyStart && genLine < renderBodyEnd) {
|
|
82
|
+
const relativeLine = genLine - renderBodyStart;
|
|
83
|
+
lineMap.push(templateContentStart + (relativeLine % templateContentCount));
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
lineMap.push(0);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
52
90
|
const map = {
|
|
53
91
|
version: 3,
|
|
54
92
|
file: filename.replace(/\.nexa$/, '.js'),
|
|
55
93
|
sources: [filename],
|
|
56
94
|
sourcesContent: [source],
|
|
57
95
|
names: [],
|
|
58
|
-
mappings: encodeMappings(generatedLines,
|
|
96
|
+
mappings: encodeMappings(generatedLines, lineMap),
|
|
59
97
|
};
|
|
60
98
|
return JSON.stringify(map);
|
|
61
99
|
}
|
package/dist/codegen/template.js
CHANGED
|
@@ -40,7 +40,13 @@ function genIf(node) {
|
|
|
40
40
|
}
|
|
41
41
|
function genFor(node) {
|
|
42
42
|
const childrenCode = node.children.length === 1 ? genNode(node.children[0]) : `h("div", null, [\n ${node.children.map(genNode).join(',\n ')}\n ])`;
|
|
43
|
-
return
|
|
43
|
+
return genForIterate(node.list, node.item, node.index, childrenCode);
|
|
44
|
+
}
|
|
45
|
+
function genForIterate(list, item, index, body) {
|
|
46
|
+
if (/^\d+$/.test(list)) {
|
|
47
|
+
return `Array.from({length: ${list}}, (_, ${item}) =>\n ${body}\n )`;
|
|
48
|
+
}
|
|
49
|
+
return `(${list}).map((${item}, ${index}) =>\n ${body}\n )`;
|
|
44
50
|
}
|
|
45
51
|
function genElement(node) {
|
|
46
52
|
const { tag, props, children } = node;
|
|
@@ -48,7 +54,7 @@ function genElement(node) {
|
|
|
48
54
|
const elementCode = buildElementCode(tag, props, childStr, node.scopeId);
|
|
49
55
|
if (node.vFor) {
|
|
50
56
|
const { item, index, list } = node.vFor;
|
|
51
|
-
return
|
|
57
|
+
return genForIterate(list, item, index, elementCode);
|
|
52
58
|
}
|
|
53
59
|
// Note: v-if is handled by transformChildren and genIf
|
|
54
60
|
return elementCode;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function validateTemplateBindings(template: string, knownBindings: string[], filename?: string): void;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
const JS_GLOBALS = new Set([
|
|
2
|
+
'Math', 'JSON', 'console', 'Object', 'Array', 'Number', 'String',
|
|
3
|
+
'Boolean', 'Date', 'RegExp', 'Map', 'Set', 'Promise', 'Symbol',
|
|
4
|
+
'parseInt', 'parseFloat', 'isNaN', 'isFinite', 'undefined', 'null',
|
|
5
|
+
'true', 'false', 'this', 'NaN', 'Infinity', 'globalThis',
|
|
6
|
+
'Error', 'TypeError', 'RangeError', 'SyntaxError', 'ReferenceError',
|
|
7
|
+
'Intl', 'URL', 'URLSearchParams', 'AbortController', 'AbortSignal',
|
|
8
|
+
'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval',
|
|
9
|
+
'requestAnimationFrame', 'cancelAnimationFrame',
|
|
10
|
+
'window', 'document', 'localStorage', 'sessionStorage',
|
|
11
|
+
'fetch', 'navigator', 'history', 'location', 'location',
|
|
12
|
+
'$event', 'event',
|
|
13
|
+
'h', 'hText', 'Fragment', 'Teleport', 'Transition',
|
|
14
|
+
'in', 'of', 'typeof', 'instanceof', 'delete', 'void',
|
|
15
|
+
]);
|
|
16
|
+
const PROPERTY_NAMES = new Set([
|
|
17
|
+
'length', 'name', 'value', 'id', 'key', 'title', 'type', 'size',
|
|
18
|
+
'x', 'y', 'w', 'h', 'top', 'bottom', 'left', 'right',
|
|
19
|
+
'slice', 'charAt', 'charCodeAt', 'toUpperCase', 'toLowerCase', 'trim',
|
|
20
|
+
'toString', 'toFixed', 'toPrecision', 'toExponential',
|
|
21
|
+
'map', 'filter', 'reduce', 'reduceRight', 'forEach', 'find', 'findIndex',
|
|
22
|
+
'push', 'pop', 'shift', 'unshift', 'splice', 'concat', 'join', 'split',
|
|
23
|
+
'indexOf', 'lastIndexOf', 'includes', 'has', 'every', 'some', 'sort',
|
|
24
|
+
'keys', 'values', 'entries', 'from', 'of',
|
|
25
|
+
'prototype', 'constructor', 'call', 'apply', 'bind',
|
|
26
|
+
'column', 'columns', 'data', 'row', 'rows', 'index', 'idx',
|
|
27
|
+
'option', 'options', 'item', 'items', 'group', 'groups',
|
|
28
|
+
'field', 'fields', 'label', 'labels', 'icon', 'icons',
|
|
29
|
+
'footer', 'header', 'slot', 'scopedSlots',
|
|
30
|
+
'message', 'messages', 'toast', 'toasts', 'tab', 'tabs',
|
|
31
|
+
'step', 'page', 'pages', 'mode', 'theme',
|
|
32
|
+
]);
|
|
33
|
+
export function validateTemplateBindings(template, knownBindings, filename) {
|
|
34
|
+
if (!template)
|
|
35
|
+
return;
|
|
36
|
+
const known = new Set(knownBindings);
|
|
37
|
+
const used = new Set();
|
|
38
|
+
const forLoopVars = new Set();
|
|
39
|
+
const mustacheRe = /\{\{(.*?)\}\}/g;
|
|
40
|
+
let m;
|
|
41
|
+
while ((m = mustacheRe.exec(template)) !== null) {
|
|
42
|
+
extractUsedIds(m[1].trim(), used, forLoopVars);
|
|
43
|
+
}
|
|
44
|
+
const attrRe = /([\w.-]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
|
|
45
|
+
while ((m = attrRe.exec(template)) !== null) {
|
|
46
|
+
const attrName = m[1];
|
|
47
|
+
const value = m[2] ?? m[3];
|
|
48
|
+
if (!value)
|
|
49
|
+
continue;
|
|
50
|
+
if (attrName === 'v-for') {
|
|
51
|
+
const forParts = value.match(/^\s*(?:\(?\s*(\w+)\s*(?:,\s*(\w+))?\s*\)?\s+)?(?:in|of)\s+([\s\S]+)$/);
|
|
52
|
+
if (forParts) {
|
|
53
|
+
if (forParts[1])
|
|
54
|
+
forLoopVars.add(forParts[1]);
|
|
55
|
+
if (forParts[2])
|
|
56
|
+
forLoopVars.add(forParts[2]);
|
|
57
|
+
const listExpr = forParts[3].trim();
|
|
58
|
+
if (!/^\d+$/.test(listExpr)) {
|
|
59
|
+
extractUsedIds(listExpr, used, forLoopVars);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else if (attrName.startsWith(':') || attrName.startsWith('@') || attrName.startsWith('v-')) {
|
|
64
|
+
extractUsedIds(value, used, forLoopVars);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const missing = [];
|
|
68
|
+
for (const id of used) {
|
|
69
|
+
if (JS_GLOBALS.has(id))
|
|
70
|
+
continue;
|
|
71
|
+
if (PROPERTY_NAMES.has(id))
|
|
72
|
+
continue;
|
|
73
|
+
if (forLoopVars.has(id))
|
|
74
|
+
continue;
|
|
75
|
+
if (known.has(id))
|
|
76
|
+
continue;
|
|
77
|
+
missing.push(id);
|
|
78
|
+
}
|
|
79
|
+
if (missing.length > 0) {
|
|
80
|
+
const source = filename ? ` in ${filename}` : '';
|
|
81
|
+
const listed = [...new Set(missing)].sort().join(', ');
|
|
82
|
+
console.warn(`[nexa-compiler]${source} template references undefined binding(s): ${listed}. ` +
|
|
83
|
+
`Make sure these are declared in <script setup> or passed as props.`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function extractUsedIds(expr, used, loopVars) {
|
|
87
|
+
let inString = null;
|
|
88
|
+
let prevDot = false;
|
|
89
|
+
let braceDepth = 0;
|
|
90
|
+
for (let i = 0; i < expr.length; i++) {
|
|
91
|
+
const c = expr[i];
|
|
92
|
+
if ((c === "'" || c === '"' || c === '`') && expr[i - 1] !== '\\') {
|
|
93
|
+
if (!inString)
|
|
94
|
+
inString = c;
|
|
95
|
+
else if (inString === c)
|
|
96
|
+
inString = null;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (inString)
|
|
100
|
+
continue;
|
|
101
|
+
if (c === '{') {
|
|
102
|
+
braceDepth++;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (c === '}') {
|
|
106
|
+
braceDepth = Math.max(0, braceDepth - 1);
|
|
107
|
+
prevDot = false;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (c === '.') {
|
|
111
|
+
prevDot = true;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (/[a-zA-Z_$]/.test(c)) {
|
|
115
|
+
const start = i;
|
|
116
|
+
while (i < expr.length && /[\w$]/.test(expr[i])) {
|
|
117
|
+
i++;
|
|
118
|
+
}
|
|
119
|
+
const ident = expr.slice(start, i);
|
|
120
|
+
if (ident && !prevDot) {
|
|
121
|
+
const isObjectKey = braceDepth > 0 && skipSpace(expr, i) === ':';
|
|
122
|
+
if (!isObjectKey) {
|
|
123
|
+
used.add(ident);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
prevDot = false;
|
|
127
|
+
i--;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
prevDot = false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function skipSpace(s, i) {
|
|
134
|
+
while (i < s.length && (s[i] === ' ' || s[i] === '\t' || s[i] === '\n' || s[i] === '\r')) {
|
|
135
|
+
i++;
|
|
136
|
+
}
|
|
137
|
+
return s[i];
|
|
138
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export interface SourceLocation {
|
|
2
|
+
line: number;
|
|
3
|
+
column: number;
|
|
4
|
+
offset: number;
|
|
5
|
+
}
|
|
6
|
+
export interface CompilerError {
|
|
7
|
+
code: string;
|
|
8
|
+
message: string;
|
|
9
|
+
location: SourceLocation;
|
|
10
|
+
endLocation?: SourceLocation;
|
|
11
|
+
suggestion?: string;
|
|
12
|
+
helpUrl?: string;
|
|
13
|
+
}
|
|
14
|
+
declare const ERROR_CODES: {
|
|
15
|
+
readonly E001: "UNCLOSED_TAG";
|
|
16
|
+
readonly E002: "UNEXPECTED_CLOSING_TAG";
|
|
17
|
+
readonly E003: "INVALID_TAG_NAME";
|
|
18
|
+
readonly E004: "MALFORMED_ATTRIBUTE";
|
|
19
|
+
readonly E005: "MISSING_ATTRIBUTE_VALUE";
|
|
20
|
+
readonly E006: "INVALID_EXPRESSION";
|
|
21
|
+
readonly E007: "UNCLOSED_MUSTACHE";
|
|
22
|
+
readonly E008: "V_FOR_REQUIRED";
|
|
23
|
+
readonly E009: "V_IF_REQUIRED";
|
|
24
|
+
readonly E010: "SELF_CLOSING_NON_VOID";
|
|
25
|
+
readonly E011: "DUPLICATE_ATTRIBUTE";
|
|
26
|
+
readonly E012: "INVALID_DIRECTIVE";
|
|
27
|
+
readonly E013: "UNCLOSED_COMMENT";
|
|
28
|
+
readonly E014: "NESTED_SFC_TAG";
|
|
29
|
+
readonly E015: "MISSING_CLOSING_BRACKET";
|
|
30
|
+
};
|
|
31
|
+
type ErrorCode = keyof typeof ERROR_CODES;
|
|
32
|
+
export declare class NexaCompilerError extends Error {
|
|
33
|
+
readonly code: string;
|
|
34
|
+
readonly location: SourceLocation;
|
|
35
|
+
readonly endLocation?: SourceLocation;
|
|
36
|
+
readonly suggestion?: string;
|
|
37
|
+
readonly helpUrl?: string;
|
|
38
|
+
constructor(error: CompilerError);
|
|
39
|
+
toString(): string;
|
|
40
|
+
toMarkdown(): string;
|
|
41
|
+
}
|
|
42
|
+
export declare function createError(code: ErrorCode, message: string, location: SourceLocation, options?: {
|
|
43
|
+
endLocation?: SourceLocation;
|
|
44
|
+
suggestion?: string;
|
|
45
|
+
helpUrl?: string;
|
|
46
|
+
}): NexaCompilerError;
|
|
47
|
+
export declare function getErrorHelp(code: string): string | undefined;
|
|
48
|
+
export { ERROR_CODES };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
const ERROR_CODES = {
|
|
2
|
+
E001: 'UNCLOSED_TAG',
|
|
3
|
+
E002: 'UNEXPECTED_CLOSING_TAG',
|
|
4
|
+
E003: 'INVALID_TAG_NAME',
|
|
5
|
+
E004: 'MALFORMED_ATTRIBUTE',
|
|
6
|
+
E005: 'MISSING_ATTRIBUTE_VALUE',
|
|
7
|
+
E006: 'INVALID_EXPRESSION',
|
|
8
|
+
E007: 'UNCLOSED_MUSTACHE',
|
|
9
|
+
E008: 'V_FOR_REQUIRED',
|
|
10
|
+
E009: 'V_IF_REQUIRED',
|
|
11
|
+
E010: 'SELF_CLOSING_NON_VOID',
|
|
12
|
+
E011: 'DUPLICATE_ATTRIBUTE',
|
|
13
|
+
E012: 'INVALID_DIRECTIVE',
|
|
14
|
+
E013: 'UNCLOSED_COMMENT',
|
|
15
|
+
E014: 'NESTED_SFC_TAG',
|
|
16
|
+
E015: 'MISSING_CLOSING_BRACKET',
|
|
17
|
+
};
|
|
18
|
+
export class NexaCompilerError extends Error {
|
|
19
|
+
code;
|
|
20
|
+
location;
|
|
21
|
+
endLocation;
|
|
22
|
+
suggestion;
|
|
23
|
+
helpUrl;
|
|
24
|
+
constructor(error) {
|
|
25
|
+
super(error.message);
|
|
26
|
+
this.name = 'NexaCompilerError';
|
|
27
|
+
this.code = error.code;
|
|
28
|
+
this.location = error.location;
|
|
29
|
+
this.endLocation = error.endLocation;
|
|
30
|
+
this.suggestion = error.suggestion;
|
|
31
|
+
this.helpUrl = error.helpUrl;
|
|
32
|
+
}
|
|
33
|
+
toString() {
|
|
34
|
+
const location = formatLocation(this.location);
|
|
35
|
+
let result = `\n${this.code}: ${this.message}\n`;
|
|
36
|
+
result += ` at ${location}\n`;
|
|
37
|
+
if (this.suggestion) {
|
|
38
|
+
result += `\n Suggestion: ${this.suggestion}\n`;
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
toMarkdown() {
|
|
43
|
+
const location = formatLocation(this.location);
|
|
44
|
+
let result = `### ${this.code}: ${this.message}\n\n`;
|
|
45
|
+
result += `**Location:** ${location}\n`;
|
|
46
|
+
if (this.suggestion) {
|
|
47
|
+
result += `\n**Suggestion:** ${this.suggestion}\n`;
|
|
48
|
+
}
|
|
49
|
+
if (this.helpUrl) {
|
|
50
|
+
result += `\n**Learn more:** ${this.helpUrl}\n`;
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function formatLocation(loc) {
|
|
56
|
+
return `${loc.line}:${loc.column}`;
|
|
57
|
+
}
|
|
58
|
+
function getLineCol(text, offset) {
|
|
59
|
+
const lines = text.slice(0, offset).split('\n');
|
|
60
|
+
return {
|
|
61
|
+
line: lines.length,
|
|
62
|
+
column: (lines[lines.length - 1]?.length || 0) + 1,
|
|
63
|
+
offset,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export function createError(code, message, location, options) {
|
|
67
|
+
return new NexaCompilerError({
|
|
68
|
+
code: ERROR_CODES[code],
|
|
69
|
+
message,
|
|
70
|
+
location,
|
|
71
|
+
...options,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
export function getErrorHelp(code) {
|
|
75
|
+
const helpUrls = {
|
|
76
|
+
E001: 'https://nexajs.dev/docs/compiler/errors#e001',
|
|
77
|
+
E002: 'https://nexajs.dev/docs/compiler/errors#e002',
|
|
78
|
+
E003: 'https://nexajs.dev/docs/compiler/errors#e003',
|
|
79
|
+
E004: 'https://nexajs.dev/docs/compiler/errors#e004',
|
|
80
|
+
E005: 'https://nexajs.dev/docs/compiler/errors#e005',
|
|
81
|
+
E006: 'https://nexajs.dev/docs/compiler/errors#e006',
|
|
82
|
+
E007: 'https://nexajs.dev/docs/compiler/errors#e007',
|
|
83
|
+
E008: 'https://nexajs.dev/docs/compiler/errors#e008',
|
|
84
|
+
E009: 'https://nexajs.dev/docs/compiler/errors#e009',
|
|
85
|
+
E010: 'https://nexajs.dev/docs/compiler/errors#e010',
|
|
86
|
+
E011: 'https://nexajs.dev/docs/compiler/errors#e011',
|
|
87
|
+
E012: 'https://nexajs.dev/docs/compiler/errors#e012',
|
|
88
|
+
E013: 'https://nexajs.dev/docs/compiler/errors#e013',
|
|
89
|
+
E014: 'https://nexajs.dev/docs/compiler/errors#e014',
|
|
90
|
+
E015: 'https://nexajs.dev/docs/compiler/errors#e015',
|
|
91
|
+
};
|
|
92
|
+
return helpUrls[code];
|
|
93
|
+
}
|
|
94
|
+
export { ERROR_CODES };
|
package/dist/parse/template.js
CHANGED
|
@@ -1,40 +1,79 @@
|
|
|
1
|
+
import { createError, getErrorHelp } from './errors.js';
|
|
1
2
|
const tagRegex = /<(\/?)(\w[\w-]*)((?:\s+[^'">\s/]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^>\s/]+))?)*)\s*(\/?)>/g;
|
|
2
|
-
const attrRegex = /([#@:]?[\w
|
|
3
|
+
const attrRegex = /([#@:]?[\w.:-]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|(\{[^}]+\})))?/g;
|
|
3
4
|
const mustacheRegex = /\{\{(.*?)\}\}/g;
|
|
5
|
+
const voidElements = new Set([
|
|
6
|
+
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
|
|
7
|
+
'link', 'meta', 'param', 'source', 'track', 'wbr',
|
|
8
|
+
]);
|
|
4
9
|
export function parseTemplate(template) {
|
|
5
10
|
template = template.replace(/<!--[\s\S]*?-->/g, '');
|
|
6
11
|
const root = { type: 'root', children: [] };
|
|
7
|
-
const stack = [
|
|
12
|
+
const stack = [];
|
|
8
13
|
let lastIndex = 0;
|
|
9
14
|
let match;
|
|
10
15
|
tagRegex.lastIndex = 0;
|
|
11
16
|
while ((match = tagRegex.exec(template)) !== null) {
|
|
12
17
|
const [, isClose, tag, attrsStr, selfClose] = match;
|
|
13
18
|
const matchStart = match.index;
|
|
14
|
-
// Process text between tags
|
|
15
19
|
if (matchStart > lastIndex) {
|
|
16
20
|
const text = template.slice(lastIndex, matchStart);
|
|
17
21
|
const nodes = parseText(text);
|
|
18
22
|
for (const node of nodes) {
|
|
19
|
-
stack[stack.length - 1]
|
|
23
|
+
const parentStack = stack.length > 0 ? stack[stack.length - 1] : null;
|
|
24
|
+
if (parentStack && 'children' in parentStack.node) {
|
|
25
|
+
parentStack.node.children.push(node);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
root.children.push(node);
|
|
29
|
+
}
|
|
20
30
|
}
|
|
21
31
|
}
|
|
22
32
|
if (isClose) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
stack.
|
|
33
|
+
if (stack.length === 0) {
|
|
34
|
+
const location = getLineCol(template, matchStart);
|
|
35
|
+
const topTag = stack.length > 0 ? stack[stack.length - 1].tag : null;
|
|
36
|
+
const suggestion = topTag
|
|
37
|
+
? `Did you mean to close </${topTag}> instead?`
|
|
38
|
+
: `This closing tag has no matching opening tag.`;
|
|
39
|
+
throw createError('E002', `Unexpected closing tag </${tag}>`, location, {
|
|
40
|
+
suggestion,
|
|
41
|
+
helpUrl: getErrorHelp('E002'),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
const parent = stack[stack.length - 1];
|
|
45
|
+
if (parent.tag !== tag) {
|
|
46
|
+
const location = getLineCol(template, matchStart);
|
|
47
|
+
throw createError('E002', `Unexpected closing tag </${tag}>. Expected </${parent.tag}>`, location, {
|
|
48
|
+
suggestion: `Did you mean </${parent.tag}>?`,
|
|
49
|
+
helpUrl: getErrorHelp('E002'),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
stack.pop();
|
|
53
|
+
lastIndex = tagRegex.lastIndex;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const attrs = parseAttrs(attrsStr.trim(), template, matchStart);
|
|
57
|
+
const isVoid = isVoidElement(tag);
|
|
58
|
+
const isSelfClosing = selfClose === '/' || isVoid;
|
|
59
|
+
if (isSelfClosing) {
|
|
60
|
+
const parentStack = stack.length > 0 ? stack[stack.length - 1] : null;
|
|
61
|
+
const node = { type: 'element', tag, attrs, children: [], isSelfClosing: true };
|
|
62
|
+
if (parentStack && 'children' in parentStack.node) {
|
|
63
|
+
parentStack.node.children.push(node);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
root.children.push(node);
|
|
26
67
|
}
|
|
27
68
|
lastIndex = tagRegex.lastIndex;
|
|
28
69
|
continue;
|
|
29
70
|
}
|
|
30
|
-
const attrs = parseAttrs(attrsStr.trim());
|
|
31
|
-
const isSelfClosing = selfClose === '/' || isVoidElement(tag);
|
|
32
71
|
const node = {
|
|
33
72
|
type: 'element',
|
|
34
73
|
tag,
|
|
35
74
|
attrs,
|
|
36
75
|
children: [],
|
|
37
|
-
isSelfClosing:
|
|
76
|
+
isSelfClosing: false,
|
|
38
77
|
};
|
|
39
78
|
const isSlot = tag === 'slot';
|
|
40
79
|
if (isSlot) {
|
|
@@ -42,41 +81,66 @@ export function parseTemplate(template) {
|
|
|
42
81
|
type: 'slot',
|
|
43
82
|
name: attrs.find(a => a.name === 'name')?.value || 'default',
|
|
44
83
|
attrs,
|
|
45
|
-
children: []
|
|
84
|
+
children: [],
|
|
46
85
|
};
|
|
47
|
-
stack[stack.length - 1]
|
|
48
|
-
if (
|
|
49
|
-
|
|
86
|
+
const parentStack = stack.length > 0 ? stack[stack.length - 1] : null;
|
|
87
|
+
if (parentStack && 'children' in parentStack.node) {
|
|
88
|
+
parentStack.node.children.push(slotNode);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
root.children.push(slotNode);
|
|
50
92
|
}
|
|
93
|
+
stack.push({ tag, node: slotNode, start: matchStart });
|
|
51
94
|
}
|
|
52
95
|
else {
|
|
53
|
-
stack[stack.length - 1]
|
|
54
|
-
if (
|
|
55
|
-
|
|
96
|
+
const parentStack = stack.length > 0 ? stack[stack.length - 1] : null;
|
|
97
|
+
if (parentStack && 'children' in parentStack.node) {
|
|
98
|
+
parentStack.node.children.push(node);
|
|
56
99
|
}
|
|
100
|
+
else {
|
|
101
|
+
root.children.push(node);
|
|
102
|
+
}
|
|
103
|
+
stack.push({ tag, node, start: matchStart });
|
|
57
104
|
}
|
|
58
105
|
lastIndex = tagRegex.lastIndex;
|
|
59
106
|
}
|
|
60
|
-
|
|
107
|
+
if (stack.length > 0) {
|
|
108
|
+
const unclosed = stack[stack.length - 1];
|
|
109
|
+
const location = getLineCol(template, unclosed.start);
|
|
110
|
+
throw createError('E001', `Unclosed tag <${unclosed.tag}>`, location, {
|
|
111
|
+
suggestion: `Add closing </${unclosed.tag}> tag after the content.`,
|
|
112
|
+
helpUrl: getErrorHelp('E001'),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
61
115
|
if (lastIndex < template.length) {
|
|
62
116
|
const text = template.slice(lastIndex);
|
|
63
117
|
const nodes = parseText(text);
|
|
64
118
|
for (const node of nodes) {
|
|
65
|
-
|
|
119
|
+
root.children.push(node);
|
|
66
120
|
}
|
|
67
121
|
}
|
|
68
122
|
return root;
|
|
69
123
|
}
|
|
70
|
-
function parseAttrs(attrsStr) {
|
|
124
|
+
function parseAttrs(attrsStr, template, tagStart) {
|
|
71
125
|
if (!attrsStr)
|
|
72
126
|
return [];
|
|
73
127
|
const attrs = [];
|
|
74
128
|
attrRegex.lastIndex = 0;
|
|
129
|
+
const seenAttrs = new Map();
|
|
75
130
|
let match;
|
|
76
131
|
while ((match = attrRegex.exec(attrsStr)) !== null) {
|
|
77
132
|
let rawName = match[1];
|
|
78
133
|
let quotedValue = match[2] ?? match[3] ?? match[4];
|
|
79
134
|
const isDynamic = rawName.startsWith(':') || rawName.startsWith('@');
|
|
135
|
+
if (seenAttrs.has(rawName)) {
|
|
136
|
+
const attrIndex = tagStart + 4 + attrsStr.indexOf(rawName);
|
|
137
|
+
const location = getLineCol(template, attrIndex);
|
|
138
|
+
throw createError('E011', `Duplicate attribute "${rawName}"`, location, {
|
|
139
|
+
suggestion: `Remove one of the duplicate attributes.`,
|
|
140
|
+
helpUrl: getErrorHelp('E011'),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
seenAttrs.set(rawName, match.index);
|
|
80
144
|
let name;
|
|
81
145
|
if (rawName.startsWith(':')) {
|
|
82
146
|
name = rawName.slice(1);
|
|
@@ -102,6 +166,17 @@ function parseAttrs(attrsStr) {
|
|
|
102
166
|
}
|
|
103
167
|
return attrs;
|
|
104
168
|
}
|
|
169
|
+
function isVoidElement(tag) {
|
|
170
|
+
return voidElements.has(tag);
|
|
171
|
+
}
|
|
172
|
+
function getLineCol(text, offset) {
|
|
173
|
+
const lines = text.slice(0, offset).split('\n');
|
|
174
|
+
return {
|
|
175
|
+
line: lines.length,
|
|
176
|
+
column: (lines[lines.length - 1]?.length || 0) + 1,
|
|
177
|
+
offset,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
105
180
|
function decodeEntities(text) {
|
|
106
181
|
return text.replace(/&[a-zA-Z#]+;/g, match => {
|
|
107
182
|
const entities = {
|
|
@@ -120,6 +195,10 @@ function parseText(text) {
|
|
|
120
195
|
if (match.index > lastIdx) {
|
|
121
196
|
nodes.push({ type: 'text', content: decodeEntities(text.slice(lastIdx, match.index)) });
|
|
122
197
|
}
|
|
198
|
+
if (!match[1].trim()) {
|
|
199
|
+
lastIdx = mustacheRegex.lastIndex;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
123
202
|
nodes.push({ type: 'expression', content: match[1].trim() });
|
|
124
203
|
lastIdx = mustacheRegex.lastIndex;
|
|
125
204
|
}
|
|
@@ -128,10 +207,3 @@ function parseText(text) {
|
|
|
128
207
|
}
|
|
129
208
|
return nodes;
|
|
130
209
|
}
|
|
131
|
-
const voidElements = new Set([
|
|
132
|
-
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
|
|
133
|
-
'link', 'meta', 'param', 'source', 'track', 'wbr',
|
|
134
|
-
]);
|
|
135
|
-
function isVoidElement(tag) {
|
|
136
|
-
return voidElements.has(tag);
|
|
137
|
-
}
|
|
@@ -7,6 +7,13 @@ function transformNode(node, scopeId) {
|
|
|
7
7
|
case 'comment':
|
|
8
8
|
return { type: 'comment' };
|
|
9
9
|
case 'element': {
|
|
10
|
+
if (node.tag === 'slot') {
|
|
11
|
+
const dynamicAttrs = node.attrs.filter(a => a.dynamic && a.name !== 'name');
|
|
12
|
+
const slotProps = dynamicAttrs.length > 0
|
|
13
|
+
? dynamicAttrs.map(a => `${a.name}: ${a.value}`).join(', ')
|
|
14
|
+
: undefined;
|
|
15
|
+
return { type: 'slot', name: node.attrs.find(a => a.name === 'name')?.value || 'default', slotProps, children: node.children?.map(c => transformNode(c)) };
|
|
16
|
+
}
|
|
10
17
|
const vForAttr = node.attrs.find(a => a.name === 'v-for');
|
|
11
18
|
const vIfAttr = node.attrs.find(a => a.name === 'v-if');
|
|
12
19
|
const filteredAttrs = node.attrs.filter(a => a.name !== 'v-if' && a.name !== 'v-for');
|
|
@@ -118,10 +125,16 @@ function genProps(attrs, tag) {
|
|
|
118
125
|
continue;
|
|
119
126
|
if (attr.name === 'class')
|
|
120
127
|
continue;
|
|
128
|
+
if (tag === 'slot' && attr.name === 'name')
|
|
129
|
+
continue;
|
|
121
130
|
let propName = attr.name;
|
|
122
131
|
if (propName.startsWith('v-')) {
|
|
123
132
|
propName = propName.split('-').map((s, i) => i === 0 ? s : s.charAt(0).toUpperCase() + s.slice(1)).join('');
|
|
124
133
|
}
|
|
134
|
+
const isComponentTag = /^[A-Z]/.test(tag) || tag.includes('-');
|
|
135
|
+
if (isComponentTag && propName.includes('-')) {
|
|
136
|
+
propName = propName.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
137
|
+
}
|
|
125
138
|
if (propName === 'vShow' && typeof attr.value === 'string') {
|
|
126
139
|
parts.push(`${propName}: ${attr.value}`);
|
|
127
140
|
}
|
|
@@ -164,7 +177,8 @@ function genProps(attrs, tag) {
|
|
|
164
177
|
handler = `($event) => { $event.stopPropagation(); (${handler})($event) }`;
|
|
165
178
|
}
|
|
166
179
|
}
|
|
167
|
-
|
|
180
|
+
const safeKey = /[^a-zA-Z0-9_$]/.test(realName) ? JSON.stringify(realName) : realName;
|
|
181
|
+
parts.push(`${safeKey}: ${handler}`);
|
|
168
182
|
}
|
|
169
183
|
else if (attr.dynamic) {
|
|
170
184
|
const key = /[^a-zA-Z0-9_$]/.test(propName) ? JSON.stringify(propName) : propName;
|