gofront 1.2.2 → 1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gofront",
3
- "version": "1.2.2",
3
+ "version": "1.3.0",
4
4
  "author": "Luuk van Venrooij",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -35,16 +35,18 @@
35
35
  "build:reactive": "node src/index.js example/reactive/src -o example/reactive/app.js",
36
36
  "build:gom": "node src/index.js example/gom/src -o example/gom/app.js",
37
37
  "build:templ": "node src/index.js example/templ/src -o example/templ/app.js",
38
- "build:all": "npm run build:simple && npm run build:reactive && npm run build:gom && npm run build:templ",
38
+ "build:webgl": "node src/index.js example/webgl/src -o example/webgl/app.js",
39
+ "build:all": "npm run build:simple && npm run build:reactive && npm run build:gom && npm run build:templ && npm run build:webgl",
39
40
  "test": "npm run test:unit",
40
- "test:all": "npm run test:unit && npm run test:examples && npm run test:examples:dom && npm run test:e2e",
41
+ "test:all": "npm run test:unit && npm run test:perf && npm run test:examples && npm run test:examples:dom && npm run test:e2e",
41
42
  "test:unit": "node test/unit/run.js",
43
+ "test:perf": "node test/e2e/perf/zero-alloc.js",
42
44
  "test:examples": "node src/index.js test example/simple/src && node src/index.js test example/simple/src/utils && node src/index.js test example/reactive/src && node src/index.js test example/reactive/src/utils && node src/index.js test example/gom/src && node src/index.js test example/gom/src/utils && node src/index.js test example/templ/src && node src/index.js test example/templ/src/utils",
43
45
  "test:examples:dom": "node src/index.js test --dom example/simple/src && node src/index.js test --dom example/gom/src && node src/index.js test --dom example/templ/src",
44
46
  "test:coverage": "c8 --include='src/**' --reporter=text --reporter=lcov node test/unit/run.js",
45
47
  "test:e2e": "npm run build:all && playwright test",
46
48
  "format": "biome format --write .",
47
- "check": "biome check . && sentrux check && sentrux gate && node src/index.js example/simple/src --check && node src/index.js example/reactive/src --check && node src/index.js example/gom/src --check && node src/index.js example/templ/src --check",
49
+ "check": "biome check . && sentrux check && sentrux gate && node src/index.js example/simple/src --check && node src/index.js example/reactive/src --check && node src/index.js example/gom/src --check && node src/index.js example/templ/src --check && node src/index.js example/webgl/src --check",
48
50
  "prepare": "lefthook install"
49
51
  },
50
52
  "devDependencies": {
package/src/cli-core.js CHANGED
@@ -1,11 +1,31 @@
1
1
  // CLI compilation core — extracted for direct import in tests.
2
2
  // index.js retains only arg parsing, file I/O, watch mode, and process.exit.
3
3
 
4
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
5
- import { basename, dirname, join, resolve } from "node:path";
4
+ import {
5
+ copyFileSync,
6
+ existsSync,
7
+ mkdirSync,
8
+ readdirSync,
9
+ readFileSync,
10
+ rmSync,
11
+ statSync,
12
+ watch,
13
+ writeFileSync,
14
+ } from "node:fs";
15
+ import {
16
+ basename,
17
+ dirname,
18
+ isAbsolute,
19
+ join,
20
+ relative,
21
+ resolve,
22
+ sep,
23
+ } from "node:path";
6
24
  import { copyAssets } from "./asset-manager.js";
7
25
  import { compileDir, compileSingleFile } from "./compiler.js";
26
+ import { createDevServer } from "./dev-server.js";
8
27
  import { minify } from "./minifier.js";
28
+ import { generatePwa } from "./pwa.js";
9
29
  import { runTests } from "./test-runner.js";
10
30
  import { bundleVendor } from "./vendor.js";
11
31
 
@@ -42,6 +62,84 @@ export function maybeMinify(js, options) {
42
62
  return minify(js, { mangle });
43
63
  }
44
64
 
65
+ function sanitizeProjectName(dirPath) {
66
+ const base = basename(resolve(dirPath));
67
+ const clean = base
68
+ .toLowerCase()
69
+ .replace(/[^a-z0-9_-]/g, "-")
70
+ .replace(/^[0-9_-]+/, "");
71
+ return clean || "gofront-app";
72
+ }
73
+
74
+ function checkScaffoldConflicts(targetDir, filePaths) {
75
+ const legacyMain = join(targetDir, "main.go");
76
+ const allToCheck = [...filePaths, legacyMain];
77
+ for (const p of allToCheck) {
78
+ if (existsSync(p)) {
79
+ throw new Error(`${p} already exists — nothing written`);
80
+ }
81
+ }
82
+ }
83
+
84
+ function getScaffoldTemplates(targetDir) {
85
+ const projName = sanitizeProjectName(targetDir);
86
+ const indexPath = join(targetDir, "app", "index.html");
87
+ const mainPath = join(targetDir, "app", "src", "main.go");
88
+ const gitignorePath = join(targetDir, ".gitignore");
89
+ const pkgPath = join(targetDir, "package.json");
90
+
91
+ const html = `<!DOCTYPE html>
92
+ <html lang="en">
93
+ <head>
94
+ <meta charset="UTF-8">
95
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
96
+ <title>${projName}</title>
97
+ </head>
98
+ <body>
99
+ <div id="app"></div>
100
+ <script type="module" src="app.js"></script>
101
+ </body>
102
+ </html>
103
+ `;
104
+
105
+ const mainGo = `package main
106
+
107
+ func main() {
108
+ console.log("Hello from GoFront!")
109
+ }
110
+ `;
111
+
112
+ const gitignore = `dist/
113
+ public/
114
+ app/vendor.js
115
+ app/app.js
116
+ node_modules/
117
+ `;
118
+
119
+ const pkgJson = `${JSON.stringify(
120
+ {
121
+ name: projName,
122
+ type: "module",
123
+ scripts: {
124
+ dev: "gofront dev",
125
+ build: "gofront build",
126
+ test: "gofront test",
127
+ check: "gofront check",
128
+ },
129
+ dependencies: {},
130
+ },
131
+ null,
132
+ 2,
133
+ )}\n`;
134
+
135
+ return [
136
+ { path: indexPath, content: html },
137
+ { path: mainPath, content: mainGo },
138
+ { path: gitignorePath, content: gitignore },
139
+ { path: pkgPath, content: pkgJson },
140
+ ];
141
+ }
142
+
45
143
  export function handleInit(targetDir) {
46
144
  try {
47
145
  mkdirSync(targetDir, { recursive: true });
@@ -49,22 +147,26 @@ export function handleInit(targetDir) {
49
147
  throw new Error(`cannot create '${targetDir}': ${e.message}`);
50
148
  }
51
149
 
52
- const mainPath = join(targetDir, "main.go");
53
- if (existsSync(mainPath)) {
54
- throw new Error(`${mainPath} already exists — nothing written`);
55
- }
56
-
57
- const pkgName = basename(targetDir);
58
- const safePkg = pkgName.replace(/[^a-zA-Z0-9_]/g, "_").replace(/^[0-9]/, "_");
59
- const template = `package ${safePkg}\n\nfunc main() {\n\tconsole.log("Hello from GoFront!")\n}\n`;
150
+ const templates = getScaffoldTemplates(targetDir);
151
+ checkScaffoldConflicts(
152
+ targetDir,
153
+ templates.map((t) => t.path),
154
+ );
60
155
 
61
- try {
62
- writeFileSync(mainPath, template);
63
- } catch (e) {
64
- throw new Error(`cannot write '${mainPath}': ${e.message}`);
156
+ for (const { path: filePath, content } of templates) {
157
+ try {
158
+ mkdirSync(dirname(filePath), { recursive: true });
159
+ writeFileSync(filePath, content);
160
+ } catch (e) {
161
+ throw new Error(`cannot write '${filePath}': ${e.message}`);
162
+ }
65
163
  }
66
164
 
67
- return { mainPath };
165
+ const mainPath = join(targetDir, "app", "src", "main.go");
166
+ return {
167
+ mainPath,
168
+ files: templates.map((t) => t.path),
169
+ };
68
170
  }
69
171
 
70
172
  export async function handlePrep(targetDir, options = {}) {
@@ -123,3 +225,406 @@ export function parseTestArgs(argv) {
123
225
  export function handleTest(targetDir, options = {}) {
124
226
  return runTests(targetDir, options);
125
227
  }
228
+
229
+ // ── Project detection ─────────────────────────────────────────
230
+
231
+ function loadProjectConfig(projectRoot) {
232
+ const gofrontJson = join(projectRoot, "gofront.json");
233
+ if (existsSync(gofrontJson)) {
234
+ try {
235
+ return JSON.parse(readFileSync(gofrontJson, "utf8"));
236
+ } catch {}
237
+ }
238
+ const pkgJson = join(projectRoot, "package.json");
239
+ if (existsSync(pkgJson)) {
240
+ try {
241
+ const pkg = JSON.parse(readFileSync(pkgJson, "utf8"));
242
+ if (pkg.gofront && typeof pkg.gofront === "object") {
243
+ return pkg.gofront;
244
+ }
245
+ } catch {}
246
+ }
247
+ return {};
248
+ }
249
+
250
+ function resolveSrcDir(projectRoot, config) {
251
+ const src = config.src || config.input;
252
+ if (src) return isAbsolute(src) ? src : resolve(projectRoot, src);
253
+ if (existsSync(join(projectRoot, "app", "src"))) {
254
+ return join(projectRoot, "app", "src");
255
+ }
256
+ if (existsSync(join(projectRoot, "src"))) {
257
+ return join(projectRoot, "src");
258
+ }
259
+ if (existsSync(join(projectRoot, "main.go"))) {
260
+ return join(projectRoot, "main.go");
261
+ }
262
+ return projectRoot;
263
+ }
264
+
265
+ function resolveServeDir(projectRoot, config) {
266
+ const serve = config.serveDir;
267
+ if (serve) return isAbsolute(serve) ? serve : resolve(projectRoot, serve);
268
+ if (existsSync(join(projectRoot, "app", "index.html"))) {
269
+ return join(projectRoot, "app");
270
+ }
271
+ if (existsSync(join(projectRoot, "index.html"))) {
272
+ return projectRoot;
273
+ }
274
+ if (existsSync(join(projectRoot, "public", "index.html"))) {
275
+ return join(projectRoot, "public");
276
+ }
277
+ return existsSync(join(projectRoot, "app"))
278
+ ? join(projectRoot, "app")
279
+ : projectRoot;
280
+ }
281
+
282
+ function resolveOutDir(projectRoot, config) {
283
+ const out =
284
+ config.outDir ||
285
+ config.dist ||
286
+ (typeof config.public === "string" ? config.public : null);
287
+ if (out) return isAbsolute(out) ? out : resolve(projectRoot, out);
288
+ return join(projectRoot, "public");
289
+ }
290
+
291
+ function resolveDevOutputFile(projectRoot, serveDir, config) {
292
+ const out = config.output;
293
+ if (out) return isAbsolute(out) ? out : resolve(projectRoot, out);
294
+ if (existsSync(join(projectRoot, "app"))) {
295
+ return join(projectRoot, "app", "app.js");
296
+ }
297
+ return join(serveDir, "app.js");
298
+ }
299
+
300
+ export function detectProject(dir = ".") {
301
+ const projectRoot = resolve(dir);
302
+ const config = loadProjectConfig(projectRoot);
303
+ const srcDir = resolveSrcDir(projectRoot, config);
304
+ const serveDir = resolveServeDir(projectRoot, config);
305
+ const devOutputFile = resolveDevOutputFile(projectRoot, serveDir, config);
306
+ const outDir = resolveOutDir(projectRoot, config);
307
+ const port = config.port || config.serverPort || 3000;
308
+
309
+ return {
310
+ projectRoot,
311
+ srcDir,
312
+ serveDir,
313
+ devOutputFile,
314
+ outDir,
315
+ port,
316
+ config,
317
+ };
318
+ }
319
+
320
+ // ── check command ─────────────────────────────────────────────
321
+
322
+ export function parseCheckArgs(argv) {
323
+ const positional = argv.filter((a) => !a.startsWith("-"));
324
+ return {
325
+ targetDir: positional[0] ?? ".",
326
+ };
327
+ }
328
+
329
+ export function handleCheck(targetDir = ".", options = {}) {
330
+ const project = detectProject(targetDir);
331
+ const targetPath = resolve(targetDir);
332
+ let isDir = false;
333
+ try {
334
+ isDir = statSync(targetPath).isDirectory();
335
+ } catch (e) {
336
+ throw new Error(`cannot access '${targetDir}': ${e.message}`);
337
+ }
338
+
339
+ let compileTarget = targetPath;
340
+ if (
341
+ isDir &&
342
+ targetPath === project.projectRoot &&
343
+ project.srcDir !== project.projectRoot
344
+ ) {
345
+ compileTarget = project.srcDir;
346
+ }
347
+
348
+ const targetIsDir = statSync(compileTarget).isDirectory();
349
+ const startMs = performance.now();
350
+ runCompile(compileTarget, targetIsDir, { ...options });
351
+ const elapsedMs = (performance.now() - startMs).toFixed(0);
352
+ return { target: targetDir, compileTarget, elapsedMs };
353
+ }
354
+
355
+ // ── build command ─────────────────────────────────────────────
356
+
357
+ export function parseBuildArgs(argv) {
358
+ let outDir = null;
359
+ const positional = [];
360
+ const pwa = argv.includes("--pwa");
361
+ const sourceMap = argv.includes("--source-map");
362
+ const noMinify = argv.includes("--no-minify");
363
+ const noMangle = argv.includes("--no-mangle");
364
+
365
+ for (let i = 0; i < argv.length; i++) {
366
+ const arg = argv[i];
367
+ if (arg === "-o" || arg === "--out-dir" || arg === "--out") {
368
+ outDir = argv[++i] ?? null;
369
+ } else if (arg.startsWith("--out-dir=")) {
370
+ outDir = arg.slice(arg.indexOf("=") + 1);
371
+ } else if (!arg.startsWith("-")) {
372
+ positional.push(arg);
373
+ }
374
+ }
375
+
376
+ return {
377
+ targetDir: positional[0] ?? ".",
378
+ outDir,
379
+ pwa,
380
+ sourceMap,
381
+ minify: !noMinify,
382
+ mangle: !noMangle && !noMinify,
383
+ };
384
+ }
385
+
386
+ function isSubdirectory(parent, child) {
387
+ const rel = relative(parent, child);
388
+ return (
389
+ rel !== "" &&
390
+ rel !== ".." &&
391
+ !rel.startsWith(`..${sep}`) &&
392
+ !isAbsolute(rel)
393
+ );
394
+ }
395
+
396
+ function cleanOutputDir(outDir, projectRoot, srcDir) {
397
+ const resolvedOut = resolve(outDir);
398
+ const resolvedRoot = resolve(projectRoot);
399
+ const resolvedSrc = resolve(srcDir);
400
+
401
+ const isSafe =
402
+ resolvedOut !== resolvedRoot &&
403
+ resolvedOut !== resolvedSrc &&
404
+ !isSubdirectory(resolvedOut, resolvedRoot) &&
405
+ !isSubdirectory(resolvedOut, resolvedSrc);
406
+
407
+ if (isSafe && existsSync(resolvedOut)) {
408
+ rmSync(resolvedOut, { recursive: true, force: true });
409
+ }
410
+ mkdirSync(resolvedOut, { recursive: true });
411
+ }
412
+
413
+ function copyReleaseAssets(project, outDir, assetConfig) {
414
+ const assetResult = copyAssets(project.projectRoot, assetConfig);
415
+
416
+ const candidates = [
417
+ join(project.serveDir, "index.html"),
418
+ join(project.projectRoot, "index.html"),
419
+ join(project.projectRoot, "app", "index.html"),
420
+ ];
421
+ const destIndex = join(outDir, "index.html");
422
+ for (const candidate of candidates) {
423
+ if (existsSync(candidate) && resolve(candidate) !== resolve(destIndex)) {
424
+ copyFileSync(candidate, destIndex);
425
+ break;
426
+ }
427
+ }
428
+ return assetResult;
429
+ }
430
+
431
+ function collectOutputFiles(outDir) {
432
+ const files = [];
433
+ try {
434
+ for (const name of readdirSync(outDir)) {
435
+ const filePath = join(outDir, name);
436
+ const stat = statSync(filePath);
437
+ if (stat.isFile()) {
438
+ files.push({ name, size: stat.size, path: filePath });
439
+ }
440
+ }
441
+ } catch {}
442
+ return files;
443
+ }
444
+
445
+ export function formatBuildSummary({ outDir, elapsedMs, files, pwa }) {
446
+ const lines = [`build complete in ${elapsedMs}ms → ${outDir}`];
447
+ for (const f of files) {
448
+ const sizeKb = (f.size / 1024).toFixed(1);
449
+ lines.push(` ${f.name} (${sizeKb} kB)`);
450
+ }
451
+ if (pwa) {
452
+ lines.push(
453
+ ` pwa: generated sw.js (${pwa.assetsCount} cached assets, ${pwa.version})`,
454
+ );
455
+ }
456
+ return lines;
457
+ }
458
+
459
+ export async function handleBuild(targetDir = ".", options = {}) {
460
+ const startMs = performance.now();
461
+ const project = detectProject(targetDir);
462
+ const outDir = options.outDir ? resolve(options.outDir) : project.outDir;
463
+ const srcDir = options.srcDir ? resolve(options.srcDir) : project.srcDir;
464
+
465
+ cleanOutputDir(outDir, project.projectRoot, srcDir);
466
+
467
+ const isDir = statSync(srcDir).isDirectory();
468
+ const outputFile = join(outDir, "app.js");
469
+ const compileResult = runCompile(srcDir, isDir, {
470
+ outputFile,
471
+ sourceMap: options.sourceMap ?? false,
472
+ });
473
+
474
+ const doMinify = options.minify ?? true;
475
+ const doMangle = options.mangle ?? true;
476
+ const js = maybeMinify(compileResult.js, {
477
+ minify: doMinify,
478
+ mangle: doMangle,
479
+ sourceMap: options.sourceMap ?? false,
480
+ });
481
+ writeFileSync(outputFile, `${js}\n`);
482
+
483
+ let vendor = null;
484
+ try {
485
+ vendor = await bundleVendor(project.projectRoot, {
486
+ dest: join(outDir, "vendor.js"),
487
+ minify: doMinify,
488
+ ...options.vendorConfig,
489
+ });
490
+ } catch (e) {
491
+ console.warn(`gofront: vendor bundle warning: ${e.message}`);
492
+ }
493
+
494
+ const assets = copyReleaseAssets(project, outDir, options.assetConfig);
495
+
496
+ let pwa = null;
497
+ if (options.pwa) {
498
+ pwa = generatePwa(outDir);
499
+ }
500
+
501
+ const elapsedMs = (performance.now() - startMs).toFixed(0);
502
+ const files = collectOutputFiles(outDir);
503
+
504
+ return {
505
+ outDir,
506
+ srcDir,
507
+ elapsedMs,
508
+ files,
509
+ assets,
510
+ vendor,
511
+ pwa,
512
+ };
513
+ }
514
+
515
+ export { generatePwa };
516
+
517
+ // ── dev command ───────────────────────────────────────────────
518
+
519
+ export function parseDevArgs(argv) {
520
+ let port = null;
521
+ let outputFile = null;
522
+ const positional = [];
523
+ for (let i = 0; i < argv.length; i++) {
524
+ const arg = argv[i];
525
+ if (arg === "-p" || arg === "--port") {
526
+ port = parseInt(argv[++i], 10);
527
+ } else if (arg.startsWith("--port=")) {
528
+ port = parseInt(arg.slice(arg.indexOf("=") + 1), 10);
529
+ } else if (arg === "-o" || arg === "--output") {
530
+ outputFile = argv[++i] ?? null;
531
+ } else if (!arg.startsWith("-")) {
532
+ positional.push(arg);
533
+ }
534
+ }
535
+ return {
536
+ targetDir: positional[0] ?? ".",
537
+ port: Number.isNaN(port) ? null : port,
538
+ outputFile,
539
+ };
540
+ }
541
+
542
+ export async function handleDev(targetDir = ".", options = {}) {
543
+ const project = detectProject(targetDir);
544
+ const port = options.port ?? project.port ?? 3000;
545
+ const serveDir = options.serveDir
546
+ ? resolve(options.serveDir)
547
+ : project.serveDir;
548
+ const srcDir = options.srcDir ? resolve(options.srcDir) : project.srcDir;
549
+ const outputFile = options.outputFile
550
+ ? resolve(options.outputFile)
551
+ : project.devOutputFile;
552
+
553
+ copyAssets(project.projectRoot, options.assetConfig);
554
+
555
+ const devServer = createDevServer(serveDir, port, options);
556
+ const isDir = statSync(srcDir).isDirectory();
557
+ let initialError = null;
558
+
559
+ try {
560
+ const result = runCompile(srcDir, isDir, {
561
+ outputFile,
562
+ sourceMap: options.sourceMap ?? true,
563
+ });
564
+ mkdirSync(dirname(outputFile), { recursive: true });
565
+ writeFileSync(outputFile, `${result.js}\n`);
566
+ } catch (err) {
567
+ initialError = err;
568
+ }
569
+
570
+ let watcher = null;
571
+ if (options.watch !== false) {
572
+ const watchTarget = isDir ? srcDir : dirname(srcDir);
573
+ let debounce = null;
574
+ let cssDebounce = null;
575
+
576
+ function buildOnce(changedFile = null) {
577
+ try {
578
+ const startMs = performance.now();
579
+ const result = runCompile(srcDir, isDir, {
580
+ outputFile,
581
+ sourceMap: options.sourceMap ?? true,
582
+ });
583
+ mkdirSync(dirname(outputFile), { recursive: true });
584
+ writeFileSync(outputFile, `${result.js}\n`);
585
+ const elapsedMs = (performance.now() - startMs).toFixed(0);
586
+ const note = changedFile ? ` — ${changedFile} changed` : "";
587
+ console.error(
588
+ `gofront: OK — wrote ${outputFile} (${elapsedMs}ms${note})`,
589
+ );
590
+ devServer.notify();
591
+ } catch (e) {
592
+ console.error(`gofront: ERROR`);
593
+ for (const line of e.message.split("\n")) console.error(` ${line}`);
594
+ devServer.notifyError(e);
595
+ }
596
+ }
597
+
598
+ function handleCss(filename) {
599
+ try {
600
+ copyAssets(project.projectRoot, options.assetConfig);
601
+ } catch {}
602
+ devServer.notifyCss(filename);
603
+ }
604
+
605
+ watcher = watch(watchTarget, { recursive: true }, (_event, filename) => {
606
+ if (!filename) return;
607
+ if (filename.endsWith(".css")) {
608
+ clearTimeout(cssDebounce);
609
+ cssDebounce = setTimeout(() => handleCss(filename), 50);
610
+ return;
611
+ }
612
+ if (!filename.endsWith(".go") && !filename.endsWith(".templ")) return;
613
+ clearTimeout(debounce);
614
+ debounce = setTimeout(() => buildOnce(filename), 80);
615
+ });
616
+ }
617
+
618
+ return {
619
+ devServer,
620
+ project,
621
+ serveDir,
622
+ srcDir,
623
+ outputFile,
624
+ initialError,
625
+ close: async () => {
626
+ if (watcher) watcher.close();
627
+ await devServer.close();
628
+ },
629
+ };
630
+ }