gofront 0.0.1
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/LICENSE +9 -0
- package/README.md +202 -0
- package/package.json +41 -0
- package/src/codegen.js +1192 -0
- package/src/compiler.js +156 -0
- package/src/dts-parser.js +762 -0
- package/src/index.js +338 -0
- package/src/lexer.js +465 -0
- package/src/parser.js +1120 -0
- package/src/resolver.js +114 -0
- package/src/typechecker.js +1252 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// GoFront compiler CLI
|
|
3
|
+
//
|
|
4
|
+
// Usage:
|
|
5
|
+
// gofront <file.go> — compile single file, print JS to stdout
|
|
6
|
+
// gofront <dir> — compile all *.go in directory as one package
|
|
7
|
+
// gofront . — compile current directory
|
|
8
|
+
// gofront <file.go> -o out.js — write to file
|
|
9
|
+
// gofront <dir> -o out.js — write bundle to file
|
|
10
|
+
// gofront <file.go> --check — type-check only (no output)
|
|
11
|
+
// gofront <file.go> --watch — watch and recompile on change
|
|
12
|
+
// gofront <file.go> --ast — dump AST of first file (debug)
|
|
13
|
+
// gofront <file.go> --tokens — dump tokens of first file (debug)
|
|
14
|
+
// gofront init [dir] — scaffold a new GoFront project
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
existsSync,
|
|
18
|
+
mkdirSync,
|
|
19
|
+
readFileSync,
|
|
20
|
+
statSync,
|
|
21
|
+
watch,
|
|
22
|
+
writeFileSync,
|
|
23
|
+
} from "node:fs";
|
|
24
|
+
import { createRequire } from "node:module";
|
|
25
|
+
|
|
26
|
+
const _require = createRequire(import.meta.url);
|
|
27
|
+
const { version } = _require("../package.json");
|
|
28
|
+
|
|
29
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
30
|
+
import { minify } from "terser";
|
|
31
|
+
import { CodeGen } from "./codegen.js";
|
|
32
|
+
import { compileDir } from "./compiler.js";
|
|
33
|
+
import { parseDts } from "./dts-parser.js";
|
|
34
|
+
import { Lexer } from "./lexer.js";
|
|
35
|
+
import { Parser } from "./parser.js";
|
|
36
|
+
import { isLocalPath, resolveAll, resolveGwDir } from "./resolver.js";
|
|
37
|
+
import { TypeChecker } from "./typechecker.js";
|
|
38
|
+
|
|
39
|
+
// ── Parse CLI args ───────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
const args = process.argv.slice(2);
|
|
42
|
+
if (args[0] === "--version" || args[0] === "-v") {
|
|
43
|
+
console.log(`gofront ${version}`);
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
48
|
+
console.log(
|
|
49
|
+
`
|
|
50
|
+
GoFront — a Go-inspired language that compiles to JavaScript
|
|
51
|
+
|
|
52
|
+
Usage:
|
|
53
|
+
gofront <file.go> Compile single file and print to stdout
|
|
54
|
+
gofront <dir> (or gofront .) Compile all *.go in directory as one bundle
|
|
55
|
+
gofront <input> -o out.js Compile and write to file
|
|
56
|
+
gofront <input> --check Type-check only
|
|
57
|
+
gofront <input> --watch Watch for changes and recompile
|
|
58
|
+
gofront <input> --source-map Append inline source map to output
|
|
59
|
+
gofront <input> --minify Minify output with terser
|
|
60
|
+
gofront <file.go> --ast Dump AST (debug)
|
|
61
|
+
gofront <file.go> --tokens Dump tokens (debug)
|
|
62
|
+
gofront init [dir] Scaffold a new GoFront project
|
|
63
|
+
gofront --version Print version
|
|
64
|
+
`.trim(),
|
|
65
|
+
);
|
|
66
|
+
process.exit(0);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── init subcommand ──────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
if (args[0] === "init") {
|
|
72
|
+
const targetArg = args[1] ?? ".";
|
|
73
|
+
const targetDir = resolve(targetArg);
|
|
74
|
+
|
|
75
|
+
if (targetArg !== ".") {
|
|
76
|
+
try {
|
|
77
|
+
mkdirSync(targetDir, { recursive: true });
|
|
78
|
+
} catch (e) {
|
|
79
|
+
console.error(`gofront: cannot create '${targetArg}': ${e.message}`);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const mainPath = join(targetDir, "main.go");
|
|
85
|
+
if (existsSync(mainPath)) {
|
|
86
|
+
console.error(`gofront: ${mainPath} already exists — nothing written`);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const pkgName =
|
|
91
|
+
targetArg === "." ? basename(resolve(".")) : basename(targetDir);
|
|
92
|
+
// package names must be valid identifiers
|
|
93
|
+
const safePkg = pkgName.replace(/[^a-zA-Z0-9_]/g, "_").replace(/^[0-9]/, "_");
|
|
94
|
+
|
|
95
|
+
const template = `package ${safePkg}
|
|
96
|
+
|
|
97
|
+
func main() {
|
|
98
|
+
\tconsole.log("Hello from GoFront!")
|
|
99
|
+
}
|
|
100
|
+
`;
|
|
101
|
+
try {
|
|
102
|
+
writeFileSync(mainPath, template);
|
|
103
|
+
} catch (e) {
|
|
104
|
+
console.error(`gofront: cannot write '${mainPath}': ${e.message}`);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
console.error(`gofront: created ${mainPath}`);
|
|
109
|
+
console.error(
|
|
110
|
+
`gofront: run gofront ${targetArg === "." ? "main.go" : `${targetArg}/main.go`} to compile`,
|
|
111
|
+
);
|
|
112
|
+
process.exit(0);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const inputArg = args[0];
|
|
116
|
+
const outputFlag = args.indexOf("-o");
|
|
117
|
+
const outputFile = outputFlag !== -1 ? args[outputFlag + 1] : null;
|
|
118
|
+
const checkOnly = args.includes("--check");
|
|
119
|
+
const dumpAst = args.includes("--ast");
|
|
120
|
+
const dumpTokens = args.includes("--tokens");
|
|
121
|
+
const sourceMap = args.includes("--source-map");
|
|
122
|
+
const watchMode = args.includes("--watch");
|
|
123
|
+
const minifyOutput = args.includes("--minify");
|
|
124
|
+
|
|
125
|
+
// ── Determine input mode ─────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
const inputPath = resolve(inputArg);
|
|
128
|
+
let isDir = false;
|
|
129
|
+
try {
|
|
130
|
+
isDir = statSync(inputPath).isDirectory();
|
|
131
|
+
} catch (e) {
|
|
132
|
+
console.error(`gofront: cannot access '${inputArg}': ${e.message}`);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ── Compile function (used for initial build and re-builds) ──
|
|
137
|
+
|
|
138
|
+
function runCompile() {
|
|
139
|
+
if (isDir) {
|
|
140
|
+
return compileDir(inputPath);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Single-file mode
|
|
144
|
+
let source;
|
|
145
|
+
try {
|
|
146
|
+
source = readFileSync(inputPath, "utf8");
|
|
147
|
+
} catch (e) {
|
|
148
|
+
throw new Error(`cannot read '${inputArg}': ${e.message}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let tokens;
|
|
152
|
+
tokens = new Lexer(source, basename(inputPath)).tokenize();
|
|
153
|
+
|
|
154
|
+
if (dumpTokens) {
|
|
155
|
+
for (const tok of tokens) console.log(tok.toString());
|
|
156
|
+
process.exit(0);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
let ast;
|
|
160
|
+
ast = new Parser(tokens, basename(inputPath), source).parse();
|
|
161
|
+
ast._source = source;
|
|
162
|
+
|
|
163
|
+
if (dumpAst) {
|
|
164
|
+
console.log(JSON.stringify(ast, null, 2));
|
|
165
|
+
process.exit(0);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const checker = new TypeChecker();
|
|
169
|
+
const fromDir = dirname(inputPath);
|
|
170
|
+
const jsImports = new Map();
|
|
171
|
+
|
|
172
|
+
for (const imp of ast.imports) {
|
|
173
|
+
for (const path of imp.paths) {
|
|
174
|
+
if (!path.startsWith("js:")) continue;
|
|
175
|
+
const dtsPath = join(fromDir, path.slice(3));
|
|
176
|
+
try {
|
|
177
|
+
const { types, values } = parseDts(readFileSync(dtsPath, "utf8"));
|
|
178
|
+
checker.addDefinitions(types, values);
|
|
179
|
+
} catch (e) {
|
|
180
|
+
throw new Error(`cannot read '${dtsPath}': ${e.message}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const resolved = resolveAll(ast.imports, inputPath, parseDts);
|
|
186
|
+
for (const [path, info] of resolved) {
|
|
187
|
+
if (!info) continue;
|
|
188
|
+
checker.addDefinitions(info.types, info.values);
|
|
189
|
+
jsImports.set(path, [...info.values.keys()]);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const bundledPackages = new Set();
|
|
193
|
+
const preambles = [];
|
|
194
|
+
|
|
195
|
+
for (const imp of ast.imports) {
|
|
196
|
+
for (const path of imp.paths) {
|
|
197
|
+
if (!isLocalPath(path)) continue;
|
|
198
|
+
const depDir = resolveGwDir(path, inputPath);
|
|
199
|
+
if (!depDir) {
|
|
200
|
+
console.error(
|
|
201
|
+
`gofront: warning: cannot find local package '${path}' relative to ${fromDir}`,
|
|
202
|
+
);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const dep = compileDir(depDir);
|
|
206
|
+
preambles.push(dep.js);
|
|
207
|
+
bundledPackages.add(dep.pkgName);
|
|
208
|
+
checker.addPackageNamespace(
|
|
209
|
+
dep.pkgName,
|
|
210
|
+
dep.exportedSymbols,
|
|
211
|
+
dep.exportedTypes,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const errors = checker.check(ast);
|
|
217
|
+
if (errors.length > 0) {
|
|
218
|
+
throw new Error(errors.map((e) => e.message).join("\n"));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const cg = new CodeGen(checker, jsImports, bundledPackages);
|
|
222
|
+
let js = cg.generate(ast);
|
|
223
|
+
|
|
224
|
+
if (sourceMap) {
|
|
225
|
+
const map = cg.getSourceMap(inputArg);
|
|
226
|
+
const b64 = Buffer.from(map).toString("base64");
|
|
227
|
+
js += `\n//# sourceMappingURL=data:application/json;base64,${b64}`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const output = preambles.length > 0 ? `${preambles.join("\n")}\n${js}` : js;
|
|
231
|
+
return { js: output, _cg: cg };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function _writeOutput(js) {
|
|
235
|
+
if (outputFile) {
|
|
236
|
+
writeFileSync(outputFile, `${js}\n`);
|
|
237
|
+
} else {
|
|
238
|
+
console.log(js);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ── Minify helper ────────────────────────────────────────────
|
|
243
|
+
|
|
244
|
+
async function maybeMinify(js) {
|
|
245
|
+
if (!minifyOutput) return js;
|
|
246
|
+
const result = await minify(js, {
|
|
247
|
+
module: true,
|
|
248
|
+
compress: true,
|
|
249
|
+
mangle: true,
|
|
250
|
+
});
|
|
251
|
+
return result.code;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ── Single-shot mode ─────────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
if (!watchMode) {
|
|
257
|
+
let result;
|
|
258
|
+
const startMs = performance.now();
|
|
259
|
+
try {
|
|
260
|
+
result = runCompile();
|
|
261
|
+
} catch (e) {
|
|
262
|
+
console.error(`gofront: ${e.message}`);
|
|
263
|
+
process.exit(1);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (checkOnly) {
|
|
267
|
+
const elapsedMs = (performance.now() - startMs).toFixed(0);
|
|
268
|
+
console.error(`gofront: ${inputArg} — OK (${elapsedMs}ms)`);
|
|
269
|
+
process.exit(0);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
let js;
|
|
273
|
+
try {
|
|
274
|
+
js = await maybeMinify(result.js);
|
|
275
|
+
} catch (e) {
|
|
276
|
+
console.error(`gofront: minify failed: ${e.message}`);
|
|
277
|
+
process.exit(1);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const elapsedMs = (performance.now() - startMs).toFixed(0);
|
|
281
|
+
|
|
282
|
+
if (outputFile) {
|
|
283
|
+
try {
|
|
284
|
+
writeFileSync(outputFile, `${js}\n`);
|
|
285
|
+
console.error(`gofront: wrote ${outputFile} (${elapsedMs}ms)`);
|
|
286
|
+
} catch (e) {
|
|
287
|
+
console.error(`gofront: cannot write '${outputFile}': ${e.message}`);
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
290
|
+
} else {
|
|
291
|
+
console.log(js);
|
|
292
|
+
}
|
|
293
|
+
process.exit(0);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ── Watch mode ───────────────────────────────────────────────
|
|
297
|
+
|
|
298
|
+
function timestamp() {
|
|
299
|
+
return new Date().toLocaleTimeString();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async function buildOnce(_label) {
|
|
303
|
+
try {
|
|
304
|
+
const startMs = performance.now();
|
|
305
|
+
const result = runCompile();
|
|
306
|
+
const js = await maybeMinify(result.js);
|
|
307
|
+
const elapsedMs = (performance.now() - startMs).toFixed(0);
|
|
308
|
+
if (outputFile) {
|
|
309
|
+
writeFileSync(outputFile, `${js}\n`);
|
|
310
|
+
console.error(
|
|
311
|
+
`[${timestamp()}] gofront: OK — wrote ${outputFile} (${elapsedMs}ms)`,
|
|
312
|
+
);
|
|
313
|
+
} else {
|
|
314
|
+
// Clear screen then print
|
|
315
|
+
process.stdout.write("\x1Bc");
|
|
316
|
+
console.log(js);
|
|
317
|
+
console.error(`[${timestamp()}] gofront: OK (${elapsedMs}ms)`);
|
|
318
|
+
}
|
|
319
|
+
} catch (e) {
|
|
320
|
+
console.error(`[${timestamp()}] gofront: ERROR`);
|
|
321
|
+
for (const line of e.message.split("\n")) console.error(` ${line}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Initial build
|
|
326
|
+
buildOnce("initial");
|
|
327
|
+
|
|
328
|
+
// Determine what to watch
|
|
329
|
+
const watchTarget = isDir ? inputPath : dirname(inputPath);
|
|
330
|
+
|
|
331
|
+
let debounce = null;
|
|
332
|
+
watch(watchTarget, { recursive: true }, (_event, filename) => {
|
|
333
|
+
if (filename && !filename.endsWith(".go")) return;
|
|
334
|
+
clearTimeout(debounce);
|
|
335
|
+
debounce = setTimeout(() => buildOnce(filename ?? "change"), 80);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
console.error(`[${timestamp()}] gofront: watching ${inputArg} ...`);
|