@cosmicdrift/kumiko-dev-server 0.19.1 → 0.20.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 +1 -1
- package/src/__tests__/build-server-bundle.test.ts +64 -0
- package/src/build-server-bundle.ts +91 -128
- package/src/build.ts +0 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-dev-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "Development server bootstrap for Kumiko apps. Bundles the client, mints dev-JWTs, injects the resolved AppSchema, and seeds an admin. Not for production.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { buildServerBundle } from "../build-server-bundle";
|
|
6
|
+
|
|
7
|
+
// Baut ein Mini-App-Fixture (bin/main.ts + bin/kumiko.ts teilen ein Modul) und
|
|
8
|
+
// prüft das Variante-B-Verhalten: ein Bun.build-Call → server.js + kumiko.js als
|
|
9
|
+
// Entries + geteilter chunk statt zwei vollen Bundles.
|
|
10
|
+
function makeFixture(): string {
|
|
11
|
+
const dir = mkdtempSync(join(tmpdir(), "server-bundle-"));
|
|
12
|
+
mkdirSync(join(dir, "bin"));
|
|
13
|
+
mkdirSync(join(dir, "src"));
|
|
14
|
+
writeFileSync(join(dir, "package.json"), `${JSON.stringify({ name: "fixture-app" })}\n`);
|
|
15
|
+
writeFileSync(join(dir, "src/shared.ts"), "export function shared(): number { return 42; }\n");
|
|
16
|
+
// main.ts → wird zu server.js umbenannt; kumiko.ts macht findRepoRoot zum
|
|
17
|
+
// repoRoot dieses Fixtures, also wird kumiko.js gebaut.
|
|
18
|
+
writeFileSync(
|
|
19
|
+
join(dir, "bin/main.ts"),
|
|
20
|
+
`import { shared } from "../src/shared";\nconsole.log("server", shared());\n`,
|
|
21
|
+
);
|
|
22
|
+
writeFileSync(
|
|
23
|
+
join(dir, "bin/kumiko.ts"),
|
|
24
|
+
`import { shared } from "../src/shared";\nconsole.log("migrate", shared());\n`,
|
|
25
|
+
);
|
|
26
|
+
return dir;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("buildServerBundle (multi-entry + splitting)", () => {
|
|
30
|
+
test("produces server.js + kumiko.js as entries + a shared chunk", async () => {
|
|
31
|
+
const dir = makeFixture();
|
|
32
|
+
try {
|
|
33
|
+
const result = await buildServerBundle({ cwd: dir, outDir: join(dir, "dist-server") });
|
|
34
|
+
const outDir = result.outDir;
|
|
35
|
+
|
|
36
|
+
// main.ts wurde zu server.js umbenannt.
|
|
37
|
+
expect(existsSync(join(outDir, "server.js"))).toBe(true);
|
|
38
|
+
expect(existsSync(join(outDir, "main.js"))).toBe(false);
|
|
39
|
+
expect(existsSync(join(outDir, "kumiko.js"))).toBe(true);
|
|
40
|
+
|
|
41
|
+
const entryFiles = result.entries.map((e) => e.file).sort();
|
|
42
|
+
expect(entryFiles).toEqual(["kumiko.js", "server.js"]);
|
|
43
|
+
|
|
44
|
+
// Das geteilte Modul liegt in einem chunk, nicht in beiden Entries inlined.
|
|
45
|
+
expect(result.chunks.length).toBeGreaterThanOrEqual(1);
|
|
46
|
+
const serverSrc = readFileSync(join(outDir, "server.js"), "utf8");
|
|
47
|
+
expect(serverSrc).toContain("chunk-");
|
|
48
|
+
|
|
49
|
+
// Kein Legacy-Drizzle-Output mehr.
|
|
50
|
+
expect(existsSync(join(outDir, "migration-hooks.js"))).toBe(false);
|
|
51
|
+
expect(existsSync(join(outDir, "drizzle.config.ts"))).toBe(false);
|
|
52
|
+
|
|
53
|
+
// Runtime-package.json mit start-Script, ohne drizzle-deps.
|
|
54
|
+
const pkg = JSON.parse(readFileSync(join(outDir, "package.json"), "utf8"));
|
|
55
|
+
expect(pkg.scripts.start).toBe("bun run server.js");
|
|
56
|
+
expect(Object.keys(pkg.dependencies)).not.toContain("drizzle-kit");
|
|
57
|
+
expect(Object.keys(pkg.dependencies)).not.toContain("drizzle-orm");
|
|
58
|
+
|
|
59
|
+
expect(result.totalBytes).toBeGreaterThan(0);
|
|
60
|
+
} finally {
|
|
61
|
+
rmSync(dir, { recursive: true, force: true });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -1,37 +1,27 @@
|
|
|
1
|
-
// buildServerBundle — Production-Server-Bundle für Kumiko-Apps. Pendant
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// ohne Monorepo bootet.
|
|
1
|
+
// buildServerBundle — Production-Server-Bundle für Kumiko-Apps. Pendant zu
|
|
2
|
+
// buildProdBundle (Client). Convention-Driven Discovery liest bin/main.ts
|
|
3
|
+
// (+ optional <repo>/bin/kumiko.ts) und produziert ein runtime-self-contained
|
|
4
|
+
// dist-server/ aus dem ein Bun-Alpine-Container ohne Monorepo bootet.
|
|
6
5
|
//
|
|
7
6
|
// Convention:
|
|
8
7
|
//
|
|
9
|
-
// bin/main.ts
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// drizzle.config.ts → dist-server/drizzle.config.ts
|
|
19
|
-
// (drizzle-kit lädt's per Convention.
|
|
20
|
-
// kumikoDrizzleConfig-Helper-Imports
|
|
21
|
-
// werden inlined damit der Container
|
|
22
|
-
// kein @cosmicdrift/kumiko-dev-server-Paket
|
|
23
|
-
// installiert haben muss.)
|
|
24
|
-
// dist-server/package.json → generiert mit Versionen aus framework +
|
|
25
|
-
// bundled-features package.json
|
|
8
|
+
// bin/main.ts → dist-server/server.js (App-Boot, ruft runProdApp)
|
|
9
|
+
// <repo>/bin/kumiko.ts → dist-server/kumiko.js (Migrate-CLI: `schema apply`,
|
|
10
|
+
// gefunden via walk-up bis bin/kumiko.ts)
|
|
11
|
+
// dist-server/package.json → runtime-deps mit gepinnten Versionen
|
|
12
|
+
//
|
|
13
|
+
// Beide Entries werden in EINEM Bun.build-Call mit `splitting` gebaut: das
|
|
14
|
+
// Framework landet als geteilte chunk-*.js, server.js + kumiko.js sind schlanke
|
|
15
|
+
// Entries die sie importieren — statt das Framework pro Entry neu zu inlinen
|
|
16
|
+
// (vorher ~14 MB × N separate Bundles).
|
|
26
17
|
//
|
|
27
18
|
// Output:
|
|
28
19
|
//
|
|
29
20
|
// dist-server/
|
|
30
|
-
// server.js
|
|
31
|
-
// kumiko.js
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
// package.json ← runtime-deps mit gepinnten Versionen
|
|
21
|
+
// server.js ← App-Boot-Entry
|
|
22
|
+
// kumiko.js ← Migrate-CLI-Entry (wenn bin/kumiko.ts gefunden)
|
|
23
|
+
// chunk-*.js ← geteilte Framework-Chunks
|
|
24
|
+
// package.json ← runtime-deps mit gepinnten Versionen
|
|
35
25
|
//
|
|
36
26
|
// Externals — Pakete die NICHT ins Bundle gebakt werden, sondern via
|
|
37
27
|
// `bun install` im Runtime-Container nachgeholt werden:
|
|
@@ -41,52 +31,34 @@
|
|
|
41
31
|
// dist-server/package.json#dependencies.
|
|
42
32
|
//
|
|
43
33
|
// BUILD_ONLY_EXTERNALS — referenziert nur transitiv im Framework, vom
|
|
44
|
-
// App-Code aber nie. Tree-Shake wirft sie aus
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
// runtime-deps.
|
|
48
|
-
//
|
|
49
|
-
// Beide Listen leben hier zentral statt pro-App. Wenn eine App ein neues
|
|
50
|
-
// natively-bound Paket nutzt, fällt es heute in die App-eigene Boilerplate
|
|
51
|
-
// (oder via opts.extraRuntimeExternals) — das wird sich mit
|
|
52
|
-
// auto-Detection entwickeln.
|
|
34
|
+
// App-Code aber nie. Tree-Shake wirft sie aus dem
|
|
35
|
+
// Bundle, der external-Marker schaltet nur das
|
|
36
|
+
// resolution-during-build ab. NICHT in runtime-deps.
|
|
53
37
|
|
|
54
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
55
|
-
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
38
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
39
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
56
40
|
import { dirname, join, resolve } from "node:path";
|
|
57
41
|
|
|
58
42
|
const hasBun = typeof (globalThis as { Bun?: unknown }).Bun !== "undefined";
|
|
59
43
|
|
|
60
|
-
// Pakete die das Bundle zur Laufzeit weiter referenziert (`from "<pkg>"`
|
|
61
|
-
//
|
|
62
|
-
// ioredis), dynamic-require (postgres, temporal-polyfill) — alles was
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
// `drizzle-kit` + `drizzle-orm` sind im Bundle 0 Refs (drizzle-orm wird
|
|
66
|
-
// inline gebakt), liegen aber in runtime-deps damit der Pre-Deploy-
|
|
67
|
-
// Migrate-Step (`bun /app/kumiko.js migrate apply`) drizzle-kit als CLI
|
|
68
|
-
// findet. drizzle-kit prüft beim Start ob drizzle-orm separat installiert
|
|
69
|
-
// ist und exit'tet sonst mit "Please install latest version of drizzle-orm".
|
|
44
|
+
// Pakete die das Bundle zur Laufzeit weiter referenziert (`from "<pkg>"` nach
|
|
45
|
+
// Tree-Shake). Native bindings (argon2), worker-thread-Loader (bullmq,
|
|
46
|
+
// ioredis), dynamic-require (postgres, temporal-polyfill) — alles was unter
|
|
47
|
+
// Bun-bundling bricht. (drizzle-kit/drizzle-orm sind raus: der Migrate-Pfad
|
|
48
|
+
// nutzt jetzt den framework-eigenen runMigrationsFromDir, kein drizzle-kit.)
|
|
70
49
|
const RUNTIME_EXTERNALS = [
|
|
71
50
|
"@node-rs/argon2",
|
|
72
51
|
"bullmq",
|
|
73
|
-
"drizzle-kit",
|
|
74
|
-
"drizzle-orm",
|
|
75
52
|
"ioredis",
|
|
76
53
|
"postgres",
|
|
77
54
|
"temporal-polyfill",
|
|
78
55
|
] as const;
|
|
79
56
|
|
|
80
|
-
// Pakete die nur im Build-Stack erscheinen (transitive Imports im
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
// drizzle-kit's dialect-resolver macht dynamic-imports zu allen DB-driver-
|
|
87
|
-
// packages (planetscale/libsql/sqlite/neon/vercel/mysql2). Wir nutzen nur
|
|
88
|
-
// postgres → diese werden never-loaded zur Runtime, aber der Bundler will
|
|
89
|
-
// sie resolven. Aufgedeckt durch C1 Empfehlung 4 (bundle-smoke).
|
|
57
|
+
// Pakete die nur im Build-Stack erscheinen (transitive Imports im Framework),
|
|
58
|
+
// aber vom App-Code nicht referenziert werden. Ohne external-Markierung
|
|
59
|
+
// scheitert bun build an dynamic-imports. Tree-Shake wirft sie eh aus dem
|
|
60
|
+
// Bundle — der Marker schaltet nur das resolution-during-build ab. NICHT in
|
|
61
|
+
// runtime-deps.
|
|
90
62
|
const BUILD_ONLY_EXTERNALS = [
|
|
91
63
|
"meilisearch",
|
|
92
64
|
"pino",
|
|
@@ -121,12 +93,20 @@ export type BuildServerBundleEntry = {
|
|
|
121
93
|
|
|
122
94
|
export type BuildServerBundleResult = {
|
|
123
95
|
readonly outDir: string;
|
|
96
|
+
/** Benannte Entry-Files (server.js, ggf. kumiko.js). */
|
|
124
97
|
readonly entries: readonly BuildServerBundleEntry[];
|
|
98
|
+
/** Geteilte Framework-Chunks (von splitting). */
|
|
99
|
+
readonly chunks: readonly BuildServerBundleEntry[];
|
|
100
|
+
/** Gesamt-Größe aller Outputs (entries + chunks) in Bytes. */
|
|
101
|
+
readonly totalBytes: number;
|
|
125
102
|
readonly runtimeDeps: Readonly<Record<string, string>>;
|
|
126
|
-
/** undefined wenn keine drizzle/migration-hooks.ts existiert. */
|
|
127
|
-
readonly migrationHooks: BuildServerBundleEntry | undefined;
|
|
128
103
|
};
|
|
129
104
|
|
|
105
|
+
// Bun benennt Entries nach Source-Basename (bin/main.ts → main.js). server.js
|
|
106
|
+
// ist die Runtime-Convention; der Entry wird von nichts importiert (nur die
|
|
107
|
+
// chunks werden referenziert, per relativem Pfad), darum ist der Rename sicher.
|
|
108
|
+
const ENTRY_RENAMES: Readonly<Record<string, string>> = { "main.js": "server.js" };
|
|
109
|
+
|
|
130
110
|
export async function buildServerBundle(
|
|
131
111
|
options: BuildServerBundleOptions = {},
|
|
132
112
|
): Promise<BuildServerBundleResult> {
|
|
@@ -151,8 +131,6 @@ export async function buildServerBundle(
|
|
|
151
131
|
`[buildServerBundle] Repo-Root erkannt (${repoRoot}), aber bin/kumiko.ts fehlt — kann Migrate-CLI nicht bündeln.`,
|
|
152
132
|
);
|
|
153
133
|
}
|
|
154
|
-
const migrationHooks = discoverMigrationHooks(cwd);
|
|
155
|
-
const drizzleConfig = discoverDrizzleConfig(cwd);
|
|
156
134
|
|
|
157
135
|
const externals = [
|
|
158
136
|
...RUNTIME_EXTERNALS,
|
|
@@ -164,29 +142,42 @@ export async function buildServerBundle(
|
|
|
164
142
|
await rm(outDir, { recursive: true, force: true });
|
|
165
143
|
await mkdir(outDir, { recursive: true });
|
|
166
144
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
145
|
+
// Ein Bun.build-Call mit allen Entries + splitting → das Framework wird
|
|
146
|
+
// einmal als shared chunk abgelegt statt pro Entry inlined.
|
|
147
|
+
const entrypoints = kumikoCli ? [serverEntry, kumikoCli] : [serverEntry];
|
|
148
|
+
const result = await Bun.build({
|
|
149
|
+
entrypoints,
|
|
150
|
+
outdir: outDir,
|
|
151
|
+
target: "bun",
|
|
152
|
+
external: externals as string[],
|
|
153
|
+
splitting: true,
|
|
154
|
+
sourcemap: "none",
|
|
155
|
+
naming: { entry: "[name].js", chunk: "chunk-[hash].js" },
|
|
156
|
+
minify: false,
|
|
157
|
+
});
|
|
158
|
+
if (!result.success) {
|
|
159
|
+
const logs = result.logs.map(String).join("\n");
|
|
160
|
+
throw new Error(`[buildServerBundle] build FAILED:\n${logs}`);
|
|
181
161
|
}
|
|
182
|
-
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
162
|
+
|
|
163
|
+
// Größe von Disk lesen, nicht aus out.size — letzteres ist für gesplittete
|
|
164
|
+
// Entry-Points unzuverlässig (meldet teils ~0 für den Entry-Stub).
|
|
165
|
+
const entries: BuildServerBundleEntry[] = [];
|
|
166
|
+
const chunks: BuildServerBundleEntry[] = [];
|
|
167
|
+
for (const out of result.outputs) {
|
|
168
|
+
const base = out.path.split("/").pop() ?? out.path;
|
|
169
|
+
if (out.kind === "entry-point") {
|
|
170
|
+
const desired = ENTRY_RENAMES[base] ?? base;
|
|
171
|
+
if (desired !== base) {
|
|
172
|
+
await rename(join(outDir, base), join(outDir, desired));
|
|
173
|
+
}
|
|
174
|
+
entries.push({ file: desired, sizeBytes: statSync(join(outDir, desired)).size });
|
|
175
|
+
} else if (out.kind === "chunk") {
|
|
176
|
+
chunks.push({ file: base, sizeBytes: statSync(join(outDir, base)).size });
|
|
177
|
+
}
|
|
189
178
|
}
|
|
179
|
+
entries.sort((a, b) => a.file.localeCompare(b.file));
|
|
180
|
+
const totalBytes = [...entries, ...chunks].reduce((sum, e) => sum + e.sizeBytes, 0);
|
|
190
181
|
|
|
191
182
|
const runtimeDeps = await resolveRuntimeDepsVersions(repoRoot, [
|
|
192
183
|
...RUNTIME_EXTERNALS,
|
|
@@ -202,32 +193,7 @@ export async function buildServerBundle(
|
|
|
202
193
|
};
|
|
203
194
|
await writeFile(join(outDir, "package.json"), `${JSON.stringify(runtimePkg, null, 2)}\n`);
|
|
204
195
|
|
|
205
|
-
return { outDir, entries,
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
async function bundleEntry(
|
|
209
|
-
entry: string,
|
|
210
|
-
outDir: string,
|
|
211
|
-
naming: string,
|
|
212
|
-
externals: readonly string[],
|
|
213
|
-
): Promise<BuildServerBundleEntry> {
|
|
214
|
-
const result = await Bun.build({
|
|
215
|
-
entrypoints: [entry],
|
|
216
|
-
outdir: outDir,
|
|
217
|
-
target: "bun",
|
|
218
|
-
external: externals as string[],
|
|
219
|
-
naming,
|
|
220
|
-
minify: false,
|
|
221
|
-
});
|
|
222
|
-
if (!result.success) {
|
|
223
|
-
const logs = result.logs.map(String).join("\n");
|
|
224
|
-
throw new Error(`[buildServerBundle] Bundle ${naming} FAILED:\n${logs}`);
|
|
225
|
-
}
|
|
226
|
-
const out = result.outputs[0];
|
|
227
|
-
if (!out) {
|
|
228
|
-
throw new Error(`[buildServerBundle] Bundle ${naming}: no output produced.`);
|
|
229
|
-
}
|
|
230
|
-
return { file: naming, sizeBytes: out.size };
|
|
196
|
+
return { outDir, entries, chunks, totalBytes, runtimeDeps };
|
|
231
197
|
}
|
|
232
198
|
|
|
233
199
|
export function discoverServerEntry(cwd: string): string | undefined {
|
|
@@ -238,20 +204,9 @@ export function discoverServerEntry(cwd: string): string | undefined {
|
|
|
238
204
|
return undefined;
|
|
239
205
|
}
|
|
240
206
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
export function discoverMigrationHooks(cwd: string): string | undefined {
|
|
247
|
-
const path = join(cwd, "drizzle/migration-hooks.ts");
|
|
248
|
-
return existsSync(path) ? path : undefined;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Walking up vom App-cwd bis ein Verzeichnis mit `bin/kumiko.ts` und einer
|
|
252
|
-
// monorepo-package.json (workspaces[]) gefunden wird. Failsafe: nach 8
|
|
253
|
-
// Levels aufgeben — nichts gefunden, kein Repo-Root, dann liefert die
|
|
254
|
-
// Funktion undefined und der Caller skippt das CLI-Bundle.
|
|
207
|
+
// Walking up vom App-cwd bis ein Verzeichnis mit `bin/kumiko.ts` gefunden wird.
|
|
208
|
+
// Failsafe: nach 8 Levels aufgeben — nichts gefunden, kein Repo-Root, dann
|
|
209
|
+
// liefert die Funktion undefined und der Caller skippt das CLI-Bundle.
|
|
255
210
|
function findRepoRoot(start: string): string | undefined {
|
|
256
211
|
let cur = start;
|
|
257
212
|
for (let i = 0; i < 8; i++) {
|
|
@@ -313,14 +268,22 @@ export function formatServerBuildResult(
|
|
|
313
268
|
const dim = "\x1b[2m";
|
|
314
269
|
const green = "\x1b[32m";
|
|
315
270
|
const reset = "\x1b[0m";
|
|
271
|
+
const mb = (bytes: number): string => (bytes / 1024 / 1024).toFixed(2);
|
|
316
272
|
const lines: string[] = [];
|
|
317
273
|
lines.push("");
|
|
318
274
|
lines.push(` ${green}✓${reset} server bundle ${dim}(${durationMs}ms)${reset}`);
|
|
319
275
|
for (const entry of result.entries) {
|
|
320
|
-
|
|
321
|
-
|
|
276
|
+
lines.push(` ${cyan}→${reset} ${entry.file} ${mb(entry.sizeBytes)} MB`);
|
|
277
|
+
}
|
|
278
|
+
if (result.chunks.length > 0) {
|
|
279
|
+
const chunkBytes = result.chunks.reduce((sum, c) => sum + c.sizeBytes, 0);
|
|
280
|
+
lines.push(
|
|
281
|
+
` ${cyan}→${reset} ${result.chunks.length} shared chunk(s) ${mb(chunkBytes)} MB`,
|
|
282
|
+
);
|
|
322
283
|
}
|
|
323
284
|
const depCount = Object.keys(result.runtimeDeps).length;
|
|
324
|
-
lines.push(
|
|
285
|
+
lines.push(
|
|
286
|
+
` ${dim}total: ${mb(result.totalBytes)} MB · runtime-deps: ${depCount} packages${reset}`,
|
|
287
|
+
);
|
|
325
288
|
return lines.join("\n");
|
|
326
289
|
}
|
package/src/build.ts
CHANGED