@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 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
+ }
@@ -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
+ }