@sprlab/wccompiler 0.0.3 → 0.2.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 +268 -51
- package/bin/wcc.js +65 -100
- package/bin/wcc.test.js +119 -0
- package/lib/codegen.js +882 -166
- package/lib/compiler.js +172 -33
- package/lib/config.js +33 -43
- package/lib/css-scoper.js +13 -0
- package/lib/dev-server.js +19 -0
- package/lib/parser.js +1001 -109
- package/lib/printer.js +92 -78
- package/lib/reactive-runtime.js +1 -0
- package/lib/tree-walker.js +721 -43
- package/lib/types.js +215 -0
- package/lib/wcc-runtime.js +26 -0
- package/package.json +14 -9
- package/types/wcc.d.ts +27 -0
- package/types/wcc.test.js +46 -0
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 `.ts`/`.js` component files 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
|
|
|
@@ -8,53 +8,283 @@ Zero-runtime compiler that transforms `.html` files with Vue-like syntax into 10
|
|
|
8
8
|
npm install -D @sprlab/wccompiler
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Quick Start
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
**1. Create a component**
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
// src/wcc-counter.js
|
|
17
|
+
import { defineComponent, signal } from 'wcc'
|
|
18
|
+
|
|
19
|
+
export default defineComponent({
|
|
20
|
+
tag: 'wcc-counter',
|
|
21
|
+
template: './wcc-counter.html',
|
|
22
|
+
styles: './wcc-counter.css',
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const count = signal(0)
|
|
26
|
+
|
|
27
|
+
function increment() {
|
|
28
|
+
count.set(count() + 1)
|
|
29
|
+
}
|
|
30
|
+
```
|
|
14
31
|
|
|
15
32
|
```html
|
|
16
33
|
<!-- src/wcc-counter.html -->
|
|
17
|
-
<
|
|
18
|
-
<
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
.counter { display: flex; gap: 8px; }
|
|
27
|
-
</style>
|
|
28
|
-
|
|
29
|
-
<script>
|
|
30
|
-
defineProps(['label'])
|
|
31
|
-
const count = 0
|
|
32
|
-
|
|
33
|
-
function increment() {
|
|
34
|
-
const count = count + 1
|
|
35
|
-
}
|
|
36
|
-
</script>
|
|
34
|
+
<div class="counter">
|
|
35
|
+
<span>{{count}}</span>
|
|
36
|
+
<button @click="increment">+</button>
|
|
37
|
+
</div>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```css
|
|
41
|
+
/* src/wcc-counter.css */
|
|
42
|
+
.counter { display: flex; gap: 8px; align-items: center; }
|
|
37
43
|
```
|
|
38
44
|
|
|
39
|
-
|
|
45
|
+
**2. Build**
|
|
40
46
|
|
|
41
47
|
```bash
|
|
42
48
|
npx wcc build
|
|
43
49
|
```
|
|
44
50
|
|
|
45
|
-
|
|
51
|
+
**3. Use**
|
|
46
52
|
|
|
47
53
|
```html
|
|
48
54
|
<script type="module" src="dist/wcc-counter.js"></script>
|
|
49
|
-
<wcc-counter
|
|
55
|
+
<wcc-counter></wcc-counter>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The compiled output is a single `.js` file with zero dependencies — works in any browser that supports custom elements.
|
|
59
|
+
|
|
60
|
+
## Reactivity
|
|
61
|
+
|
|
62
|
+
### Signals
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
const count = signal(0) // create
|
|
66
|
+
count() // read → 0
|
|
67
|
+
count.set(5) // write → 5
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Computed
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
const doubled = computed(() => count() * 2)
|
|
74
|
+
doubled() // auto-updates when count changes
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Effects
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
effect(() => {
|
|
81
|
+
console.log('Count is:', count()) // re-runs on change
|
|
82
|
+
})
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Constants
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
const TAX_RATE = 0.21 // non-reactive, no signal() wrapper
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Props
|
|
92
|
+
|
|
93
|
+
```js
|
|
94
|
+
const props = defineProps({ label: 'Click', count: 0 })
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
```html
|
|
98
|
+
<wcc-counter label="Clicks:" count="5"></wcc-counter>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
TypeScript generics:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
const props = defineProps<{ label: string, count: number }>({ label: 'Click', count: 0 })
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Props are reactive — they update when attributes change. Supports boolean and number coercion.
|
|
108
|
+
|
|
109
|
+
## Custom Events
|
|
110
|
+
|
|
111
|
+
```js
|
|
112
|
+
const emit = defineEmits(['change', 'reset'])
|
|
113
|
+
|
|
114
|
+
function handleClick() {
|
|
115
|
+
emit('change', count())
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
TypeScript call signatures:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
const emit = defineEmits<{ (e: 'change', value: number): void }>()
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The compiler validates emit calls against declared events at compile time.
|
|
126
|
+
|
|
127
|
+
## Template Directives
|
|
128
|
+
|
|
129
|
+
### Text Interpolation
|
|
130
|
+
|
|
131
|
+
```html
|
|
132
|
+
<span>{{count}}</span>
|
|
133
|
+
<p>Hello, {{name}}! You have {{count}} items.</p>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Event Binding
|
|
137
|
+
|
|
138
|
+
```html
|
|
139
|
+
<button @click="increment">+</button>
|
|
140
|
+
<input @input="handleInput">
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Conditional Rendering
|
|
144
|
+
|
|
145
|
+
```html
|
|
146
|
+
<div if="status === 'active'">Active</div>
|
|
147
|
+
<div else-if="status === 'pending'">Pending</div>
|
|
148
|
+
<div else>Inactive</div>
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### List Rendering
|
|
152
|
+
|
|
153
|
+
```html
|
|
154
|
+
<li each="item in items">{{item.name}}</li>
|
|
155
|
+
<li each="(item, index) in items">{{index}}: {{item.name}}</li>
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Visibility Toggle
|
|
159
|
+
|
|
160
|
+
```html
|
|
161
|
+
<div show="isVisible">Shown or hidden via CSS display</div>
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Two-Way Binding
|
|
165
|
+
|
|
166
|
+
```html
|
|
167
|
+
<input type="text" model="name">
|
|
168
|
+
<input type="number" model="age">
|
|
169
|
+
<input type="checkbox" model="agree">
|
|
170
|
+
<input type="radio" name="color" value="red" model="color">
|
|
171
|
+
<select model="country">...</select>
|
|
172
|
+
<textarea model="bio"></textarea>
|
|
50
173
|
```
|
|
51
174
|
|
|
52
|
-
|
|
175
|
+
### Attribute Binding
|
|
176
|
+
|
|
177
|
+
```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>
|
|
182
|
+
```
|
|
53
183
|
|
|
54
|
-
|
|
55
|
-
- `wcc dev` — Build + watch + dev server with live-reload
|
|
184
|
+
### Template Refs
|
|
56
185
|
|
|
57
|
-
|
|
186
|
+
```js
|
|
187
|
+
const canvas = templateRef('myCanvas')
|
|
188
|
+
|
|
189
|
+
onMount(() => {
|
|
190
|
+
const ctx = canvas.value.getContext('2d')
|
|
191
|
+
})
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
```html
|
|
195
|
+
<canvas ref="myCanvas"></canvas>
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Slots
|
|
199
|
+
|
|
200
|
+
### Named Slots
|
|
201
|
+
|
|
202
|
+
Component template:
|
|
203
|
+
```html
|
|
204
|
+
<div class="card">
|
|
205
|
+
<slot name="header">Default Header</slot>
|
|
206
|
+
<slot>Default Body</slot>
|
|
207
|
+
<slot name="footer">Default Footer</slot>
|
|
208
|
+
</div>
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Consumer:
|
|
212
|
+
```html
|
|
213
|
+
<wcc-card>
|
|
214
|
+
<template #header><strong>Custom Header</strong></template>
|
|
215
|
+
<p>Custom body content</p>
|
|
216
|
+
<template #footer>Custom footer</template>
|
|
217
|
+
</wcc-card>
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Scoped Slots
|
|
221
|
+
|
|
222
|
+
Component template (passes reactive data to consumer):
|
|
223
|
+
```html
|
|
224
|
+
<slot name="stats" :likes="likes">Likes: {{likes}}</slot>
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Consumer (receives data via template props):
|
|
228
|
+
```html
|
|
229
|
+
<wcc-card>
|
|
230
|
+
<template #stats="{ likes }">🔥 {{likes}} likes!</template>
|
|
231
|
+
</wcc-card>
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Lifecycle Hooks
|
|
235
|
+
|
|
236
|
+
```js
|
|
237
|
+
onMount(() => {
|
|
238
|
+
console.log('Component connected to DOM')
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
onDestroy(() => {
|
|
242
|
+
console.log('Component removed from DOM')
|
|
243
|
+
})
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## CSS Scoping
|
|
247
|
+
|
|
248
|
+
Styles are automatically scoped to the component using tag-name prefixing:
|
|
249
|
+
|
|
250
|
+
```css
|
|
251
|
+
/* Input */
|
|
252
|
+
.counter { display: flex; }
|
|
253
|
+
|
|
254
|
+
/* Output */
|
|
255
|
+
wcc-counter .counter { display: flex; }
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
`@media` rules are recursively scoped. `@keyframes` are preserved without prefixing.
|
|
259
|
+
|
|
260
|
+
## TypeScript
|
|
261
|
+
|
|
262
|
+
Use `.ts` files with full type support:
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
import { defineComponent, defineProps, signal, computed, templateBindings } from 'wcc'
|
|
266
|
+
|
|
267
|
+
const props = defineProps<{ title: string }>({ title: 'Demo' })
|
|
268
|
+
const count = signal<number>(0)
|
|
269
|
+
const doubled = computed<number>(() => count() * 2)
|
|
270
|
+
|
|
271
|
+
function increment(): void {
|
|
272
|
+
count.set(count() + 1)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
templateBindings({ doubled, increment })
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
`templateBindings()` declares which variables are used in the template, eliminating TypeScript "unused variable" warnings.
|
|
279
|
+
|
|
280
|
+
## CLI
|
|
281
|
+
|
|
282
|
+
```bash
|
|
283
|
+
wcc build # Compile all .ts/.js files from input/ to output/
|
|
284
|
+
wcc dev # Build + watch + live-reload dev server
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Configuration
|
|
58
288
|
|
|
59
289
|
Create `wcc.config.js` in your project root:
|
|
60
290
|
|
|
@@ -63,37 +293,24 @@ export default {
|
|
|
63
293
|
port: 4100, // dev server port
|
|
64
294
|
input: 'src', // source directory
|
|
65
295
|
output: 'dist' // output directory
|
|
66
|
-
}
|
|
296
|
+
}
|
|
67
297
|
```
|
|
68
298
|
|
|
69
299
|
All options are optional — defaults shown above.
|
|
70
300
|
|
|
71
|
-
##
|
|
72
|
-
|
|
73
|
-
- `{{var}}` text interpolation
|
|
74
|
-
- `defineProps([...])` for external props
|
|
75
|
-
- `const x = value` for reactive internal state
|
|
76
|
-
- `computed(() => expr)` for derived values
|
|
77
|
-
- `watch('prop', (new, old) => {...})` for side effects
|
|
78
|
-
- `@event="handler"` for DOM events
|
|
79
|
-
- `emit('name', data)` for custom events
|
|
80
|
-
- `<slot>`, `<slot name="x">`, scoped slots with slotProps
|
|
81
|
-
- `<style>` with automatic scoped CSS
|
|
82
|
-
- Zero runtime — output is pure vanilla JS
|
|
83
|
-
|
|
84
|
-
## Optional Runtime Helper
|
|
301
|
+
## Runtime Helper
|
|
85
302
|
|
|
86
|
-
An optional `wcc-runtime.js` is copied to your output directory for declarative bindings:
|
|
303
|
+
An optional `wcc-runtime.js` is copied to your output directory for declarative host-page bindings:
|
|
87
304
|
|
|
88
305
|
```html
|
|
89
|
-
<wcc-counter :
|
|
306
|
+
<wcc-counter :count="count" @change="handleChange"></wcc-counter>
|
|
90
307
|
|
|
91
308
|
<script type="module">
|
|
92
|
-
import './dist/wcc-counter.js'
|
|
93
|
-
import { init, set, get
|
|
309
|
+
import './dist/wcc-counter.js'
|
|
310
|
+
import { init, on, set, get } from './dist/wcc-runtime.js'
|
|
94
311
|
|
|
95
|
-
on('
|
|
96
|
-
init({
|
|
312
|
+
on('handleChange', (e) => set('count', e.detail))
|
|
313
|
+
init({ count: 0 })
|
|
97
314
|
</script>
|
|
98
315
|
```
|
|
99
316
|
|
package/bin/wcc.js
CHANGED
|
@@ -1,131 +1,96 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
*
|
|
6
|
-
* Commands:
|
|
7
|
-
* wcc build — Compile all .html files from input/ to .js in output/
|
|
8
|
-
* wcc dev — Build + watch input/ for changes + start dev server
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { readdir, writeFile, mkdir, watch, copyFile } from 'node:fs/promises';
|
|
12
|
-
import { existsSync, watchFile } from 'node:fs';
|
|
13
|
-
import { resolve, join, basename, dirname } from 'node:path';
|
|
3
|
+
import { readdirSync, writeFileSync, mkdirSync, existsSync, watch, copyFileSync } from 'node:fs';
|
|
4
|
+
import { resolve, relative, extname, basename, dirname, join } from 'node:path';
|
|
14
5
|
import { fileURLToPath } from 'node:url';
|
|
15
6
|
import { loadConfig } from '../lib/config.js';
|
|
16
7
|
import { compile } from '../lib/compiler.js';
|
|
17
8
|
import { startDevServer } from '../lib/dev-server.js';
|
|
18
9
|
|
|
19
|
-
const
|
|
10
|
+
const command = process.argv[2];
|
|
20
11
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
*/
|
|
25
|
-
async function compileFile(filePath, outputDir) {
|
|
26
|
-
const fileName = basename(filePath, '.html');
|
|
27
|
-
try {
|
|
28
|
-
const code = compile(filePath);
|
|
29
|
-
const outPath = join(outputDir, `${fileName}.js`);
|
|
30
|
-
await writeFile(outPath, code, 'utf-8');
|
|
31
|
-
return true;
|
|
32
|
-
} catch (err) {
|
|
33
|
-
console.error(`Error compilando '${basename(filePath)}': ${err.message}`);
|
|
34
|
-
return false;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
12
|
+
async function build(config, cwd) {
|
|
13
|
+
const inputDir = resolve(cwd, config.input);
|
|
14
|
+
const outputDir = resolve(cwd, config.output);
|
|
37
15
|
|
|
38
|
-
|
|
39
|
-
* Compile all .html files from inputDir to outputDir.
|
|
40
|
-
* Returns { success, errors } counts.
|
|
41
|
-
*/
|
|
42
|
-
async function buildAll(inputDir, outputDir) {
|
|
43
|
-
// Create output dir if needed
|
|
44
|
-
if (!existsSync(outputDir)) {
|
|
45
|
-
await mkdir(outputDir, { recursive: true });
|
|
46
|
-
}
|
|
16
|
+
if (!existsSync(outputDir)) mkdirSync(outputDir, { recursive: true });
|
|
47
17
|
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
try {
|
|
51
|
-
const entries = await readdir(inputDir);
|
|
52
|
-
files = entries.filter(f => f.endsWith('.html'));
|
|
53
|
-
} catch {
|
|
54
|
-
console.error(`Error: la carpeta de entrada '${inputDir}' no existe`);
|
|
55
|
-
process.exit(1);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
let success = 0;
|
|
18
|
+
// Discover source files
|
|
19
|
+
const files = discoverFiles(inputDir);
|
|
59
20
|
let errors = 0;
|
|
60
21
|
|
|
61
22
|
for (const file of files) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
23
|
+
try {
|
|
24
|
+
const output = await compile(file);
|
|
25
|
+
const relPath = relative(inputDir, file);
|
|
26
|
+
const outPath = resolve(outputDir, relPath.replace(/\.ts$/, '.js'));
|
|
27
|
+
const outDir = dirname(outPath);
|
|
28
|
+
if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
|
|
29
|
+
writeFileSync(outPath, output);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
console.error(`Error compiling ${file}: ${err.message}`);
|
|
32
|
+
errors++;
|
|
33
|
+
}
|
|
66
34
|
}
|
|
67
35
|
|
|
68
|
-
// Copy
|
|
36
|
+
// Copy wcc-runtime.js to output directory
|
|
69
37
|
const __filename = fileURLToPath(import.meta.url);
|
|
70
38
|
const __dirname = dirname(__filename);
|
|
71
39
|
const runtimeSrc = resolve(__dirname, '../lib/wcc-runtime.js');
|
|
72
40
|
const runtimeDest = join(outputDir, 'wcc-runtime.js');
|
|
73
|
-
|
|
41
|
+
copyFileSync(runtimeSrc, runtimeDest);
|
|
74
42
|
|
|
75
|
-
return
|
|
43
|
+
return errors;
|
|
76
44
|
}
|
|
77
45
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
process.exit(0);
|
|
46
|
+
function discoverFiles(dir) {
|
|
47
|
+
const results = [];
|
|
48
|
+
const entries = readdirSync(dir, { withFileTypes: true, recursive: true });
|
|
49
|
+
for (const entry of entries) {
|
|
50
|
+
if (!entry.isFile()) continue;
|
|
51
|
+
const ext = extname(entry.name);
|
|
52
|
+
if (ext !== '.ts' && ext !== '.js') continue;
|
|
53
|
+
if (entry.name.includes('.test.')) continue;
|
|
54
|
+
if (entry.name.endsWith('.d.ts')) continue;
|
|
55
|
+
const fullPath = resolve(dir, entry.parentPath ? relative(dir, entry.parentPath) : '', entry.name);
|
|
56
|
+
results.push(fullPath);
|
|
90
57
|
}
|
|
58
|
+
return results;
|
|
59
|
+
}
|
|
91
60
|
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
const
|
|
61
|
+
async function main() {
|
|
62
|
+
const cwd = process.cwd();
|
|
63
|
+
const config = await loadConfig(cwd);
|
|
95
64
|
|
|
96
65
|
if (command === 'build') {
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
66
|
+
const errors = await build(config, cwd);
|
|
67
|
+
if (errors > 0) process.exit(1);
|
|
68
|
+
} else if (command === 'dev') {
|
|
69
|
+
await build(config, cwd);
|
|
70
|
+
const outputDir = resolve(cwd, config.output);
|
|
71
|
+
startDevServer({ port: config.port, root: cwd, outputDir });
|
|
72
|
+
const inputDir = resolve(cwd, config.input);
|
|
73
|
+
console.log(`Watching ${inputDir} for changes...`);
|
|
74
|
+
watch(inputDir, { recursive: true }, async (eventType, filename) => {
|
|
75
|
+
if (!filename) return;
|
|
76
|
+
const ext = extname(filename);
|
|
77
|
+
if (ext !== '.ts' && ext !== '.js') return;
|
|
78
|
+
if (filename.includes('.test.')) return;
|
|
79
|
+
const filePath = resolve(inputDir, filename);
|
|
80
|
+
try {
|
|
81
|
+
const output = await compile(filePath);
|
|
82
|
+
const outPath = resolve(outputDir, filename.replace(/\.ts$/, '.js'));
|
|
83
|
+
const outDir = dirname(outPath);
|
|
84
|
+
if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
|
|
85
|
+
writeFileSync(outPath, output);
|
|
86
|
+
console.log(`Compiled: ${filename}`);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
console.error(`Error compiling ${filename}: ${err.message}`);
|
|
120
89
|
}
|
|
121
|
-
})();
|
|
122
|
-
|
|
123
|
-
// Start dev server
|
|
124
|
-
startDevServer({
|
|
125
|
-
port: config.port,
|
|
126
|
-
root: projectRoot,
|
|
127
|
-
outputDir,
|
|
128
90
|
});
|
|
91
|
+
} else {
|
|
92
|
+
console.error('Usage: wcc <build|dev>');
|
|
93
|
+
process.exit(1);
|
|
129
94
|
}
|
|
130
95
|
}
|
|
131
96
|
|
package/bin/wcc.test.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { execFileSync, execFile } from 'node:child_process';
|
|
3
|
+
import { mkdirSync, writeFileSync, rmSync, existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
4
|
+
import { resolve, join } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
8
|
+
const cliPath = resolve(__dirname, 'wcc.js');
|
|
9
|
+
|
|
10
|
+
describe('wcc CLI', () => {
|
|
11
|
+
const tmpDir = resolve(__dirname, '__tmp_cli_test__');
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
if (existsSync(tmpDir)) rmSync(tmpDir, { recursive: true });
|
|
15
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
16
|
+
mkdirSync(join(tmpDir, 'src'), { recursive: true });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
if (existsSync(tmpDir)) rmSync(tmpDir, { recursive: true });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
function writeComponent(name, dir = 'src') {
|
|
24
|
+
const srcDir = join(tmpDir, dir);
|
|
25
|
+
if (!existsSync(srcDir)) mkdirSync(srcDir, { recursive: true });
|
|
26
|
+
|
|
27
|
+
// Write a minimal component source
|
|
28
|
+
writeFileSync(join(srcDir, `${name}.js`), `
|
|
29
|
+
import { defineComponent, signal } from 'wcc'
|
|
30
|
+
|
|
31
|
+
export default defineComponent({
|
|
32
|
+
tag: '${name}',
|
|
33
|
+
template: './${name}.html',
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const count = signal(0)
|
|
37
|
+
|
|
38
|
+
function increment() {
|
|
39
|
+
count.set(count() + 1)
|
|
40
|
+
}
|
|
41
|
+
`);
|
|
42
|
+
// Write a minimal template
|
|
43
|
+
writeFileSync(join(srcDir, `${name}.html`), `<div>{{count}}</div>`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function writeConfig(config) {
|
|
47
|
+
writeFileSync(join(tmpDir, 'wcc.config.js'), `export default ${JSON.stringify(config)};\n`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
it('discovers .ts and .js files, excludes *.test.* and *.d.ts', () => {
|
|
51
|
+
// Create various files
|
|
52
|
+
writeComponent('wcc-counter');
|
|
53
|
+
writeFileSync(join(tmpDir, 'src', 'helper.test.js'), 'test file');
|
|
54
|
+
writeFileSync(join(tmpDir, 'src', 'types.d.ts'), 'declare module "x" {}');
|
|
55
|
+
writeFileSync(join(tmpDir, 'src', 'readme.md'), '# readme');
|
|
56
|
+
|
|
57
|
+
// Run build — it should only compile wcc-counter.js (not test, d.ts, or md files)
|
|
58
|
+
const result = execFileSync('node', [cliPath, 'build'], {
|
|
59
|
+
cwd: tmpDir,
|
|
60
|
+
encoding: 'utf-8',
|
|
61
|
+
timeout: 30000,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Check that output was created
|
|
65
|
+
expect(existsSync(join(tmpDir, 'dist', 'wcc-counter.js'))).toBe(true);
|
|
66
|
+
// Check that test/d.ts files were NOT compiled
|
|
67
|
+
expect(existsSync(join(tmpDir, 'dist', 'helper.test.js'))).toBe(false);
|
|
68
|
+
expect(existsSync(join(tmpDir, 'dist', 'types.d.ts'))).toBe(false);
|
|
69
|
+
expect(existsSync(join(tmpDir, 'dist', 'readme.md'))).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('writes compiled output to the configured output directory', () => {
|
|
73
|
+
writeComponent('wcc-app');
|
|
74
|
+
writeConfig({ output: 'out' });
|
|
75
|
+
|
|
76
|
+
execFileSync('node', [cliPath, 'build'], {
|
|
77
|
+
cwd: tmpDir,
|
|
78
|
+
encoding: 'utf-8',
|
|
79
|
+
timeout: 30000,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
expect(existsSync(join(tmpDir, 'out', 'wcc-app.js'))).toBe(true);
|
|
83
|
+
const content = readFileSync(join(tmpDir, 'out', 'wcc-app.js'), 'utf-8');
|
|
84
|
+
// Should contain the compiled component
|
|
85
|
+
expect(content).toContain('customElements.define');
|
|
86
|
+
expect(content).toContain('wcc-app');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
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;');
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
execFileSync('node', [cliPath, 'build'], {
|
|
95
|
+
cwd: tmpDir,
|
|
96
|
+
encoding: 'utf-8',
|
|
97
|
+
timeout: 30000,
|
|
98
|
+
});
|
|
99
|
+
// Should not reach here
|
|
100
|
+
expect.fail('Expected non-zero exit code');
|
|
101
|
+
} catch (err) {
|
|
102
|
+
expect(err.status).not.toBe(0);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('prints usage and exits with non-zero code for unknown command', () => {
|
|
107
|
+
try {
|
|
108
|
+
execFileSync('node', [cliPath, 'unknown'], {
|
|
109
|
+
cwd: tmpDir,
|
|
110
|
+
encoding: 'utf-8',
|
|
111
|
+
timeout: 30000,
|
|
112
|
+
});
|
|
113
|
+
expect.fail('Expected non-zero exit code');
|
|
114
|
+
} catch (err) {
|
|
115
|
+
expect(err.status).not.toBe(0);
|
|
116
|
+
expect(err.stderr).toContain('Usage');
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
});
|