@arcote.tech/arc-cli 0.8.1 → 0.8.3
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/dist/index.js +369 -67
- package/package.json +10 -10
- package/src/builder/chunk-planner.ts +14 -0
- package/src/builder/module-builder.ts +432 -30
- package/src/commands/platform-build.ts +6 -1
- package/src/commands/platform-pairing-token.ts +53 -0
- package/src/deploy/caddyfile.ts +3 -0
- package/src/index.ts +14 -2
- package/src/platform/server.ts +6 -8
- package/src/platform/shared.ts +68 -21
- package/src/platform/startup.ts +32 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arcote.tech/arc-cli",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.3",
|
|
4
4
|
"description": "CLI tool for Arc framework",
|
|
5
5
|
"module": "index.ts",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -12,13 +12,13 @@
|
|
|
12
12
|
"build": "bun build --target=bun ./src/index.ts --outdir=dist --external @arcote.tech/arc --external @arcote.tech/arc-ds --external @arcote.tech/arc-react --external @arcote.tech/platform --external @arcote.tech/arc-map --external '@opentelemetry/*' && chmod +x dist/index.js"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@arcote.tech/arc": "^0.8.
|
|
16
|
-
"@arcote.tech/arc-ds": "^0.8.
|
|
17
|
-
"@arcote.tech/arc-react": "^0.8.
|
|
18
|
-
"@arcote.tech/arc-host": "^0.8.
|
|
19
|
-
"@arcote.tech/arc-adapter-db-sqlite": "^0.8.
|
|
20
|
-
"@arcote.tech/arc-adapter-db-postgres": "^0.8.
|
|
21
|
-
"@arcote.tech/arc-otel": "^0.8.
|
|
15
|
+
"@arcote.tech/arc": "^0.8.3",
|
|
16
|
+
"@arcote.tech/arc-ds": "^0.8.3",
|
|
17
|
+
"@arcote.tech/arc-react": "^0.8.3",
|
|
18
|
+
"@arcote.tech/arc-host": "^0.8.3",
|
|
19
|
+
"@arcote.tech/arc-adapter-db-sqlite": "^0.8.3",
|
|
20
|
+
"@arcote.tech/arc-adapter-db-postgres": "^0.8.3",
|
|
21
|
+
"@arcote.tech/arc-otel": "^0.8.3",
|
|
22
22
|
"@opentelemetry/api": "^1.9.0",
|
|
23
23
|
"@opentelemetry/api-logs": "^0.57.0",
|
|
24
24
|
"@opentelemetry/core": "^1.30.0",
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
"@opentelemetry/sdk-trace-base": "^1.30.0",
|
|
32
32
|
"@opentelemetry/sdk-trace-node": "^1.30.0",
|
|
33
33
|
"@opentelemetry/semantic-conventions": "^1.27.0",
|
|
34
|
-
"@arcote.tech/platform": "^0.8.
|
|
35
|
-
"@arcote.tech/arc-map": "^0.8.
|
|
34
|
+
"@arcote.tech/platform": "^0.8.3",
|
|
35
|
+
"@arcote.tech/arc-map": "^0.8.3",
|
|
36
36
|
"@clack/prompts": "^0.9.0",
|
|
37
37
|
"commander": "^11.1.0",
|
|
38
38
|
"chokidar": "^3.5.3",
|
|
@@ -120,6 +120,20 @@ function collectModuleNames(dir: string, acc: Set<string>): void {
|
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Czy pakiet DEKLARUJE moduł Arc (`module("...")` w źródłach). Tylko takie
|
|
125
|
+
* pakiety są „członkami" wchodzącymi do initial/grup — czysta biblioteka bez
|
|
126
|
+
* modułu (np. pakiet z szablonami maili) NIE może być side-effect-importowana
|
|
127
|
+
* przez initial, bo z `sideEffects:true` wciągałaby całe swoje drzewo
|
|
128
|
+
* zależności do pierwszego wczytania (patrz: @react-email w NDT — 1,4 MB
|
|
129
|
+
* stosu maili w bundlu klienta przez samo `import "@ndt/email"`).
|
|
130
|
+
*/
|
|
131
|
+
export function packageDeclaresModule(pkg: WorkspacePackage): boolean {
|
|
132
|
+
const names = new Set<string>();
|
|
133
|
+
collectModuleNames(join(pkg.path, "src"), names);
|
|
134
|
+
return names.size > 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
123
137
|
/**
|
|
124
138
|
* Assert every workspace package declares at most one `module()`. Throws a
|
|
125
139
|
* build error listing offenders, with the fix (split each extra module into
|
|
@@ -239,6 +239,51 @@ function workspaceSourcePlugin(srcByName: Map<string, string>): import("bun").Bu
|
|
|
239
239
|
};
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
+
// Builtiny node, które w przeglądarce NIGDY nie mają sensu (I/O, krypto,
|
|
243
|
+
// procesy, sieć). Domyślnie Bun polyfilluje je ciężkimi shimami (crypto →
|
|
244
|
+
// crypto-browserify ~360 KB gzip) — a że kod serwerowy (webhooki, hashowanie,
|
|
245
|
+
// szyfrowanie OAuth) importuje je statycznie na górze modułu, polyfill ląduje
|
|
246
|
+
// w bundlu klienta, mimo że ten kod nigdy nie biegnie w przeglądarce.
|
|
247
|
+
//
|
|
248
|
+
// CELOWO NIE stubujemy „przeglądarkowo-bezpiecznych" builtinów (buffer, util,
|
|
249
|
+
// events, path, url, string_decoder, querystring, assert, process) — te bywają
|
|
250
|
+
// realnie używane po stronie klienta i mają lekkie, działające polyfille.
|
|
251
|
+
const SERVER_ONLY_NODE_BUILTINS = new Set([
|
|
252
|
+
"crypto", "stream", "zlib", "fs", "fs/promises", "net", "tls", "http",
|
|
253
|
+
"https", "http2", "dns", "dgram", "child_process", "worker_threads",
|
|
254
|
+
"cluster", "os", "v8", "vm", "readline", "repl", "tty", "perf_hooks",
|
|
255
|
+
"inspector", "async_hooks", "diagnostics_channel", "module", "constants",
|
|
256
|
+
]);
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Zastępuje serwerowe builtiny node (SERVER_ONLY_NODE_BUILTINS) rzucającym
|
|
260
|
+
* stubem w buildzie przeglądarki — zamiast polyfilla. Dowolny named/default
|
|
261
|
+
* import przechodzi (interop CJS-Proxy), ale UŻYCIE na kliencie rzuca jasny
|
|
262
|
+
* błąd „server-only". Efekt: server-only kod importujący `node:crypto` itd.
|
|
263
|
+
* nie wciąga polyfilla do bundla klienta (NDT: −~360 KB gzip).
|
|
264
|
+
*/
|
|
265
|
+
function nodeServerBuiltinStubPlugin(): import("bun").BunPlugin {
|
|
266
|
+
return {
|
|
267
|
+
name: "node-server-builtin-stub",
|
|
268
|
+
setup(build) {
|
|
269
|
+
build.onResolve({ filter: /^(node:)?[a-z_/]+$/ }, (args) => {
|
|
270
|
+
const bare = args.path.replace(/^node:/, "");
|
|
271
|
+
if (!SERVER_ONLY_NODE_BUILTINS.has(bare)) return undefined;
|
|
272
|
+
return { path: args.path, namespace: "node-server-stub" };
|
|
273
|
+
});
|
|
274
|
+
build.onLoad({ filter: /.*/, namespace: "node-server-stub" }, (args) => ({
|
|
275
|
+
// CJS Proxy: `import { createHash } from "node:crypto"` czyta
|
|
276
|
+
// `module.exports.createHash` przez interop → getter rzuca dopiero przy
|
|
277
|
+
// realnym użyciu. `__esModule:false` żeby interop potraktował to jak CJS.
|
|
278
|
+
contents:
|
|
279
|
+
`const e=()=>{throw new Error("Node builtin '${args.path}' jest server-only — niedostępny w przeglądarce");};` +
|
|
280
|
+
`module.exports=new Proxy({},{get:(_,p)=>p==="__esModule"?false:e});`,
|
|
281
|
+
loader: "js",
|
|
282
|
+
}));
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
242
287
|
function jsxDevShimPlugin(): import("bun").BunPlugin {
|
|
243
288
|
return {
|
|
244
289
|
name: "jsx-dev-runtime-shim",
|
|
@@ -456,7 +501,13 @@ async function buildContextClient(
|
|
|
456
501
|
format: "esm",
|
|
457
502
|
naming: "index.[ext]",
|
|
458
503
|
external: externals,
|
|
459
|
-
plugins: [
|
|
504
|
+
plugins: [
|
|
505
|
+
jsxDevShimPlugin(),
|
|
506
|
+
// Browser dist: serwerowe builtiny node → rzucający stub zamiast ciężkiego
|
|
507
|
+
// polyfilla (np. @ndt/social szyfruje tokeny OAuth przez node:crypto —
|
|
508
|
+
// server-only, ale ląduje w dist/browser i wciągało crypto-browserify).
|
|
509
|
+
...(client.target === "browser" ? [nodeServerBuiltinStubPlugin()] : []),
|
|
510
|
+
],
|
|
460
511
|
define: client.defines,
|
|
461
512
|
});
|
|
462
513
|
|
|
@@ -677,9 +728,13 @@ export async function buildServerApp(
|
|
|
677
728
|
|
|
678
729
|
let result;
|
|
679
730
|
try {
|
|
731
|
+
// Bez `outdir` — artefakty w pamięci, pliki piszemy sami (niżej).
|
|
732
|
+
// Równoległy builder tego samego workspace'u (dev watcher) potrafi
|
|
733
|
+
// wipe'ować serverDir w trakcie zapisu outputów Buna (losowe `ENOENT:
|
|
734
|
+
// writing sourcemap for chunk` wywalało cały build). Ten sam wzorzec
|
|
735
|
+
// ochrony co w buildBrowserApp.
|
|
680
736
|
result = await Bun.build({
|
|
681
737
|
entrypoints: [entrySrc],
|
|
682
|
-
outdir: serverDir,
|
|
683
738
|
target: "bun",
|
|
684
739
|
format: "esm",
|
|
685
740
|
// splitting:true is the whole point — it makes Bun emit eager top-level
|
|
@@ -720,6 +775,15 @@ export async function buildServerApp(
|
|
|
720
775
|
);
|
|
721
776
|
}
|
|
722
777
|
|
|
778
|
+
// Zapis wszystkich artefaktów (entry + chunki + sourcemapy) własną ręką —
|
|
779
|
+
// patrz komentarz przy Bun.build wyżej.
|
|
780
|
+
for (const out of result.outputs) {
|
|
781
|
+
writeFileSync(
|
|
782
|
+
join(serverDir, basename(out.path)),
|
|
783
|
+
new Uint8Array(await out.arrayBuffer()),
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
|
|
723
787
|
// Third-party externals only — framework peers carry their own resolution in
|
|
724
788
|
// collectFrameworkDeps, so drop them here to avoid double-sourcing versions.
|
|
725
789
|
const externals: ServerExternal[] = [...recorded]
|
|
@@ -787,6 +851,194 @@ export interface BrowserAppResult {
|
|
|
787
851
|
readonly cached: boolean;
|
|
788
852
|
}
|
|
789
853
|
|
|
854
|
+
/** Output builda przeglądarki trzymany w pamięci — bajty prosto z artefaktu
|
|
855
|
+
* Bun.build, zapisywane na dysk przez NAS (patrz komentarz przy runBuild). */
|
|
856
|
+
interface BrowserOutFile {
|
|
857
|
+
kind: "entry-point" | "chunk" | string;
|
|
858
|
+
name: string;
|
|
859
|
+
bytes: Uint8Array;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// ---------------------------------------------------------------------------
|
|
863
|
+
// Build stats (ARC_BUILD_STATS=1) — rozbicie każdego chunku na biblioteki.
|
|
864
|
+
//
|
|
865
|
+
// Skąd dane: build w trybie stats emituje sourcemapy (`sourcemap: "external"`),
|
|
866
|
+
// których `sources[]` niosą ścieżki KAŻDEGO pliku źródłowego wmieszanego do
|
|
867
|
+
// chunku, a `sourcesContent[]` jego treść. Przypisujemy każdy plik do pakietu
|
|
868
|
+
// (node_modules → zewnętrzny, packages/* → wewnętrzny) i sumujemy bajty źródeł
|
|
869
|
+
// jako wagę wkładu. To odpowiada na pytanie "co i skąd wpadło do bundla" —
|
|
870
|
+
// w szczególności KTÓRE wejście (initial vs grupa tokenu) ciągnie ciężką libę.
|
|
871
|
+
// ---------------------------------------------------------------------------
|
|
872
|
+
|
|
873
|
+
/** Czy build ma policzyć i wypisać rozbicie chunków na biblioteki. */
|
|
874
|
+
export function buildStatsEnabled(): boolean {
|
|
875
|
+
return process.env.ARC_BUILD_STATS === "1";
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
interface PkgWeight {
|
|
879
|
+
pkg: string;
|
|
880
|
+
bytes: number;
|
|
881
|
+
external: boolean;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/** Ścieżka źródła (z sourcemapy) → nazwa pakietu + czy zewnętrzny. */
|
|
885
|
+
function classifySource(src: string): { pkg: string; external: boolean } {
|
|
886
|
+
const nm = src.lastIndexOf("node_modules/");
|
|
887
|
+
if (nm >= 0) {
|
|
888
|
+
const rest = src.slice(nm + "node_modules/".length);
|
|
889
|
+
const parts = rest.split("/");
|
|
890
|
+
const pkg = parts[0].startsWith("@") ? `${parts[0]}/${parts[1]}` : parts[0];
|
|
891
|
+
// @ndt/* i @arcote.tech/* rozwiązane przez symlink w node_modules to
|
|
892
|
+
// faktycznie kod wewnętrzny/frameworkowy — nie "zewnętrzna zależność npm".
|
|
893
|
+
const external = !pkg.startsWith("@ndt/") && !pkg.startsWith("@arcote.tech/");
|
|
894
|
+
return { pkg, external };
|
|
895
|
+
}
|
|
896
|
+
// Źródło workspace (np. "packages/mailing/src/ids.ts").
|
|
897
|
+
const m = src.match(/(?:^|\/)packages\/([^/]+)\//);
|
|
898
|
+
if (m) return { pkg: `packages/${m[1]}`, external: false };
|
|
899
|
+
return { pkg: src.split("/").slice(0, 2).join("/") || src, external: false };
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/** Rozbij chunk (po sourcemapie) na wagi per pakiet, malejąco. */
|
|
903
|
+
function chunkPkgWeights(map: {
|
|
904
|
+
sources?: string[];
|
|
905
|
+
sourcesContent?: (string | null)[];
|
|
906
|
+
}): PkgWeight[] {
|
|
907
|
+
const acc = new Map<string, PkgWeight>();
|
|
908
|
+
const sources = map.sources ?? [];
|
|
909
|
+
const contents = map.sourcesContent ?? [];
|
|
910
|
+
for (let i = 0; i < sources.length; i++) {
|
|
911
|
+
const bytes = contents[i]?.length ?? 0;
|
|
912
|
+
const { pkg, external } = classifySource(sources[i]);
|
|
913
|
+
const cur = acc.get(pkg) ?? { pkg, bytes: 0, external };
|
|
914
|
+
cur.bytes += bytes;
|
|
915
|
+
acc.set(pkg, cur);
|
|
916
|
+
}
|
|
917
|
+
return [...acc.values()].sort((a, b) => b.bytes - a.bytes);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
const kb = (n: number) => `${Math.round(n / 1024)} KB`;
|
|
921
|
+
|
|
922
|
+
/** Usuń końcowe linie `//# debugId=` / `//# sourceMappingURL=` dopięte przez
|
|
923
|
+
* Bun przy `sourcemap:"external"`, tak by bajty JS == build bez sourcemap. */
|
|
924
|
+
function stripTrailingSourcemapComment(bytes: Uint8Array): Uint8Array {
|
|
925
|
+
let text = Buffer.from(bytes).toString("utf8");
|
|
926
|
+
text = text.replace(/\n*\/\/# (?:debugId|sourceMappingURL)=[^\n]*\s*$/, "");
|
|
927
|
+
if (!text.endsWith("\n")) text += "\n";
|
|
928
|
+
return new Uint8Array(Buffer.from(text, "utf8"));
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* Wypisz rozbicie buildu: dla każdego WEJŚCIA (initial + grupy tokenów)
|
|
933
|
+
* jego pierwsze wczytanie (statyczne domknięcie chunków) z wagą raw/gzip
|
|
934
|
+
* i najcięższymi bibliotekami, plus globalny ranking zewnętrznych libów
|
|
935
|
+
* z informacją, w którym chunku i pod którym wejściem lądują.
|
|
936
|
+
*/
|
|
937
|
+
function reportBrowserBuildStats(opts: {
|
|
938
|
+
label: string;
|
|
939
|
+
entries: { name: string; file: string }[];
|
|
940
|
+
jsByName: Map<string, Uint8Array>;
|
|
941
|
+
mapsByJs: Map<string, { sources?: string[]; sourcesContent?: (string | null)[] }>;
|
|
942
|
+
}): void {
|
|
943
|
+
const { label, entries, jsByName, mapsByJs } = opts;
|
|
944
|
+
|
|
945
|
+
// Graf statycznych importów (bare `import"..."` / `from"..."`, NIE `import(...)`).
|
|
946
|
+
const staticEdge =
|
|
947
|
+
/(?:^|[;}\s])import\s*["'](\.\/[^"']+\.js)["']|from\s*["'](\.\/[^"']+\.js)["']/g;
|
|
948
|
+
const staticImports = (name: string): string[] => {
|
|
949
|
+
const bytes = jsByName.get(name);
|
|
950
|
+
if (!bytes) return [];
|
|
951
|
+
const s = Buffer.from(bytes).toString("utf8");
|
|
952
|
+
const out: string[] = [];
|
|
953
|
+
let m: RegExpExecArray | null;
|
|
954
|
+
staticEdge.lastIndex = 0;
|
|
955
|
+
while ((m = staticEdge.exec(s))) out.push(basename(m[1] ?? m[2]!));
|
|
956
|
+
return out;
|
|
957
|
+
};
|
|
958
|
+
const closure = (start: string): Set<string> => {
|
|
959
|
+
const seen = new Set<string>();
|
|
960
|
+
const stack = [start];
|
|
961
|
+
while (stack.length) {
|
|
962
|
+
const f = stack.pop()!;
|
|
963
|
+
if (seen.has(f) || !jsByName.has(f)) continue;
|
|
964
|
+
seen.add(f);
|
|
965
|
+
stack.push(...staticImports(f));
|
|
966
|
+
}
|
|
967
|
+
return seen;
|
|
968
|
+
};
|
|
969
|
+
|
|
970
|
+
const gz = (bytes: Uint8Array) =>
|
|
971
|
+
Bun.gzipSync(bytes as Uint8Array<ArrayBuffer>, { level: 6 }).length;
|
|
972
|
+
|
|
973
|
+
console.log(`\n━━━━━ Build stats: ${label} ━━━━━`);
|
|
974
|
+
const globalExt = new Map<string, { bytes: number; chunks: Set<string>; entries: Set<string> }>();
|
|
975
|
+
|
|
976
|
+
for (const entry of entries) {
|
|
977
|
+
const files = closure(entry.file);
|
|
978
|
+
let raw = 0;
|
|
979
|
+
let gzip = 0;
|
|
980
|
+
const pkgAcc = new Map<string, PkgWeight>();
|
|
981
|
+
for (const f of files) {
|
|
982
|
+
const bytes = jsByName.get(f)!;
|
|
983
|
+
raw += bytes.length;
|
|
984
|
+
gzip += gz(bytes);
|
|
985
|
+
const map = mapsByJs.get(f);
|
|
986
|
+
if (!map) continue;
|
|
987
|
+
for (const w of chunkPkgWeights(map)) {
|
|
988
|
+
const cur = pkgAcc.get(w.pkg) ?? { pkg: w.pkg, bytes: 0, external: w.external };
|
|
989
|
+
cur.bytes += w.bytes;
|
|
990
|
+
pkgAcc.set(w.pkg, cur);
|
|
991
|
+
if (w.external) {
|
|
992
|
+
const g = globalExt.get(w.pkg) ?? { bytes: 0, chunks: new Set(), entries: new Set() };
|
|
993
|
+
g.bytes = Math.max(g.bytes, w.bytes);
|
|
994
|
+
g.chunks.add(f);
|
|
995
|
+
g.entries.add(entry.name);
|
|
996
|
+
globalExt.set(w.pkg, g);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
const top = [...pkgAcc.values()].sort((a, b) => b.bytes - a.bytes).slice(0, 10);
|
|
1001
|
+
console.log(
|
|
1002
|
+
`\n▸ ${entry.name}: pierwsze wczytanie ${kb(raw)} raw / ${kb(gzip)} gzip (${files.size} plików)`,
|
|
1003
|
+
);
|
|
1004
|
+
for (const w of top) {
|
|
1005
|
+
const tag = w.external ? "ext" : "wewn";
|
|
1006
|
+
console.log(` ${kb(w.bytes).padStart(8)} [${tag}] ${w.pkg}`);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
const heaviest = [...globalExt.entries()].sort((a, b) => b[1].bytes - a[1].bytes).slice(0, 15);
|
|
1011
|
+
if (heaviest.length) {
|
|
1012
|
+
console.log(`\n▸ Najcięższe biblioteki ZEWNĘTRZNE (źródła) i gdzie lądują:`);
|
|
1013
|
+
for (const [pkg, g] of heaviest) {
|
|
1014
|
+
console.log(
|
|
1015
|
+
` ${kb(g.bytes).padStart(8)} ${pkg} → ${[...g.entries].join(", ")}`,
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
console.log(`\n━━━━━ koniec stats: ${label} ━━━━━\n`);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
/**
|
|
1023
|
+
* Heurystyka "czy output wygląda na zminifikowany": w plikach ≥ 16 KB liczymy
|
|
1024
|
+
* średnią długość linii pierwszych 64 KB. Kod niezminifikowany z Bun to
|
|
1025
|
+
* ~35-45 B/linię; zminifikowany to pojedyncze, bardzo długie linie (tysiące
|
|
1026
|
+
* bajtów). Próg 200 B/linię zostawia szeroki margines po obu stronach.
|
|
1027
|
+
* Małe pliki pomijamy — trywialny entry (np. federowany initial, ~150 B)
|
|
1028
|
+
* ma za mało treści, żeby heurystyka miała sens.
|
|
1029
|
+
*/
|
|
1030
|
+
function looksMinified(files: readonly BrowserOutFile[]): boolean {
|
|
1031
|
+
for (const f of files) {
|
|
1032
|
+
if (!f.name.endsWith(".js") || f.bytes.length < 16 * 1024) continue;
|
|
1033
|
+
const sample = f.bytes.subarray(0, 64 * 1024);
|
|
1034
|
+
let newlines = 0;
|
|
1035
|
+
for (const b of sample) if (b === 10) newlines++;
|
|
1036
|
+
const avgLineLen = sample.length / Math.max(1, newlines);
|
|
1037
|
+
if (avgLineLen < 200) return false;
|
|
1038
|
+
}
|
|
1039
|
+
return true;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
790
1042
|
export async function buildBrowserApp(
|
|
791
1043
|
rootDir: string,
|
|
792
1044
|
outDir: string,
|
|
@@ -810,10 +1062,19 @@ export async function buildBrowserApp(
|
|
|
810
1062
|
* — React inline, bez lazy-race. Patrz peer-shell.ts.
|
|
811
1063
|
*/
|
|
812
1064
|
exposeRuntime?: boolean;
|
|
1065
|
+
/**
|
|
1066
|
+
* Minifikacja bundli przeglądarki (domyślnie true). Poza rozmiarem
|
|
1067
|
+
* kluczowy jest jej `syntax`-pass: składa `ONLY_SERVER && (...)` do
|
|
1068
|
+
* martwego kodu i usuwa go, dzięki czemu tree-shaking wycina serwerowe
|
|
1069
|
+
* zależności (np. szablony maili, klientów płatności) z bundla klienta.
|
|
1070
|
+
* Dev wyłącza (czytelny kod, szybszy rebuild watchera).
|
|
1071
|
+
*/
|
|
1072
|
+
minify?: boolean;
|
|
813
1073
|
},
|
|
814
1074
|
): Promise<BrowserAppResult> {
|
|
815
1075
|
const federated = opts?.federated ?? false;
|
|
816
1076
|
const exposeRuntime = opts?.exposeRuntime ?? false;
|
|
1077
|
+
const minify = opts?.minify ?? true;
|
|
817
1078
|
mkdirSync(outDir, { recursive: true });
|
|
818
1079
|
|
|
819
1080
|
const publicMembers = plan.groups.get("public") ?? [];
|
|
@@ -844,6 +1105,7 @@ export async function buildBrowserApp(
|
|
|
844
1105
|
define: { ONLY_SERVER: "false", ONLY_BROWSER: "true", ONLY_CLIENT: "true" },
|
|
845
1106
|
federated,
|
|
846
1107
|
exposeRuntime,
|
|
1108
|
+
minify,
|
|
847
1109
|
// Zmiana listy peers (SHELL_EXTERNALS) musi unieważnić build federowany —
|
|
848
1110
|
// stary bundle miałby zbundlowany peer, który teraz jest external.
|
|
849
1111
|
externals: federated ? [...SHELL_EXTERNALS] : [],
|
|
@@ -852,7 +1114,9 @@ export async function buildBrowserApp(
|
|
|
852
1114
|
builderRev: 3,
|
|
853
1115
|
});
|
|
854
1116
|
|
|
855
|
-
|
|
1117
|
+
// Tryb stats zawsze buduje od nowa — cache-hit pominąłby analizę i nie
|
|
1118
|
+
// byłoby czego wypisać.
|
|
1119
|
+
if (!noCache && !buildStatsEnabled() && isCacheHit(cache, unitId, inputHash)) {
|
|
856
1120
|
const cached = cache.units[unitId]?.outputHashes;
|
|
857
1121
|
if (cached?._manifest) {
|
|
858
1122
|
try {
|
|
@@ -885,30 +1149,41 @@ export async function buildBrowserApp(
|
|
|
885
1149
|
}
|
|
886
1150
|
}
|
|
887
1151
|
|
|
888
|
-
|
|
889
|
-
|
|
1152
|
+
// Entries POZA outDir (osobny katalog per unitId): równoległy builder tego
|
|
1153
|
+
// samego workspace'u (dev watcher) wipe'uje i sprząta browser/_entries,
|
|
1154
|
+
// wycinając nam pliki wejściowe spod Bun.build (ENOENT na wszystkich
|
|
1155
|
+
// entry). Dedykowana ścieżka zawęża pole rażenia; resztę domyka retry
|
|
1156
|
+
// (writeEntryFiles przed każdą próbą).
|
|
1157
|
+
const tmpDir = join(rootDir, ".arc", "tmp", `entries-${unitId}`);
|
|
890
1158
|
|
|
891
1159
|
const importLines = (pkgs: { pkg: { name: string } }[]): string =>
|
|
892
1160
|
pkgs.map((m) => `import "${m.pkg.name}";`).join("\n");
|
|
893
1161
|
|
|
894
1162
|
const initialEntry = join(tmpDir, "initial.ts");
|
|
895
|
-
writeFileSync(
|
|
896
|
-
initialEntry,
|
|
897
|
-
// Prelude ekspozycji runtime MUSI być PIERWSZY (przed importami modułów) —
|
|
898
|
-
// globalThis.__ARC_RT gotowe zanim cokolwiek innego się załaduje.
|
|
899
|
-
(exposeRuntime ? runtimeExposePrelude() + "\n" : "") +
|
|
900
|
-
`${importLines(publicMembers)}\nexport { startApp } from "@arcote.tech/platform";\n`,
|
|
901
|
-
);
|
|
902
|
-
|
|
903
1163
|
const entryPaths: string[] = [initialEntry];
|
|
904
1164
|
const groupModuleMap = new Map<string, string[]>();
|
|
905
1165
|
for (const g of protectedGroups) {
|
|
906
|
-
|
|
907
|
-
writeFileSync(entry, `${importLines(g.members)}\n`);
|
|
908
|
-
entryPaths.push(entry);
|
|
1166
|
+
entryPaths.push(join(tmpDir, `${g.name}.ts`));
|
|
909
1167
|
groupModuleMap.set(g.name, g.members.map((m) => m.moduleName));
|
|
910
1168
|
}
|
|
911
1169
|
|
|
1170
|
+
// Idempotentny zapis WSZYSTKICH plików entry. Wołany przed każdą próbą
|
|
1171
|
+
// Bun.build (nie raz) — retry po wyścigu minify musi odtworzyć entries,
|
|
1172
|
+
// gdyby cokolwiek zdążyło je sprzątnąć między próbami.
|
|
1173
|
+
const writeEntryFiles = () => {
|
|
1174
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
1175
|
+
writeFileSync(
|
|
1176
|
+
initialEntry,
|
|
1177
|
+
// Prelude ekspozycji runtime MUSI być PIERWSZY (przed importami modułów) —
|
|
1178
|
+
// globalThis.__ARC_RT gotowe zanim cokolwiek innego się załaduje.
|
|
1179
|
+
(exposeRuntime ? runtimeExposePrelude() + "\n" : "") +
|
|
1180
|
+
`${importLines(publicMembers)}\nexport { startApp } from "@arcote.tech/platform";\n`,
|
|
1181
|
+
);
|
|
1182
|
+
for (const g of protectedGroups) {
|
|
1183
|
+
writeFileSync(join(tmpDir, `${g.name}.ts`), `${importLines(g.members)}\n`);
|
|
1184
|
+
}
|
|
1185
|
+
};
|
|
1186
|
+
|
|
912
1187
|
// ---------------------------------------------------------------------
|
|
913
1188
|
// Temporarily flip `"sideEffects"` on every workspace package's
|
|
914
1189
|
// package.json from `false` to `true` for the duration of the build.
|
|
@@ -941,11 +1216,22 @@ export async function buildBrowserApp(
|
|
|
941
1216
|
patchedPkgJsons.push({ path: pkgJsonPath, original });
|
|
942
1217
|
}
|
|
943
1218
|
|
|
1219
|
+
const statsMode = buildStatsEnabled();
|
|
1220
|
+
// JS-name → sparsowana sourcemapa (tylko w trybie stats).
|
|
1221
|
+
const mapsByJs = new Map<string, { sources?: string[]; sourcesContent?: (string | null)[] }>();
|
|
1222
|
+
|
|
944
1223
|
let result;
|
|
945
|
-
|
|
946
|
-
|
|
1224
|
+
const runBuild = () => {
|
|
1225
|
+
writeEntryFiles();
|
|
1226
|
+
// CELOWO bez `outdir` — artefakty zostają w pamięci i to MY piszemy pliki
|
|
1227
|
+
// (niżej). Chroni build przed RÓWNOLEGŁYM procesem piszącym do tego samego
|
|
1228
|
+
// workspace'u (klasyk: `arc platform dev` w drugim terminalu — patch
|
|
1229
|
+
// sideEffects w package.json triggeruje jego watcher, a jego rebuild
|
|
1230
|
+
// wypluwa NIEZMINIFIKOWANE bundle i wipe'uje outDir w trakcie naszego
|
|
1231
|
+
// builda). Bajty z artefaktów + własny zapis + weryfikacja po zapisie
|
|
1232
|
+
// (niżej) gwarantują, że wynik pochodzi z jednego spójnego passu.
|
|
1233
|
+
return Bun.build({
|
|
947
1234
|
entrypoints: entryPaths,
|
|
948
|
-
outdir: outDir,
|
|
949
1235
|
// splitting:true is the whole point: shared deps (workspace context,
|
|
950
1236
|
// framework, lucide, etc.) land in chunk-<hash>.js, referenced by both
|
|
951
1237
|
// initial and token-group entries. One instance, no provider duplication.
|
|
@@ -964,9 +1250,19 @@ export async function buildBrowserApp(
|
|
|
964
1250
|
// teraz eager re-exportem, więc działa też przy external jsx-runtime.
|
|
965
1251
|
...(federated ? [] : [singleReactPlugin(rootDir)]),
|
|
966
1252
|
jsxDevShimPlugin(),
|
|
1253
|
+
// Serwerowe builtiny node → rzucający stub zamiast ciężkiego polyfilla
|
|
1254
|
+
// (patrz plugin). Musi być PRZED i18nExtractPlugin (onResolve pierwszy
|
|
1255
|
+
// wygrywa nie dotyczy onLoad, ale kolejność trzymamy spójnie).
|
|
1256
|
+
nodeServerBuiltinStubPlugin(),
|
|
967
1257
|
i18nExtractPlugin(i18nCollector, rootDir),
|
|
968
1258
|
],
|
|
969
1259
|
naming: "[name].[ext]",
|
|
1260
|
+
minify,
|
|
1261
|
+
// Tryb stats: sourcemapy niosą `sources[]`/`sourcesContent[]` do rozbicia
|
|
1262
|
+
// chunków na biblioteki. NIE zapisujemy .map na dysk (analiza w pamięci),
|
|
1263
|
+
// a komentarz `//# debugId=` zdejmujemy z JS niżej — bajty i hashe zostają
|
|
1264
|
+
// identyczne jak w zwykłym buildzie.
|
|
1265
|
+
sourcemap: statsMode ? "external" : "none",
|
|
970
1266
|
define: {
|
|
971
1267
|
ONLY_SERVER: "false",
|
|
972
1268
|
ONLY_BROWSER: "true",
|
|
@@ -976,6 +1272,58 @@ export async function buildBrowserApp(
|
|
|
976
1272
|
"process.env.NODE_ENV": '"production"',
|
|
977
1273
|
},
|
|
978
1274
|
});
|
|
1275
|
+
};
|
|
1276
|
+
|
|
1277
|
+
let outFiles: BrowserOutFile[] | null = null;
|
|
1278
|
+
try {
|
|
1279
|
+
// Retry: równoległy proces budujący ten sam workspace (dev watcher)
|
|
1280
|
+
// potrafi w trakcie naszego passu podmienić wejścia/wyjścia — objawy to
|
|
1281
|
+
// niezminifikowany output mimo minify:true albo ENOENT na plikach entry.
|
|
1282
|
+
// Ponawiamy do 3 prób; ostatnia porażka = twardy fail (lepszy głośny
|
|
1283
|
+
// build-error niż niezauważony, ~4-5× cięższy deploy).
|
|
1284
|
+
for (let attempt = 1; ; attempt++) {
|
|
1285
|
+
result = await runBuild();
|
|
1286
|
+
if (result.success) {
|
|
1287
|
+
const files: BrowserOutFile[] = [];
|
|
1288
|
+
mapsByJs.clear();
|
|
1289
|
+
for (const out of result.outputs) {
|
|
1290
|
+
const name = basename(out.path);
|
|
1291
|
+
// Sourcemapy (tylko stats): sparsuj do pamięci, nie zapisuj na dysk.
|
|
1292
|
+
if (name.endsWith(".map")) {
|
|
1293
|
+
try {
|
|
1294
|
+
mapsByJs.set(name.replace(/\.map$/, ""), JSON.parse(await out.text()));
|
|
1295
|
+
} catch {
|
|
1296
|
+
// uszkodzona mapa — pomiń, stats po prostu ją opuści
|
|
1297
|
+
}
|
|
1298
|
+
continue;
|
|
1299
|
+
}
|
|
1300
|
+
let bytes: Uint8Array = new Uint8Array(await out.arrayBuffer());
|
|
1301
|
+
// Zdejmij dopięty przez `sourcemap:"external"` komentarz `//# debugId=`
|
|
1302
|
+
// — inaczej tryb stats zmieniałby bajty (a więc i content-hash) JS.
|
|
1303
|
+
if (statsMode && name.endsWith(".js")) {
|
|
1304
|
+
bytes = stripTrailingSourcemapComment(bytes);
|
|
1305
|
+
}
|
|
1306
|
+
files.push({ kind: out.kind, name, bytes });
|
|
1307
|
+
}
|
|
1308
|
+
if (!minify || looksMinified(files)) {
|
|
1309
|
+
outFiles = files;
|
|
1310
|
+
break;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
if (attempt >= 3) {
|
|
1314
|
+
if (!result.success) break; // standardowa ścieżka błędu niżej wypisze logi
|
|
1315
|
+
throw new Error(
|
|
1316
|
+
"Browser app build: output NIEZMINIFIKOWANY mimo minify:true (3 próby). Przerwane, żeby nie wypuścić ciężkich bundli.",
|
|
1317
|
+
);
|
|
1318
|
+
}
|
|
1319
|
+
console.warn(
|
|
1320
|
+
` ! browser build pass ${attempt} ${
|
|
1321
|
+
result.success
|
|
1322
|
+
? "niezminifikowany mimo minify:true (wyścig minify Bun.build)"
|
|
1323
|
+
: "nieudany (wyścig Bun.build — np. zniknięte pliki entry)"
|
|
1324
|
+
} — powtarzam`,
|
|
1325
|
+
);
|
|
1326
|
+
}
|
|
979
1327
|
} finally {
|
|
980
1328
|
// Always restore — a crash here MUST NOT leave the user with mutated
|
|
981
1329
|
// workspace package.json files.
|
|
@@ -984,7 +1332,7 @@ export async function buildBrowserApp(
|
|
|
984
1332
|
|
|
985
1333
|
rmSync(tmpDir, { recursive: true, force: true });
|
|
986
1334
|
|
|
987
|
-
if (!result.success) {
|
|
1335
|
+
if (!result.success || !outFiles) {
|
|
988
1336
|
for (const log of result.logs) console.error(log);
|
|
989
1337
|
throw new Error("Browser app build failed");
|
|
990
1338
|
}
|
|
@@ -998,17 +1346,12 @@ export async function buildBrowserApp(
|
|
|
998
1346
|
const groups: Record<string, BrowserGroupEntry> = {};
|
|
999
1347
|
const sharedChunks: string[] = [];
|
|
1000
1348
|
|
|
1001
|
-
for (const out of
|
|
1002
|
-
const name = basename(out.path);
|
|
1349
|
+
for (const out of outFiles) {
|
|
1003
1350
|
if (out.kind === "entry-point") {
|
|
1004
|
-
const
|
|
1005
|
-
const
|
|
1006
|
-
const stem = name.replace(/\.js$/, "");
|
|
1351
|
+
const hash = sha256Hex(out.bytes).slice(0, 16);
|
|
1352
|
+
const stem = out.name.replace(/\.js$/, "");
|
|
1007
1353
|
const finalName = `${stem}.${hash}.js`;
|
|
1008
|
-
|
|
1009
|
-
rmSync(finalPath, { force: true });
|
|
1010
|
-
writeFileSync(finalPath, bytes);
|
|
1011
|
-
rmSync(out.path, { force: true });
|
|
1354
|
+
writeFileSync(join(outDir, finalName), out.bytes);
|
|
1012
1355
|
|
|
1013
1356
|
if (stem === "initial") {
|
|
1014
1357
|
initialFile = finalName;
|
|
@@ -1021,7 +1364,8 @@ export async function buildBrowserApp(
|
|
|
1021
1364
|
};
|
|
1022
1365
|
}
|
|
1023
1366
|
} else if (out.kind === "chunk") {
|
|
1024
|
-
|
|
1367
|
+
writeFileSync(join(outDir, out.name), out.bytes);
|
|
1368
|
+
sharedChunks.push(out.name);
|
|
1025
1369
|
}
|
|
1026
1370
|
}
|
|
1027
1371
|
|
|
@@ -1029,6 +1373,34 @@ export async function buildBrowserApp(
|
|
|
1029
1373
|
throw new Error("Browser app build: initial entry not found in outputs");
|
|
1030
1374
|
}
|
|
1031
1375
|
|
|
1376
|
+
// Weryfikacja po zapisie: katalog ma zawierać DOKŁADNIE nasze pliki z
|
|
1377
|
+
// DOKŁADNIE naszymi bajtami. Równoległy builder tego samego workspace'u
|
|
1378
|
+
// (dev watcher) dorzucał do outDir własny, niezminifikowany zestaw plików
|
|
1379
|
+
// (patrz komentarz przy runBuild) — jakikolwiek obcy plik/rozjazd treści
|
|
1380
|
+
// = głośny fail zamiast cichego wypuszczenia niespójnego builda.
|
|
1381
|
+
const written = new Map<string, string>();
|
|
1382
|
+
for (const out of outFiles) {
|
|
1383
|
+
const name =
|
|
1384
|
+
out.kind === "entry-point"
|
|
1385
|
+
? `${out.name.replace(/\.js$/, "")}.${sha256Hex(out.bytes).slice(0, 16)}.js`
|
|
1386
|
+
: out.name;
|
|
1387
|
+
if (out.kind === "entry-point" || out.kind === "chunk") {
|
|
1388
|
+
written.set(name, sha256Hex(out.bytes));
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
for (const f of readdirSync(outDir)) {
|
|
1392
|
+
if (!f.endsWith(".js")) continue;
|
|
1393
|
+
const expected = written.get(f);
|
|
1394
|
+
const actual = sha256Hex(readFileSync(join(outDir, f)));
|
|
1395
|
+
if (!expected || expected !== actual) {
|
|
1396
|
+
throw new Error(
|
|
1397
|
+
`Browser app build: plik "${f}" w ${outDir} nie zgadza się z artefaktami builda ` +
|
|
1398
|
+
`(${expected ? "treść rozjechana z zapisem" : "obcy plik — coś innego pisze do outDir"}). ` +
|
|
1399
|
+
`Przerwane, żeby nie wypuścić niespójnego builda.`,
|
|
1400
|
+
);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1032
1404
|
const manifest: BrowserAppResult = {
|
|
1033
1405
|
initial: { file: initialFile, hash: initialHash },
|
|
1034
1406
|
groups,
|
|
@@ -1036,6 +1408,36 @@ export async function buildBrowserApp(
|
|
|
1036
1408
|
cached: false,
|
|
1037
1409
|
};
|
|
1038
1410
|
|
|
1411
|
+
if (statsMode) {
|
|
1412
|
+
// Zmapuj bajty i sourcemapy na FINALNE nazwy plików (entry z hashem),
|
|
1413
|
+
// żeby graf importów raportu (chunki referują `chunk-<hash>.js`) spinał się
|
|
1414
|
+
// z tym, co realnie leży na dysku.
|
|
1415
|
+
const jsByName = new Map<string, Uint8Array>();
|
|
1416
|
+
const mapsByFinal = new Map<
|
|
1417
|
+
string,
|
|
1418
|
+
{ sources?: string[]; sourcesContent?: (string | null)[] }
|
|
1419
|
+
>();
|
|
1420
|
+
const finalNameOf = (out: BrowserOutFile) =>
|
|
1421
|
+
out.kind === "entry-point"
|
|
1422
|
+
? `${out.name.replace(/\.js$/, "")}.${sha256Hex(out.bytes).slice(0, 16)}.js`
|
|
1423
|
+
: out.name;
|
|
1424
|
+
for (const out of outFiles) {
|
|
1425
|
+
const fn = finalNameOf(out);
|
|
1426
|
+
jsByName.set(fn, out.bytes);
|
|
1427
|
+
const map = mapsByJs.get(out.name);
|
|
1428
|
+
if (map) mapsByFinal.set(fn, map);
|
|
1429
|
+
}
|
|
1430
|
+
reportBrowserBuildStats({
|
|
1431
|
+
label: federated ? "browser-fed" : "browser",
|
|
1432
|
+
entries: [
|
|
1433
|
+
{ name: "initial", file: initialFile },
|
|
1434
|
+
...Object.entries(groups).map(([stem, g]) => ({ name: stem, file: g.file })),
|
|
1435
|
+
],
|
|
1436
|
+
jsByName,
|
|
1437
|
+
mapsByJs: mapsByFinal,
|
|
1438
|
+
});
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1039
1441
|
updateCache(cache, unitId, inputHash, {
|
|
1040
1442
|
outputHashes: { _manifest: JSON.stringify(manifest) },
|
|
1041
1443
|
});
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { buildAll, ok, resolveWorkspace } from "../platform/shared";
|
|
2
2
|
|
|
3
|
-
export async function platformBuild(
|
|
3
|
+
export async function platformBuild(
|
|
4
|
+
opts: { noCache?: boolean; stats?: boolean } = {},
|
|
5
|
+
): Promise<void> {
|
|
6
|
+
// `--stats` = alias na ARC_BUILD_STATS=1 (env zostaje jako escape hatch, np.
|
|
7
|
+
// dla buildu w `dev`). Tryb stats wymusza pełny build (patrz buildBrowserApp).
|
|
8
|
+
if (opts.stats) process.env.ARC_BUILD_STATS = "1";
|
|
4
9
|
const ws = resolveWorkspace();
|
|
5
10
|
const manifest = await buildAll(ws, { noCache: opts.noCache });
|
|
6
11
|
const groupCount = Object.keys(manifest.groups).length;
|