@sprlab/wccompiler 0.3.0 → 0.4.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
@@ -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
@@ -94,13 +122,18 @@ effect(() => {
94
122
  ### Watch
95
123
 
96
124
  ```js
97
- watch('count', (newVal, oldVal) => {
125
+ // Watch a signal directly
126
+ watch(count, (newVal, oldVal) => {
98
127
  console.log(`Changed from ${oldVal} to ${newVal}`)
99
- if (newVal > 10) api.save(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}`)
100
133
  })
101
134
  ```
102
135
 
103
- `watch` observes a specific signal/prop/computed and provides both old and new values. The callback does not run on initial mount — only on subsequent changes.
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.
104
137
 
105
138
  ### Constants
106
139
 
@@ -118,6 +151,16 @@ const props = defineProps({ label: 'Click', count: 0 })
118
151
  <wcc-counter label="Clicks:" count="5"></wcc-counter>
119
152
  ```
120
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
+
121
164
  TypeScript generics:
122
165
 
123
166
  ```ts
@@ -148,9 +191,19 @@ The compiler validates emit calls against declared events at compile time.
148
191
 
149
192
  ### Text Interpolation
150
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
+
151
204
  ```html
152
- <span>{{count}}</span>
153
- <p>Hello, {{name}}! You have {{count}} items.</p>
205
+ <span>{{label}}</span>
206
+ <p>Hello, {{name}}!</p>
154
207
  ```
155
208
 
156
209
  ### Event Binding
@@ -160,25 +213,34 @@ The compiler validates emit calls against declared events at compile time.
160
213
  <input @input="handleInput">
161
214
  ```
162
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
+
163
223
  ### Conditional Rendering
164
224
 
165
225
  ```html
166
- <div if="status === 'active'">Active</div>
167
- <div else-if="status === 'pending'">Pending</div>
226
+ <div if="status() === 'active'">Active</div>
227
+ <div else-if="status() === 'pending'">Pending</div>
168
228
  <div else>Inactive</div>
169
229
  ```
170
230
 
171
231
  ### List Rendering
172
232
 
173
233
  ```html
174
- <li each="item in items">{{item.name}}</li>
175
- <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>
176
236
  ```
177
237
 
238
+ The source expression calls the signal (`items()`) to read the current array.
239
+
178
240
  ### Visibility Toggle
179
241
 
180
242
  ```html
181
- <div show="isVisible">Shown or hidden via CSS display</div>
243
+ <div show="isVisible()">Shown or hidden via CSS display</div>
182
244
  ```
183
245
 
184
246
  ### Two-Way Binding
@@ -195,10 +257,10 @@ The compiler validates emit calls against declared events at compile time.
195
257
  ### Attribute Binding
196
258
 
197
259
  ```html
198
- <a :href="url">Link</a>
199
- <button :disabled="isLoading">Submit</button>
200
- <div :class="{ active: isActive, error: hasError }">...</div>
201
- <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>
202
264
  ```
203
265
 
204
266
  ### Template Refs
@@ -286,31 +348,58 @@ wcc-counter .counter { display: flex; }
286
348
 
287
349
  ## TypeScript
288
350
 
289
- Use `.ts` files with full type support:
351
+ Use `<script lang="ts">` in your `.wcc` file for full type support:
290
352
 
291
- ```ts
292
- 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'
293
356
 
294
- const props = defineProps<{ title: string }>({ title: 'Demo' })
295
- const count = signal<number>(0)
296
- const doubled = computed<number>(() => count() * 2)
357
+ export default defineComponent({
358
+ tag: 'wcc-typescript',
359
+ })
297
360
 
298
- function increment(): void {
299
- 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())
300
373
  }
301
374
 
302
- 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>
303
390
  ```
304
391
 
305
- `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.
306
393
 
307
394
  ## CLI
308
395
 
309
396
  ```bash
310
- wcc build # Compile all .ts/.js files from input/ to output/
397
+ wcc build # Compile all .wcc files from input/ to output/
311
398
  wcc dev # Build + watch + live-reload dev server
312
399
  ```
313
400
 
401
+ The CLI discovers all `.wcc` files in your source directory and compiles each into a standalone `.js` file.
402
+
314
403
  ### Configuration
315
404
 
316
405
  Create `wcc.config.js` in your project root:
@@ -325,6 +414,10 @@ export default {
325
414
 
326
415
  All options are optional — defaults shown above.
327
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
+
328
421
  ## Runtime Helper
329
422
 
330
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
  }
@@ -68,24 +67,25 @@ async function main() {
68
67
  } else if (command === 'dev') {
69
68
  await build(config, cwd);
70
69
  const outputDir = resolve(cwd, config.output);
71
- startDevServer({ port: config.port, root: cwd, outputDir });
70
+ const devServer = startDevServer({ port: config.port, root: cwd, outputDir });
72
71
  const inputDir = resolve(cwd, config.input);
73
72
  console.log(`Watching ${inputDir} for changes...`);
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);
86
85
  console.log(`Compiled: ${filename}`);
87
86
  } catch (err) {
88
87
  console.error(`Error compiling ${filename}: ${err.message}`);
88
+ devServer.notifyError(`${filename}\n\n${err.message}`);
89
89
  }
90
90
  });
91
91
  } else {
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'], {