gofront 1.0.1 → 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 +35 -2
- package/package.json +7 -7
- package/src/asset-manager.js +125 -0
- package/src/cli-core.js +9 -0
- package/src/dev-server.js +96 -33
- package/src/index.js +53 -2
- package/src/vendor.js +228 -0
package/README.md
CHANGED
|
@@ -387,13 +387,15 @@ gofront <dir> compile all *.go in directory → stdout
|
|
|
387
387
|
gofront <input> -o out.js write output to file (prints elapsed compile time e.g. "15ms")
|
|
388
388
|
gofront <input> --check type-check only
|
|
389
389
|
gofront <input> --watch watch for changes and recompile
|
|
390
|
-
gofront <input> -o out.js --serve watch + serve with live reload (default port 3000)
|
|
390
|
+
gofront <input> -o out.js --serve watch + serve with live reload & SPA fallback (default port 3000)
|
|
391
391
|
gofront <input> -o out.js --serve --port 8080 use a custom port
|
|
392
|
+
gofront <input> -o out.js --copy-assets compile and synchronize static assets from package.json assetCopy
|
|
392
393
|
gofront <input> --source-map append inline source map (single file or directory; multi-file packages emit per-file mappings)
|
|
393
394
|
gofront <input> --minify minify output (built-in minifier)
|
|
394
395
|
gofront <input> --minify --mangle minify and rename local identifiers
|
|
395
396
|
gofront <file.go> --ast dump AST (debug)
|
|
396
397
|
gofront <file.go> --tokens dump tokens (debug)
|
|
398
|
+
gofront prep [dir] copy static assets and bundle vendor dependencies
|
|
397
399
|
gofront init [dir] scaffold a new project
|
|
398
400
|
gofront --version / -v print version
|
|
399
401
|
gofront --help / -h print this help
|
|
@@ -401,6 +403,37 @@ gofront --help / -h print this help
|
|
|
401
403
|
|
|
402
404
|
---
|
|
403
405
|
|
|
406
|
+
## Static assets & vendor bundling
|
|
407
|
+
|
|
408
|
+
GoFront includes native tooling to manage static assets and package external npm dependencies for the browser without requiring external wrappers or complex build tools.
|
|
409
|
+
|
|
410
|
+
### Asset copying (`assetCopy`)
|
|
411
|
+
|
|
412
|
+
Specify static assets (fonts, stylesheets, images) in `package.json` (or `gofront.json`):
|
|
413
|
+
|
|
414
|
+
```json
|
|
415
|
+
{
|
|
416
|
+
"assetCopy": [
|
|
417
|
+
{ "source": "node_modules/@fontsource/raleway/files", "dest": "app/fonts" },
|
|
418
|
+
{ "source": "node_modules/prismjs/themes", "dest": "app/css/prism-themes" }
|
|
419
|
+
]
|
|
420
|
+
}
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
Run `gofront prep` to copy them, or pass `--copy-assets` to automatically synchronize assets on build.
|
|
424
|
+
|
|
425
|
+
### Vendor bundling (`gofront prep`)
|
|
426
|
+
|
|
427
|
+
Package external npm dependencies for the browser:
|
|
428
|
+
|
|
429
|
+
```sh
|
|
430
|
+
gofront prep
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
GoFront inspects your `dependencies`, dynamically uses `rolldown` or `esbuild` from your `devDependencies`, and bundles them into an ES module (defaulting to `app/vendor.js` or `vendor.js`).
|
|
434
|
+
|
|
435
|
+
---
|
|
436
|
+
|
|
404
437
|
## Multi-file packages
|
|
405
438
|
|
|
406
439
|
All `.go` files in a directory share the same namespace and are compiled as one unit.
|
|
@@ -625,7 +658,7 @@ you know exactly what to expect.
|
|
|
625
658
|
|
|
626
659
|
## Roadmap
|
|
627
660
|
|
|
628
|
-
See [`docs/
|
|
661
|
+
See [`docs/roadmap.md`](docs/roadmap.md) for the full roadmap and release history.
|
|
629
662
|
Design documents for planned features are organised by release under `docs/v*/`
|
|
630
663
|
(e.g. [`docs/v0.0.8/`](docs/v0.0.8/), [`docs/v0.0.9/`](docs/v0.0.9/)).
|
|
631
664
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gofront",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"author": "Luuk van Venrooij",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -42,14 +42,14 @@
|
|
|
42
42
|
"test:e2e": "npm run build:all && playwright test",
|
|
43
43
|
"format": "biome format --write .",
|
|
44
44
|
"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",
|
|
45
|
-
"prepare": "
|
|
45
|
+
"prepare": "lefthook install"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@biomejs/biome": "^2.
|
|
49
|
-
"@playwright/test": "^1.
|
|
50
|
-
"c8": "^
|
|
51
|
-
"
|
|
52
|
-
"
|
|
48
|
+
"@biomejs/biome": "^2.5.14",
|
|
49
|
+
"@playwright/test": "^1.63.0",
|
|
50
|
+
"c8": "^12.0.0",
|
|
51
|
+
"jsdom": "^30.1.0",
|
|
52
|
+
"lefthook": "^2.1.14",
|
|
53
53
|
"serve": "^14.2.6"
|
|
54
54
|
}
|
|
55
55
|
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Native static asset manager for GoFront projects.
|
|
2
|
+
// Zero-dependency file and directory copier using node:fs.
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
copyFileSync,
|
|
6
|
+
cpSync,
|
|
7
|
+
existsSync,
|
|
8
|
+
mkdirSync,
|
|
9
|
+
readFileSync,
|
|
10
|
+
statSync,
|
|
11
|
+
} from "node:fs";
|
|
12
|
+
import {
|
|
13
|
+
basename,
|
|
14
|
+
dirname,
|
|
15
|
+
isAbsolute,
|
|
16
|
+
join,
|
|
17
|
+
relative,
|
|
18
|
+
resolve,
|
|
19
|
+
} from "node:path";
|
|
20
|
+
|
|
21
|
+
export function loadAssetConfig(projectDir) {
|
|
22
|
+
const gofrontJsonPath = join(projectDir, "gofront.json");
|
|
23
|
+
if (existsSync(gofrontJsonPath)) {
|
|
24
|
+
try {
|
|
25
|
+
const parsed = JSON.parse(readFileSync(gofrontJsonPath, "utf8"));
|
|
26
|
+
if (Array.isArray(parsed.assetCopy)) return parsed.assetCopy;
|
|
27
|
+
} catch {}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const pkgJsonPath = join(projectDir, "package.json");
|
|
31
|
+
if (existsSync(pkgJsonPath)) {
|
|
32
|
+
try {
|
|
33
|
+
const parsed = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
|
|
34
|
+
if (Array.isArray(parsed.assetCopy)) return parsed.assetCopy;
|
|
35
|
+
} catch {}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function copyFileAsset(srcPath, destPath, dest) {
|
|
42
|
+
let targetFile = destPath;
|
|
43
|
+
if (
|
|
44
|
+
dest.endsWith("/") ||
|
|
45
|
+
(existsSync(destPath) && statSync(destPath).isDirectory())
|
|
46
|
+
) {
|
|
47
|
+
targetFile = join(destPath, basename(srcPath));
|
|
48
|
+
}
|
|
49
|
+
mkdirSync(dirname(targetFile), { recursive: true });
|
|
50
|
+
copyFileSync(srcPath, targetFile);
|
|
51
|
+
return targetFile;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function copyAssetEntry(entry, projectRoot) {
|
|
55
|
+
if (!entry || typeof entry !== "object") return null;
|
|
56
|
+
const { source, dest } = entry;
|
|
57
|
+
if (!source || !dest) return null;
|
|
58
|
+
|
|
59
|
+
const destPath = resolve(projectRoot, dest);
|
|
60
|
+
const relDest = relative(projectRoot, destPath);
|
|
61
|
+
if (relDest.startsWith("..") || isAbsolute(relDest)) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`gofront: asset destination '${dest}' is outside project directory`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const srcPath = resolve(projectRoot, source);
|
|
68
|
+
if (!existsSync(srcPath)) {
|
|
69
|
+
console.warn(`gofront: asset source '${source}' not found — skipping`);
|
|
70
|
+
return {
|
|
71
|
+
status: "skipped",
|
|
72
|
+
item: { source, dest, status: "skipped", reason: "source not found" },
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const stat = statSync(srcPath);
|
|
78
|
+
if (stat.isDirectory()) {
|
|
79
|
+
mkdirSync(destPath, { recursive: true });
|
|
80
|
+
cpSync(srcPath, destPath, { recursive: true, force: true });
|
|
81
|
+
return {
|
|
82
|
+
status: "copied",
|
|
83
|
+
item: { source, dest, type: "directory", status: "copied" },
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const targetFile = copyFileAsset(srcPath, destPath, dest);
|
|
87
|
+
return {
|
|
88
|
+
status: "copied",
|
|
89
|
+
item: { source, dest: targetFile, type: "file", status: "copied" },
|
|
90
|
+
};
|
|
91
|
+
} catch (err) {
|
|
92
|
+
console.warn(`gofront: failed to copy asset '${source}': ${err.message}`);
|
|
93
|
+
return {
|
|
94
|
+
status: "failed",
|
|
95
|
+
item: { source, dest, status: "failed", reason: err.message },
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function copyAssets(projectDir = ".", config = null) {
|
|
101
|
+
const projectRoot = resolve(projectDir);
|
|
102
|
+
let entries;
|
|
103
|
+
|
|
104
|
+
if (Array.isArray(config)) {
|
|
105
|
+
entries = config;
|
|
106
|
+
} else if (config && Array.isArray(config.assetCopy)) {
|
|
107
|
+
entries = config.assetCopy;
|
|
108
|
+
} else {
|
|
109
|
+
entries = loadAssetConfig(projectRoot);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const items = [];
|
|
113
|
+
let copied = 0;
|
|
114
|
+
let skipped = 0;
|
|
115
|
+
|
|
116
|
+
for (const entry of entries) {
|
|
117
|
+
const result = copyAssetEntry(entry, projectRoot);
|
|
118
|
+
if (!result) continue;
|
|
119
|
+
if (result.status === "copied") copied++;
|
|
120
|
+
else skipped++;
|
|
121
|
+
items.push(result.item);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { copied, skipped, items };
|
|
125
|
+
}
|
package/src/cli-core.js
CHANGED
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
|
|
4
4
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { basename, dirname, join, resolve } from "node:path";
|
|
6
|
+
import { copyAssets } from "./asset-manager.js";
|
|
6
7
|
import { compileDir, compileSingleFile } from "./compiler.js";
|
|
7
8
|
import { minify } from "./minifier.js";
|
|
9
|
+
import { bundleVendor } from "./vendor.js";
|
|
8
10
|
|
|
9
11
|
export function runCompile(inputPath, isDir, options) {
|
|
10
12
|
const {
|
|
@@ -63,3 +65,10 @@ export function handleInit(targetDir) {
|
|
|
63
65
|
|
|
64
66
|
return { mainPath };
|
|
65
67
|
}
|
|
68
|
+
|
|
69
|
+
export async function handlePrep(targetDir, options = {}) {
|
|
70
|
+
const resolvedTarget = resolve(targetDir);
|
|
71
|
+
const assets = copyAssets(resolvedTarget, options.assetConfig);
|
|
72
|
+
const vendor = await bundleVendor(resolvedTarget, options.vendorConfig);
|
|
73
|
+
return { assets, vendor };
|
|
74
|
+
}
|
package/src/dev-server.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// Minimal dev server for gofront --serve watch mode.
|
|
2
2
|
// Serves static files and pushes a reload event via SSE after each build.
|
|
3
3
|
|
|
4
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
5
5
|
import { createServer } from "node:http";
|
|
6
|
-
import { extname, join } from "node:path";
|
|
6
|
+
import { extname, join, resolve } from "node:path";
|
|
7
7
|
|
|
8
|
-
const MIME = {
|
|
8
|
+
export const MIME = {
|
|
9
9
|
".html": "text/html; charset=utf-8",
|
|
10
10
|
".js": "application/javascript; charset=utf-8",
|
|
11
11
|
".css": "text/css; charset=utf-8",
|
|
@@ -22,42 +22,97 @@ const MIME = {
|
|
|
22
22
|
// Injected at the bottom of compiled JS in serve mode.
|
|
23
23
|
export const liveReloadClient = `(function(){var es=new EventSource('/_gofront/events');es.addEventListener('reload',function(){location.reload();});})();`;
|
|
24
24
|
|
|
25
|
-
export function
|
|
26
|
-
|
|
25
|
+
export function handleDevRequest(req, res, serveDir, clients = new Set()) {
|
|
26
|
+
// SSE endpoint — browser connects here to receive reload events
|
|
27
|
+
if (req.url === "/_gofront/events") {
|
|
28
|
+
res.writeHead(200, {
|
|
29
|
+
"Content-Type": "text/event-stream",
|
|
30
|
+
"Cache-Control": "no-cache",
|
|
31
|
+
Connection: "keep-alive",
|
|
32
|
+
});
|
|
33
|
+
res.write(": connected\n\n");
|
|
34
|
+
clients.add(res);
|
|
35
|
+
req.on?.("close", () => clients.delete(res));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
27
38
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
res.write(": connected\n\n");
|
|
37
|
-
clients.add(res);
|
|
38
|
-
req.on("close", () => clients.delete(res));
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
39
|
+
let urlPath = req.url.split("?")[0].split("#")[0];
|
|
40
|
+
try {
|
|
41
|
+
urlPath = decodeURIComponent(urlPath);
|
|
42
|
+
} catch {
|
|
43
|
+
// Ignore malformed URL encoding and use raw path
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (urlPath === "/" || urlPath === "") urlPath = "/index.html";
|
|
41
47
|
|
|
42
|
-
|
|
43
|
-
|
|
48
|
+
const resolvedServe = resolve(serveDir);
|
|
49
|
+
const filePath = resolve(serveDir, `.${urlPath}`);
|
|
50
|
+
if (!filePath.startsWith(resolvedServe)) {
|
|
51
|
+
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
52
|
+
res.end("Forbidden");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!existsSync(filePath)) {
|
|
57
|
+
// SPA fallback: clean paths with no file extension fall back to index.html
|
|
58
|
+
const ext = extname(urlPath);
|
|
59
|
+
if (!ext) {
|
|
60
|
+
const indexPath = join(serveDir, "index.html");
|
|
61
|
+
if (existsSync(indexPath)) {
|
|
62
|
+
try {
|
|
63
|
+
const data = readFileSync(indexPath);
|
|
64
|
+
res.writeHead(200, { "Content-Type": MIME[".html"] });
|
|
65
|
+
res.end(data);
|
|
66
|
+
return;
|
|
67
|
+
} catch {
|
|
68
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
69
|
+
res.end("Server error");
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
75
|
+
res.end("Not found");
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
44
78
|
|
|
45
|
-
|
|
46
|
-
|
|
79
|
+
try {
|
|
80
|
+
const stat = statSync(filePath);
|
|
81
|
+
if (stat.isDirectory()) {
|
|
82
|
+
const dirIndex = join(filePath, "index.html");
|
|
83
|
+
if (existsSync(dirIndex)) {
|
|
84
|
+
const data = readFileSync(dirIndex);
|
|
85
|
+
res.writeHead(200, { "Content-Type": MIME[".html"] });
|
|
86
|
+
res.end(data);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const rootIndex = join(serveDir, "index.html");
|
|
90
|
+
if (existsSync(rootIndex)) {
|
|
91
|
+
const data = readFileSync(rootIndex);
|
|
92
|
+
res.writeHead(200, { "Content-Type": MIME[".html"] });
|
|
93
|
+
res.end(data);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
47
96
|
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
48
97
|
res.end("Not found");
|
|
49
98
|
return;
|
|
50
99
|
}
|
|
51
100
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
101
|
+
const data = readFileSync(filePath);
|
|
102
|
+
const mime = MIME[extname(filePath)] ?? "application/octet-stream";
|
|
103
|
+
res.writeHead(200, { "Content-Type": mime });
|
|
104
|
+
res.end(data);
|
|
105
|
+
} catch {
|
|
106
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
107
|
+
res.end("Server error");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function createDevServer(serveDir, port = 3000) {
|
|
112
|
+
const clients = new Set();
|
|
113
|
+
|
|
114
|
+
const server = createServer((req, res) => {
|
|
115
|
+
handleDevRequest(req, res, serveDir, clients);
|
|
61
116
|
});
|
|
62
117
|
|
|
63
118
|
server.on("error", (err) => {
|
|
@@ -70,8 +125,12 @@ export function createDevServer(serveDir, port) {
|
|
|
70
125
|
}
|
|
71
126
|
process.exit(1);
|
|
72
127
|
});
|
|
128
|
+
|
|
73
129
|
server.listen(port, () => {
|
|
74
|
-
|
|
130
|
+
const actualPort = server.address()?.port ?? port;
|
|
131
|
+
if (port !== 0) {
|
|
132
|
+
console.error(`gofront: dev server → http://localhost:${actualPort}`);
|
|
133
|
+
}
|
|
75
134
|
});
|
|
76
135
|
|
|
77
136
|
function notify() {
|
|
@@ -80,5 +139,9 @@ export function createDevServer(serveDir, port) {
|
|
|
80
139
|
}
|
|
81
140
|
}
|
|
82
141
|
|
|
83
|
-
return {
|
|
142
|
+
return {
|
|
143
|
+
notify,
|
|
144
|
+
server,
|
|
145
|
+
close: () => new Promise((res) => server.close(res)),
|
|
146
|
+
};
|
|
84
147
|
}
|
package/src/index.js
CHANGED
|
@@ -18,9 +18,10 @@ import { createRequire } from "node:module";
|
|
|
18
18
|
const _require = createRequire(import.meta.url);
|
|
19
19
|
const { version } = _require("../package.json");
|
|
20
20
|
|
|
21
|
-
import { statSync, watch, writeFileSync } from "node:fs";
|
|
21
|
+
import { mkdirSync, statSync, watch, writeFileSync } from "node:fs";
|
|
22
22
|
import { dirname, resolve } from "node:path";
|
|
23
|
-
import {
|
|
23
|
+
import { copyAssets } from "./asset-manager.js";
|
|
24
|
+
import { handleInit, handlePrep, maybeMinify, runCompile } from "./cli-core.js";
|
|
24
25
|
import { createDevServer, liveReloadClient } from "./dev-server.js";
|
|
25
26
|
|
|
26
27
|
// ── Parse CLI args ───────────────────────────────────────────
|
|
@@ -44,11 +45,13 @@ Usage:
|
|
|
44
45
|
gofront <input> --watch Watch for changes and recompile
|
|
45
46
|
gofront <input> -o out.js --serve Watch + serve with live reload (default port 3000)
|
|
46
47
|
gofront <input> -o out.js --serve --port 8080 Use a custom port
|
|
48
|
+
gofront <input> -o out.js --copy-assets Compile and synchronize static assets
|
|
47
49
|
gofront <input> --source-map Append inline source map to output
|
|
48
50
|
gofront <input> --minify Minify output
|
|
49
51
|
gofront <input> --minify --mangle Minify and mangle identifiers
|
|
50
52
|
gofront <file.go> --ast Dump AST (debug)
|
|
51
53
|
gofront <file.go> --tokens Dump tokens (debug)
|
|
54
|
+
gofront prep [dir] Copy static assets and bundle vendor dependencies
|
|
52
55
|
gofront init [dir] Scaffold a new GoFront project
|
|
53
56
|
gofront --version Print version
|
|
54
57
|
`.trim(),
|
|
@@ -56,6 +59,30 @@ Usage:
|
|
|
56
59
|
process.exit(0);
|
|
57
60
|
}
|
|
58
61
|
|
|
62
|
+
// ── prep / vendor subcommand ──────────────────────────────────
|
|
63
|
+
|
|
64
|
+
if (args[0] === "prep" || args[0] === "vendor") {
|
|
65
|
+
const targetArg = args[1] ?? ".";
|
|
66
|
+
const targetDir = resolve(targetArg);
|
|
67
|
+
try {
|
|
68
|
+
const { assets, vendor } = await handlePrep(targetDir);
|
|
69
|
+
if (assets.copied > 0 || assets.skipped > 0) {
|
|
70
|
+
console.error(
|
|
71
|
+
`gofront: copied ${assets.copied} assets (${assets.skipped} skipped)`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
if (vendor.bundled?.length > 0) {
|
|
75
|
+
console.error(
|
|
76
|
+
`gofront: bundled ${vendor.bundled.length} vendor dependencies → ${vendor.dest}`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
} catch (e) {
|
|
80
|
+
console.error(`gofront: ${e.message}`);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
process.exit(0);
|
|
84
|
+
}
|
|
85
|
+
|
|
59
86
|
// ── init subcommand ──────────────────────────────────────────
|
|
60
87
|
|
|
61
88
|
if (args[0] === "init") {
|
|
@@ -84,6 +111,7 @@ const dumpTokens = args.includes("--tokens");
|
|
|
84
111
|
const sourceMap = args.includes("--source-map");
|
|
85
112
|
const serveMode = args.includes("--serve");
|
|
86
113
|
const watchMode = args.includes("--watch") || serveMode;
|
|
114
|
+
const copyAssetsFlag = args.includes("--copy-assets");
|
|
87
115
|
const minifyOutput = args.includes("--minify");
|
|
88
116
|
const mangleOutput = args.includes("--mangle");
|
|
89
117
|
const portFlag = args.indexOf("--port");
|
|
@@ -148,6 +176,7 @@ if (!watchMode) {
|
|
|
148
176
|
|
|
149
177
|
if (outputFile) {
|
|
150
178
|
try {
|
|
179
|
+
mkdirSync(dirname(resolve(outputFile)), { recursive: true });
|
|
151
180
|
writeFileSync(outputFile, `${js}\n`);
|
|
152
181
|
console.error(`gofront: wrote ${outputFile} (${elapsedMs}ms)`);
|
|
153
182
|
} catch (e) {
|
|
@@ -157,6 +186,19 @@ if (!watchMode) {
|
|
|
157
186
|
} else {
|
|
158
187
|
console.log(js);
|
|
159
188
|
}
|
|
189
|
+
|
|
190
|
+
if (copyAssetsFlag) {
|
|
191
|
+
try {
|
|
192
|
+
const projectDir = resolve(".");
|
|
193
|
+
const { copied, skipped } = copyAssets(projectDir);
|
|
194
|
+
if (copied > 0 || skipped > 0) {
|
|
195
|
+
console.error(`gofront: copied ${copied} assets (${skipped} skipped)`);
|
|
196
|
+
}
|
|
197
|
+
} catch (e) {
|
|
198
|
+
console.error(`gofront: asset copy failed: ${e.message}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
160
202
|
process.exit(0);
|
|
161
203
|
}
|
|
162
204
|
|
|
@@ -210,6 +252,15 @@ function buildOnce(changedFile = null) {
|
|
|
210
252
|
`[${timestamp()}] gofront: OK (${elapsedMs}ms${changeNote})`,
|
|
211
253
|
);
|
|
212
254
|
}
|
|
255
|
+
|
|
256
|
+
if (copyAssetsFlag) {
|
|
257
|
+
try {
|
|
258
|
+
copyAssets(resolve("."));
|
|
259
|
+
} catch (e) {
|
|
260
|
+
console.error(`gofront: asset copy failed: ${e.message}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
213
264
|
devServer?.notify();
|
|
214
265
|
} catch (e) {
|
|
215
266
|
console.error(`[${timestamp()}] gofront: 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
|
+
}
|