@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 +154 -34
- package/bin/wcc.js +4 -5
- package/bin/wcc.test.js +23 -16
- package/lib/codegen.js +336 -114
- package/lib/compiler-browser.js +526 -0
- package/lib/compiler.js +225 -91
- package/lib/dev-server.js +55 -17
- package/lib/parser-extractors.js +1029 -0
- package/lib/parser.js +36 -929
- package/lib/reactive-runtime.js +35 -4
- package/lib/sfc-parser.js +262 -0
- package/lib/tree-walker.js +18 -10
- package/lib/types.js +11 -0
- package/package.json +3 -3
- package/types/wcc.d.ts +6 -6
- package/types/wcc.test.js +2 -2
- package/lib/printer.js +0 -118
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# wcCompiler
|
|
2
2
|
|
|
3
|
-
Zero-runtime compiler that transforms `.
|
|
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
|
-
```
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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>{{
|
|
133
|
-
<p>Hello, {{name}}
|
|
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 `.
|
|
351
|
+
Use `<script lang="ts">` in your `.wcc` file for full type support:
|
|
263
352
|
|
|
264
|
-
```
|
|
265
|
-
|
|
353
|
+
```html
|
|
354
|
+
<script lang="ts">
|
|
355
|
+
import { defineComponent, defineProps, defineEmits, signal, computed, watch, defineExpose } from 'wcc'
|
|
266
356
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
357
|
+
export default defineComponent({
|
|
358
|
+
tag: 'wcc-typescript',
|
|
359
|
+
})
|
|
270
360
|
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
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
|
-
`
|
|
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 .
|
|
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(/\.
|
|
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 !== '.
|
|
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
|
|
28
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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 .
|
|
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.
|
|
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/
|
|
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
|
-
|
|
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'], {
|