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.
- package/README.md +36 -3
- package/package.json +7 -7
- package/src/asset-manager.js +125 -0
- package/src/cli-core.js +74 -0
- package/src/codegen/expressions.js +3 -0
- package/src/codegen/statements.js +3 -0
- package/src/codegen/stdlib/builder.js +3 -0
- package/src/codegen/stdlib/bytes.js +3 -0
- package/src/codegen/stdlib/errors.js +3 -0
- package/src/codegen/stdlib/fmt.js +3 -0
- package/src/codegen/stdlib/gom.js +3 -0
- package/src/codegen/stdlib/html.js +3 -0
- package/src/codegen/stdlib/index.js +3 -0
- package/src/codegen/stdlib/io.js +3 -0
- package/src/codegen/stdlib/maps.js +3 -0
- package/src/codegen/stdlib/math.js +3 -0
- package/src/codegen/stdlib/os.js +3 -0
- package/src/codegen/stdlib/path.js +3 -0
- package/src/codegen/stdlib/rand.js +3 -0
- package/src/codegen/stdlib/regexp.js +3 -0
- package/src/codegen/stdlib/slices.js +3 -0
- package/src/codegen/stdlib/sort.js +3 -0
- package/src/codegen/stdlib/strconv.js +3 -0
- package/src/codegen/stdlib/strings.js +3 -0
- package/src/codegen/stdlib/time.js +3 -0
- package/src/codegen/stdlib/unicode.js +3 -0
- package/src/codegen/stdlib/utf8.js +3 -0
- package/src/codegen/templ.js +3 -0
- package/src/compiler.js +63 -4
- package/src/dev-server.js +96 -33
- package/src/index.js +80 -123
- package/src/parser/declarations.js +347 -0
- package/src/parser/index.js +3 -346
- package/src/typechecker/assignability.js +3 -0
- package/src/typechecker/expressions.js +9 -0
- package/src/typechecker/index.js +2 -1
- package/src/typechecker/resolve.js +9 -2
- package/src/typechecker/statements.js +3 -0
- package/src/typechecker/termination.js +3 -0
- package/src/typechecker/types.js +1 -0
- 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)
|
package/src/typechecker/index.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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))
|
|
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 (
|
package/src/typechecker/types.js
CHANGED
|
@@ -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
|
+
}
|