@sprlab/wccompiler 0.4.2 → 0.5.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/bin/wcc.js CHANGED
@@ -15,16 +15,28 @@ async function build(config, cwd) {
15
15
 
16
16
  if (!existsSync(outputDir)) mkdirSync(outputDir, { recursive: true });
17
17
 
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 };\n';
23
+ const signalsDest = join(outputDir, '__wcc-signals.js');
24
+ writeFileSync(signalsDest, signalsContent);
25
+
18
26
  // Discover source files
19
27
  const files = discoverFiles(inputDir);
20
28
  let errors = 0;
21
29
 
22
30
  for (const file of files) {
23
31
  try {
24
- const output = await compile(file);
32
+ // Calculate relative path from component output to __wcc-signals.js
25
33
  const relPath = relative(inputDir, file);
26
34
  const outPath = resolve(outputDir, relPath.replace(/\.wcc$/, '.js'));
27
35
  const outDir = dirname(outPath);
36
+ const runtimeRelPath = relative(outDir, signalsDest).replace(/\\/g, '/');
37
+ const runtimeImportPath = runtimeRelPath.startsWith('.') ? runtimeRelPath : './' + runtimeRelPath;
38
+
39
+ const output = await compile(file, { runtimeImportPath });
28
40
  if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
29
41
  writeFileSync(outPath, output);
30
42
  } catch (err) {
@@ -34,8 +46,6 @@ async function build(config, cwd) {
34
46
  }
35
47
 
36
48
  // Copy wcc-runtime.js to output directory
37
- const __filename = fileURLToPath(import.meta.url);
38
- const __dirname = dirname(__filename);
39
49
  const runtimeSrc = resolve(__dirname, '../lib/wcc-runtime.js');
40
50
  const runtimeDest = join(outputDir, 'wcc-runtime.js');
41
51
  copyFileSync(runtimeSrc, runtimeDest);
package/lib/codegen.js CHANGED
@@ -485,9 +485,10 @@ function generateItemSetup(lines, forBlock, itemVar, indexVar, propNames, signal
485
485
  * Generate a fully self-contained JS component from a ParseResult.
486
486
  *
487
487
  * @param {ParseResult} parseResult — Complete IR with bindings/events
488
+ * @param {{ runtimeImportPath?: string }} [options] — Optional generation options
488
489
  * @returns {string} JavaScript source code
489
490
  */
490
- export function generateComponent(parseResult) {
491
+ export function generateComponent(parseResult, options = {}) {
491
492
  const {
492
493
  tagName,
493
494
  className,
@@ -529,8 +530,12 @@ export function generateComponent(parseResult) {
529
530
 
530
531
  const lines = [];
531
532
 
532
- // ── 1. Inline reactive runtime ──
533
- lines.push(reactiveRuntime.trim());
533
+ // ── 1. Reactive runtime (shared import or inline) ──
534
+ if (options.runtimeImportPath) {
535
+ lines.push(`import { __signal, __computed, __effect, __batch } from '${options.runtimeImportPath}';`);
536
+ } else {
537
+ lines.push(reactiveRuntime.trim());
538
+ }
534
539
  lines.push('');
535
540
 
536
541
  // ── 1b. Child component imports ──
package/lib/compiler.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * Takes a .wcc file path and produces a self-contained JavaScript web component string.
7
7
  */
8
8
 
9
- import { JSDOM } from 'jsdom';
9
+ import { parseHTML } from 'linkedom';
10
10
  import { readFileSync, existsSync } from 'node:fs';
11
11
  import { dirname, extname, basename, resolve } from 'node:path';
12
12
  import { walkTree, processIfChains, processForBlocks, recomputeAnchorPath, detectRefs } from './tree-walker.js';
@@ -223,9 +223,9 @@ async function compileSFC(filePath, config) {
223
223
  exposeNames,
224
224
  };
225
225
 
226
- // 16. Process template through jsdom → tree-walker → codegen (same as compile())
227
- const dom = new JSDOM(`<div id="__root">${template}</div>`);
228
- const rootEl = dom.window.document.getElementById('__root');
226
+ // 16. Process template through linkedom → tree-walker → codegen
227
+ const { document } = parseHTML(`<div id="__root">${template}</div>`);
228
+ const rootEl = document.getElementById('__root');
229
229
 
230
230
  const signalNames = new Set(signals.map(s => s.name));
231
231
  const computedNames = new Set(computeds.map(c => c.name));
@@ -323,7 +323,7 @@ async function compileSFC(filePath, config) {
323
323
  parseResult.processedTemplate = rootEl.innerHTML;
324
324
 
325
325
  // 20. Generate component
326
- return generateComponent(parseResult);
326
+ return generateComponent(parseResult, config);
327
327
  }
328
328
 
329
329
  /**
@@ -16,7 +16,7 @@
16
16
  export const reactiveRuntime = `
17
17
  let __currentEffect = null;
18
18
  let __batchDepth = 0;
19
- const __pendingEffects = [];
19
+ const __pendingEffects = new Set();
20
20
 
21
21
  function __signal(initial) {
22
22
  let _value = initial;
@@ -30,9 +30,7 @@ function __signal(initial) {
30
30
  _value = args[0];
31
31
  if (old !== _value) {
32
32
  if (__batchDepth > 0) {
33
- for (const fn of _subs) {
34
- if (!__pendingEffects.includes(fn)) __pendingEffects.push(fn);
35
- }
33
+ for (const fn of _subs) __pendingEffects.add(fn);
36
34
  } else {
37
35
  for (const fn of [..._subs]) fn();
38
36
  }
@@ -46,9 +44,7 @@ function __computed(fn) {
46
44
  const recompute = () => {
47
45
  _dirty = true;
48
46
  if (__batchDepth > 0) {
49
- for (const fn of _subs) {
50
- if (!__pendingEffects.includes(fn)) __pendingEffects.push(fn);
51
- }
47
+ for (const fn of _subs) __pendingEffects.add(fn);
52
48
  } else {
53
49
  for (const fn of [..._subs]) fn();
54
50
  }
@@ -85,7 +81,8 @@ function __batch(fn) {
85
81
  } finally {
86
82
  __batchDepth--;
87
83
  if (__batchDepth === 0) {
88
- const pending = __pendingEffects.splice(0);
84
+ const pending = [...__pendingEffects];
85
+ __pendingEffects.clear();
89
86
  for (const f of pending) f();
90
87
  }
91
88
  }
@@ -12,7 +12,7 @@
12
12
  * extracts branch templates, and replaces chains with comment anchors.
13
13
  */
14
14
 
15
- import { JSDOM } from 'jsdom';
15
+ import { parseHTML } from 'linkedom';
16
16
  import { BOOLEAN_ATTRIBUTES } from './types.js';
17
17
 
18
18
  /** @import { Binding, EventBinding, IfBlock, IfBranch, ShowBinding, AttrBinding, ForBlock, ModelBinding, SlotBinding, SlotProp, RefBinding, ChildComponentBinding, ChildPropBinding } from './types.js' */
@@ -358,8 +358,8 @@ function isChainPredecessor(el) {
358
358
  * @returns {{ bindings: Binding[], events: EventBinding[], showBindings: ShowBinding[], attrBindings: AttrBinding[], modelBindings: ModelBinding[], slots: SlotBinding[], processedHtml: string }}
359
359
  */
360
360
  export function walkBranch(html, signalNames, computedNames, propNames) {
361
- const dom = new JSDOM(`<div id="__branchRoot">${html}</div>`);
362
- const branchRoot = dom.window.document.getElementById('__branchRoot');
361
+ const { document } = parseHTML(`<div id="__branchRoot">${html}</div>`);
362
+ const branchRoot = document.getElementById('__branchRoot');
363
363
 
364
364
  // Use walkTree on the branch root to discover bindings/events
365
365
  const result = walkTree(branchRoot, signalNames, computedNames, propNames);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sprlab/wccompiler",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
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": {
@@ -32,11 +32,11 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "esbuild": "^0.27.0",
35
- "jsdom": "^29.0.2"
35
+ "linkedom": "^0.18.12"
36
36
  },
37
37
  "devDependencies": {
38
- "@types/jsdom": "^28.0.1",
39
38
  "fast-check": "^4.1.1",
39
+ "jsdom": "^29.1.1",
40
40
  "typescript": "^6.0.3",
41
41
  "vitest": "^3.2.1"
42
42
  },