@sprlab/wccompiler 0.0.2 → 0.2.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 `.html` files with Vue-like syntax into 100% native web components. No dependencies in the output — just vanilla JavaScript.
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
- ## Usage
11
+ ## Quick Start
12
12
 
13
- ### 1. Create a component
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
- <template>
18
- <div class="counter">
19
- <span>{{label}}</span>
20
- <span>{{count}}</span>
21
- <button @click="increment">+</button>
22
- </div>
23
- </template>
24
-
25
- <style>
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
- ### 2. Build
45
+ **2. Build**
40
46
 
41
47
  ```bash
42
48
  npx wcc build
43
49
  ```
44
50
 
45
- ### 3. Use
51
+ **3. Use**
46
52
 
47
53
  ```html
48
54
  <script type="module" src="dist/wcc-counter.js"></script>
49
- <wcc-counter label="Clicks:"></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
- ## Commands
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
- - `wcc build` — Compile all `.html` files from input to output
55
- - `wcc dev` — Build + watch + dev server with live-reload
184
+ ### Template Refs
56
185
 
57
- ## Configuration
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
- ## Features
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 :label="myLabel" @on-click="handler"></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, on } from './dist/wcc-runtime.js';
309
+ import './dist/wcc-counter.js'
310
+ import { init, on, set, get } from './dist/wcc-runtime.js'
94
311
 
95
- on('handler', (e) => console.log(e.detail));
96
- init({ myLabel: 'Clicks:' });
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
- * wcc CLI entry point for the wcCompiler.
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 projectRoot = process.cwd();
10
+ const command = process.argv[2];
20
11
 
21
- /**
22
- * Compile a single file and write the output.
23
- * Returns true on success, false on error.
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
- // Find all .html files
49
- let files;
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
- const filePath = join(inputDir, file);
63
- const ok = await compileFile(filePath, outputDir);
64
- if (ok) success++;
65
- else errors++;
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 optional wcc-runtime.js to output
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
- await copyFile(runtimeSrc, runtimeDest);
41
+ copyFileSync(runtimeSrc, runtimeDest);
74
42
 
75
- return { success, errors };
43
+ return errors;
76
44
  }
77
45
 
78
- // ── Main ──
79
-
80
- async function main() {
81
- const command = process.argv[2];
82
-
83
- if (!command || (command !== 'build' && command !== 'dev')) {
84
- console.log('Usage: wcc <command>');
85
- console.log('');
86
- console.log('Commands:');
87
- console.log(' build Compile all .html files');
88
- console.log(' dev Build + watch + dev server');
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
- const config = await loadConfig(projectRoot);
93
- const inputDir = resolve(projectRoot, config.input);
94
- const outputDir = resolve(projectRoot, config.output);
61
+ async function main() {
62
+ const cwd = process.cwd();
63
+ const config = await loadConfig(cwd);
95
64
 
96
65
  if (command === 'build') {
97
- const { success, errors } = await buildAll(inputDir, outputDir);
98
- console.log(`Build complete: ${success} compiled, ${errors} error(s)`);
99
- process.exit(errors > 0 ? 1 : 0);
100
- }
101
-
102
- if (command === 'dev') {
103
- // Initial build
104
- const { success, errors } = await buildAll(inputDir, outputDir);
105
- console.log(`Initial build: ${success} compiled, ${errors} error(s)`);
106
-
107
- // Watch input/ for changes
108
- console.log(`Watching ${config.input}/ for changes...`);
109
- const watcher = watch(inputDir, { recursive: true });
110
- (async () => {
111
- for await (const event of watcher) {
112
- if (event.filename && event.filename.endsWith('.html')) {
113
- const filePath = join(inputDir, event.filename);
114
- if (existsSync(filePath)) {
115
- console.log(`Change detected: ${event.filename}`);
116
- const ok = await compileFile(filePath, outputDir);
117
- if (ok) console.log(`Recompiled: ${event.filename}`);
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
 
@@ -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
+ });