gofront 1.0.0 → 1.1.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.
Files changed (41) hide show
  1. package/README.md +36 -3
  2. package/package.json +7 -7
  3. package/src/asset-manager.js +125 -0
  4. package/src/cli-core.js +74 -0
  5. package/src/codegen/expressions.js +3 -0
  6. package/src/codegen/statements.js +3 -0
  7. package/src/codegen/stdlib/builder.js +3 -0
  8. package/src/codegen/stdlib/bytes.js +3 -0
  9. package/src/codegen/stdlib/errors.js +3 -0
  10. package/src/codegen/stdlib/fmt.js +3 -0
  11. package/src/codegen/stdlib/gom.js +3 -0
  12. package/src/codegen/stdlib/html.js +3 -0
  13. package/src/codegen/stdlib/index.js +3 -0
  14. package/src/codegen/stdlib/io.js +3 -0
  15. package/src/codegen/stdlib/maps.js +3 -0
  16. package/src/codegen/stdlib/math.js +3 -0
  17. package/src/codegen/stdlib/os.js +3 -0
  18. package/src/codegen/stdlib/path.js +3 -0
  19. package/src/codegen/stdlib/rand.js +3 -0
  20. package/src/codegen/stdlib/regexp.js +3 -0
  21. package/src/codegen/stdlib/slices.js +3 -0
  22. package/src/codegen/stdlib/sort.js +3 -0
  23. package/src/codegen/stdlib/strconv.js +3 -0
  24. package/src/codegen/stdlib/strings.js +3 -0
  25. package/src/codegen/stdlib/time.js +3 -0
  26. package/src/codegen/stdlib/unicode.js +3 -0
  27. package/src/codegen/stdlib/utf8.js +3 -0
  28. package/src/codegen/templ.js +3 -0
  29. package/src/compiler.js +63 -4
  30. package/src/dev-server.js +96 -33
  31. package/src/index.js +80 -123
  32. package/src/parser/declarations.js +347 -0
  33. package/src/parser/index.js +3 -346
  34. package/src/typechecker/assignability.js +3 -0
  35. package/src/typechecker/expressions.js +9 -0
  36. package/src/typechecker/index.js +2 -1
  37. package/src/typechecker/resolve.js +9 -2
  38. package/src/typechecker/statements.js +3 -0
  39. package/src/typechecker/termination.js +3 -0
  40. package/src/typechecker/types.js +1 -0
  41. package/src/vendor.js +228 -0
@@ -24,6 +24,8 @@ import {
24
24
  UNTYPED_INT,
25
25
  } from "./types.js";
26
26
 
27
+ /** @typedef {import('./index.js').TypeChecker} TypeChecker */
28
+
27
29
  // Untyped constant assignability: maps source.base → Set of compatible target.name values
28
30
  const UNTYPED_COMPAT = {
29
31
  int: new Set(["int", "float64", "complex128", "complex64"]),
@@ -33,6 +35,7 @@ const UNTYPED_COMPAT = {
33
35
  complex128: new Set(["complex128", "complex64"]),
34
36
  };
35
37
 
38
+ /** @type {ThisType<TypeChecker>} */
36
39
  export const assignabilityMethods = {
37
40
  // ── Binary result types ───────────────────────────────────────────
38
41
 
@@ -17,6 +17,7 @@ import {
17
17
  NIL,
18
18
  Scope,
19
19
  STRING,
20
+ TAINTED_ANY,
20
21
  typeStr,
21
22
  UNTYPED_BOOL,
22
23
  UNTYPED_COMPLEX,
@@ -26,6 +27,8 @@ import {
26
27
  VOID,
27
28
  } from "./types.js";
28
29
 
30
+ /** @typedef {import('./index.js').TypeChecker} TypeChecker */
31
+
29
32
  // Dispatch table for builtin function type checking.
30
33
  // Each value is (self, name, expr, argTypes, scope) => returnType.
31
34
  const BUILTIN_CHECK = {
@@ -80,6 +83,7 @@ const CHECK_EXPR_DELEGATE = {
80
83
  InstantiationExpr: "_checkInstantiationExpr",
81
84
  };
82
85
 
86
+ /** @type {ThisType<TypeChecker>} */
83
87
  export const expressionCheckMethods = {
84
88
  checkExpr(expr, scope) {
85
89
  const t = this._checkExpr(expr, scope);
@@ -115,6 +119,7 @@ export const expressionCheckMethods = {
115
119
  case "BinaryExpr": {
116
120
  const lt = this.checkExpr(expr.left, scope);
117
121
  const rt = this.checkExpr(expr.right, scope);
122
+ if (lt?._tainted || rt?._tainted) return TAINTED_ANY;
118
123
  if (isAny(lt) || isAny(rt))
119
124
  return this.binaryResultType(expr.op, ANY, ANY, expr);
120
125
  return this.binaryResultType(expr.op, lt, rt, expr);
@@ -185,6 +190,7 @@ export const expressionCheckMethods = {
185
190
 
186
191
  _checkSelectorExpr(expr, scope) {
187
192
  const baseType = this.checkExpr(expr.expr, scope);
193
+ if (baseType?._tainted) return TAINTED_ANY;
188
194
  if (expr.expr._isTypeRef) {
189
195
  let base = baseType.kind === "named" ? baseType.underlying : baseType;
190
196
  base = this.resolveType(base);
@@ -230,6 +236,7 @@ export const expressionCheckMethods = {
230
236
  _checkIndexExpr(expr, scope) {
231
237
  const bt = this.checkExpr(expr.expr, scope);
232
238
  this.checkExpr(expr.index, scope);
239
+ if (bt?._tainted) return TAINTED_ANY;
233
240
  if (isAny(bt)) return ANY;
234
241
  const btu =
235
242
  bt.kind === "named"
@@ -252,6 +259,7 @@ export const expressionCheckMethods = {
252
259
  if (expr.low) this.checkExpr(expr.low, scope);
253
260
  if (expr.high) this.checkExpr(expr.high, scope);
254
261
  if (expr.max) this.checkExpr(expr.max, scope);
262
+ if (bt?._tainted) return TAINTED_ANY;
255
263
  if (isAny(bt)) return ANY;
256
264
  if (bt.kind === "slice" || bt.kind === "array")
257
265
  return { kind: "slice", elem: bt.elem };
@@ -468,6 +476,7 @@ export const expressionCheckMethods = {
468
476
 
469
477
  const argTypes = this._resolveCallArgTypes(expr, scope);
470
478
 
479
+ if (fnType?._tainted) return TAINTED_ANY;
471
480
  if (isAny(fnType)) return ANY;
472
481
 
473
482
  // Named type used as a constructor/conversion: Greeter(fn), NodeFunc(fn)
@@ -22,6 +22,7 @@ import {
22
22
  ERROR,
23
23
  isVoid,
24
24
  Scope,
25
+ TAINTED_ANY,
25
26
  TypeCheckError,
26
27
  typeStr,
27
28
  VOID,
@@ -124,7 +125,7 @@ export class TypeChecker {
124
125
  this._currentSource,
125
126
  );
126
127
  this.errors.push(e);
127
- return ANY; // recovery type
128
+ return TAINTED_ANY; // tainted recovery type — suppresses downstream cascade errors
128
129
  }
129
130
 
130
131
  _reportUnused(scope, node) {
@@ -7,10 +7,13 @@ import {
7
7
  defaultType,
8
8
  isAny,
9
9
  Scope,
10
+ TAINTED_ANY,
10
11
  typeStr,
11
12
  VOID,
12
13
  } from "./types.js";
13
14
 
15
+ /** @typedef {import('./index.js').TypeChecker} TypeChecker */
16
+
14
17
  // Dispatch table for substituteType — keyed by type.kind.
15
18
  const SUBSTITUTE_DISPATCH = {
16
19
  basic: (_s, type) => type,
@@ -69,6 +72,7 @@ const SUBSTITUTE_DISPATCH = {
69
72
  }),
70
73
  };
71
74
 
75
+ /** @type {ThisType<TypeChecker>} */
72
76
  export const resolveMethods = {
73
77
  resolveTypeNode(node, scope) {
74
78
  if (!node) return ANY;
@@ -217,7 +221,9 @@ export const resolveMethods = {
217
221
  }
218
222
  }
219
223
  }
220
- return { kind: "interface", methods };
224
+ const result = { kind: "interface", methods };
225
+ if (node.unionConstraint) result.unionConstraint = node.unionConstraint;
226
+ return result;
221
227
  },
222
228
 
223
229
  // ── Generics — substitution, instantiation, inference ────────────
@@ -354,7 +360,8 @@ export const resolveMethods = {
354
360
 
355
361
  fieldType(baseType, field, node) {
356
362
  baseType = this.resolveType(baseType);
357
- if (!baseType || isAny(baseType)) return ANY;
363
+ if (!baseType || isAny(baseType))
364
+ return baseType?._tainted ? TAINTED_ANY : ANY;
358
365
  if (this._isTransparentPointerField(baseType, field))
359
366
  baseType = this.resolveType(baseType.base);
360
367
  if (baseType.kind === "named" && baseType.methods?.has(field))
@@ -26,6 +26,9 @@ import {
26
26
  Scope,
27
27
  } from "./types.js";
28
28
 
29
+ /** @typedef {import('./index.js').TypeChecker} TypeChecker */
30
+
31
+ /** @type {ThisType<TypeChecker>} */
29
32
  export const statementCheckMethods = {
30
33
  checkBlock(block, scope, returnType) {
31
34
  for (const stmt of block.stmts) {
@@ -1,6 +1,9 @@
1
1
  // Termination analysis — determines whether a statement/block always returns.
2
2
  // Installed as a mixin on TypeChecker.prototype.
3
3
 
4
+ /** @typedef {import('./index.js').TypeChecker} TypeChecker */
5
+
6
+ /** @type {ThisType<TypeChecker>} */
4
7
  export const terminationMethods = {
5
8
  _isPanicCall(expr) {
6
9
  return (
@@ -48,6 +48,7 @@ export const FLOAT64 = { kind: "basic", name: "float64" };
48
48
  export const STRING = { kind: "basic", name: "string" };
49
49
  export const BOOL = { kind: "basic", name: "bool" };
50
50
  export const ANY = { kind: "basic", name: "any" };
51
+ export const TAINTED_ANY = { kind: "basic", name: "any", _tainted: true };
51
52
  export const VOID = { kind: "basic", name: "void" };
52
53
  export const NIL = { kind: "basic", name: "nil" };
53
54
  export const ERROR = {
package/src/vendor.js ADDED
@@ -0,0 +1,228 @@
1
+ // Vendor dependency bundler for GoFront projects.
2
+ // Dynamically loads a bundler (rolldown or esbuild) from consumer devDependencies.
3
+
4
+ import {
5
+ existsSync,
6
+ mkdirSync,
7
+ readFileSync,
8
+ rmSync,
9
+ writeFileSync,
10
+ } from "node:fs";
11
+ import { createRequire } from "node:module";
12
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
13
+ import { pathToFileURL } from "node:url";
14
+
15
+ export function getExportNames(pkgName) {
16
+ const base = pkgName.replace(/^@[^/]+\//, "");
17
+ const clean = base.replace(/[^a-zA-Z0-9_]/g, "_");
18
+ const names = new Set([pkgName, base, clean]);
19
+
20
+ if (pkgName === "@emailjs/browser") names.add("emailjs");
21
+ if (pkgName === "fuse.js") {
22
+ names.add("Fuse");
23
+ names.add("fuse");
24
+ }
25
+ if (pkgName === "prismjs") {
26
+ names.add("Prism");
27
+ names.add("prism");
28
+ }
29
+ if (pkgName === "marked") {
30
+ names.add("marked");
31
+ }
32
+
33
+ return [...names];
34
+ }
35
+
36
+ export function generateVendorEntry(packages) {
37
+ const imports = [];
38
+ const assignments = [];
39
+ const exports = [];
40
+
41
+ packages.forEach((pkg, idx) => {
42
+ const id = `_dep_${idx}`;
43
+ imports.push(`import * as ${id} from ${JSON.stringify(pkg)};`);
44
+
45
+ const names = getExportNames(pkg);
46
+ for (const name of names) {
47
+ assignments.push(
48
+ ` window[${JSON.stringify(name)}] = ${id}.default || ${id};`,
49
+ );
50
+ }
51
+
52
+ const cleanId = pkg
53
+ .replace(/^@[^/]+\//, "")
54
+ .replace(/[^a-zA-Z0-9_$]/g, "_");
55
+ exports.push(` ${id} as ${cleanId}`);
56
+ });
57
+
58
+ return `// Auto-generated GoFront vendor entry point
59
+ ${imports.join("\n")}
60
+
61
+ if (typeof window !== "undefined") {
62
+ ${assignments.join("\n")}
63
+ }
64
+
65
+ export {
66
+ ${exports.join(",\n")}
67
+ };
68
+ `;
69
+ }
70
+
71
+ export function findBundler(projectDir) {
72
+ const pkgPath = join(projectDir, "package.json");
73
+ let req;
74
+ if (existsSync(pkgPath)) {
75
+ req = createRequire(pkgPath);
76
+ } else {
77
+ req = createRequire(join(projectDir, "dummy.js"));
78
+ }
79
+
80
+ try {
81
+ const path = req.resolve("rolldown");
82
+ return { name: "rolldown", path };
83
+ } catch {}
84
+
85
+ try {
86
+ const path = req.resolve("esbuild");
87
+ return { name: "esbuild", path };
88
+ } catch {}
89
+
90
+ return null;
91
+ }
92
+
93
+ export function loadVendorConfig(projectDir) {
94
+ const pkgPath = join(projectDir, "package.json");
95
+ let pkg = {};
96
+ if (existsSync(pkgPath)) {
97
+ try {
98
+ pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
99
+ } catch {}
100
+ }
101
+
102
+ const gfjPath = join(projectDir, "gofront.json");
103
+ let gfj = {};
104
+ if (existsSync(gfjPath)) {
105
+ try {
106
+ gfj = JSON.parse(readFileSync(gfjPath, "utf8"));
107
+ } catch {}
108
+ }
109
+
110
+ const vendorConfig = gfj.vendor ?? pkg.vendor ?? null;
111
+ const dependencies = Object.keys(pkg.dependencies ?? {});
112
+
113
+ let dest = null;
114
+ let packages = dependencies;
115
+
116
+ if (typeof vendorConfig === "string") {
117
+ dest = vendorConfig;
118
+ } else if (vendorConfig && typeof vendorConfig === "object") {
119
+ if (vendorConfig.dest) dest = vendorConfig.dest;
120
+ if (Array.isArray(vendorConfig.packages)) packages = vendorConfig.packages;
121
+ }
122
+
123
+ if (!dest) {
124
+ dest = existsSync(join(projectDir, "app")) ? "app/vendor.js" : "vendor.js";
125
+ }
126
+
127
+ return { dest, packages };
128
+ }
129
+
130
+ export async function bundleVendor(projectDir = ".", options = {}) {
131
+ const projectRoot = resolve(projectDir);
132
+ const config = loadVendorConfig(projectRoot);
133
+
134
+ const packages = options.packages ?? config.packages;
135
+ const dest = options.dest ?? config.dest;
136
+
137
+ if (!packages || packages.length === 0) {
138
+ return {
139
+ bundled: [],
140
+ skipped: 0,
141
+ dest,
142
+ message: "no external dependencies found",
143
+ };
144
+ }
145
+
146
+ const destPath = resolve(projectRoot, dest);
147
+ const relDest = relative(projectRoot, destPath);
148
+ if (relDest.startsWith("..") || isAbsolute(relDest)) {
149
+ throw new Error(
150
+ `gofront: vendor destination '${dest}' is outside project directory`,
151
+ );
152
+ }
153
+
154
+ const bundlerInfo = options.bundler ?? findBundler(projectRoot);
155
+ if (!bundlerInfo) {
156
+ console.warn(
157
+ "gofront: no bundler found (rolldown or esbuild). Run 'npm install --save-dev rolldown' to enable vendor bundling.",
158
+ );
159
+ return {
160
+ bundled: [],
161
+ skipped: packages.length,
162
+ dest,
163
+ bundler: null,
164
+ reason: "no bundler installed",
165
+ };
166
+ }
167
+
168
+ mkdirSync(dirname(destPath), { recursive: true });
169
+
170
+ // Allow custom bundler execution function (useful for tests and plugins)
171
+ if (typeof bundlerInfo.bundle === "function") {
172
+ await bundlerInfo.bundle({
173
+ projectRoot,
174
+ packages,
175
+ dest: destPath,
176
+ });
177
+ return { bundled: packages, bundler: bundlerInfo.name ?? "custom", dest };
178
+ }
179
+
180
+ const entryCode = generateVendorEntry(packages);
181
+ const entryFile = join(
182
+ projectRoot,
183
+ `.gofront-vendor-entry-${Date.now()}.mjs`,
184
+ );
185
+ writeFileSync(entryFile, entryCode);
186
+
187
+ try {
188
+ if (bundlerInfo.name === "rolldown") {
189
+ const rolldownModule =
190
+ bundlerInfo.instance ??
191
+ (await import(pathToFileURL(bundlerInfo.path).href));
192
+ const rolldownFn =
193
+ rolldownModule.rolldown ||
194
+ rolldownModule.default?.rolldown ||
195
+ rolldownModule.default;
196
+
197
+ const bundle = await rolldownFn({
198
+ input: entryFile,
199
+ cwd: projectRoot,
200
+ });
201
+ await bundle.write({
202
+ file: destPath,
203
+ format: "esm",
204
+ });
205
+ await bundle.close();
206
+ } else if (bundlerInfo.name === "esbuild") {
207
+ const esbuildModule =
208
+ bundlerInfo.instance ??
209
+ (await import(pathToFileURL(bundlerInfo.path).href));
210
+ const buildFn =
211
+ esbuildModule.build || esbuildModule.default?.build || esbuildModule;
212
+
213
+ await buildFn({
214
+ entryPoints: [entryFile],
215
+ bundle: true,
216
+ format: "esm",
217
+ outfile: destPath,
218
+ absWorkingDir: projectRoot,
219
+ });
220
+ }
221
+ } finally {
222
+ if (existsSync(entryFile)) {
223
+ rmSync(entryFile, { force: true });
224
+ }
225
+ }
226
+
227
+ return { bundled: packages, bundler: bundlerInfo.name, dest };
228
+ }