@sprlab/wccompiler 0.0.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 +102 -0
- package/bin/wcc.js +135 -0
- package/lib/codegen.js +324 -0
- package/lib/compiler.js +49 -0
- package/lib/config.js +61 -0
- package/lib/css-scoper.js +167 -0
- package/lib/dev-server.js +107 -0
- package/lib/parser.js +269 -0
- package/lib/printer.js +104 -0
- package/lib/reactive-runtime.js +61 -0
- package/lib/tree-walker.js +163 -0
- package/lib/wcc-runtime.js +42 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# wcCompiler
|
|
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.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -D wccompiler
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### 1. Create a component
|
|
14
|
+
|
|
15
|
+
```html
|
|
16
|
+
<!-- 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>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 2. Build
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npx wcc build
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 3. Use
|
|
46
|
+
|
|
47
|
+
```html
|
|
48
|
+
<script type="module" src="dist/wcc-counter.js"></script>
|
|
49
|
+
<wcc-counter label="Clicks:"></wcc-counter>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Commands
|
|
53
|
+
|
|
54
|
+
- `wcc build` — Compile all `.html` files from input to output
|
|
55
|
+
- `wcc dev` — Build + watch + dev server with live-reload
|
|
56
|
+
|
|
57
|
+
## Configuration
|
|
58
|
+
|
|
59
|
+
Create `wcc.config.js` in your project root:
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
export default {
|
|
63
|
+
port: 4100, // dev server port
|
|
64
|
+
input: 'src', // source directory
|
|
65
|
+
output: 'dist' // output directory
|
|
66
|
+
};
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
All options are optional — defaults shown above.
|
|
70
|
+
|
|
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
|
|
85
|
+
|
|
86
|
+
An optional `wcc-runtime.js` is copied to your output directory for declarative bindings:
|
|
87
|
+
|
|
88
|
+
```html
|
|
89
|
+
<wcc-counter :label="myLabel" @on-click="handler"></wcc-counter>
|
|
90
|
+
|
|
91
|
+
<script type="module">
|
|
92
|
+
import './dist/wcc-counter.js';
|
|
93
|
+
import { init, set, get, on } from './dist/wcc-runtime.js';
|
|
94
|
+
|
|
95
|
+
on('handler', (e) => console.log(e.detail));
|
|
96
|
+
init({ myLabel: 'Clicks:' });
|
|
97
|
+
</script>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT
|
package/bin/wcc.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
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';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { loadConfig } from '../lib/config.js';
|
|
16
|
+
import { compile } from '../lib/compiler.js';
|
|
17
|
+
import { startDevServer } from '../lib/dev-server.js';
|
|
18
|
+
|
|
19
|
+
const projectRoot = process.cwd();
|
|
20
|
+
|
|
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
|
+
}
|
|
37
|
+
|
|
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
|
+
}
|
|
47
|
+
|
|
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;
|
|
59
|
+
let errors = 0;
|
|
60
|
+
|
|
61
|
+
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++;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Copy optional wcc-runtime.js to output
|
|
69
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
70
|
+
const __dirname = dirname(__filename);
|
|
71
|
+
const runtimeSrc = resolve(__dirname, '../lib/wcc-runtime.js');
|
|
72
|
+
const runtimeDest = join(outputDir, 'wcc-runtime.js');
|
|
73
|
+
await copyFile(runtimeSrc, runtimeDest);
|
|
74
|
+
|
|
75
|
+
return { success, errors };
|
|
76
|
+
}
|
|
77
|
+
|
|
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);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const config = await loadConfig(projectRoot);
|
|
93
|
+
const inputDir = resolve(projectRoot, config.input);
|
|
94
|
+
const outputDir = resolve(projectRoot, config.output);
|
|
95
|
+
|
|
96
|
+
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
|
+
}
|
|
120
|
+
}
|
|
121
|
+
})();
|
|
122
|
+
|
|
123
|
+
// Start dev server
|
|
124
|
+
startDevServer({
|
|
125
|
+
port: config.port,
|
|
126
|
+
root: projectRoot,
|
|
127
|
+
outputDir,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
main().catch(err => {
|
|
133
|
+
console.error(err.message);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
});
|
package/lib/codegen.js
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code Generator for compiled web components.
|
|
3
|
+
*
|
|
4
|
+
* Takes a complete ParseResult (with bindings, events, slots populated by tree-walker)
|
|
5
|
+
* and produces a self-contained JavaScript string with:
|
|
6
|
+
* - Inline mini reactive runtime (zero imports)
|
|
7
|
+
* - Scoped CSS injection
|
|
8
|
+
* - HTMLElement class with signals, effects, slots, events
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { reactiveRuntime } from './reactive-runtime.js';
|
|
12
|
+
import { scopeCSS } from './css-scoper.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Convert a path array to a JS expression string.
|
|
16
|
+
* Inlined here to avoid pulling in jsdom via tree-walker.js.
|
|
17
|
+
* e.g. pathExpr(['childNodes[0]', 'childNodes[1]'], '__root') => '__root.childNodes[0].childNodes[1]'
|
|
18
|
+
*/
|
|
19
|
+
function pathExpr(parts, rootVar) {
|
|
20
|
+
return parts.length === 0 ? rootVar : rootVar + '.' + parts.join('.');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Generate a fully self-contained JS component from a ParseResult.
|
|
25
|
+
*
|
|
26
|
+
* @param {import('./parser.js').ParseResult} parseResult - Complete IR with bindings/events/slots
|
|
27
|
+
* @returns {string} JavaScript source code
|
|
28
|
+
*/
|
|
29
|
+
export function generateComponent(parseResult) {
|
|
30
|
+
const {
|
|
31
|
+
tagName,
|
|
32
|
+
className,
|
|
33
|
+
style,
|
|
34
|
+
props,
|
|
35
|
+
reactiveVars,
|
|
36
|
+
computeds,
|
|
37
|
+
watchers,
|
|
38
|
+
methods,
|
|
39
|
+
bindings,
|
|
40
|
+
events,
|
|
41
|
+
slots,
|
|
42
|
+
processedTemplate,
|
|
43
|
+
} = parseResult;
|
|
44
|
+
|
|
45
|
+
const propsSet = new Set(props);
|
|
46
|
+
const computedNames = new Set(computeds.map(c => c.name));
|
|
47
|
+
const rootVarNames = new Set(reactiveVars.map(v => v.name));
|
|
48
|
+
|
|
49
|
+
const lines = [];
|
|
50
|
+
|
|
51
|
+
// ── 1. Inline reactive runtime (task 6.1) ──
|
|
52
|
+
lines.push(reactiveRuntime.trim());
|
|
53
|
+
lines.push('');
|
|
54
|
+
|
|
55
|
+
// ── 2. CSS injection (task 6.6) ──
|
|
56
|
+
if (style) {
|
|
57
|
+
const scoped = scopeCSS(style, tagName);
|
|
58
|
+
lines.push(`const __css_${className} = document.createElement('style');`);
|
|
59
|
+
lines.push(`__css_${className}.textContent = \`${scoped}\`;`);
|
|
60
|
+
lines.push(`document.head.appendChild(__css_${className});`);
|
|
61
|
+
lines.push('');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── 3. Template ──
|
|
65
|
+
lines.push(`const __t_${className} = document.createElement('template');`);
|
|
66
|
+
lines.push(`__t_${className}.innerHTML = \`${processedTemplate}\`;`);
|
|
67
|
+
lines.push('');
|
|
68
|
+
|
|
69
|
+
// ── 4. Class definition (task 6.1) ──
|
|
70
|
+
lines.push(`class ${className} extends HTMLElement {`);
|
|
71
|
+
|
|
72
|
+
// observedAttributes
|
|
73
|
+
lines.push(' static get observedAttributes() {');
|
|
74
|
+
lines.push(` return [${props.map(p => `'${p}'`).join(', ')}];`);
|
|
75
|
+
lines.push(' }');
|
|
76
|
+
lines.push('');
|
|
77
|
+
|
|
78
|
+
// constructor
|
|
79
|
+
lines.push(' constructor() {');
|
|
80
|
+
lines.push(' super();');
|
|
81
|
+
|
|
82
|
+
// Slot resolution code (task 6.5) — must read childNodes BEFORE replacing innerHTML
|
|
83
|
+
if (slots.length > 0) {
|
|
84
|
+
lines.push(' const __slotMap = {};');
|
|
85
|
+
lines.push(' const __defaultSlotNodes = [];');
|
|
86
|
+
lines.push(' for (const child of Array.from(this.childNodes)) {');
|
|
87
|
+
lines.push(" if (child.nodeName === 'TEMPLATE') {");
|
|
88
|
+
lines.push(' for (const attr of child.attributes) {');
|
|
89
|
+
lines.push(" if (attr.name.startsWith('#')) {");
|
|
90
|
+
lines.push(' const slotName = attr.name.slice(1);');
|
|
91
|
+
lines.push(' __slotMap[slotName] = { content: child.innerHTML, propsExpr: attr.value };');
|
|
92
|
+
lines.push(' }');
|
|
93
|
+
lines.push(' }');
|
|
94
|
+
lines.push(" } else if (child.nodeType === 1 || (child.nodeType === 3 && child.textContent.trim())) {");
|
|
95
|
+
lines.push(' __defaultSlotNodes.push(child);');
|
|
96
|
+
lines.push(' }');
|
|
97
|
+
lines.push(' }');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Clone template and assign DOM refs
|
|
101
|
+
lines.push(` const __root = __t_${className}.content.cloneNode(true);`);
|
|
102
|
+
|
|
103
|
+
const allNodes = [...bindings, ...events, ...slots];
|
|
104
|
+
for (const n of allNodes) {
|
|
105
|
+
lines.push(` this.${n.varName} = ${pathExpr(n.path, '__root')};`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
lines.push(" this.innerHTML = '';");
|
|
109
|
+
lines.push(' this.appendChild(__root);');
|
|
110
|
+
|
|
111
|
+
// Static slot injection (task 6.5)
|
|
112
|
+
for (const s of slots) {
|
|
113
|
+
if (s.name && s.slotProps.length > 0) {
|
|
114
|
+
// Scoped slot: store template for reactive effect
|
|
115
|
+
lines.push(` if (__slotMap['${s.name}']) { this.__slotTpl_${s.name} = __slotMap['${s.name}'].content; }`);
|
|
116
|
+
} else if (s.name) {
|
|
117
|
+
// Named slot: inject content directly
|
|
118
|
+
lines.push(` if (__slotMap['${s.name}']) { this.${s.varName}.innerHTML = __slotMap['${s.name}'].content; }`);
|
|
119
|
+
} else {
|
|
120
|
+
// Default slot
|
|
121
|
+
lines.push(` if (__defaultSlotNodes.length) { this.${s.varName}.textContent = ''; __defaultSlotNodes.forEach(n => this.${s.varName}.appendChild(n.cloneNode(true))); }`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Signal inits (task 6.2)
|
|
126
|
+
for (const p of props) {
|
|
127
|
+
lines.push(` this._s_${p} = __signal(null);`);
|
|
128
|
+
}
|
|
129
|
+
for (const v of reactiveVars) {
|
|
130
|
+
lines.push(` this._${v.name} = __signal(${v.value});`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Computed inits (task 6.2)
|
|
134
|
+
for (const c of computeds) {
|
|
135
|
+
const body = transformExpr(c.body, propsSet, rootVarNames, computedNames);
|
|
136
|
+
lines.push(` this._c_${c.name} = __computed(() => ${body});`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Watcher prev inits (task 6.3)
|
|
140
|
+
for (const w of watchers) {
|
|
141
|
+
lines.push(` this.__prev_${w.target} = undefined;`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
lines.push(' }');
|
|
145
|
+
lines.push('');
|
|
146
|
+
|
|
147
|
+
// connectedCallback
|
|
148
|
+
lines.push(' connectedCallback() {');
|
|
149
|
+
|
|
150
|
+
// Binding effects (task 6.2)
|
|
151
|
+
if (bindings.length > 0) {
|
|
152
|
+
lines.push(' __effect(() => {');
|
|
153
|
+
for (const b of bindings) {
|
|
154
|
+
lines.push(` this.${b.varName}.textContent = ${bindingRef(b)} ?? '';`);
|
|
155
|
+
}
|
|
156
|
+
lines.push(' });');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Reactive slot effects (task 6.5)
|
|
160
|
+
for (const s of slots) {
|
|
161
|
+
if (s.name && s.slotProps.length > 0) {
|
|
162
|
+
const propsObj = s.slotProps.map(sp => `${sp.prop}: ${slotPropRef(sp.source, propsSet, computedNames, rootVarNames)}`).join(', ');
|
|
163
|
+
lines.push(` if (this.__slotTpl_${s.name}) {`);
|
|
164
|
+
lines.push(' __effect(() => {');
|
|
165
|
+
lines.push(` const __props = { ${propsObj} };`);
|
|
166
|
+
lines.push(` let __html = this.__slotTpl_${s.name};`);
|
|
167
|
+
lines.push(" for (const [k, v] of Object.entries(__props)) {");
|
|
168
|
+
lines.push(` __html = __html.replace(new RegExp('\\\\{\\\\{\\\\s*' + k + '\\\\s*\\\\}\\\\}', 'g'), v ?? '');`);
|
|
169
|
+
lines.push(' }');
|
|
170
|
+
lines.push(` this.${s.varName}.innerHTML = __html;`);
|
|
171
|
+
lines.push(' });');
|
|
172
|
+
lines.push(' }');
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Watcher effects (task 6.3)
|
|
177
|
+
for (const w of watchers) {
|
|
178
|
+
const watchRef = signalRef(w.target, propsSet, computedNames, rootVarNames);
|
|
179
|
+
let body = transformExpr(w.body, propsSet, rootVarNames, computedNames);
|
|
180
|
+
body = body.replace(/\bemit\(/g, 'this._emit(');
|
|
181
|
+
lines.push(' __effect(() => {');
|
|
182
|
+
lines.push(` const ${w.newParam} = ${watchRef};`);
|
|
183
|
+
lines.push(` if (this.__prev_${w.target} !== undefined) {`);
|
|
184
|
+
lines.push(` const ${w.oldParam} = this.__prev_${w.target};`);
|
|
185
|
+
lines.push(` ${body}`);
|
|
186
|
+
lines.push(' }');
|
|
187
|
+
lines.push(` this.__prev_${w.target} = ${w.newParam};`);
|
|
188
|
+
lines.push(' });');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Event listeners (task 6.4)
|
|
192
|
+
for (const e of events) {
|
|
193
|
+
lines.push(` this.${e.varName}.addEventListener('${e.event}', this._${e.handler}.bind(this));`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
lines.push(' }');
|
|
197
|
+
lines.push('');
|
|
198
|
+
|
|
199
|
+
// attributeChangedCallback (task 6.1)
|
|
200
|
+
lines.push(' attributeChangedCallback(name, oldValue, newValue) {');
|
|
201
|
+
for (const p of props) {
|
|
202
|
+
lines.push(` if (name === '${p}') this._s_${p}(newValue);`);
|
|
203
|
+
}
|
|
204
|
+
lines.push(' }');
|
|
205
|
+
lines.push('');
|
|
206
|
+
|
|
207
|
+
// Public getters/setters (task 6.2)
|
|
208
|
+
for (const p of props) {
|
|
209
|
+
lines.push(` get ${p}() { return this._s_${p}(); }`);
|
|
210
|
+
lines.push(` set ${p}(val) { this._s_${p}(val); }`);
|
|
211
|
+
lines.push('');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// _emit method (task 6.4)
|
|
215
|
+
if (events.length > 0 || hasEmitCall(methods)) {
|
|
216
|
+
lines.push(' _emit(name, detail) {');
|
|
217
|
+
lines.push(' this.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true }));');
|
|
218
|
+
lines.push(' }');
|
|
219
|
+
lines.push('');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// User methods (task 6.4 — transform emit, variable refs)
|
|
223
|
+
for (const m of methods) {
|
|
224
|
+
let body = transformMethodBody(m.body, propsSet, rootVarNames, computedNames);
|
|
225
|
+
lines.push(` _${m.name}(${m.params}) {`);
|
|
226
|
+
lines.push(` ${body}`);
|
|
227
|
+
lines.push(' }');
|
|
228
|
+
lines.push('');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
lines.push('}');
|
|
232
|
+
lines.push('');
|
|
233
|
+
|
|
234
|
+
// customElements.define (task 6.1)
|
|
235
|
+
lines.push(`customElements.define('${tagName}', ${className});`);
|
|
236
|
+
lines.push('');
|
|
237
|
+
|
|
238
|
+
return lines.join('\n');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ── Helpers ──────────────────────────────────────────────────────────
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Transform an expression by replacing bare variable references with signal calls.
|
|
245
|
+
* Props → this._s_name(), reactive vars → this._name(), computeds → this._c_name()
|
|
246
|
+
*/
|
|
247
|
+
function transformExpr(expr, propsSet, rootVarNames, computedNames) {
|
|
248
|
+
let r = expr;
|
|
249
|
+
for (const p of propsSet) {
|
|
250
|
+
r = r.replace(new RegExp(`\\b${p}\\b`, 'g'), `this._s_${p}()`);
|
|
251
|
+
}
|
|
252
|
+
for (const n of rootVarNames) {
|
|
253
|
+
r = r.replace(new RegExp(`\\b${n}\\b`, 'g'), `this._${n}()`);
|
|
254
|
+
}
|
|
255
|
+
for (const n of computedNames) {
|
|
256
|
+
r = r.replace(new RegExp(`\\b${n}\\b`, 'g'), `this._c_${n}()`);
|
|
257
|
+
}
|
|
258
|
+
return r;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Transform a method body: replace variable refs, assignments, and emit calls.
|
|
263
|
+
*/
|
|
264
|
+
function transformMethodBody(body, propsSet, rootVarNames, computedNames) {
|
|
265
|
+
let r = body;
|
|
266
|
+
|
|
267
|
+
// Replace assignments: `const varName = expr;` or `varName = expr;` → `this._varName(expr);`
|
|
268
|
+
for (const n of rootVarNames) {
|
|
269
|
+
r = r.replace(new RegExp(`(?:const|let|var)\\s+${n}\\s*=\\s*(.+?);?\\s*$`, 'gm'), `this._${n}($1);`);
|
|
270
|
+
r = r.replace(new RegExp(`(?<!\\.)\\b${n}\\s*=\\s*(.+?);?\\s*$`, 'gm'), `this._${n}($1);`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Replace reads (not followed by = or preceded by this._)
|
|
274
|
+
for (const n of rootVarNames) {
|
|
275
|
+
r = r.replace(new RegExp(`(?<!this\\._)(?<!\\.)\\b${n}\\b(?!\\s*[=(])`, 'g'), `this._${n}()`);
|
|
276
|
+
}
|
|
277
|
+
for (const p of propsSet) {
|
|
278
|
+
r = r.replace(new RegExp(`(?<!this\\._s_)(?<!\\.)\\b${p}\\b(?!\\s*[=(])`, 'g'), `this._s_${p}()`);
|
|
279
|
+
}
|
|
280
|
+
for (const n of computedNames) {
|
|
281
|
+
r = r.replace(new RegExp(`(?<!this\\._c_)(?<!\\.)\\b${n}\\b`, 'g'), `this._c_${n}()`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Replace emit() → this._emit()
|
|
285
|
+
r = r.replace(/\bemit\(/g, 'this._emit(');
|
|
286
|
+
|
|
287
|
+
return r;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Get the signal reference expression for a binding.
|
|
292
|
+
*/
|
|
293
|
+
function bindingRef(b) {
|
|
294
|
+
if (b.type === 'prop') return `this._s_${b.name}()`;
|
|
295
|
+
if (b.type === 'computed') return `this._c_${b.name}()`;
|
|
296
|
+
return `this._${b.name}()`;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Get the signal reference for a watcher target or slot prop source.
|
|
301
|
+
*/
|
|
302
|
+
function signalRef(name, propsSet, computedNames, rootVarNames) {
|
|
303
|
+
if (propsSet.has(name)) return `this._s_${name}()`;
|
|
304
|
+
if (computedNames.has(name)) return `this._c_${name}()`;
|
|
305
|
+
if (rootVarNames.has(name)) return `this._${name}()`;
|
|
306
|
+
return `this._${name}()`;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Get the signal reference for a slot prop source.
|
|
311
|
+
*/
|
|
312
|
+
function slotPropRef(source, propsSet, computedNames, rootVarNames) {
|
|
313
|
+
if (propsSet.has(source)) return `this._s_${source}()`;
|
|
314
|
+
if (computedNames.has(source)) return `this._c_${source}()`;
|
|
315
|
+
if (rootVarNames.has(source)) return `this._${source}()`;
|
|
316
|
+
return `'${source}'`;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Check if any method body contains an emit() call.
|
|
321
|
+
*/
|
|
322
|
+
function hasEmitCall(methods) {
|
|
323
|
+
return methods.some(m => /\bemit\(/.test(m.body));
|
|
324
|
+
}
|
package/lib/compiler.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compiler — integrates parser, tree-walker, css-scoper, and codegen
|
|
3
|
+
* into a single compile(filePath, config) function.
|
|
4
|
+
*
|
|
5
|
+
* This is the main entry point for compiling a .html source file
|
|
6
|
+
* into a self-contained .js web component.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFileSync } from 'node:fs';
|
|
10
|
+
import { basename } from 'node:path';
|
|
11
|
+
import { JSDOM } from 'jsdom';
|
|
12
|
+
import { parse } from './parser.js';
|
|
13
|
+
import { walkTree } from './tree-walker.js';
|
|
14
|
+
import { generateComponent } from './codegen.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Compile a single .html source file into a self-contained JS component string.
|
|
18
|
+
*
|
|
19
|
+
* @param {string} filePath - Absolute or relative path to the .html source file
|
|
20
|
+
* @param {object} [config] - Optional config (currently unused, reserved for future options)
|
|
21
|
+
* @returns {string} The generated JavaScript component code
|
|
22
|
+
*/
|
|
23
|
+
export function compile(filePath, config) {
|
|
24
|
+
// 1. Read the source file
|
|
25
|
+
const html = readFileSync(filePath, 'utf-8');
|
|
26
|
+
const fileName = basename(filePath);
|
|
27
|
+
|
|
28
|
+
// 2. Parse the HTML into the IR
|
|
29
|
+
const parseResult = parse(html, fileName);
|
|
30
|
+
|
|
31
|
+
// 3. Create a jsdom DOM from the template and walk it
|
|
32
|
+
const dom = new JSDOM(`<div id="__root">${parseResult.template}</div>`);
|
|
33
|
+
const rootEl = dom.window.document.getElementById('__root');
|
|
34
|
+
|
|
35
|
+
const propsSet = new Set(parseResult.props);
|
|
36
|
+
const computedNames = new Set(parseResult.computeds.map(c => c.name));
|
|
37
|
+
const rootVarNames = new Set(parseResult.reactiveVars.map(v => v.name));
|
|
38
|
+
|
|
39
|
+
const { bindings, events, slots } = walkTree(rootEl, propsSet, computedNames, rootVarNames);
|
|
40
|
+
|
|
41
|
+
// 4. Update the parseResult with tree-walker results
|
|
42
|
+
parseResult.bindings = bindings;
|
|
43
|
+
parseResult.events = events;
|
|
44
|
+
parseResult.slots = slots;
|
|
45
|
+
parseResult.processedTemplate = rootEl.innerHTML;
|
|
46
|
+
|
|
47
|
+
// 5. Generate the component code
|
|
48
|
+
return generateComponent(parseResult);
|
|
49
|
+
}
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { pathToFileURL } from 'node:url';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { resolve, join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const DEFAULTS = {
|
|
6
|
+
port: 4100,
|
|
7
|
+
input: 'src',
|
|
8
|
+
output: 'dist',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Load and validate wcc.config.js from the given project root.
|
|
13
|
+
* Returns defaults if the config file doesn't exist.
|
|
14
|
+
*
|
|
15
|
+
* @param {string} projectRoot - Absolute path to the project root
|
|
16
|
+
* @returns {Promise<{port: number, input: string, output: string}>}
|
|
17
|
+
*/
|
|
18
|
+
export async function loadConfig(projectRoot) {
|
|
19
|
+
const configPath = resolve(projectRoot, 'wcc.config.js');
|
|
20
|
+
|
|
21
|
+
if (!existsSync(configPath)) {
|
|
22
|
+
return { ...DEFAULTS };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const fileUrl = pathToFileURL(configPath).href;
|
|
26
|
+
const mod = await import(fileUrl);
|
|
27
|
+
const raw = mod.default ?? mod;
|
|
28
|
+
|
|
29
|
+
const config = { ...DEFAULTS };
|
|
30
|
+
const errors = [];
|
|
31
|
+
|
|
32
|
+
if ('port' in raw) {
|
|
33
|
+
if (typeof raw.port !== 'number' || !Number.isFinite(raw.port)) {
|
|
34
|
+
errors.push("la propiedad 'port' debe ser un número válido");
|
|
35
|
+
} else {
|
|
36
|
+
config.port = raw.port;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if ('input' in raw) {
|
|
41
|
+
if (typeof raw.input !== 'string' || raw.input.trim() === '') {
|
|
42
|
+
errors.push("la propiedad 'input' debe ser un string no vacío");
|
|
43
|
+
} else {
|
|
44
|
+
config.input = raw.input;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if ('output' in raw) {
|
|
49
|
+
if (typeof raw.output !== 'string' || raw.output.trim() === '') {
|
|
50
|
+
errors.push("la propiedad 'output' debe ser un string no vacío");
|
|
51
|
+
} else {
|
|
52
|
+
config.output = raw.output;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (errors.length > 0) {
|
|
57
|
+
throw new Error(`Error en wcc.config.js: ${errors.join('; ')}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return config;
|
|
61
|
+
}
|