@sprlab/wccompiler 0.5.13 → 0.6.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 +31 -0
- package/bin/wcc.js +45 -15
- package/lib/compiler.js +28 -4
- package/lib/config.js +12 -2
- package/lib/sfc-parser.js +30 -2
- package/lib/tree-walker.js +10 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -423,6 +423,37 @@ export default {
|
|
|
423
423
|
|
|
424
424
|
All options are optional — defaults shown above.
|
|
425
425
|
|
|
426
|
+
### Standalone Mode
|
|
427
|
+
|
|
428
|
+
Controls whether the reactive runtime is inlined in each component or imported from a shared module.
|
|
429
|
+
|
|
430
|
+
```js
|
|
431
|
+
// wcc.config.js
|
|
432
|
+
export default {
|
|
433
|
+
standalone: true // inline runtime in every component (default: false)
|
|
434
|
+
}
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
- `standalone: false` (default) — Components import the runtime from a shared `__wcc-signals.js` file. Smaller per-component size when using multiple components.
|
|
438
|
+
- `standalone: true` — Each component includes the full reactive runtime inline. Zero external dependencies per component.
|
|
439
|
+
|
|
440
|
+
#### Per-Component Override
|
|
441
|
+
|
|
442
|
+
Override the global setting for individual components:
|
|
443
|
+
|
|
444
|
+
```html
|
|
445
|
+
<script>
|
|
446
|
+
import { defineComponent, signal } from 'wcc'
|
|
447
|
+
|
|
448
|
+
export default defineComponent({
|
|
449
|
+
tag: 'wcc-widget',
|
|
450
|
+
standalone: true, // this component is self-contained regardless of global config
|
|
451
|
+
})
|
|
452
|
+
</script>
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
Component-level `standalone` always takes precedence over the global config. This lets you have a project with shared runtime but mark specific components as fully self-contained for distribution.
|
|
456
|
+
|
|
426
457
|
## Editor Support
|
|
427
458
|
|
|
428
459
|
The **wcCompiler (.wcc) Language Support** extension is available on the VS Code Marketplace. It provides syntax highlighting, completions, and diagnostics for `.wcc` files.
|
package/bin/wcc.js
CHANGED
|
@@ -7,6 +7,9 @@ import { loadConfig } from '../lib/config.js';
|
|
|
7
7
|
import { compile } from '../lib/compiler.js';
|
|
8
8
|
import { startDevServer } from '../lib/dev-server.js';
|
|
9
9
|
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = dirname(__filename);
|
|
12
|
+
|
|
10
13
|
const command = process.argv[2];
|
|
11
14
|
|
|
12
15
|
async function build(config, cwd) {
|
|
@@ -15,36 +18,44 @@ async function build(config, cwd) {
|
|
|
15
18
|
|
|
16
19
|
if (!existsSync(outputDir)) mkdirSync(outputDir, { recursive: true });
|
|
17
20
|
|
|
18
|
-
// Generate shared reactive runtime
|
|
19
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
20
|
-
const __dirname = dirname(__filename);
|
|
21
|
-
const { reactiveRuntime } = await import('../lib/reactive-runtime.js');
|
|
22
|
-
const signalsContent = reactiveRuntime.trim().replace(/^/gm, '') + '\nexport { __signal, __computed, __effect, __batch, __untrack };\n';
|
|
23
|
-
const signalsDest = join(outputDir, '__wcc-signals.js');
|
|
24
|
-
writeFileSync(signalsDest, signalsContent);
|
|
25
|
-
|
|
26
|
-
// Discover source files
|
|
27
21
|
const files = discoverFiles(inputDir);
|
|
28
22
|
let errors = 0;
|
|
23
|
+
let needsSharedRuntime = false;
|
|
29
24
|
|
|
30
25
|
for (const file of files) {
|
|
31
26
|
try {
|
|
32
|
-
// Calculate relative path from component output to __wcc-signals.js
|
|
33
27
|
const relPath = relative(inputDir, file);
|
|
34
28
|
const outPath = resolve(outputDir, relPath.replace(/\.wcc$/, '.js'));
|
|
35
29
|
const outDir = dirname(outPath);
|
|
30
|
+
|
|
31
|
+
// Calculate runtimeImportPath (always calculate it — the compiler decides whether to use it)
|
|
32
|
+
const signalsDest = join(outputDir, '__wcc-signals.js');
|
|
36
33
|
const runtimeRelPath = relative(outDir, signalsDest).replace(/\\/g, '/');
|
|
37
34
|
const runtimeImportPath = runtimeRelPath.startsWith('.') ? runtimeRelPath : './' + runtimeRelPath;
|
|
38
35
|
|
|
39
|
-
const
|
|
36
|
+
const { code, usesSharedRuntime } = await compile(file, {
|
|
37
|
+
standalone: config.standalone,
|
|
38
|
+
runtimeImportPath,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (usesSharedRuntime) needsSharedRuntime = true;
|
|
42
|
+
|
|
40
43
|
if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
|
|
41
|
-
writeFileSync(outPath,
|
|
44
|
+
writeFileSync(outPath, code);
|
|
42
45
|
} catch (err) {
|
|
43
46
|
console.error(`Error compiling ${file}: ${err.message}`);
|
|
44
47
|
errors++;
|
|
45
48
|
}
|
|
46
49
|
}
|
|
47
50
|
|
|
51
|
+
// Generate shared runtime ONLY if needed
|
|
52
|
+
if (needsSharedRuntime) {
|
|
53
|
+
const { reactiveRuntime } = await import('../lib/reactive-runtime.js');
|
|
54
|
+
const signalsContent = reactiveRuntime.trim() + '\nexport { __signal, __computed, __effect, __batch, __untrack };\n';
|
|
55
|
+
const signalsDest = join(outputDir, '__wcc-signals.js');
|
|
56
|
+
writeFileSync(signalsDest, signalsContent);
|
|
57
|
+
}
|
|
58
|
+
|
|
48
59
|
// Copy wcc-runtime.js to output directory
|
|
49
60
|
const runtimeSrc = resolve(__dirname, '../lib/wcc-runtime.js');
|
|
50
61
|
const runtimeDest = join(outputDir, 'wcc-runtime.js');
|
|
@@ -87,11 +98,30 @@ async function main() {
|
|
|
87
98
|
if (filename.includes('.test.')) return;
|
|
88
99
|
const filePath = resolve(inputDir, filename);
|
|
89
100
|
try {
|
|
90
|
-
const
|
|
91
|
-
const outPath = resolve(outputDir,
|
|
101
|
+
const relPath = filename;
|
|
102
|
+
const outPath = resolve(outputDir, relPath.replace(/\.ts$/, '.js').replace(/\.wcc$/, '.js'));
|
|
92
103
|
const outDir = dirname(outPath);
|
|
104
|
+
|
|
105
|
+
// Calculate runtimeImportPath for this file
|
|
106
|
+
const signalsDest = join(outputDir, '__wcc-signals.js');
|
|
107
|
+
const runtimeRelPath = relative(outDir, signalsDest).replace(/\\/g, '/');
|
|
108
|
+
const runtimeImportPath = runtimeRelPath.startsWith('.') ? runtimeRelPath : './' + runtimeRelPath;
|
|
109
|
+
|
|
110
|
+
const { code, usesSharedRuntime } = await compile(filePath, {
|
|
111
|
+
standalone: config.standalone,
|
|
112
|
+
runtimeImportPath,
|
|
113
|
+
});
|
|
114
|
+
|
|
93
115
|
if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
|
|
94
|
-
writeFileSync(outPath,
|
|
116
|
+
writeFileSync(outPath, code);
|
|
117
|
+
|
|
118
|
+
// If this component uses shared runtime and the file doesn't exist yet, generate it
|
|
119
|
+
if (usesSharedRuntime && !existsSync(signalsDest)) {
|
|
120
|
+
const { reactiveRuntime } = await import('../lib/reactive-runtime.js');
|
|
121
|
+
const signalsContent = reactiveRuntime.trim() + '\nexport { __signal, __computed, __effect, __batch, __untrack };\n';
|
|
122
|
+
writeFileSync(signalsDest, signalsContent);
|
|
123
|
+
}
|
|
124
|
+
|
|
95
125
|
console.log(`Compiled: ${filename}`);
|
|
96
126
|
} catch (err) {
|
|
97
127
|
console.error(`Error compiling ${filename}: ${err.message}`);
|
package/lib/compiler.js
CHANGED
|
@@ -355,9 +355,32 @@ async function compileSFC(filePath, config) {
|
|
|
355
355
|
parseResult.childImports = childImports;
|
|
356
356
|
parseResult.processedTemplate = rootEl.innerHTML;
|
|
357
357
|
|
|
358
|
-
// 20.
|
|
358
|
+
// 20. Resolve standalone and generate component
|
|
359
|
+
const standaloneResolved = resolveStandalone(descriptor.standalone, config?.standalone ?? false);
|
|
359
360
|
const genOptions = { ...config, sourceFile: fileName };
|
|
360
|
-
|
|
361
|
+
|
|
362
|
+
if (standaloneResolved) {
|
|
363
|
+
// Force inline runtime — ignore any runtimeImportPath
|
|
364
|
+
genOptions.runtimeImportPath = undefined;
|
|
365
|
+
}
|
|
366
|
+
// If standaloneResolved is false, keep config.runtimeImportPath as-is (CLI provides it)
|
|
367
|
+
|
|
368
|
+
const code = generateComponent(parseResult, genOptions);
|
|
369
|
+
const usesSharedRuntime = !standaloneResolved && !!genOptions.runtimeImportPath;
|
|
370
|
+
return { code, usesSharedRuntime };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Resolve the final standalone value.
|
|
375
|
+
* Component-level has priority over global.
|
|
376
|
+
*
|
|
377
|
+
* @param {boolean | undefined} componentValue — standalone from defineComponent (true, false, or undefined)
|
|
378
|
+
* @param {boolean} globalValue — standalone from config (true or false)
|
|
379
|
+
* @returns {boolean}
|
|
380
|
+
*/
|
|
381
|
+
export function resolveStandalone(componentValue, globalValue) {
|
|
382
|
+
if (componentValue === true || componentValue === false) return componentValue;
|
|
383
|
+
return globalValue;
|
|
361
384
|
}
|
|
362
385
|
|
|
363
386
|
/**
|
|
@@ -365,8 +388,9 @@ async function compileSFC(filePath, config) {
|
|
|
365
388
|
*
|
|
366
389
|
* @param {string} filePath — Absolute or relative path to the .wcc file
|
|
367
390
|
* @param {object} [config] — Optional config (reserved for future options)
|
|
368
|
-
* @returns {Promise<string>} The generated JavaScript component code
|
|
391
|
+
* @returns {Promise<{code: string, usesSharedRuntime: boolean}>} The generated JavaScript component code and metadata
|
|
369
392
|
*/
|
|
370
393
|
export async function compile(filePath, config) {
|
|
371
|
-
|
|
394
|
+
const result = await compileSFC(filePath, config);
|
|
395
|
+
return result;
|
|
372
396
|
}
|
package/lib/config.js
CHANGED
|
@@ -7,6 +7,7 @@ import { pathToFileURL } from 'node:url';
|
|
|
7
7
|
* @property {number} port — Dev server port (default: 4100)
|
|
8
8
|
* @property {string} input — Source directory (default: 'src')
|
|
9
9
|
* @property {string} output — Output directory (default: 'dist')
|
|
10
|
+
* @property {boolean} standalone — Inline runtime in each component (default: false)
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -18,7 +19,7 @@ import { pathToFileURL } from 'node:url';
|
|
|
18
19
|
* @returns {Promise<WccConfig>}
|
|
19
20
|
*/
|
|
20
21
|
export async function loadConfig(projectRoot) {
|
|
21
|
-
const defaults = { port: 4100, input: 'src', output: 'dist' };
|
|
22
|
+
const defaults = { port: 4100, input: 'src', output: 'dist', standalone: false };
|
|
22
23
|
const configPath = resolve(projectRoot, 'wcc.config.js');
|
|
23
24
|
|
|
24
25
|
if (!existsSync(configPath)) return defaults;
|
|
@@ -26,7 +27,11 @@ export async function loadConfig(projectRoot) {
|
|
|
26
27
|
const configUrl = pathToFileURL(configPath).href;
|
|
27
28
|
// Add cache-busting query to avoid ESM module cache issues
|
|
28
29
|
const mod = await import(`${configUrl}?t=${Date.now()}`);
|
|
29
|
-
|
|
30
|
+
// Unwrap ESM module namespace: handle double-nesting from dynamic import
|
|
31
|
+
let userConfig = mod.default || mod;
|
|
32
|
+
if (userConfig.__esModule && userConfig.default) {
|
|
33
|
+
userConfig = userConfig.default;
|
|
34
|
+
}
|
|
30
35
|
|
|
31
36
|
const config = { ...defaults, ...userConfig };
|
|
32
37
|
|
|
@@ -46,6 +51,11 @@ export async function loadConfig(projectRoot) {
|
|
|
46
51
|
error.code = 'INVALID_CONFIG';
|
|
47
52
|
throw error;
|
|
48
53
|
}
|
|
54
|
+
if (typeof config.standalone !== 'boolean') {
|
|
55
|
+
const error = new Error(`Error en wcc.config.js: standalone debe ser un booleano`);
|
|
56
|
+
error.code = 'INVALID_CONFIG';
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
49
59
|
|
|
50
60
|
return config;
|
|
51
61
|
}
|
package/lib/sfc-parser.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* @property {string} style — Content of the <style> block ('' if absent)
|
|
16
16
|
* @property {string} lang — 'ts' | 'js'
|
|
17
17
|
* @property {string} tag — Tag name extracted from defineComponent({ tag })
|
|
18
|
+
* @property {boolean | undefined} standalone — Standalone option from defineComponent
|
|
18
19
|
*/
|
|
19
20
|
|
|
20
21
|
// ── Helpers ─────────────────────────────────────────────────────────
|
|
@@ -129,6 +130,29 @@ function extractLang(attrs) {
|
|
|
129
130
|
|
|
130
131
|
// ── Phase 2: Validation ─────────────────────────────────────────────
|
|
131
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
|
+
|
|
132
156
|
/**
|
|
133
157
|
* Extract the tag name from a defineComponent({ tag: '...' }) call.
|
|
134
158
|
*
|
|
@@ -169,7 +193,7 @@ function extractTagFromDefineComponent(script, fileName) {
|
|
|
169
193
|
);
|
|
170
194
|
}
|
|
171
195
|
|
|
172
|
-
return tagMatch[1];
|
|
196
|
+
return { tag: tagMatch[1], body };
|
|
173
197
|
}
|
|
174
198
|
|
|
175
199
|
/**
|
|
@@ -275,7 +299,10 @@ export function parseSFC(source, fileName = '<unknown>') {
|
|
|
275
299
|
const lang = extractLang(scriptBlocks[0].attrs);
|
|
276
300
|
|
|
277
301
|
// Validate defineComponent and extract tag
|
|
278
|
-
const tag = extractTagFromDefineComponent(scriptContent, fileName);
|
|
302
|
+
const { tag, body } = extractTagFromDefineComponent(scriptContent, fileName);
|
|
303
|
+
|
|
304
|
+
// Extract standalone option from defineComponent body
|
|
305
|
+
const standalone = extractStandaloneOption(body, fileName);
|
|
279
306
|
|
|
280
307
|
return {
|
|
281
308
|
script: scriptContent,
|
|
@@ -283,6 +310,7 @@ export function parseSFC(source, fileName = '<unknown>') {
|
|
|
283
310
|
style: styleContent,
|
|
284
311
|
lang,
|
|
285
312
|
tag,
|
|
313
|
+
standalone,
|
|
286
314
|
};
|
|
287
315
|
}
|
|
288
316
|
|
package/lib/tree-walker.js
CHANGED
|
@@ -369,17 +369,19 @@ export function walkBranch(html, signalNames, computedNames, propNames) {
|
|
|
369
369
|
const { document } = parseHTML(`<div id="__branchRoot">${html}</div>`);
|
|
370
370
|
const branchRoot = document.getElementById('__branchRoot');
|
|
371
371
|
|
|
372
|
-
//
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
//
|
|
372
|
+
// Process nested structural directives FIRST (before walkTree modifies the DOM).
|
|
373
|
+
// This is critical because walkTree clears textContent of elements with sole
|
|
374
|
+
// {{interpolation}} children, which would destroy content needed by
|
|
375
|
+
// processForBlocks/processIfChains when they clone nested elements for their
|
|
376
|
+
// own walkBranch calls.
|
|
376
377
|
const forBlocks = processForBlocks(branchRoot, [], signalNames, computedNames, propNames);
|
|
377
|
-
|
|
378
|
-
// Detect nested if/else-if/else chains within the branch template
|
|
379
378
|
const ifBlocks = processIfChains(branchRoot, [], signalNames, computedNames, propNames);
|
|
380
379
|
|
|
381
|
-
//
|
|
382
|
-
//
|
|
380
|
+
// Now run walkTree on the remaining DOM (nested directive elements have been
|
|
381
|
+
// replaced with comment nodes, so walkTree won't process their contents).
|
|
382
|
+
const result = walkTree(branchRoot, signalNames, computedNames, propNames);
|
|
383
|
+
|
|
384
|
+
// Capture the processed HTML AFTER all processing
|
|
383
385
|
const processedHtml = branchRoot.innerHTML;
|
|
384
386
|
|
|
385
387
|
// Strip the first path segment from all paths since at runtime
|
package/package.json
CHANGED