@sprlab/wccompiler 0.13.0 → 0.15.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/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
+ }