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 +6 -4
- package/src/cli-core.js +520 -15
- package/src/codegen/expressions.js +229 -30
- package/src/codegen/index.js +38 -20
- package/src/codegen/runtime.js +14 -2
- package/src/codegen/statements.js +95 -15
- package/src/codegen/stdlib/builder.js +2 -2
- package/src/codegen/stdlib/bytes.js +1 -1
- package/src/codegen/stdlib/fmt.js +3 -4
- package/src/codegen/stdlib/io.js +1 -2
- package/src/codegen/templ.js +1 -1
- package/src/dev-server.js +279 -83
- package/src/index.js +87 -16
- package/src/minifier.js +152 -34
- package/src/parser/expressions.js +11 -0
- package/src/pwa.js +138 -0
- package/src/typechecker/assignability.js +61 -8
- package/src/typechecker/resolve.js +37 -3
- package/src/typechecker/stdlib/core.js +16 -13
- package/src/typechecker/stdlib/extended.js +11 -2
- package/src/typechecker/stdlib/web.js +418 -0
- package/src/typechecker/stdlib.js +2 -0
- package/src/typechecker/types.js +78 -17
- package/src/vendor.js +158 -43
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gofront",
|
|
3
|
-
"version": "1.
|
|
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:
|
|
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 {
|
|
5
|
-
|
|
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
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
+
}
|