@sprlab/wccompiler 0.12.1 → 0.14.0
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/README.md +998 -998
- package/adapters/angular-compiled/angular.d.ts +197 -197
- package/adapters/angular-compiled/angular.mjs +488 -488
- package/adapters/angular.js +54 -54
- package/adapters/angular.ts +630 -630
- package/adapters/react.js +114 -114
- package/adapters/vue.js +103 -103
- package/bin/wcc.js +412 -412
- package/bin/wcc.test.js +126 -126
- package/integrations/angular.js +73 -73
- package/integrations/react.js +859 -859
- package/integrations/vue.js +253 -253
- package/lib/codegen.js +2074 -2029
- package/lib/compiler-browser.js +545 -545
- package/lib/compiler.js +483 -473
- package/lib/config.js +71 -71
- package/lib/css-scoper.js +180 -180
- package/lib/dev-server.js +193 -193
- package/lib/import-resolver.js +160 -160
- package/lib/parser-extractors.js +1240 -1169
- package/lib/parser.js +273 -269
- package/lib/reactive-runtime.js +143 -143
- package/lib/sfc-parser.js +333 -333
- package/lib/template-normalizer.js +114 -109
- package/lib/tree-walker.js +1013 -923
- package/lib/types.js +262 -240
- package/lib/wcc-runtime.js +68 -68
- package/package.json +85 -85
- package/types/wcc.d.ts +28 -28
- package/types/wcc.test.js +46 -46
package/lib/sfc-parser.js
CHANGED
|
@@ -1,333 +1,333 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SFC Parser — extracts <script>, <template> and <style> blocks from .wcc files.
|
|
3
|
-
*
|
|
4
|
-
* Pure ESM module with no Node.js dependencies (usable in browser and server).
|
|
5
|
-
*
|
|
6
|
-
* Two-phase algorithm:
|
|
7
|
-
* Phase 1: Block extraction via regex
|
|
8
|
-
* Phase 2: Validation (required blocks, duplicates, unexpected content, defineComponent)
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* @typedef {Object} SFCDescriptor
|
|
13
|
-
* @property {string} script — Content of the <script> block
|
|
14
|
-
* @property {string} template — Content of the <template> block
|
|
15
|
-
* @property {string} style — Content of the <style> block ('' if absent)
|
|
16
|
-
* @property {string} lang — 'ts' | 'js'
|
|
17
|
-
* @property {string} tag — Tag name extracted from defineComponent({ tag })
|
|
18
|
-
* @property {boolean | undefined} standalone — Standalone option from defineComponent
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
// ── Helpers ─────────────────────────────────────────────────────────
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Create an Error with a `.code` property (project convention).
|
|
25
|
-
* @param {string} code
|
|
26
|
-
* @param {string} message
|
|
27
|
-
* @returns {Error}
|
|
28
|
-
*/
|
|
29
|
-
function sfcError(code, message) {
|
|
30
|
-
const error = new Error(message);
|
|
31
|
-
/** @ts-expect-error — custom error code for programmatic handling */
|
|
32
|
-
error.code = code;
|
|
33
|
-
return error;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// ── Phase 1: Block extraction ───────────────────────────────────────
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* @typedef {Object} BlockMatch
|
|
40
|
-
* @property {string} content — Inner content between open and close tags
|
|
41
|
-
* @property {string} attrs — Attributes string from the opening tag
|
|
42
|
-
* @property {number} start — Start index of the opening tag in source
|
|
43
|
-
* @property {number} end — End index (after closing tag) in source
|
|
44
|
-
*/
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Find all occurrences of a given block type in the source.
|
|
48
|
-
*
|
|
49
|
-
* @param {string} source
|
|
50
|
-
* @param {string} blockName — 'script' | 'template' | 'style'
|
|
51
|
-
* @returns {BlockMatch[]}
|
|
52
|
-
*/
|
|
53
|
-
function findBlocks(source, blockName) {
|
|
54
|
-
const openRe = new RegExp(`<${blockName}(\\s[^>]*)?>`, 'g');
|
|
55
|
-
const closeTag = `</${blockName}>`;
|
|
56
|
-
/** @type {BlockMatch[]} */
|
|
57
|
-
const matches = [];
|
|
58
|
-
let m;
|
|
59
|
-
|
|
60
|
-
while ((m = openRe.exec(source)) !== null) {
|
|
61
|
-
const attrs = m[1] || '';
|
|
62
|
-
|
|
63
|
-
// For template blocks: skip <template #name> (slot content, not SFC block)
|
|
64
|
-
if (blockName === 'template' && /#/.test(attrs)) {
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const openEnd = m.index + m[0].length;
|
|
69
|
-
|
|
70
|
-
// Use depth counting to find the matching close tag (handles nested <template #name>)
|
|
71
|
-
if (blockName === 'template') {
|
|
72
|
-
let depth = 1;
|
|
73
|
-
let searchPos = openEnd;
|
|
74
|
-
let closeIdx = -1;
|
|
75
|
-
const openTagRe = /<template[\s>]/g;
|
|
76
|
-
const closeTagStr = '</template>';
|
|
77
|
-
|
|
78
|
-
while (depth > 0 && searchPos < source.length) {
|
|
79
|
-
const nextClose = source.indexOf(closeTagStr, searchPos);
|
|
80
|
-
if (nextClose === -1) break;
|
|
81
|
-
|
|
82
|
-
// Check for any opening <template> between searchPos and nextClose
|
|
83
|
-
openTagRe.lastIndex = searchPos;
|
|
84
|
-
let openMatch;
|
|
85
|
-
while ((openMatch = openTagRe.exec(source)) !== null && openMatch.index < nextClose) {
|
|
86
|
-
depth++;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
depth--; // for the </template> we found
|
|
90
|
-
if (depth === 0) {
|
|
91
|
-
closeIdx = nextClose;
|
|
92
|
-
break;
|
|
93
|
-
}
|
|
94
|
-
searchPos = nextClose + closeTagStr.length;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (closeIdx === -1) continue;
|
|
98
|
-
matches.push({
|
|
99
|
-
content: source.slice(openEnd, closeIdx),
|
|
100
|
-
attrs,
|
|
101
|
-
start: m.index,
|
|
102
|
-
end: closeIdx + closeTag.length,
|
|
103
|
-
});
|
|
104
|
-
} else {
|
|
105
|
-
const closeIdx = source.indexOf(closeTag, openEnd);
|
|
106
|
-
if (closeIdx === -1) continue;
|
|
107
|
-
matches.push({
|
|
108
|
-
content: source.slice(openEnd, closeIdx),
|
|
109
|
-
attrs,
|
|
110
|
-
start: m.index,
|
|
111
|
-
end: closeIdx + closeTag.length,
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return matches;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Extract the `lang` attribute value from an attributes string.
|
|
121
|
-
* Returns 'ts' if lang="ts", otherwise 'js'.
|
|
122
|
-
*
|
|
123
|
-
* @param {string} attrs
|
|
124
|
-
* @returns {'ts' | 'js'}
|
|
125
|
-
*/
|
|
126
|
-
function extractLang(attrs) {
|
|
127
|
-
const langMatch = attrs.match(/lang\s*=\s*["']([^"']+)["']/);
|
|
128
|
-
return langMatch && langMatch[1] === 'ts' ? 'ts' : 'js';
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// ── Phase 2: Validation ─────────────────────────────────────────────
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Extract the `standalone` option from the body of defineComponent().
|
|
135
|
-
*
|
|
136
|
-
* @param {string} body — The inner content of defineComponent({ ... })
|
|
137
|
-
* @param {string} fileName
|
|
138
|
-
* @returns {boolean | undefined}
|
|
139
|
-
*/
|
|
140
|
-
function extractStandaloneOption(body, fileName) {
|
|
141
|
-
const standaloneMatch = body.match(/standalone\s*:\s*(true|false|[^\s,}]+)/);
|
|
142
|
-
if (!standaloneMatch) {
|
|
143
|
-
return undefined;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const value = standaloneMatch[1];
|
|
147
|
-
if (value === 'true') return true;
|
|
148
|
-
if (value === 'false') return false;
|
|
149
|
-
|
|
150
|
-
throw sfcError(
|
|
151
|
-
'INVALID_STANDALONE_OPTION',
|
|
152
|
-
`Error en '${fileName}': standalone debe ser true o false`
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Extract the tag name from a defineComponent({ tag: '...' }) call.
|
|
158
|
-
*
|
|
159
|
-
* @param {string} script
|
|
160
|
-
* @param {string} fileName
|
|
161
|
-
* @returns {string}
|
|
162
|
-
*/
|
|
163
|
-
function extractTagFromDefineComponent(script, fileName) {
|
|
164
|
-
const dcMatch = script.match(/defineComponent\(\s*\{([^}]*)\}\s*\)/);
|
|
165
|
-
if (!dcMatch) {
|
|
166
|
-
throw sfcError(
|
|
167
|
-
'MISSING_DEFINE_COMPONENT',
|
|
168
|
-
`Error en '${fileName}': defineComponent() es obligatorio`
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const body = dcMatch[1];
|
|
173
|
-
|
|
174
|
-
// Reject template/styles fields inside defineComponent in SFC mode
|
|
175
|
-
if (/\btemplate\s*:/.test(body)) {
|
|
176
|
-
throw sfcError(
|
|
177
|
-
'SFC_INLINE_PATHS_FORBIDDEN',
|
|
178
|
-
`SFC file '${fileName}': template/styles paths are not allowed in SFC mode (content is inline)`
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
|
-
if (/\bstyles\s*:/.test(body)) {
|
|
182
|
-
throw sfcError(
|
|
183
|
-
'SFC_INLINE_PATHS_FORBIDDEN',
|
|
184
|
-
`SFC file '${fileName}': template/styles paths are not allowed in SFC mode (content is inline)`
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const tagMatch = body.match(/tag\s*:\s*['"]([^'"]+)['"]/);
|
|
189
|
-
if (!tagMatch) {
|
|
190
|
-
throw sfcError(
|
|
191
|
-
'MISSING_DEFINE_COMPONENT',
|
|
192
|
-
`Error en '${fileName}': defineComponent() must include a tag field`
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
return { tag: tagMatch[1], body };
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Check that no non-whitespace content exists outside the recognized blocks.
|
|
201
|
-
*
|
|
202
|
-
* @param {string} source
|
|
203
|
-
* @param {Array<{start: number, end: number}>} blockRanges — sorted by start
|
|
204
|
-
* @param {string} fileName
|
|
205
|
-
*/
|
|
206
|
-
function validateNoUnexpectedContent(source, blockRanges, fileName) {
|
|
207
|
-
let cursor = 0;
|
|
208
|
-
|
|
209
|
-
for (const range of blockRanges) {
|
|
210
|
-
const outside = source.slice(cursor, range.start);
|
|
211
|
-
if (outside.trim().length > 0) {
|
|
212
|
-
throw sfcError(
|
|
213
|
-
'SFC_UNEXPECTED_CONTENT',
|
|
214
|
-
`SFC file '${fileName}' contains unexpected content outside blocks`
|
|
215
|
-
);
|
|
216
|
-
}
|
|
217
|
-
cursor = range.end;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Check trailing content after last block
|
|
221
|
-
const trailing = source.slice(cursor);
|
|
222
|
-
if (trailing.trim().length > 0) {
|
|
223
|
-
throw sfcError(
|
|
224
|
-
'SFC_UNEXPECTED_CONTENT',
|
|
225
|
-
`SFC file '${fileName}' contains unexpected content outside blocks`
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// ── Public API ──────────────────────────────────────────────────────
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* Parse an SFC source string and extract its blocks.
|
|
234
|
-
*
|
|
235
|
-
* @param {string} source — Full content of the .wcc file
|
|
236
|
-
* @param {string} [fileName='<unknown>'] — File name for error messages
|
|
237
|
-
* @returns {SFCDescriptor}
|
|
238
|
-
* @throws {Error} with codes: SFC_MISSING_TEMPLATE, SFC_MISSING_SCRIPT,
|
|
239
|
-
* SFC_DUPLICATE_BLOCK, SFC_UNEXPECTED_CONTENT,
|
|
240
|
-
* SFC_INLINE_PATHS_FORBIDDEN, MISSING_DEFINE_COMPONENT
|
|
241
|
-
*/
|
|
242
|
-
export function parseSFC(source, fileName = '<unknown>') {
|
|
243
|
-
// ── Phase 1: Extract blocks ─────────────────────────────────────
|
|
244
|
-
|
|
245
|
-
const scriptBlocks = findBlocks(source, 'script');
|
|
246
|
-
const templateBlocks = findBlocks(source, 'template');
|
|
247
|
-
const styleBlocks = findBlocks(source, 'style');
|
|
248
|
-
|
|
249
|
-
// Check for duplicates
|
|
250
|
-
if (scriptBlocks.length > 1) {
|
|
251
|
-
throw sfcError(
|
|
252
|
-
'SFC_DUPLICATE_BLOCK',
|
|
253
|
-
`SFC file '${fileName}' contains duplicate <script> blocks`
|
|
254
|
-
);
|
|
255
|
-
}
|
|
256
|
-
if (templateBlocks.length > 1) {
|
|
257
|
-
throw sfcError(
|
|
258
|
-
'SFC_DUPLICATE_BLOCK',
|
|
259
|
-
`SFC file '${fileName}' contains duplicate <template> blocks`
|
|
260
|
-
);
|
|
261
|
-
}
|
|
262
|
-
if (styleBlocks.length > 1) {
|
|
263
|
-
throw sfcError(
|
|
264
|
-
'SFC_DUPLICATE_BLOCK',
|
|
265
|
-
`SFC file '${fileName}' contains duplicate <style> blocks`
|
|
266
|
-
);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// ── Phase 2: Validation ─────────────────────────────────────────
|
|
270
|
-
|
|
271
|
-
// Required blocks
|
|
272
|
-
if (templateBlocks.length === 0) {
|
|
273
|
-
throw sfcError(
|
|
274
|
-
'SFC_MISSING_TEMPLATE',
|
|
275
|
-
`SFC file '${fileName}' is missing a <template> block`
|
|
276
|
-
);
|
|
277
|
-
}
|
|
278
|
-
if (scriptBlocks.length === 0) {
|
|
279
|
-
throw sfcError(
|
|
280
|
-
'SFC_MISSING_SCRIPT',
|
|
281
|
-
`SFC file '${fileName}' is missing a <script> block`
|
|
282
|
-
);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Collect all block ranges for unexpected-content check
|
|
286
|
-
/** @type {Array<{start: number, end: number}>} */
|
|
287
|
-
const allRanges = [
|
|
288
|
-
...scriptBlocks,
|
|
289
|
-
...templateBlocks,
|
|
290
|
-
...styleBlocks,
|
|
291
|
-
].sort((a, b) => a.start - b.start);
|
|
292
|
-
|
|
293
|
-
validateNoUnexpectedContent(source, allRanges, fileName);
|
|
294
|
-
|
|
295
|
-
// Extract block contents
|
|
296
|
-
const scriptContent = scriptBlocks[0].content;
|
|
297
|
-
const templateContent = templateBlocks[0].content;
|
|
298
|
-
const styleContent = styleBlocks.length > 0 ? styleBlocks[0].content : '';
|
|
299
|
-
const lang = extractLang(scriptBlocks[0].attrs);
|
|
300
|
-
|
|
301
|
-
// Validate defineComponent and extract tag
|
|
302
|
-
const { tag, body } = extractTagFromDefineComponent(scriptContent, fileName);
|
|
303
|
-
|
|
304
|
-
// Extract standalone option from defineComponent body
|
|
305
|
-
const standalone = extractStandaloneOption(body, fileName);
|
|
306
|
-
|
|
307
|
-
return {
|
|
308
|
-
script: scriptContent,
|
|
309
|
-
template: templateContent,
|
|
310
|
-
style: styleContent,
|
|
311
|
-
lang,
|
|
312
|
-
tag,
|
|
313
|
-
standalone,
|
|
314
|
-
};
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* Pretty-printer: reconstruct an SFC string from a descriptor.
|
|
319
|
-
*
|
|
320
|
-
* @param {SFCDescriptor} descriptor
|
|
321
|
-
* @returns {string}
|
|
322
|
-
*/
|
|
323
|
-
export function printSFC(descriptor) {
|
|
324
|
-
const langAttr = descriptor.lang === 'ts' ? ' lang="ts"' : '';
|
|
325
|
-
let result = `<script${langAttr}>${descriptor.script}</script>\n\n`;
|
|
326
|
-
result += `<template>${descriptor.template}</template>`;
|
|
327
|
-
|
|
328
|
-
if (descriptor.style && descriptor.style.length > 0) {
|
|
329
|
-
result += `\n\n<style>${descriptor.style}</style>`;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
return result;
|
|
333
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* SFC Parser — extracts <script>, <template> and <style> blocks from .wcc files.
|
|
3
|
+
*
|
|
4
|
+
* Pure ESM module with no Node.js dependencies (usable in browser and server).
|
|
5
|
+
*
|
|
6
|
+
* Two-phase algorithm:
|
|
7
|
+
* Phase 1: Block extraction via regex
|
|
8
|
+
* Phase 2: Validation (required blocks, duplicates, unexpected content, defineComponent)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {Object} SFCDescriptor
|
|
13
|
+
* @property {string} script — Content of the <script> block
|
|
14
|
+
* @property {string} template — Content of the <template> block
|
|
15
|
+
* @property {string} style — Content of the <style> block ('' if absent)
|
|
16
|
+
* @property {string} lang — 'ts' | 'js'
|
|
17
|
+
* @property {string} tag — Tag name extracted from defineComponent({ tag })
|
|
18
|
+
* @property {boolean | undefined} standalone — Standalone option from defineComponent
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
// ── Helpers ─────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Create an Error with a `.code` property (project convention).
|
|
25
|
+
* @param {string} code
|
|
26
|
+
* @param {string} message
|
|
27
|
+
* @returns {Error}
|
|
28
|
+
*/
|
|
29
|
+
function sfcError(code, message) {
|
|
30
|
+
const error = new Error(message);
|
|
31
|
+
/** @ts-expect-error — custom error code for programmatic handling */
|
|
32
|
+
error.code = code;
|
|
33
|
+
return error;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ── Phase 1: Block extraction ───────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @typedef {Object} BlockMatch
|
|
40
|
+
* @property {string} content — Inner content between open and close tags
|
|
41
|
+
* @property {string} attrs — Attributes string from the opening tag
|
|
42
|
+
* @property {number} start — Start index of the opening tag in source
|
|
43
|
+
* @property {number} end — End index (after closing tag) in source
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Find all occurrences of a given block type in the source.
|
|
48
|
+
*
|
|
49
|
+
* @param {string} source
|
|
50
|
+
* @param {string} blockName — 'script' | 'template' | 'style'
|
|
51
|
+
* @returns {BlockMatch[]}
|
|
52
|
+
*/
|
|
53
|
+
function findBlocks(source, blockName) {
|
|
54
|
+
const openRe = new RegExp(`<${blockName}(\\s[^>]*)?>`, 'g');
|
|
55
|
+
const closeTag = `</${blockName}>`;
|
|
56
|
+
/** @type {BlockMatch[]} */
|
|
57
|
+
const matches = [];
|
|
58
|
+
let m;
|
|
59
|
+
|
|
60
|
+
while ((m = openRe.exec(source)) !== null) {
|
|
61
|
+
const attrs = m[1] || '';
|
|
62
|
+
|
|
63
|
+
// For template blocks: skip <template #name> (slot content, not SFC block)
|
|
64
|
+
if (blockName === 'template' && /#/.test(attrs)) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const openEnd = m.index + m[0].length;
|
|
69
|
+
|
|
70
|
+
// Use depth counting to find the matching close tag (handles nested <template #name>)
|
|
71
|
+
if (blockName === 'template') {
|
|
72
|
+
let depth = 1;
|
|
73
|
+
let searchPos = openEnd;
|
|
74
|
+
let closeIdx = -1;
|
|
75
|
+
const openTagRe = /<template[\s>]/g;
|
|
76
|
+
const closeTagStr = '</template>';
|
|
77
|
+
|
|
78
|
+
while (depth > 0 && searchPos < source.length) {
|
|
79
|
+
const nextClose = source.indexOf(closeTagStr, searchPos);
|
|
80
|
+
if (nextClose === -1) break;
|
|
81
|
+
|
|
82
|
+
// Check for any opening <template> between searchPos and nextClose
|
|
83
|
+
openTagRe.lastIndex = searchPos;
|
|
84
|
+
let openMatch;
|
|
85
|
+
while ((openMatch = openTagRe.exec(source)) !== null && openMatch.index < nextClose) {
|
|
86
|
+
depth++;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
depth--; // for the </template> we found
|
|
90
|
+
if (depth === 0) {
|
|
91
|
+
closeIdx = nextClose;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
searchPos = nextClose + closeTagStr.length;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (closeIdx === -1) continue;
|
|
98
|
+
matches.push({
|
|
99
|
+
content: source.slice(openEnd, closeIdx),
|
|
100
|
+
attrs,
|
|
101
|
+
start: m.index,
|
|
102
|
+
end: closeIdx + closeTag.length,
|
|
103
|
+
});
|
|
104
|
+
} else {
|
|
105
|
+
const closeIdx = source.indexOf(closeTag, openEnd);
|
|
106
|
+
if (closeIdx === -1) continue;
|
|
107
|
+
matches.push({
|
|
108
|
+
content: source.slice(openEnd, closeIdx),
|
|
109
|
+
attrs,
|
|
110
|
+
start: m.index,
|
|
111
|
+
end: closeIdx + closeTag.length,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return matches;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Extract the `lang` attribute value from an attributes string.
|
|
121
|
+
* Returns 'ts' if lang="ts", otherwise 'js'.
|
|
122
|
+
*
|
|
123
|
+
* @param {string} attrs
|
|
124
|
+
* @returns {'ts' | 'js'}
|
|
125
|
+
*/
|
|
126
|
+
function extractLang(attrs) {
|
|
127
|
+
const langMatch = attrs.match(/lang\s*=\s*["']([^"']+)["']/);
|
|
128
|
+
return langMatch && langMatch[1] === 'ts' ? 'ts' : 'js';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── Phase 2: Validation ─────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Extract the `standalone` option from the body of defineComponent().
|
|
135
|
+
*
|
|
136
|
+
* @param {string} body — The inner content of defineComponent({ ... })
|
|
137
|
+
* @param {string} fileName
|
|
138
|
+
* @returns {boolean | undefined}
|
|
139
|
+
*/
|
|
140
|
+
function extractStandaloneOption(body, fileName) {
|
|
141
|
+
const standaloneMatch = body.match(/standalone\s*:\s*(true|false|[^\s,}]+)/);
|
|
142
|
+
if (!standaloneMatch) {
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const value = standaloneMatch[1];
|
|
147
|
+
if (value === 'true') return true;
|
|
148
|
+
if (value === 'false') return false;
|
|
149
|
+
|
|
150
|
+
throw sfcError(
|
|
151
|
+
'INVALID_STANDALONE_OPTION',
|
|
152
|
+
`Error en '${fileName}': standalone debe ser true o false`
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Extract the tag name from a defineComponent({ tag: '...' }) call.
|
|
158
|
+
*
|
|
159
|
+
* @param {string} script
|
|
160
|
+
* @param {string} fileName
|
|
161
|
+
* @returns {string}
|
|
162
|
+
*/
|
|
163
|
+
function extractTagFromDefineComponent(script, fileName) {
|
|
164
|
+
const dcMatch = script.match(/defineComponent\(\s*\{([^}]*)\}\s*\)/);
|
|
165
|
+
if (!dcMatch) {
|
|
166
|
+
throw sfcError(
|
|
167
|
+
'MISSING_DEFINE_COMPONENT',
|
|
168
|
+
`Error en '${fileName}': defineComponent() es obligatorio`
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const body = dcMatch[1];
|
|
173
|
+
|
|
174
|
+
// Reject template/styles fields inside defineComponent in SFC mode
|
|
175
|
+
if (/\btemplate\s*:/.test(body)) {
|
|
176
|
+
throw sfcError(
|
|
177
|
+
'SFC_INLINE_PATHS_FORBIDDEN',
|
|
178
|
+
`SFC file '${fileName}': template/styles paths are not allowed in SFC mode (content is inline)`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
if (/\bstyles\s*:/.test(body)) {
|
|
182
|
+
throw sfcError(
|
|
183
|
+
'SFC_INLINE_PATHS_FORBIDDEN',
|
|
184
|
+
`SFC file '${fileName}': template/styles paths are not allowed in SFC mode (content is inline)`
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const tagMatch = body.match(/tag\s*:\s*['"]([^'"]+)['"]/);
|
|
189
|
+
if (!tagMatch) {
|
|
190
|
+
throw sfcError(
|
|
191
|
+
'MISSING_DEFINE_COMPONENT',
|
|
192
|
+
`Error en '${fileName}': defineComponent() must include a tag field`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return { tag: tagMatch[1], body };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Check that no non-whitespace content exists outside the recognized blocks.
|
|
201
|
+
*
|
|
202
|
+
* @param {string} source
|
|
203
|
+
* @param {Array<{start: number, end: number}>} blockRanges — sorted by start
|
|
204
|
+
* @param {string} fileName
|
|
205
|
+
*/
|
|
206
|
+
function validateNoUnexpectedContent(source, blockRanges, fileName) {
|
|
207
|
+
let cursor = 0;
|
|
208
|
+
|
|
209
|
+
for (const range of blockRanges) {
|
|
210
|
+
const outside = source.slice(cursor, range.start);
|
|
211
|
+
if (outside.trim().length > 0) {
|
|
212
|
+
throw sfcError(
|
|
213
|
+
'SFC_UNEXPECTED_CONTENT',
|
|
214
|
+
`SFC file '${fileName}' contains unexpected content outside blocks`
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
cursor = range.end;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Check trailing content after last block
|
|
221
|
+
const trailing = source.slice(cursor);
|
|
222
|
+
if (trailing.trim().length > 0) {
|
|
223
|
+
throw sfcError(
|
|
224
|
+
'SFC_UNEXPECTED_CONTENT',
|
|
225
|
+
`SFC file '${fileName}' contains unexpected content outside blocks`
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ── Public API ──────────────────────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Parse an SFC source string and extract its blocks.
|
|
234
|
+
*
|
|
235
|
+
* @param {string} source — Full content of the .wcc file
|
|
236
|
+
* @param {string} [fileName='<unknown>'] — File name for error messages
|
|
237
|
+
* @returns {SFCDescriptor}
|
|
238
|
+
* @throws {Error} with codes: SFC_MISSING_TEMPLATE, SFC_MISSING_SCRIPT,
|
|
239
|
+
* SFC_DUPLICATE_BLOCK, SFC_UNEXPECTED_CONTENT,
|
|
240
|
+
* SFC_INLINE_PATHS_FORBIDDEN, MISSING_DEFINE_COMPONENT
|
|
241
|
+
*/
|
|
242
|
+
export function parseSFC(source, fileName = '<unknown>') {
|
|
243
|
+
// ── Phase 1: Extract blocks ─────────────────────────────────────
|
|
244
|
+
|
|
245
|
+
const scriptBlocks = findBlocks(source, 'script');
|
|
246
|
+
const templateBlocks = findBlocks(source, 'template');
|
|
247
|
+
const styleBlocks = findBlocks(source, 'style');
|
|
248
|
+
|
|
249
|
+
// Check for duplicates
|
|
250
|
+
if (scriptBlocks.length > 1) {
|
|
251
|
+
throw sfcError(
|
|
252
|
+
'SFC_DUPLICATE_BLOCK',
|
|
253
|
+
`SFC file '${fileName}' contains duplicate <script> blocks`
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
if (templateBlocks.length > 1) {
|
|
257
|
+
throw sfcError(
|
|
258
|
+
'SFC_DUPLICATE_BLOCK',
|
|
259
|
+
`SFC file '${fileName}' contains duplicate <template> blocks`
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
if (styleBlocks.length > 1) {
|
|
263
|
+
throw sfcError(
|
|
264
|
+
'SFC_DUPLICATE_BLOCK',
|
|
265
|
+
`SFC file '${fileName}' contains duplicate <style> blocks`
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ── Phase 2: Validation ─────────────────────────────────────────
|
|
270
|
+
|
|
271
|
+
// Required blocks
|
|
272
|
+
if (templateBlocks.length === 0) {
|
|
273
|
+
throw sfcError(
|
|
274
|
+
'SFC_MISSING_TEMPLATE',
|
|
275
|
+
`SFC file '${fileName}' is missing a <template> block`
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
if (scriptBlocks.length === 0) {
|
|
279
|
+
throw sfcError(
|
|
280
|
+
'SFC_MISSING_SCRIPT',
|
|
281
|
+
`SFC file '${fileName}' is missing a <script> block`
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Collect all block ranges for unexpected-content check
|
|
286
|
+
/** @type {Array<{start: number, end: number}>} */
|
|
287
|
+
const allRanges = [
|
|
288
|
+
...scriptBlocks,
|
|
289
|
+
...templateBlocks,
|
|
290
|
+
...styleBlocks,
|
|
291
|
+
].sort((a, b) => a.start - b.start);
|
|
292
|
+
|
|
293
|
+
validateNoUnexpectedContent(source, allRanges, fileName);
|
|
294
|
+
|
|
295
|
+
// Extract block contents
|
|
296
|
+
const scriptContent = scriptBlocks[0].content;
|
|
297
|
+
const templateContent = templateBlocks[0].content;
|
|
298
|
+
const styleContent = styleBlocks.length > 0 ? styleBlocks[0].content : '';
|
|
299
|
+
const lang = extractLang(scriptBlocks[0].attrs);
|
|
300
|
+
|
|
301
|
+
// Validate defineComponent and extract tag
|
|
302
|
+
const { tag, body } = extractTagFromDefineComponent(scriptContent, fileName);
|
|
303
|
+
|
|
304
|
+
// Extract standalone option from defineComponent body
|
|
305
|
+
const standalone = extractStandaloneOption(body, fileName);
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
script: scriptContent,
|
|
309
|
+
template: templateContent,
|
|
310
|
+
style: styleContent,
|
|
311
|
+
lang,
|
|
312
|
+
tag,
|
|
313
|
+
standalone,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Pretty-printer: reconstruct an SFC string from a descriptor.
|
|
319
|
+
*
|
|
320
|
+
* @param {SFCDescriptor} descriptor
|
|
321
|
+
* @returns {string}
|
|
322
|
+
*/
|
|
323
|
+
export function printSFC(descriptor) {
|
|
324
|
+
const langAttr = descriptor.lang === 'ts' ? ' lang="ts"' : '';
|
|
325
|
+
let result = `<script${langAttr}>${descriptor.script}</script>\n\n`;
|
|
326
|
+
result += `<template>${descriptor.template}</template>`;
|
|
327
|
+
|
|
328
|
+
if (descriptor.style && descriptor.style.length > 0) {
|
|
329
|
+
result += `\n\n<style>${descriptor.style}</style>`;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return result;
|
|
333
|
+
}
|