@sprlab/wccompiler 0.2.1 → 0.4.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # wcCompiler
2
2
 
3
- Zero-runtime compiler that transforms `.ts`/`.js` component files into native web components. No framework, no virtual DOM, no runtime — just vanilla JavaScript custom elements with signals-based reactivity.
3
+ Zero-runtime compiler that transforms `.wcc` single-file components into native web components. No framework, no virtual DOM, no runtime — just vanilla JavaScript custom elements with signals-based reactivity.
4
4
 
5
5
  ## Install
6
6
 
@@ -12,14 +12,13 @@ npm install -D @sprlab/wccompiler
12
12
 
13
13
  **1. Create a component**
14
14
 
15
- ```js
16
- // src/wcc-counter.js
15
+ ```html
16
+ <!-- src/wcc-counter.wcc -->
17
+ <script>
17
18
  import { defineComponent, signal } from 'wcc'
18
19
 
19
20
  export default defineComponent({
20
21
  tag: 'wcc-counter',
21
- template: './wcc-counter.html',
22
- styles: './wcc-counter.css',
23
22
  })
24
23
 
25
24
  const count = signal(0)
@@ -27,19 +26,18 @@ const count = signal(0)
27
26
  function increment() {
28
27
  count.set(count() + 1)
29
28
  }
30
- ```
29
+ </script>
31
30
 
32
- ```html
33
- <!-- src/wcc-counter.html -->
31
+ <template>
34
32
  <div class="counter">
35
- <span>{{count}}</span>
33
+ <span>{{count()}}</span>
36
34
  <button @click="increment">+</button>
37
35
  </div>
38
- ```
36
+ </template>
39
37
 
40
- ```css
41
- /* src/wcc-counter.css */
38
+ <style>
42
39
  .counter { display: flex; gap: 8px; align-items: center; }
40
+ </style>
43
41
  ```
44
42
 
45
43
  **2. Build**
@@ -57,6 +55,36 @@ npx wcc build
57
55
 
58
56
  The compiled output is a single `.js` file with zero dependencies — works in any browser that supports custom elements.
59
57
 
58
+ ## Single File Component (.wcc)
59
+
60
+ wcCompiler uses a single-file component format with the `.wcc` extension. Each file contains three blocks:
61
+
62
+ - `<script>` — Component logic (signals, props, events, lifecycle)
63
+ - `<template>` — HTML template with directives
64
+ - `<style>` — Scoped CSS
65
+
66
+ ```html
67
+ <script>
68
+ import { defineComponent, signal } from 'wcc'
69
+
70
+ export default defineComponent({
71
+ tag: 'wcc-my-component',
72
+ })
73
+
74
+ const message = signal('Hello')
75
+ </script>
76
+
77
+ <template>
78
+ <p>{{message()}}</p>
79
+ </template>
80
+
81
+ <style>
82
+ p { color: steelblue; }
83
+ </style>
84
+ ```
85
+
86
+ Use `<script lang="ts">` for TypeScript support. The CLI discovers and compiles all `.wcc` files in your source directory.
87
+
60
88
  ## Reactivity
61
89
 
62
90
  ### Signals
@@ -82,6 +110,31 @@ effect(() => {
82
110
  })
83
111
  ```
84
112
 
113
+ Effects support cleanup — return a function to run before re-execution:
114
+
115
+ ```js
116
+ effect(() => {
117
+ const id = setInterval(() => tick.set(tick() + 1), 1000)
118
+ return () => clearInterval(id) // called before re-run
119
+ })
120
+ ```
121
+
122
+ ### Watch
123
+
124
+ ```js
125
+ // Watch a signal directly
126
+ watch(count, (newVal, oldVal) => {
127
+ console.log(`Changed from ${oldVal} to ${newVal}`)
128
+ })
129
+
130
+ // Watch a getter function (useful for props or derived values)
131
+ watch(() => props.count, (newVal, oldVal) => {
132
+ console.log(`Prop changed: ${oldVal} → ${newVal}`)
133
+ })
134
+ ```
135
+
136
+ `watch` observes a specific signal or getter and provides both old and new values. The callback does not run on initial mount — only on subsequent changes.
137
+
85
138
  ### Constants
86
139
 
87
140
  ```js
@@ -98,6 +151,16 @@ const props = defineProps({ label: 'Click', count: 0 })
98
151
  <wcc-counter label="Clicks:" count="5"></wcc-counter>
99
152
  ```
100
153
 
154
+ You can also call `defineProps` without assignment — the props are available by name in the template:
155
+
156
+ ```js
157
+ defineProps({ label: 'Click' })
158
+ ```
159
+
160
+ ```html
161
+ <span>{{label}}</span>
162
+ ```
163
+
101
164
  TypeScript generics:
102
165
 
103
166
  ```ts
@@ -128,9 +191,19 @@ The compiler validates emit calls against declared events at compile time.
128
191
 
129
192
  ### Text Interpolation
130
193
 
194
+ Signals and computeds require `()` to read their value in templates:
195
+
196
+ ```html
197
+ <span>{{count()}}</span>
198
+ <p>You have {{items().length}} items.</p>
199
+ <span>{{doubled()}}</span>
200
+ ```
201
+
202
+ Props accessed without assignment use their name directly (no parentheses):
203
+
131
204
  ```html
132
- <span>{{count}}</span>
133
- <p>Hello, {{name}}! You have {{count}} items.</p>
205
+ <span>{{label}}</span>
206
+ <p>Hello, {{name}}!</p>
134
207
  ```
135
208
 
136
209
  ### Event Binding
@@ -140,25 +213,34 @@ The compiler validates emit calls against declared events at compile time.
140
213
  <input @input="handleInput">
141
214
  ```
142
215
 
216
+ Event handlers support expressions and inline arguments:
217
+
218
+ ```html
219
+ <button @click="removeItem(item)">×</button>
220
+ <button @click="() => doSomething()">Do it</button>
221
+ ```
222
+
143
223
  ### Conditional Rendering
144
224
 
145
225
  ```html
146
- <div if="status === 'active'">Active</div>
147
- <div else-if="status === 'pending'">Pending</div>
226
+ <div if="status() === 'active'">Active</div>
227
+ <div else-if="status() === 'pending'">Pending</div>
148
228
  <div else>Inactive</div>
149
229
  ```
150
230
 
151
231
  ### List Rendering
152
232
 
153
233
  ```html
154
- <li each="item in items">{{item.name}}</li>
155
- <li each="(item, index) in items">{{index}}: {{item.name}}</li>
234
+ <li each="item in items()">{{item.name}}</li>
235
+ <li each="(item, index) in items()">{{index}}: {{item.name}}</li>
156
236
  ```
157
237
 
238
+ The source expression calls the signal (`items()`) to read the current array.
239
+
158
240
  ### Visibility Toggle
159
241
 
160
242
  ```html
161
- <div show="isVisible">Shown or hidden via CSS display</div>
243
+ <div show="isVisible()">Shown or hidden via CSS display</div>
162
244
  ```
163
245
 
164
246
  ### Two-Way Binding
@@ -175,10 +257,10 @@ The compiler validates emit calls against declared events at compile time.
175
257
  ### Attribute Binding
176
258
 
177
259
  ```html
178
- <a :href="url">Link</a>
179
- <button :disabled="isLoading">Submit</button>
180
- <div :class="{ active: isActive, error: hasError }">...</div>
181
- <div :style="{ color: textColor }">...</div>
260
+ <a :href="url()">Link</a>
261
+ <button :disabled="isLoading()">Submit</button>
262
+ <div :class="{ active: isActive(), error: hasError() }">...</div>
263
+ <div :style="{ color: textColor() }">...</div>
182
264
  ```
183
265
 
184
266
  ### Template Refs
@@ -238,11 +320,18 @@ onMount(() => {
238
320
  console.log('Component connected to DOM')
239
321
  })
240
322
 
323
+ onMount(async () => {
324
+ const data = await fetch('/api/items').then(r => r.json())
325
+ items.set(data)
326
+ })
327
+
241
328
  onDestroy(() => {
242
329
  console.log('Component removed from DOM')
243
330
  })
244
331
  ```
245
332
 
333
+ Async callbacks are wrapped in an IIFE — `connectedCallback` itself stays synchronous.
334
+
246
335
  ## CSS Scoping
247
336
 
248
337
  Styles are automatically scoped to the component using tag-name prefixing:
@@ -259,31 +348,58 @@ wcc-counter .counter { display: flex; }
259
348
 
260
349
  ## TypeScript
261
350
 
262
- Use `.ts` files with full type support:
351
+ Use `<script lang="ts">` in your `.wcc` file for full type support:
263
352
 
264
- ```ts
265
- import { defineComponent, defineProps, signal, computed, templateBindings } from 'wcc'
353
+ ```html
354
+ <script lang="ts">
355
+ import { defineComponent, defineProps, defineEmits, signal, computed, watch, defineExpose } from 'wcc'
266
356
 
267
- const props = defineProps<{ title: string }>({ title: 'Demo' })
268
- const count = signal<number>(0)
269
- const doubled = computed<number>(() => count() * 2)
357
+ export default defineComponent({
358
+ tag: 'wcc-typescript',
359
+ })
270
360
 
271
- function increment(): void {
272
- count.set(count() + 1)
361
+ const props = defineProps<{ title: string, count: number }>({ title: 'Demo', count: 0 })
362
+ const emit = defineEmits<{ (e: 'update', value: number): void }>()
363
+
364
+ const doubled = computed<number>(() => props.count * 2)
365
+ const watchLog = signal<string>('(no changes yet)')
366
+
367
+ watch(() => props.count, (newVal, oldVal) => {
368
+ watchLog.set(`count changed: ${oldVal} → ${newVal}`)
369
+ })
370
+
371
+ function handleUpdate(): void {
372
+ emit('update', doubled())
273
373
  }
274
374
 
275
- templateBindings({ doubled, increment })
375
+ defineExpose({ doubled, handleUpdate, watchLog })
376
+ </script>
377
+
378
+ <template>
379
+ <div class="demo">
380
+ <span>{{title}}: {{count}}</span>
381
+ <span>Doubled: {{doubled()}}</span>
382
+ <span>Watch: {{watchLog()}}</span>
383
+ <button @click="handleUpdate">Update</button>
384
+ </div>
385
+ </template>
386
+
387
+ <style>
388
+ .demo { font-family: sans-serif; }
389
+ </style>
276
390
  ```
277
391
 
278
- `templateBindings()` declares which variables are used in the template, eliminating TypeScript "unused variable" warnings.
392
+ `defineExpose()` exposes methods and properties for external access via ref.
279
393
 
280
394
  ## CLI
281
395
 
282
396
  ```bash
283
- wcc build # Compile all .ts/.js files from input/ to output/
397
+ wcc build # Compile all .wcc files from input/ to output/
284
398
  wcc dev # Build + watch + live-reload dev server
285
399
  ```
286
400
 
401
+ The CLI discovers all `.wcc` files in your source directory and compiles each into a standalone `.js` file.
402
+
287
403
  ### Configuration
288
404
 
289
405
  Create `wcc.config.js` in your project root:
@@ -298,6 +414,10 @@ export default {
298
414
 
299
415
  All options are optional — defaults shown above.
300
416
 
417
+ ## Editor Support
418
+
419
+ The **wcCompiler (.wcc) Language Support** extension is available on the VS Code Marketplace. It provides syntax highlighting, completions, and diagnostics for `.wcc` files.
420
+
301
421
  ## Runtime Helper
302
422
 
303
423
  An optional `wcc-runtime.js` is copied to your output directory for declarative host-page bindings:
package/bin/wcc.js CHANGED
@@ -23,7 +23,7 @@ async function build(config, cwd) {
23
23
  try {
24
24
  const output = await compile(file);
25
25
  const relPath = relative(inputDir, file);
26
- const outPath = resolve(outputDir, relPath.replace(/\.ts$/, '.js'));
26
+ const outPath = resolve(outputDir, relPath.replace(/\.wcc$/, '.js'));
27
27
  const outDir = dirname(outPath);
28
28
  if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
29
29
  writeFileSync(outPath, output);
@@ -49,9 +49,8 @@ function discoverFiles(dir) {
49
49
  for (const entry of entries) {
50
50
  if (!entry.isFile()) continue;
51
51
  const ext = extname(entry.name);
52
- if (ext !== '.ts' && ext !== '.js') continue;
52
+ if (ext !== '.wcc') continue;
53
53
  if (entry.name.includes('.test.')) continue;
54
- if (entry.name.endsWith('.d.ts')) continue;
55
54
  const fullPath = resolve(dir, entry.parentPath ? relative(dir, entry.parentPath) : '', entry.name);
56
55
  results.push(fullPath);
57
56
  }
@@ -74,12 +73,12 @@ async function main() {
74
73
  watch(inputDir, { recursive: true }, async (eventType, filename) => {
75
74
  if (!filename) return;
76
75
  const ext = extname(filename);
77
- if (ext !== '.ts' && ext !== '.js') return;
76
+ if (ext !== '.ts' && ext !== '.js' && ext !== '.wcc') return;
78
77
  if (filename.includes('.test.')) return;
79
78
  const filePath = resolve(inputDir, filename);
80
79
  try {
81
80
  const output = await compile(filePath);
82
- const outPath = resolve(outputDir, filename.replace(/\.ts$/, '.js'));
81
+ const outPath = resolve(outputDir, filename.replace(/\.ts$/, '.js').replace(/\.wcc$/, '.js'));
83
82
  const outDir = dirname(outPath);
84
83
  if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
85
84
  writeFileSync(outPath, output);
package/bin/wcc.test.js CHANGED
@@ -24,37 +24,37 @@ describe('wcc CLI', () => {
24
24
  const srcDir = join(tmpDir, dir);
25
25
  if (!existsSync(srcDir)) mkdirSync(srcDir, { recursive: true });
26
26
 
27
- // Write a minimal component source
28
- writeFileSync(join(srcDir, `${name}.js`), `
27
+ // Write a minimal .wcc SFC component
28
+ const sfcContent = `<script>
29
29
  import { defineComponent, signal } from 'wcc'
30
30
 
31
- export default defineComponent({
32
- tag: '${name}',
33
- template: './${name}.html',
34
- })
31
+ export default defineComponent({ tag: '${name}' })
35
32
 
36
33
  const count = signal(0)
37
34
 
38
35
  function increment() {
39
36
  count.set(count() + 1)
40
37
  }
41
- `);
42
- // Write a minimal template
43
- writeFileSync(join(srcDir, `${name}.html`), `<div>{{count}}</div>`);
38
+ </script>
39
+
40
+ <template>
41
+ <div>{{count()}}</div>
42
+ </template>
43
+ `;
44
+ writeFileSync(join(srcDir, `${name}.wcc`), sfcContent);
44
45
  }
45
46
 
46
47
  function writeConfig(config) {
47
48
  writeFileSync(join(tmpDir, 'wcc.config.js'), `export default ${JSON.stringify(config)};\n`);
48
49
  }
49
50
 
50
- it('discovers .ts and .js files, excludes *.test.* and *.d.ts', () => {
51
+ it('discovers .wcc files, excludes *.test.* files', () => {
51
52
  // Create various files
52
53
  writeComponent('wcc-counter');
53
54
  writeFileSync(join(tmpDir, 'src', 'helper.test.js'), 'test file');
54
- writeFileSync(join(tmpDir, 'src', 'types.d.ts'), 'declare module "x" {}');
55
55
  writeFileSync(join(tmpDir, 'src', 'readme.md'), '# readme');
56
56
 
57
- // Run build — it should only compile wcc-counter.js (not test, d.ts, or md files)
57
+ // Run build — it should only compile wcc-counter.wcc (not test or md files)
58
58
  const result = execFileSync('node', [cliPath, 'build'], {
59
59
  cwd: tmpDir,
60
60
  encoding: 'utf-8',
@@ -63,9 +63,8 @@ function increment() {
63
63
 
64
64
  // Check that output was created
65
65
  expect(existsSync(join(tmpDir, 'dist', 'wcc-counter.js'))).toBe(true);
66
- // Check that test/d.ts files were NOT compiled
66
+ // Check that test/md files were NOT compiled
67
67
  expect(existsSync(join(tmpDir, 'dist', 'helper.test.js'))).toBe(false);
68
- expect(existsSync(join(tmpDir, 'dist', 'types.d.ts'))).toBe(false);
69
68
  expect(existsSync(join(tmpDir, 'dist', 'readme.md'))).toBe(false);
70
69
  });
71
70
 
@@ -87,8 +86,16 @@ function increment() {
87
86
  });
88
87
 
89
88
  it('exits with non-zero code on compilation error', () => {
90
- // Write an invalid component (no defineComponent)
91
- writeFileSync(join(tmpDir, 'src', 'bad.js'), 'const x = 1;');
89
+ // Write an invalid .wcc component (no defineComponent)
90
+ const badSfc = `<script>
91
+ const x = 1;
92
+ </script>
93
+
94
+ <template>
95
+ <div>hello</div>
96
+ </template>
97
+ `;
98
+ writeFileSync(join(tmpDir, 'src', 'bad.wcc'), badSfc);
92
99
 
93
100
  try {
94
101
  execFileSync('node', [cliPath, 'build'], {