@vorra/cli 0.3.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/CHANGELOG.md +26 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +748 -0
- package/dist/bin.js.map +1 -0
- package/dist/commands/build.d.ts +9 -0
- package/dist/commands/build.d.ts.map +1 -0
- package/dist/commands/build.js +42 -0
- package/dist/commands/build.js.map +1 -0
- package/dist/commands/dev.d.ts +10 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/dev.js +221 -0
- package/dist/commands/dev.js.map +1 -0
- package/dist/commands/new.d.ts +19 -0
- package/dist/commands/new.d.ts.map +1 -0
- package/dist/commands/new.js +235 -0
- package/dist/commands/new.js.map +1 -0
- package/dist/commands/typecheck.d.ts +7 -0
- package/dist/commands/typecheck.d.ts.map +1 -0
- package/dist/commands/typecheck.js +26 -0
- package/dist/commands/typecheck.js.map +1 -0
- package/dist/index.cjs +24 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/utils/config.d.ts +11 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +43 -0
- package/dist/utils/config.js.map +1 -0
- package/package.json +52 -0
package/dist/bin.js
ADDED
|
@@ -0,0 +1,748 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { build, rolldown } from "rolldown";
|
|
5
|
+
import { generateScopeId, vorraPlugin } from "@vorra/compiler";
|
|
6
|
+
import * as url from "node:url";
|
|
7
|
+
import { createRequire } from "node:module";
|
|
8
|
+
import * as http from "node:http";
|
|
9
|
+
import { spawnSync } from "node:child_process";
|
|
10
|
+
//#region src/utils/config.ts
|
|
11
|
+
const CONFIG_CANDIDATES = [
|
|
12
|
+
"vorra.config.js",
|
|
13
|
+
"vorra.config.mjs",
|
|
14
|
+
"vorra.config.cjs"
|
|
15
|
+
];
|
|
16
|
+
/**
|
|
17
|
+
* Loads vorra.config.{js,mjs,cjs} from `cwd` and returns the config object.
|
|
18
|
+
* Returns an empty object if no config file is found.
|
|
19
|
+
*
|
|
20
|
+
* Note: `.ts` configs are not supported at CLI runtime without a TS loader.
|
|
21
|
+
* Compile `vorra.config.ts` to `.js` first, or run with `tsx`:
|
|
22
|
+
* npx tsx node_modules/.bin/forge dev
|
|
23
|
+
*/
|
|
24
|
+
async function loadConfig(cwd) {
|
|
25
|
+
for (const candidate of CONFIG_CANDIDATES) {
|
|
26
|
+
const configPath = path.join(cwd, candidate);
|
|
27
|
+
if (fs.existsSync(configPath)) {
|
|
28
|
+
const mod = await import(url.pathToFileURL(configPath).href);
|
|
29
|
+
if (mod.default !== null && mod.default !== void 0 && typeof mod.default === "object") return mod.default;
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const tsConfigPath = path.join(cwd, "vorra.config.ts");
|
|
34
|
+
if (fs.existsSync(tsConfigPath)) console.warn("[Vorra CLI] vorra.config.ts found but cannot be loaded without a TS loader.\n Tip: Compile it first (tsc vorra.config.ts) or run via: npx tsx .../bin.js dev");
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/utils/vorra-dedupe-plugin.ts
|
|
39
|
+
const _require = createRequire(import.meta.url);
|
|
40
|
+
/**
|
|
41
|
+
* Resolves an `@vorra/*` package to an absolute path inside its `dist/`
|
|
42
|
+
* directory. Uses the CJS main entry as an anchor so we never need to access
|
|
43
|
+
* `pkg/package.json` directly (which would require the package's `exports`
|
|
44
|
+
* field to allow that subpath).
|
|
45
|
+
*/
|
|
46
|
+
function resolveVorraPackage(name, subpath = "dist/index.js") {
|
|
47
|
+
const mainCjs = _require.resolve(name);
|
|
48
|
+
const pkgRoot = path.dirname(path.dirname(mainCjs));
|
|
49
|
+
return path.join(pkgRoot, subpath);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Lazily-built alias map. Populated on first use inside `resolveId` so that
|
|
53
|
+
* importing this module never triggers `_require.resolve()` at load time.
|
|
54
|
+
* This keeps the module safe to import in test environments where `dist/`
|
|
55
|
+
* files may not exist yet.
|
|
56
|
+
*/
|
|
57
|
+
let _aliases;
|
|
58
|
+
function getVorraAliases() {
|
|
59
|
+
if (_aliases === void 0) _aliases = {
|
|
60
|
+
"@vorra/core": resolveVorraPackage("@vorra/core"),
|
|
61
|
+
"@vorra/core/dom": resolveVorraPackage("@vorra/core", "dist/dom.js"),
|
|
62
|
+
"@vorra/core/reactivity": resolveVorraPackage("@vorra/core", "dist/reactivity.js"),
|
|
63
|
+
"@vorra/core/di": resolveVorraPackage("@vorra/core", "dist/di.js"),
|
|
64
|
+
"@vorra/forms": resolveVorraPackage("@vorra/forms"),
|
|
65
|
+
"@vorra/router": resolveVorraPackage("@vorra/router")
|
|
66
|
+
};
|
|
67
|
+
return _aliases;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Rolldown plugin that forces all `@vorra/*` imports to their canonical dist
|
|
71
|
+
* entry points. A `resolveId` hook applies unconditionally to every import —
|
|
72
|
+
* including those inside pre-built dist files — unlike `resolve.alias` which
|
|
73
|
+
* Rolldown skips for files it considers already-resolved.
|
|
74
|
+
*/
|
|
75
|
+
const vorraDedupePlugin = {
|
|
76
|
+
name: "vorra-dedupe",
|
|
77
|
+
resolveId(id) {
|
|
78
|
+
const resolved = getVorraAliases()[id];
|
|
79
|
+
if (resolved !== void 0) return {
|
|
80
|
+
id: resolved,
|
|
81
|
+
external: false
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
//#endregion
|
|
86
|
+
//#region src/commands/build.ts
|
|
87
|
+
/**
|
|
88
|
+
* Runs a production build.
|
|
89
|
+
*
|
|
90
|
+
* CLI flags (override vorra.config.js):
|
|
91
|
+
* --entry <path> Entry file (default: src/main.ts)
|
|
92
|
+
* --outDir <path> Output directory (default: dist)
|
|
93
|
+
*/
|
|
94
|
+
async function runBuild(args) {
|
|
95
|
+
const cwd = process.cwd();
|
|
96
|
+
const config = await loadConfig(cwd);
|
|
97
|
+
const entryIdx = args.indexOf("--entry");
|
|
98
|
+
const outDirIdx = args.indexOf("--outDir");
|
|
99
|
+
const entry = (entryIdx !== -1 ? args[entryIdx + 1] : void 0) ?? config.entry ?? "src/main.ts";
|
|
100
|
+
const outDir = (outDirIdx !== -1 ? args[outDirIdx + 1] : void 0) ?? config.outDir ?? "dist";
|
|
101
|
+
const entryAbs = path.join(cwd, entry);
|
|
102
|
+
const outDirAbs = path.join(cwd, outDir);
|
|
103
|
+
const entryName = path.basename(entry, path.extname(entry));
|
|
104
|
+
const userPlugins = config.plugins ?? [];
|
|
105
|
+
const plugins = [
|
|
106
|
+
vorraDedupePlugin,
|
|
107
|
+
vorraPlugin({
|
|
108
|
+
...config.css ? { css: path.join(cwd, config.css) } : {},
|
|
109
|
+
...config.postcss ? { postcss: config.postcss } : {}
|
|
110
|
+
}),
|
|
111
|
+
...userPlugins
|
|
112
|
+
];
|
|
113
|
+
console.log(`[vorra build] ${entry} → ${outDir}/`);
|
|
114
|
+
const build = await rolldown({
|
|
115
|
+
input: entryAbs,
|
|
116
|
+
plugins
|
|
117
|
+
});
|
|
118
|
+
const sourcemap = config.sourcemap ?? false;
|
|
119
|
+
await build.write({
|
|
120
|
+
dir: outDirAbs,
|
|
121
|
+
format: "es",
|
|
122
|
+
sourcemap,
|
|
123
|
+
entryFileNames: "[name].js",
|
|
124
|
+
chunkFileNames: "[name]-[hash].js"
|
|
125
|
+
});
|
|
126
|
+
const htmlSrc = path.join(cwd, "index.html");
|
|
127
|
+
if (fs.existsSync(htmlSrc)) {
|
|
128
|
+
const outDirRel = path.relative(cwd, outDirAbs).replace(/\\/g, "/");
|
|
129
|
+
const outDirPattern = new RegExp(`((?:src|href)=["'])(?:\\./)?(${escapeRegExp(outDirRel)}/)`, "g");
|
|
130
|
+
let html = fs.readFileSync(htmlSrc, "utf8");
|
|
131
|
+
html = html.replace(outDirPattern, "$1");
|
|
132
|
+
if (!/<script\s[^>]*type=["']module["']/i.test(html)) {
|
|
133
|
+
const tag = ` <script type="module" src="./${entryName}.js"><\/script>`;
|
|
134
|
+
html = html.includes("</body>") ? html.replace("</body>", `${tag}\n</body>`) : html + tag;
|
|
135
|
+
}
|
|
136
|
+
fs.writeFileSync(path.join(outDirAbs, "index.html"), html, "utf8");
|
|
137
|
+
console.log(`[vorra build] Copied index.html → ${outDir}/index.html`);
|
|
138
|
+
} else console.warn("[vorra build] No index.html found in project root — skipping HTML copy.");
|
|
139
|
+
console.log("[vorra build] Done.");
|
|
140
|
+
}
|
|
141
|
+
/** Escapes special regex characters in a literal string. */
|
|
142
|
+
function escapeRegExp(str) {
|
|
143
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
144
|
+
}
|
|
145
|
+
//#endregion
|
|
146
|
+
//#region src/commands/dev.ts
|
|
147
|
+
const HMR_ENDPOINT = "/__vorra_hmr";
|
|
148
|
+
/**
|
|
149
|
+
* Injected before </body> in every HTML response.
|
|
150
|
+
*
|
|
151
|
+
* Sets up window.__vorra_hmr with an instance registry, then opens an SSE
|
|
152
|
+
* connection to receive either component-level HMR updates or full reloads.
|
|
153
|
+
*
|
|
154
|
+
* 'hmr-update' — one or more .vorra chunks changed; each is re-imported with
|
|
155
|
+
* a cache-busting timestamp. The new chunk calls window.__vorra_hmr.accept()
|
|
156
|
+
* which triggers the in-place component swap implemented in @vorra/core.
|
|
157
|
+
*
|
|
158
|
+
* 'reload' — a non-component file changed; fall back to a full page reload.
|
|
159
|
+
*/
|
|
160
|
+
const HMR_CLIENT_SCRIPT = `<script type="module">
|
|
161
|
+
(function () {
|
|
162
|
+
if (!window.__vorra_hmr) window.__vorra_hmr = {};
|
|
163
|
+
var hmr = window.__vorra_hmr;
|
|
164
|
+
if (!hmr.instances) hmr.instances = new Map();
|
|
165
|
+
|
|
166
|
+
var es = new EventSource('${HMR_ENDPOINT}');
|
|
167
|
+
|
|
168
|
+
es.addEventListener('hmr-update', function (e) {
|
|
169
|
+
var data = JSON.parse(e.data);
|
|
170
|
+
data.updates.forEach(function (update) {
|
|
171
|
+
console.log('[vorra hmr] updating component ' + update.id);
|
|
172
|
+
import(update.url + '?t=' + Date.now()).catch(function (err) {
|
|
173
|
+
console.error('[vorra hmr] failed to load update, falling back to reload', err);
|
|
174
|
+
location.reload();
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
es.addEventListener('reload', function () {
|
|
180
|
+
console.log('[vorra hmr] full reload');
|
|
181
|
+
location.reload();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
es.addEventListener('error', function () { es.close(); });
|
|
185
|
+
})();
|
|
186
|
+
<\/script>`;
|
|
187
|
+
const MIME = {
|
|
188
|
+
".html": "text/html; charset=utf-8",
|
|
189
|
+
".js": "application/javascript; charset=utf-8",
|
|
190
|
+
".mjs": "application/javascript; charset=utf-8",
|
|
191
|
+
".css": "text/css; charset=utf-8",
|
|
192
|
+
".json": "application/json; charset=utf-8",
|
|
193
|
+
".svg": "image/svg+xml",
|
|
194
|
+
".png": "image/png",
|
|
195
|
+
".jpg": "image/jpeg",
|
|
196
|
+
".jpeg": "image/jpeg",
|
|
197
|
+
".ico": "image/x-icon",
|
|
198
|
+
".woff": "font/woff",
|
|
199
|
+
".woff2": "font/woff2",
|
|
200
|
+
".ttf": "font/ttf"
|
|
201
|
+
};
|
|
202
|
+
/**
|
|
203
|
+
* Starts the vorra development server.
|
|
204
|
+
*
|
|
205
|
+
* CLI flags (override vorra.config.js):
|
|
206
|
+
* --port <number> Dev server port (default: 3000)
|
|
207
|
+
* --entry <path> Entry file (default: src/main.ts)
|
|
208
|
+
* --outDir <path> Output directory (default: dist)
|
|
209
|
+
*/
|
|
210
|
+
async function runDev(args) {
|
|
211
|
+
const cwd = process.cwd();
|
|
212
|
+
const config = await loadConfig(cwd);
|
|
213
|
+
const portIdx = args.indexOf("--port");
|
|
214
|
+
const entryIdx = args.indexOf("--entry");
|
|
215
|
+
const outDirIdx = args.indexOf("--outDir");
|
|
216
|
+
const port = portIdx !== -1 ? parseInt(args[portIdx + 1] ?? "3000", 10) : config.port ?? 3e3;
|
|
217
|
+
const entry = (entryIdx !== -1 ? args[entryIdx + 1] : void 0) ?? config.entry ?? "src/main.ts";
|
|
218
|
+
const outDir = (outDirIdx !== -1 ? args[outDirIdx + 1] : void 0) ?? config.devOutDir ?? ".vorra";
|
|
219
|
+
const entryAbs = path.join(cwd, entry);
|
|
220
|
+
const outDirAbs = path.join(cwd, outDir);
|
|
221
|
+
const outDirNorm = outDirAbs.replace(/\\/g, "/") + "/";
|
|
222
|
+
const outDirRel = path.relative(cwd, outDirAbs).replace(/\\/g, "/");
|
|
223
|
+
const userPlugins = config.plugins ?? [];
|
|
224
|
+
const devDefinePlugin = {
|
|
225
|
+
name: "vorra-dev-define",
|
|
226
|
+
transform(code) {
|
|
227
|
+
if (!code.includes("__vorra_dev")) return null;
|
|
228
|
+
let result = code.replace(/declare\s+const\s+__vorra_dev\b[^\n]*\n?/g, "");
|
|
229
|
+
result = result.replaceAll("__vorra_dev", "true");
|
|
230
|
+
return { code: result };
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
const plugins = [
|
|
234
|
+
vorraDedupePlugin,
|
|
235
|
+
vorraPlugin({
|
|
236
|
+
hmr: true,
|
|
237
|
+
...config.css ? { css: path.join(cwd, config.css) } : {},
|
|
238
|
+
...config.postcss ? { postcss: config.postcss } : {}
|
|
239
|
+
}),
|
|
240
|
+
devDefinePlugin,
|
|
241
|
+
...userPlugins
|
|
242
|
+
];
|
|
243
|
+
fs.mkdirSync(outDirAbs, { recursive: true });
|
|
244
|
+
const clients = /* @__PURE__ */ new Set();
|
|
245
|
+
/** Broadcasts an SSE event to all connected browser clients. */
|
|
246
|
+
function broadcast(event, data) {
|
|
247
|
+
for (const client of clients) try {
|
|
248
|
+
client.write(`event: ${event}\ndata: ${data}\n\n`);
|
|
249
|
+
} catch {
|
|
250
|
+
clients.delete(client);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/** Absolute paths of .vorra files that changed since the last build. */
|
|
254
|
+
const changedVorraFiles = /* @__PURE__ */ new Set();
|
|
255
|
+
/** True if any non-.vorra source file changed since the last build. */
|
|
256
|
+
let hasNonVorraChanges = false;
|
|
257
|
+
let isBuilding = false;
|
|
258
|
+
let pendingRebuild = false;
|
|
259
|
+
const devOutput = {
|
|
260
|
+
dir: outDirAbs,
|
|
261
|
+
format: "es",
|
|
262
|
+
sourcemap: true,
|
|
263
|
+
entryFileNames: "[name].js",
|
|
264
|
+
chunkFileNames: "[name].js",
|
|
265
|
+
manualChunks(id) {
|
|
266
|
+
if (id.endsWith(".vorra")) return path.relative(cwd, id).replace(/\\/g, "/").replace(".vorra", "");
|
|
267
|
+
if (id.includes(path.join("node_modules", "@vorra"))) return "vorra-runtime";
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
async function runBuild() {
|
|
271
|
+
if (isBuilding) {
|
|
272
|
+
pendingRebuild = true;
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
isBuilding = true;
|
|
276
|
+
pendingRebuild = false;
|
|
277
|
+
const currentVorraChanges = new Set(changedVorraFiles);
|
|
278
|
+
const currentHasNonVorra = hasNonVorraChanges;
|
|
279
|
+
changedVorraFiles.clear();
|
|
280
|
+
hasNonVorraChanges = false;
|
|
281
|
+
try {
|
|
282
|
+
await build({
|
|
283
|
+
input: entryAbs,
|
|
284
|
+
plugins,
|
|
285
|
+
output: devOutput
|
|
286
|
+
});
|
|
287
|
+
if (currentVorraChanges.size > 0 && !currentHasNonVorra) {
|
|
288
|
+
const updates = Array.from(currentVorraChanges).map((filePath) => {
|
|
289
|
+
const id = generateScopeId(filePath);
|
|
290
|
+
const rel = path.relative(cwd, filePath).replace(/\\/g, "/").replace(".vorra", "");
|
|
291
|
+
return {
|
|
292
|
+
id,
|
|
293
|
+
url: `/${path.join(outDir, rel).replace(/\\/g, "/")}.js`
|
|
294
|
+
};
|
|
295
|
+
});
|
|
296
|
+
console.log(`[vorra hmr] Hot-updating ${updates.length} component(s)...`);
|
|
297
|
+
broadcast("hmr-update", JSON.stringify({ updates }));
|
|
298
|
+
} else {
|
|
299
|
+
console.log("[vorra dev] Rebuilt — notifying clients...");
|
|
300
|
+
broadcast("reload", "{}");
|
|
301
|
+
}
|
|
302
|
+
} catch {
|
|
303
|
+
console.error("[vorra dev] Build error — check the terminal above for details.");
|
|
304
|
+
} finally {
|
|
305
|
+
isBuilding = false;
|
|
306
|
+
if (pendingRebuild) runBuild();
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
let debounceTimer = null;
|
|
310
|
+
const fsWatcher = fs.watch(cwd, { recursive: true }, (_event, filename) => {
|
|
311
|
+
if (!filename) return;
|
|
312
|
+
const rel = filename.replace(/\\/g, "/");
|
|
313
|
+
if (rel.startsWith(outDirRel + "/")) return;
|
|
314
|
+
if (rel.startsWith("node_modules/")) return;
|
|
315
|
+
if ((path.resolve(cwd, filename).replace(/\\/g, "/") + "/").startsWith(outDirNorm)) return;
|
|
316
|
+
const SOURCE_EXTENSIONS = [
|
|
317
|
+
".ts",
|
|
318
|
+
".tsx",
|
|
319
|
+
".js",
|
|
320
|
+
".mjs",
|
|
321
|
+
".cjs",
|
|
322
|
+
".json",
|
|
323
|
+
".html",
|
|
324
|
+
".css"
|
|
325
|
+
];
|
|
326
|
+
if (filename.endsWith(".vorra")) changedVorraFiles.add(path.resolve(cwd, filename));
|
|
327
|
+
else if (SOURCE_EXTENSIONS.some((ext) => filename.endsWith(ext))) hasNonVorraChanges = true;
|
|
328
|
+
else return;
|
|
329
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
330
|
+
debounceTimer = setTimeout(() => {
|
|
331
|
+
debounceTimer = null;
|
|
332
|
+
runBuild();
|
|
333
|
+
}, 80);
|
|
334
|
+
});
|
|
335
|
+
const devScriptSrc = `/${outDir}/${path.basename(entry, path.extname(entry))}.js`;
|
|
336
|
+
console.log(`[vorra dev] Server: http://localhost:${port}`);
|
|
337
|
+
console.log(`[vorra dev] Entry: ${entry}`);
|
|
338
|
+
console.log(`[vorra dev] Output: ${outDir}/`);
|
|
339
|
+
console.log("[vorra dev] HMR: enabled");
|
|
340
|
+
console.log("[vorra dev] Building...");
|
|
341
|
+
await runBuild();
|
|
342
|
+
console.log("[vorra dev] Watching for changes...\n");
|
|
343
|
+
const server = http.createServer((req, res) => {
|
|
344
|
+
const rawUrl = req.url ?? "/";
|
|
345
|
+
if (rawUrl === HMR_ENDPOINT) {
|
|
346
|
+
res.writeHead(200, {
|
|
347
|
+
"Content-Type": "text/event-stream",
|
|
348
|
+
"Cache-Control": "no-cache",
|
|
349
|
+
Connection: "keep-alive",
|
|
350
|
+
"Access-Control-Allow-Origin": "*"
|
|
351
|
+
});
|
|
352
|
+
res.write(":\n\n");
|
|
353
|
+
clients.add(res);
|
|
354
|
+
res.on("close", () => {
|
|
355
|
+
clients.delete(res);
|
|
356
|
+
});
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
const urlPath = rawUrl.split("?")[0] ?? "/";
|
|
360
|
+
const filePath = resolveFilePath(urlPath, cwd, outDirAbs);
|
|
361
|
+
if (filePath === null) {
|
|
362
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
363
|
+
res.end(`Not found: ${urlPath}`);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
serveFile(filePath, res, devScriptSrc);
|
|
367
|
+
});
|
|
368
|
+
server.listen(port);
|
|
369
|
+
process.on("SIGINT", () => {
|
|
370
|
+
console.log("\n[vorra dev] Stopping...");
|
|
371
|
+
fsWatcher.close();
|
|
372
|
+
for (const client of clients) try {
|
|
373
|
+
client.destroy();
|
|
374
|
+
} catch {}
|
|
375
|
+
clients.clear();
|
|
376
|
+
server.closeAllConnections();
|
|
377
|
+
server.close(() => process.exit(0));
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Resolves a URL path to a file on disk.
|
|
382
|
+
*
|
|
383
|
+
* Search order:
|
|
384
|
+
* 1. `<cwd>/<urlPath>` — public assets, index.html in project root
|
|
385
|
+
* 2. `<outDir>/<urlPath>` — compiled JS / CSS output
|
|
386
|
+
* 3. `<cwd>/index.html` — SPA fallback for unknown paths
|
|
387
|
+
*
|
|
388
|
+
* Returns null if no file is found anywhere.
|
|
389
|
+
*/
|
|
390
|
+
function resolveFilePath(urlPath, cwd, outDirAbs) {
|
|
391
|
+
const normalized = urlPath === "/" ? "/index.html" : urlPath;
|
|
392
|
+
const fromCwd = path.join(cwd, normalized);
|
|
393
|
+
if (fs.existsSync(fromCwd) && fs.statSync(fromCwd).isFile()) return fromCwd;
|
|
394
|
+
const fromDist = path.join(outDirAbs, normalized);
|
|
395
|
+
if (fs.existsSync(fromDist) && fs.statSync(fromDist).isFile()) return fromDist;
|
|
396
|
+
const indexHtml = path.join(cwd, "index.html");
|
|
397
|
+
if (fs.existsSync(indexHtml)) return indexHtml;
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Reads a file from disk and writes it to the HTTP response.
|
|
402
|
+
*
|
|
403
|
+
* For HTML files, two scripts are injected before </body>:
|
|
404
|
+
* 1. The compiled entry module — only when no <script type="module"> is
|
|
405
|
+
* already present in the file, so users don't need to add the tag
|
|
406
|
+
* manually; the framework adds it for them.
|
|
407
|
+
* 2. The HMR client — always present in dev mode.
|
|
408
|
+
*/
|
|
409
|
+
function serveFile(filePath, res, entryScript) {
|
|
410
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
411
|
+
const contentType = MIME[ext] ?? "application/octet-stream";
|
|
412
|
+
let body;
|
|
413
|
+
try {
|
|
414
|
+
body = fs.readFileSync(filePath);
|
|
415
|
+
} catch {
|
|
416
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
417
|
+
res.end("Internal server error");
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if (ext === ".html") {
|
|
421
|
+
let html = body.toString("utf8");
|
|
422
|
+
if (entryScript && !/<script\s[^>]*type=["']module["']/i.test(html)) {
|
|
423
|
+
const tag = `<script type="module" src="${entryScript}"><\/script>`;
|
|
424
|
+
html = html.includes("</body>") ? html.replace("</body>", `${tag}\n</body>`) : html + tag;
|
|
425
|
+
}
|
|
426
|
+
const injected = html.includes("</body>") ? html.replace("</body>", `${HMR_CLIENT_SCRIPT}\n</body>`) : html + HMR_CLIENT_SCRIPT;
|
|
427
|
+
const injectedBuf = Buffer.from(injected, "utf8");
|
|
428
|
+
res.writeHead(200, {
|
|
429
|
+
"Content-Type": contentType,
|
|
430
|
+
"Content-Length": injectedBuf.length
|
|
431
|
+
});
|
|
432
|
+
res.end(injectedBuf);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
res.writeHead(200, {
|
|
436
|
+
"Content-Type": contentType,
|
|
437
|
+
"Content-Length": body.length
|
|
438
|
+
});
|
|
439
|
+
res.end(body);
|
|
440
|
+
}
|
|
441
|
+
//#endregion
|
|
442
|
+
//#region src/commands/new.ts
|
|
443
|
+
/**
|
|
444
|
+
* Scaffolds a new project.
|
|
445
|
+
*
|
|
446
|
+
* Usage: vorra new <project-name>
|
|
447
|
+
*
|
|
448
|
+
* Creates the following structure:
|
|
449
|
+
* <name>/
|
|
450
|
+
* .gitignore
|
|
451
|
+
* index.html
|
|
452
|
+
* vorra.config.js
|
|
453
|
+
* package.json
|
|
454
|
+
* tsconfig.json
|
|
455
|
+
* src/
|
|
456
|
+
* env.d.ts
|
|
457
|
+
* main.ts
|
|
458
|
+
* App.vorra
|
|
459
|
+
*/
|
|
460
|
+
function runNew(args) {
|
|
461
|
+
const name = args[0];
|
|
462
|
+
if (!name) {
|
|
463
|
+
console.error("[Vorra CLI] Usage: vorra new <project-name>");
|
|
464
|
+
process.exit(1);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (!/^[a-z][a-z0-9-]*$/.test(name)) {
|
|
468
|
+
console.error("[Vorra CLI] Project name must be lowercase letters/digits/hyphens, starting with a letter.");
|
|
469
|
+
process.exit(1);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const projectDir = path.join(process.cwd(), name);
|
|
473
|
+
if (fs.existsSync(projectDir)) {
|
|
474
|
+
console.error(`[Vorra CLI] Directory "${name}" already exists.`);
|
|
475
|
+
process.exit(1);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
console.log(`\n Scaffolding Vorra app: ${name}\n`);
|
|
479
|
+
fs.mkdirSync(path.join(projectDir, "src"), { recursive: true });
|
|
480
|
+
write(projectDir, "package.json", tplPackageJson(name));
|
|
481
|
+
write(projectDir, "tsconfig.json", tplTsconfig());
|
|
482
|
+
write(projectDir, "vorra.config.js", tplVorraConfig());
|
|
483
|
+
write(projectDir, "index.html", tplIndexHtml(name));
|
|
484
|
+
write(projectDir, ".gitignore", tplGitignore());
|
|
485
|
+
write(projectDir, "src/env.d.ts", tplEnvDts());
|
|
486
|
+
write(projectDir, "src/main.ts", tplMainTs());
|
|
487
|
+
write(projectDir, "src/App.vorra", tplAppVorra());
|
|
488
|
+
for (const f of [
|
|
489
|
+
"package.json",
|
|
490
|
+
"tsconfig.json",
|
|
491
|
+
"vorra.config.js",
|
|
492
|
+
"index.html",
|
|
493
|
+
".gitignore",
|
|
494
|
+
"src/env.d.ts",
|
|
495
|
+
"src/main.ts",
|
|
496
|
+
"src/App.vorra"
|
|
497
|
+
]) console.log(` \u2713 ${name}/${f}`);
|
|
498
|
+
console.log(`
|
|
499
|
+
Done! Next steps:
|
|
500
|
+
|
|
501
|
+
cd ${name}
|
|
502
|
+
npm install
|
|
503
|
+
npm run dev
|
|
504
|
+
`);
|
|
505
|
+
}
|
|
506
|
+
function write(dir, filename, content) {
|
|
507
|
+
fs.writeFileSync(path.join(dir, filename), content, "utf8");
|
|
508
|
+
}
|
|
509
|
+
function tplPackageJson(name) {
|
|
510
|
+
return JSON.stringify({
|
|
511
|
+
name,
|
|
512
|
+
version: "0.1.0",
|
|
513
|
+
private: true,
|
|
514
|
+
type: "module",
|
|
515
|
+
scripts: {
|
|
516
|
+
dev: "vorra dev",
|
|
517
|
+
build: "vorra build",
|
|
518
|
+
typecheck: "vorra typecheck"
|
|
519
|
+
},
|
|
520
|
+
dependencies: { "@vorra/core": "^0.1.0" },
|
|
521
|
+
devDependencies: {
|
|
522
|
+
"@vorra/cli": "^0.1.0",
|
|
523
|
+
"@vorra/compiler": "^0.1.0",
|
|
524
|
+
rolldown: "^0.14.0",
|
|
525
|
+
typescript: "^5.4.0"
|
|
526
|
+
},
|
|
527
|
+
engines: { node: ">=20.0.0" }
|
|
528
|
+
}, null, 2);
|
|
529
|
+
}
|
|
530
|
+
function tplTsconfig() {
|
|
531
|
+
return JSON.stringify({
|
|
532
|
+
compilerOptions: {
|
|
533
|
+
target: "ES2022",
|
|
534
|
+
module: "ESNext",
|
|
535
|
+
moduleResolution: "Bundler",
|
|
536
|
+
strict: true,
|
|
537
|
+
exactOptionalPropertyTypes: true,
|
|
538
|
+
noUncheckedIndexedAccess: true,
|
|
539
|
+
experimentalDecorators: true,
|
|
540
|
+
useDefineForClassFields: false,
|
|
541
|
+
skipLibCheck: true,
|
|
542
|
+
noEmit: true
|
|
543
|
+
},
|
|
544
|
+
include: ["src"]
|
|
545
|
+
}, null, 2);
|
|
546
|
+
}
|
|
547
|
+
function tplVorraConfig() {
|
|
548
|
+
return `// vorra.config.js
|
|
549
|
+
import { defineConfig } from '@vorra/cli';
|
|
550
|
+
|
|
551
|
+
export default defineConfig({
|
|
552
|
+
entry: 'src/main.ts',
|
|
553
|
+
outDir: 'dist',
|
|
554
|
+
port: 3000,
|
|
555
|
+
});
|
|
556
|
+
`;
|
|
557
|
+
}
|
|
558
|
+
function tplIndexHtml(name) {
|
|
559
|
+
return `<!DOCTYPE html>
|
|
560
|
+
<html lang="en">
|
|
561
|
+
<head>
|
|
562
|
+
<meta charset="UTF-8" />
|
|
563
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
564
|
+
<title>${name}</title>
|
|
565
|
+
</head>
|
|
566
|
+
<body>
|
|
567
|
+
<div id="app"></div>
|
|
568
|
+
<!-- entry script injected automatically by vorra dev / vorra build -->
|
|
569
|
+
</body>
|
|
570
|
+
</html>
|
|
571
|
+
`;
|
|
572
|
+
}
|
|
573
|
+
function tplGitignore() {
|
|
574
|
+
return `node_modules/
|
|
575
|
+
dist/
|
|
576
|
+
.vorra/
|
|
577
|
+
*.tsbuildinfo
|
|
578
|
+
`;
|
|
579
|
+
}
|
|
580
|
+
function tplEnvDts() {
|
|
581
|
+
return `// Type declarations for .vorra single-file components.
|
|
582
|
+
import type { ComponentContext } from '@vorra/core';
|
|
583
|
+
|
|
584
|
+
declare module '*.vorra' {
|
|
585
|
+
const component: (ctx: ComponentContext) => Node;
|
|
586
|
+
export default component;
|
|
587
|
+
}
|
|
588
|
+
`;
|
|
589
|
+
}
|
|
590
|
+
function tplMainTs() {
|
|
591
|
+
return `import { bootstrapApp } from '@vorra/core';
|
|
592
|
+
import { createComponent, mountComponent } from '@vorra/core';
|
|
593
|
+
import App from './App.vorra';
|
|
594
|
+
|
|
595
|
+
const appEl = document.getElementById('app');
|
|
596
|
+
if (appEl === null) throw new Error('[App] #app element not found in index.html');
|
|
597
|
+
|
|
598
|
+
const root = bootstrapApp();
|
|
599
|
+
const ctx = createComponent(root);
|
|
600
|
+
mountComponent(App, appEl, ctx);
|
|
601
|
+
`;
|
|
602
|
+
}
|
|
603
|
+
function tplAppVorra() {
|
|
604
|
+
return `<script>
|
|
605
|
+
import { signal } from '@vorra/core';
|
|
606
|
+
|
|
607
|
+
const count = signal(0);
|
|
608
|
+
|
|
609
|
+
function increment(): void {
|
|
610
|
+
count.update((n) => n + 1);
|
|
611
|
+
}
|
|
612
|
+
<\/script>
|
|
613
|
+
|
|
614
|
+
<template>
|
|
615
|
+
<div class="app">
|
|
616
|
+
<h1>Welcome to Vorra ⚡</h1>
|
|
617
|
+
<p class="counter">Count: {count()}</p>
|
|
618
|
+
<button @click="increment">Increment</button>
|
|
619
|
+
</div>
|
|
620
|
+
</template>
|
|
621
|
+
|
|
622
|
+
<style scoped>
|
|
623
|
+
.app {
|
|
624
|
+
font-family: system-ui, sans-serif;
|
|
625
|
+
max-width: 480px;
|
|
626
|
+
margin: 4rem auto;
|
|
627
|
+
padding: 0 1rem;
|
|
628
|
+
text-align: center;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
h1 {
|
|
632
|
+
font-size: 2rem;
|
|
633
|
+
margin-bottom: 1rem;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
.counter {
|
|
637
|
+
font-size: 1.25rem;
|
|
638
|
+
margin-bottom: 1.5rem;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
button {
|
|
642
|
+
padding: 0.5rem 1.5rem;
|
|
643
|
+
font-size: 1rem;
|
|
644
|
+
cursor: pointer;
|
|
645
|
+
border: 2px solid currentColor;
|
|
646
|
+
border-radius: 6px;
|
|
647
|
+
background: transparent;
|
|
648
|
+
transition: background 0.2s;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
button:hover {
|
|
652
|
+
background: #f0f0f0;
|
|
653
|
+
}
|
|
654
|
+
</style>
|
|
655
|
+
`;
|
|
656
|
+
}
|
|
657
|
+
//#endregion
|
|
658
|
+
//#region src/commands/typecheck.ts
|
|
659
|
+
/**
|
|
660
|
+
* Runs TypeScript type checking via `tsc --build`.
|
|
661
|
+
* Passes `--noEmit` unless the project tsconfig already disables emit.
|
|
662
|
+
* Exits with the same code as tsc.
|
|
663
|
+
*/
|
|
664
|
+
function runTypecheck(args) {
|
|
665
|
+
const noEmit = args.includes("--emit") ? [] : ["--noEmit"];
|
|
666
|
+
console.log("[vorra typecheck] Running tsc --build...");
|
|
667
|
+
const result = spawnSync("tsc", ["--build", ...noEmit], {
|
|
668
|
+
stdio: "inherit",
|
|
669
|
+
shell: true,
|
|
670
|
+
cwd: process.cwd()
|
|
671
|
+
});
|
|
672
|
+
if (result.error !== void 0) {
|
|
673
|
+
console.error("[Vorra CLI] Failed to run tsc:", result.error.message);
|
|
674
|
+
process.exit(1);
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
process.exit(result.status ?? 0);
|
|
678
|
+
}
|
|
679
|
+
//#endregion
|
|
680
|
+
//#region src/bin.ts
|
|
681
|
+
const args = process.argv.slice(2);
|
|
682
|
+
const command = args[0];
|
|
683
|
+
const commandArgs = args.slice(1);
|
|
684
|
+
const VERSION = "0.1.0";
|
|
685
|
+
const HELP = `
|
|
686
|
+
⚡ Vorra — compiled signal-first framework for enterprise apps
|
|
687
|
+
|
|
688
|
+
Usage: vorra <command> [options]
|
|
689
|
+
|
|
690
|
+
Commands:
|
|
691
|
+
new <name> Scaffold a new Vorra application
|
|
692
|
+
dev Start the development server (with live reload)
|
|
693
|
+
build Build for production
|
|
694
|
+
typecheck Run TypeScript type checking
|
|
695
|
+
|
|
696
|
+
Options:
|
|
697
|
+
--help, -h Show this help message
|
|
698
|
+
--version, -v Show the Vorra version
|
|
699
|
+
|
|
700
|
+
Flags per command:
|
|
701
|
+
dev --port <n> Dev server port (default: 3000)
|
|
702
|
+
--entry <p> Entry file (default: src/main.ts)
|
|
703
|
+
--outDir <p> Output dir (default: dist)
|
|
704
|
+
build --entry <p> Entry file (default: src/main.ts)
|
|
705
|
+
--outDir <p> Output dir (default: dist)
|
|
706
|
+
|
|
707
|
+
Examples:
|
|
708
|
+
vorra new my-app
|
|
709
|
+
vorra dev --port 4000
|
|
710
|
+
vorra build --outDir dist
|
|
711
|
+
`;
|
|
712
|
+
switch (command) {
|
|
713
|
+
case "new":
|
|
714
|
+
runNew(commandArgs);
|
|
715
|
+
break;
|
|
716
|
+
case "dev":
|
|
717
|
+
runDev(commandArgs).catch((err) => {
|
|
718
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
719
|
+
console.error("[Vorra CLI] dev error:", msg);
|
|
720
|
+
process.exit(1);
|
|
721
|
+
});
|
|
722
|
+
break;
|
|
723
|
+
case "build":
|
|
724
|
+
runBuild(commandArgs).catch((err) => {
|
|
725
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
726
|
+
console.error("[Vorra CLI] build error:", msg);
|
|
727
|
+
process.exit(1);
|
|
728
|
+
});
|
|
729
|
+
break;
|
|
730
|
+
case "typecheck":
|
|
731
|
+
runTypecheck(commandArgs);
|
|
732
|
+
break;
|
|
733
|
+
case "--version":
|
|
734
|
+
case "-v":
|
|
735
|
+
console.log(VERSION);
|
|
736
|
+
break;
|
|
737
|
+
case "--help":
|
|
738
|
+
case "-h":
|
|
739
|
+
case void 0:
|
|
740
|
+
console.log(HELP);
|
|
741
|
+
break;
|
|
742
|
+
default:
|
|
743
|
+
console.error(`[Vorra CLI] Unknown command: "${command}". Run "vorra --help" for usage.`);
|
|
744
|
+
process.exit(1);
|
|
745
|
+
}
|
|
746
|
+
//#endregion
|
|
747
|
+
|
|
748
|
+
//# sourceMappingURL=bin.js.map
|