@sprlab/wccompiler 0.11.5 → 0.11.8

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/bin/wcc.js CHANGED
@@ -36,6 +36,7 @@ async function build(config, cwd) {
36
36
  const { code, usesSharedRuntime } = await compile(file, {
37
37
  standalone: config.standalone,
38
38
  minify: config.minify,
39
+ comments: config.comments,
39
40
  runtimeImportPath,
40
41
  });
41
42
 
@@ -212,6 +213,7 @@ async function main() {
212
213
 
213
214
  // CLI flags override config
214
215
  if (process.argv.includes('--minify')) config.minify = true;
216
+ if (process.argv.includes('--comments')) config.comments = true;
215
217
 
216
218
  if (command === 'build') {
217
219
  const errors = await build(config, cwd);
@@ -241,6 +243,7 @@ async function main() {
241
243
  const { code, usesSharedRuntime } = await compile(filePath, {
242
244
  standalone: config.standalone,
243
245
  minify: config.minify,
246
+ comments: config.comments,
244
247
  runtimeImportPath,
245
248
  });
246
249
 
package/lib/codegen.js CHANGED
@@ -13,7 +13,7 @@
13
13
  * No props, emits, slots, if, for, model, show, attr, refs, or lifecycle hooks.
14
14
  */
15
15
 
16
- import { reactiveRuntime } from './reactive-runtime.js';
16
+ import { reactiveRuntime, buildInlineRuntime } from './reactive-runtime.js';
17
17
  import { scopeCSS } from './css-scoper.js';
18
18
  import { camelToKebab } from './parser-extractors.js';
19
19
 
@@ -893,6 +893,7 @@ export function generateComponent(parseResult, options = {}) {
893
893
  }
894
894
 
895
895
  const lines = [];
896
+ const comment = options.comments ? (text) => lines.push(` // --- ${text} ---`) : () => {};
896
897
 
897
898
  // ── 0. Source comment ──
898
899
  if (options.sourceFile) {
@@ -900,16 +901,22 @@ export function generateComponent(parseResult, options = {}) {
900
901
  }
901
902
 
902
903
  // ── 1. Reactive runtime (shared import or inline) ──
904
+ // Determine which runtime functions this component needs
905
+ const needsEffect = effects.length > 0 || bindings.length > 0 || showBindings.length > 0 || modelBindings.length > 0 || modelPropBindings.length > 0 || attrBindings.length > 0 || ifBlocks.length > 0 || forBlocks.length > 0 || watchers.length > 0 || childComponents.length > 0 || slots.some(s => s.slotProps.length > 0);
906
+ const needsComputed = computeds.length > 0;
907
+ const needsUntrack = watchers.length > 0;
908
+
903
909
  if (options.runtimeImportPath) {
904
910
  // Tree-shake: only import what this component actually uses
905
911
  const usedRuntime = new Set(['__signal']); // always need __signal
906
- if (computeds.length > 0) usedRuntime.add('__computed');
907
- if (effects.length > 0 || bindings.length > 0 || showBindings.length > 0 || modelBindings.length > 0 || modelPropBindings.length > 0 || attrBindings.length > 0 || ifBlocks.length > 0 || forBlocks.length > 0 || watchers.length > 0 || childComponents.length > 0 || slots.some(s => s.slotProps.length > 0)) usedRuntime.add('__effect');
908
- if (watchers.length > 0) usedRuntime.add('__untrack');
912
+ if (needsComputed) usedRuntime.add('__computed');
913
+ if (needsEffect) usedRuntime.add('__effect');
914
+ if (needsUntrack) usedRuntime.add('__untrack');
909
915
  const imports = [...usedRuntime].join(', ');
910
916
  lines.push(`import { ${imports} } from '${options.runtimeImportPath}';`);
911
917
  } else {
912
- lines.push(reactiveRuntime.trim());
918
+ // Standalone: inline only the runtime functions this component needs
919
+ lines.push(buildInlineRuntime({ needsComputed, needsEffect, needsBatch: false, needsUntrack }).trim());
913
920
  }
914
921
  lines.push('');
915
922
 
@@ -998,6 +1005,7 @@ export function generateComponent(parseResult, options = {}) {
998
1005
 
999
1006
  // Signal initialization
1000
1007
  for (const s of signals) {
1008
+ if (s === signals[0]) comment('Signals');
1001
1009
  lines.push(` this._${s.name} = __signal(${s.value});`);
1002
1010
  }
1003
1011
 
@@ -1013,6 +1021,7 @@ export function generateComponent(parseResult, options = {}) {
1013
1021
 
1014
1022
  // Computed initialization
1015
1023
  for (const c of computeds) {
1024
+ if (c === computeds[0]) comment('Computed');
1016
1025
  const body = transformExpr(c.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
1017
1026
  lines.push(` this._c_${c.name} = __computed(() => ${body});`);
1018
1027
  }
@@ -1260,6 +1269,7 @@ export function generateComponent(parseResult, options = {}) {
1260
1269
  lines.push('');
1261
1270
 
1262
1271
  // Binding effects — one __effect per binding
1272
+ if (bindings.length > 0) comment('Text bindings');
1263
1273
  for (const b of bindings) {
1264
1274
  if (b.type === 'prop') {
1265
1275
  lines.push(' this.__disposers.push(__effect(() => {');
@@ -1409,12 +1419,14 @@ export function generateComponent(parseResult, options = {}) {
1409
1419
  }
1410
1420
 
1411
1421
  // Event listeners (with AbortController signal for cleanup)
1422
+ if (events.length > 0) comment('Event listeners');
1412
1423
  for (const e of events) {
1413
1424
  const handlerExpr = generateEventHandler(e.handler, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, modelVarMap);
1414
1425
  lines.push(` this.${e.varName}.addEventListener('${e.event}', ${handlerExpr}, { signal: this.__ac.signal });`);
1415
1426
  }
1416
1427
 
1417
1428
  // Show effects — one __effect per ShowBinding
1429
+ if (showBindings.length > 0) comment('Show directives');
1418
1430
  for (const sb of showBindings) {
1419
1431
  const expr = transformExpr(sb.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
1420
1432
  lines.push(' this.__disposers.push(__effect(() => {');
@@ -1423,6 +1435,7 @@ export function generateComponent(parseResult, options = {}) {
1423
1435
  }
1424
1436
 
1425
1437
  // Model effects — signal → DOM (one __effect per ModelBinding)
1438
+ if (modelBindings.length > 0) comment('Model bindings (signal → DOM)');
1426
1439
  for (const mb of modelBindings) {
1427
1440
  if (mb.prop === 'checked' && mb.radioValue !== null) {
1428
1441
  // Radio: compare signal value to radioValue
@@ -1849,6 +1862,8 @@ export function generateComponent(parseResult, options = {}) {
1849
1862
  }
1850
1863
 
1851
1864
  // User methods (prefixed with _)
1865
+ if (methods.length > 0 && options.comments) lines.push('');
1866
+ if (methods.length > 0 && options.comments) lines.push(' // --- Methods ---');
1852
1867
  for (const m of methods) {
1853
1868
  const body = transformMethodBody(m.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames, modelVarMap);
1854
1869
  lines.push(` _${m.name}(${m.params}) {`);
package/lib/compiler.js CHANGED
@@ -118,11 +118,11 @@ async function compileSFC(filePath, config) {
118
118
  const style = descriptor.style;
119
119
 
120
120
  // 7. Extract lifecycle hooks (before other extractions)
121
- const { onMountHooks, onDestroyHooks } = extractLifecycleHooks(source);
121
+ const { onMountHooks, onDestroyHooks, onAdoptHooks } = extractLifecycleHooks(source);
122
122
 
123
123
  // 7b. Strip lifecycle/watcher blocks from source for extraction
124
124
  let sourceForExtraction = source;
125
- const hookLinePattern = /\bonMount\s*\(|\bonDestroy\s*\(|\bwatch\s*\(/;
125
+ const hookLinePattern = /\bonMount\s*\(|\bonDestroy\s*\(|\bonAdopt\s*\(|\bwatch\s*\(/;
126
126
  const sourceLines = sourceForExtraction.split('\n');
127
127
  const filteredLines = [];
128
128
  let skipDepth = 0;
@@ -276,6 +276,7 @@ async function compileSFC(filePath, config) {
276
276
  forBlocks: [],
277
277
  onMountHooks,
278
278
  onDestroyHooks,
279
+ onAdoptHooks,
279
280
  modelBindings: [],
280
281
  modelPropBindings: [],
281
282
  attrBindings: [],
package/lib/config.js CHANGED
@@ -19,7 +19,7 @@ import { pathToFileURL } from 'node:url';
19
19
  * @returns {Promise<WccConfig>}
20
20
  */
21
21
  export async function loadConfig(projectRoot) {
22
- const defaults = { port: 4100, input: 'src', output: 'dist', standalone: false, minify: false };
22
+ const defaults = { port: 4100, input: 'src', output: 'dist', standalone: false, minify: false, comments: false };
23
23
  const configPath = resolve(projectRoot, 'wcc.config.js');
24
24
 
25
25
  if (!existsSync(configPath)) return defaults;
@@ -61,6 +61,11 @@ export async function loadConfig(projectRoot) {
61
61
  error.code = 'INVALID_CONFIG';
62
62
  throw error;
63
63
  }
64
+ if (typeof config.comments !== 'boolean') {
65
+ const error = new Error(`Error en wcc.config.js: comments debe ser un booleano`);
66
+ error.code = 'INVALID_CONFIG';
67
+ throw error;
68
+ }
64
69
 
65
70
  return config;
66
71
  }
@@ -8,16 +8,21 @@
8
8
  * - __computed(fn): cached derived value that auto-invalidates
9
9
  * - __effect(fn): runs fn immediately and re-runs when dependencies change
10
10
  * - __batch(fn): batch multiple signal writes, flush effects once at the end
11
+ * - __untrack(fn): run fn without tracking dependencies
11
12
  *
12
13
  * Dependency tracking uses a global stack (__currentEffect).
13
14
  * Batching uses a depth counter — nested batches are supported.
14
15
  */
15
- /** @type {string} */
16
- export const reactiveRuntime = `
16
+
17
+ /** Shared globals (always included) */
18
+ const runtimeGlobals = `
17
19
  let __currentEffect = null;
18
20
  let __batchDepth = 0;
19
21
  const __pendingEffects = new Set();
22
+ `;
20
23
 
24
+ /** __signal — always included */
25
+ const runtimeSignal = `
21
26
  function __signal(initial) {
22
27
  let _value = initial;
23
28
  const _subs = new Set();
@@ -37,7 +42,10 @@ function __signal(initial) {
37
42
  }
38
43
  };
39
44
  }
45
+ `;
40
46
 
47
+ /** __computed — only if component uses computed() */
48
+ const runtimeComputed = `
41
49
  function __computed(fn) {
42
50
  let _cached, _dirty = true;
43
51
  const _subs = new Set();
@@ -61,7 +69,10 @@ function __computed(fn) {
61
69
  return _cached;
62
70
  };
63
71
  }
72
+ `;
64
73
 
74
+ /** __effect — only if component uses effects/bindings/show/model/attr/if/for/watchers/slots */
75
+ const runtimeEffect = `
65
76
  function __effect(fn) {
66
77
  let _cleanup = null;
67
78
  let _active = true;
@@ -81,7 +92,10 @@ function __effect(fn) {
81
92
  run();
82
93
  return () => { _active = false; if (typeof _cleanup === 'function') _cleanup(); };
83
94
  }
95
+ `;
84
96
 
97
+ /** __batch — only if component uses batch() */
98
+ const runtimeBatch = `
85
99
  function __batch(fn) {
86
100
  __batchDepth++;
87
101
  try {
@@ -95,7 +109,10 @@ function __batch(fn) {
95
109
  }
96
110
  }
97
111
  }
112
+ `;
98
113
 
114
+ /** __untrack — only if component uses watchers */
115
+ const runtimeUntrack = `
99
116
  function __untrack(fn) {
100
117
  const prev = __currentEffect;
101
118
  __currentEffect = null;
@@ -103,3 +120,24 @@ function __untrack(fn) {
103
120
  finally { __currentEffect = prev; }
104
121
  }
105
122
  `;
123
+
124
+ /**
125
+ * Full runtime (for backward compatibility and shared mode export).
126
+ * @type {string}
127
+ */
128
+ export const reactiveRuntime = runtimeGlobals + runtimeSignal + runtimeComputed + runtimeEffect + runtimeBatch + runtimeUntrack;
129
+
130
+ /**
131
+ * Build a tree-shaken inline runtime containing only the functions this component needs.
132
+ *
133
+ * @param {{ needsComputed: boolean, needsEffect: boolean, needsBatch: boolean, needsUntrack: boolean }} usage
134
+ * @returns {string}
135
+ */
136
+ export function buildInlineRuntime(usage) {
137
+ let code = runtimeGlobals + runtimeSignal;
138
+ if (usage.needsComputed) code += runtimeComputed;
139
+ if (usage.needsEffect) code += runtimeEffect;
140
+ if (usage.needsBatch) code += runtimeBatch;
141
+ if (usage.needsUntrack) code += runtimeUntrack;
142
+ return code;
143
+ }
@@ -159,7 +159,7 @@ export function walkTree(rootEl, signalNames, computedNames, propNames = new Set
159
159
  if (attr.name.startsWith('@')) {
160
160
  const eventName = attr.name.slice(1);
161
161
  const handlerName = attr.value.replace(/[^a-zA-Z0-9_]/g, '_').slice(0, 20);
162
- const varName = `__evt_${eventName.replace(/-/g, '_')}_${handlerName}`;
162
+ const varName = `__evt_${eventName.replace(/-/g, '_')}_${handlerName}_${eventIdx}`;
163
163
  eventIdx++;
164
164
  events.push({
165
165
  varName,
@@ -298,7 +298,7 @@ export function walkTree(rootEl, signalNames, computedNames, propNames = new Set
298
298
  if (soleMatch && parent.childNodes.length === 1) {
299
299
  const name = baseName(soleMatch[1]);
300
300
  const safeName = name.replace(/[^a-zA-Z0-9_]/g, '_').slice(0, 30);
301
- const varName = `__text_${safeName}`;
301
+ const varName = `__text_${safeName}_${bindIdx}`;
302
302
  bindIdx++;
303
303
  bindings.push({
304
304
  varName,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sprlab/wccompiler",
3
- "version": "0.11.5",
3
+ "version": "0.11.8",
4
4
  "description": "Zero-runtime compiler that transforms .wcc single-file components into native web components with signals-based reactivity",
5
5
  "type": "module",
6
6
  "exports": {