gofront 1.3.8 → 1.3.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -401,7 +401,23 @@ gofront init [dir] scaffold a new project
401
401
  gofront --version / -v print version
402
402
  gofront --help / -h print this help
403
403
  ```
404
+ ### Project configuration
404
405
 
406
+ `gofront dev`, `build`, `prep`, `check` and `test` read optional settings from a `gofront.json`
407
+ file or a `"gofront": { … }` object in `package.json`:
408
+
409
+ | Key | Default | Description |
410
+ |---|---|---|
411
+ | `src` | `app/src`, `src` or `main.go` | Package directory (or single file) to compile |
412
+ | `serveDir` | `app`, `.` or `public` (first with `index.html`) | Directory served by `dev` and mirrored into the build |
413
+ | `outDir` | `public` | Release output directory for `build` |
414
+ | `output` | `app/app.js` | Dev-mode compiled bundle path |
415
+ | `port` | `3000` | Dev server port |
416
+ | `assetExtensions` | `[]` | Extra file extensions (e.g. `[".bmesh", ".mat"]`) copied from `serveDir` into `outDir` on `build`, in addition to the built-in web asset list (html, css, js, json, images, fonts, audio, video, wasm) |
417
+ | `vendor` | `app/vendor.js` | Vendor bundle written by `prep`/`build` from `package.json` `dependencies`. Either a destination path string or `{ "dest": string \| string[], "packages": string[], "minify": boolean, "globals": { "<pkg>": string \| string[] } }` |
418
+ | `assetCopy` | `[]` | Static files copied by `prep`/`build`: `[{ "source": "node_modules/x/font.woff2", "dest": "app/fonts/font.woff2" }]` |
419
+
420
+ All settings live in this one place — top-level `"vendor"` / `"assetCopy"` keys in `package.json` are not read.
405
421
  ---
406
422
 
407
423
  ## Multi-file packages
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gofront",
3
- "version": "1.3.8",
3
+ "version": "1.3.10",
4
4
  "author": "Luuk van Venrooij",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,14 +1,7 @@
1
1
  // Native static asset manager for GoFront projects.
2
2
  // Zero-dependency file and directory copier using node:fs.
3
3
 
4
- import {
5
- copyFileSync,
6
- cpSync,
7
- existsSync,
8
- mkdirSync,
9
- readFileSync,
10
- statSync,
11
- } from "node:fs";
4
+ import { copyFileSync, cpSync, existsSync, mkdirSync, statSync } from "node:fs";
12
5
  import {
13
6
  basename,
14
7
  dirname,
@@ -18,25 +11,11 @@ import {
18
11
  resolve,
19
12
  sep,
20
13
  } from "node:path";
14
+ import { loadProjectConfig } from "./project-config.js";
21
15
 
22
16
  export function loadAssetConfig(projectDir) {
23
- const gofrontJsonPath = join(projectDir, "gofront.json");
24
- if (existsSync(gofrontJsonPath)) {
25
- try {
26
- const parsed = JSON.parse(readFileSync(gofrontJsonPath, "utf8"));
27
- if (Array.isArray(parsed.assetCopy)) return parsed.assetCopy;
28
- } catch {}
29
- }
30
-
31
- const pkgJsonPath = join(projectDir, "package.json");
32
- if (existsSync(pkgJsonPath)) {
33
- try {
34
- const parsed = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
35
- if (Array.isArray(parsed.assetCopy)) return parsed.assetCopy;
36
- } catch {}
37
- }
38
-
39
- return [];
17
+ const entries = loadProjectConfig(projectDir).assetCopy;
18
+ return Array.isArray(entries) ? entries : [];
40
19
  }
41
20
 
42
21
  function copyFileAsset(srcPath, destPath, dest) {
package/src/cli-core.js CHANGED
@@ -26,6 +26,7 @@ import { copyAssets } from "./asset-manager.js";
26
26
  import { compileDir, compileSingleFile } from "./compiler.js";
27
27
  import { createDevServer } from "./dev-server.js";
28
28
  import { minify } from "./minifier.js";
29
+ import { loadProjectConfig } from "./project-config.js";
29
30
  import { generatePwa } from "./pwa.js";
30
31
  import { runTests } from "./test-runner.js";
31
32
  import { bundleVendor } from "./vendor.js";
@@ -314,25 +315,6 @@ export function findPackageDirs(root) {
314
315
 
315
316
  // ── Project detection ─────────────────────────────────────────
316
317
 
317
- function loadProjectConfig(projectRoot) {
318
- const gofrontJson = join(projectRoot, "gofront.json");
319
- if (existsSync(gofrontJson)) {
320
- try {
321
- return JSON.parse(readFileSync(gofrontJson, "utf8"));
322
- } catch {}
323
- }
324
- const pkgJson = join(projectRoot, "package.json");
325
- if (existsSync(pkgJson)) {
326
- try {
327
- const pkg = JSON.parse(readFileSync(pkgJson, "utf8"));
328
- if (pkg.gofront && typeof pkg.gofront === "object") {
329
- return pkg.gofront;
330
- }
331
- } catch {}
332
- }
333
- return {};
334
- }
335
-
336
318
  function resolveSrcDir(projectRoot, config) {
337
319
  const src = config.src || config.input;
338
320
  if (src) return isAbsolute(src) ? src : resolve(projectRoot, src);
@@ -562,10 +544,23 @@ const STATIC_ASSET_EXTS = new Set([
562
544
 
563
545
  const ROOT_CONFIG_EXTS = new Set([".js", ".mjs", ".json"]);
564
546
 
547
+ // Builds the asset extension whitelist: defaults plus any `assetExtensions` from project config.
548
+ export function resolveAssetExtensions(config = {}) {
549
+ const exts = new Set(STATIC_ASSET_EXTS);
550
+ const extra = config.assetExtensions;
551
+ if (!Array.isArray(extra)) return exts;
552
+ for (const raw of extra) {
553
+ if (typeof raw !== "string" || raw.trim() === "") continue;
554
+ const ext = raw.trim().toLowerCase();
555
+ exts.add(ext.startsWith(".") ? ext : `.${ext}`);
556
+ }
557
+ return exts;
558
+ }
559
+
565
560
  // Whether a file in the served directory is a web asset worth copying to the build output.
566
- function isStaticAsset(name, atProjectRoot) {
561
+ function isStaticAsset(name, atProjectRoot, exts = STATIC_ASSET_EXTS) {
567
562
  const ext = extname(name).toLowerCase();
568
- if (!STATIC_ASSET_EXTS.has(ext)) return false;
563
+ if (!exts.has(ext)) return false;
569
564
  // At the project root, .js/.json files are tooling config, not web assets.
570
565
  if (atProjectRoot && ROOT_CONFIG_EXTS.has(ext))
571
566
  return name === "manifest.json";
@@ -578,6 +573,7 @@ function copyServeDirAssets(project, outDir, srcDir) {
578
573
  const out = resolve(outDir);
579
574
  if (!existsSync(serveDir) || serveDir === out) return 0;
580
575
  const isRoot = serveDir === resolve(project.projectRoot);
576
+ const exts = resolveAssetExtensions(project.config);
581
577
  const skipDirs = [out, resolve(srcDir)];
582
578
  const devOutput = resolve(project.devOutputFile);
583
579
  const skipFiles = new Set([devOutput, `${devOutput}.map`]);
@@ -600,7 +596,7 @@ function copyServeDirAssets(project, outDir, srcDir) {
600
596
  } else if (
601
597
  entry.isFile() &&
602
598
  !skipFiles.has(full) &&
603
- isStaticAsset(entry.name, isRoot)
599
+ isStaticAsset(entry.name, isRoot, exts)
604
600
  ) {
605
601
  copyFile(full);
606
602
  }
@@ -92,21 +92,36 @@ export const templGenMethods = {
92
92
  _genTemplElement(node, p) {
93
93
  const { tag, attrs, children } = node;
94
94
  const elVar = this._freshVar("e");
95
- this.line(
96
- `const ${elVar} = document.createElement(${JSON.stringify(tag)});`,
97
- );
95
+ // <svg> and its descendants must be created in the SVG namespace.
96
+ const isSvg = tag === "svg" || this._templSvgDepth > 0;
97
+ if (isSvg) {
98
+ this.line(
99
+ `const ${elVar} = document.createElementNS("http://www.w3.org/2000/svg", ${JSON.stringify(tag)});`,
100
+ );
101
+ } else {
102
+ this.line(
103
+ `const ${elVar} = document.createElement(${JSON.stringify(tag)});`,
104
+ );
105
+ }
106
+ this._templSvgDepth = (this._templSvgDepth || 0) + (isSvg ? 1 : 0);
98
107
  for (const attr of attrs) {
99
- this._genTemplAttr(attr, elVar);
108
+ this._genTemplAttr(attr, elVar, isSvg);
100
109
  }
101
110
  this._genTemplNodes(children, elVar);
111
+ if (isSvg) this._templSvgDepth--;
102
112
  this.line(`${p}.appendChild(${elVar});`);
103
113
  },
104
114
 
105
- _genTemplAttr(attr, el) {
115
+ _genTemplAttr(attr, el, isSvg) {
106
116
  const { name } = attr;
117
+ // SVGElement.className is a read-only SVGAnimatedString.
118
+ const setClass = (js) =>
119
+ isSvg
120
+ ? `${el}.setAttribute("class", ${js});`
121
+ : `${el}.className = ${js};`;
107
122
  if (attr.kind === "static") {
108
123
  if (name === "class") {
109
- this.line(`${el}.className = ${JSON.stringify(attr.value)};`);
124
+ this.line(setClass(JSON.stringify(attr.value)));
110
125
  } else {
111
126
  this.line(
112
127
  `${el}.setAttribute(${JSON.stringify(name)}, ${JSON.stringify(attr.value)});`,
@@ -115,7 +130,7 @@ export const templGenMethods = {
115
130
  } else if (attr.kind === "expr") {
116
131
  const exprJs = this._genTemplTokenExpr(attr.tokens);
117
132
  if (name === "class") {
118
- this.line(`${el}.className = ${exprJs};`);
133
+ this.line(setClass(exprJs));
119
134
  } else {
120
135
  this.line(
121
136
  `${el}.setAttribute(${JSON.stringify(name)}, String(${exprJs}));`,
package/src/minifier.js CHANGED
@@ -117,10 +117,14 @@ const REGEX_CONTEXT_VALUES = new Set([
117
117
  "||",
118
118
  ]);
119
119
 
120
+ // Ops after which `/` is division, not a regex start
121
+ const DIVISION_AFTER_OPS = new Set([")", "]", "++", "--"]);
122
+
120
123
  function _isRegexContext(tokens) {
121
124
  const prev = lastNonWhitespaceToken(tokens);
122
125
  if (!prev) return true;
123
- return prev.type === "op" || REGEX_CONTEXT_VALUES.has(prev.value);
126
+ if (prev.type === "op") return !DIVISION_AFTER_OPS.has(prev.value);
127
+ return REGEX_CONTEXT_VALUES.has(prev.value);
124
128
  }
125
129
 
126
130
  function _scanRegexLiteral(code, i, len) {
@@ -129,6 +133,7 @@ function _scanRegexLiteral(code, i, len) {
129
133
  let inCharClass = false;
130
134
  while (j < len) {
131
135
  const rc = code[j];
136
+ if (rc === "\n") break;
132
137
  if (escaped) {
133
138
  escaped = false;
134
139
  } else if (rc === "\\") {
@@ -457,12 +462,29 @@ function _collectVarLocalDecls(tokens, i, declaredLocals) {
457
462
  collectDestructured(tokens, next.idx, declaredLocals);
458
463
  }
459
464
 
465
+ function _matchingParen(tokens, openIdx) {
466
+ let depth = 1;
467
+ for (let i = openIdx + 1; i < tokens.length; i++) {
468
+ const v = tokens[i].value;
469
+ if (v === "(") depth++;
470
+ else if (v === ")" && --depth === 0) return i;
471
+ }
472
+ return -1;
473
+ }
474
+
475
+ // A `(` opens a parameter list only when its `)` is followed by a body `{` or `=>`;
476
+ // otherwise `name(...)` is a call and its arguments may be free globals.
460
477
  function _collectParamDecls(tokens, i, declaredLocals) {
461
478
  const prev = findPrevNonWsToken(tokens, i);
462
479
  if (
463
- prev &&
464
- (prev.value === "function" || prev.type === "ident" || prev.value === ")")
480
+ !prev ||
481
+ (prev.value !== "function" && prev.type !== "ident" && prev.value !== ")")
465
482
  )
483
+ return;
484
+ const close = _matchingParen(tokens, i);
485
+ if (close < 0) return;
486
+ const after = nextNonWs(tokens, close);
487
+ if (after && (after.tok.value === "{" || after.tok.value === "=>"))
466
488
  collectParams(tokens, i, declaredLocals);
467
489
  }
468
490
 
@@ -514,6 +536,33 @@ const MANGLE_GLOBALS = new Set([
514
536
  "Infinity",
515
537
  "TextEncoder",
516
538
  "TextDecoder",
539
+ "globalThis",
540
+ "Reflect",
541
+ "Proxy",
542
+ "WeakMap",
543
+ "WeakSet",
544
+ "ArrayBuffer",
545
+ "DataView",
546
+ "Int8Array",
547
+ "Uint8Array",
548
+ "Uint8ClampedArray",
549
+ "Int16Array",
550
+ "Uint16Array",
551
+ "Int32Array",
552
+ "Uint32Array",
553
+ "Float32Array",
554
+ "Float64Array",
555
+ "BigInt64Array",
556
+ "BigUint64Array",
557
+ "Image",
558
+ "URL",
559
+ "Event",
560
+ "fetch",
561
+ "navigator",
562
+ "performance",
563
+ "localStorage",
564
+ "requestAnimationFrame",
565
+ "cancelAnimationFrame",
517
566
  "setTimeout",
518
567
  "setInterval",
519
568
  "clearTimeout",
@@ -0,0 +1,23 @@
1
+ // Project configuration loader shared by the CLI, asset manager and vendor bundler.
2
+ // Settings live in `gofront.json` at the project root, or under `"gofront"` in `package.json`.
3
+
4
+ import { existsSync, readFileSync } from "node:fs";
5
+ import { join } from "node:path";
6
+
7
+ export function loadProjectConfig(projectRoot) {
8
+ const gofrontJson = join(projectRoot, "gofront.json");
9
+ if (existsSync(gofrontJson)) {
10
+ try {
11
+ const parsed = JSON.parse(readFileSync(gofrontJson, "utf8"));
12
+ if (parsed && typeof parsed === "object") return parsed;
13
+ } catch {}
14
+ }
15
+ const pkgJson = join(projectRoot, "package.json");
16
+ if (existsSync(pkgJson)) {
17
+ try {
18
+ const pkg = JSON.parse(readFileSync(pkgJson, "utf8"));
19
+ if (pkg.gofront && typeof pkg.gofront === "object") return pkg.gofront;
20
+ } catch {}
21
+ }
22
+ return {};
23
+ }
package/src/vendor.js CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  import { createRequire } from "node:module";
13
13
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
14
14
  import { pathToFileURL } from "node:url";
15
+ import { loadProjectConfig } from "./project-config.js";
15
16
 
16
17
  export function getExportNames(pkgName, globals = {}) {
17
18
  const base = pkgName.replace(/^@[^/]+\//, "");
@@ -111,15 +112,7 @@ export function loadVendorConfig(projectDir) {
111
112
  } catch {}
112
113
  }
113
114
 
114
- const gfjPath = join(projectDir, "gofront.json");
115
- let gfj = {};
116
- if (existsSync(gfjPath)) {
117
- try {
118
- gfj = JSON.parse(readFileSync(gfjPath, "utf8"));
119
- } catch {}
120
- }
121
-
122
- const vendorConfig = gfj.vendor ?? pkg.vendor ?? null;
115
+ const vendorConfig = loadProjectConfig(projectDir).vendor ?? null;
123
116
  const dependencies = Object.keys(pkg.dependencies ?? {});
124
117
 
125
118
  let dest = null;