@sprlab/wccompiler 0.5.14 → 0.6.1

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 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 output = await compile(file, { runtimeImportPath });
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, output);
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 output = await compile(filePath);
91
- const outPath = resolve(outputDir, filename.replace(/\.ts$/, '.js').replace(/\.wcc$/, '.js'));
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, output);
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. Generate component
358
+ // 20. Resolve standalone and generate component
359
+ const standaloneResolved = resolveStandalone(descriptor.standalone, config?.standalone ?? false);
359
360
  const genOptions = { ...config, sourceFile: fileName };
360
- return generateComponent(parseResult, genOptions);
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
- return compileSFC(filePath, config);
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
- const userConfig = mod.default || mod;
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sprlab/wccompiler",
3
- "version": "0.5.14",
3
+ "version": "0.6.1",
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
  "bin": {