@arcote.tech/arc-cli 0.5.6 → 0.5.8
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 +768 -401
- package/package.json +7 -7
- package/src/builder/build-cache.ts +79 -0
- package/src/builder/hash.ts +100 -0
- package/src/builder/module-builder.ts +393 -197
- package/src/builder/parallel.ts +26 -0
- package/src/commands/platform-build.ts +2 -2
- package/src/commands/platform-deploy.ts +1 -1
- package/src/commands/platform-dev.ts +28 -45
- package/src/index.ts +8 -2
- package/src/platform/server.ts +10 -0
- package/src/platform/shared.ts +349 -143
|
@@ -4,18 +4,32 @@ import {
|
|
|
4
4
|
mkdirSync,
|
|
5
5
|
readFileSync,
|
|
6
6
|
readdirSync,
|
|
7
|
+
rmSync,
|
|
7
8
|
writeFileSync,
|
|
8
9
|
} from "fs";
|
|
9
|
-
import { dirname, join, relative } from "path";
|
|
10
|
+
import { basename, dirname, join, relative } from "path";
|
|
10
11
|
import type { BuildManifest, ModuleDescriptor } from "@arcote.tech/platform";
|
|
11
|
-
import {
|
|
12
|
-
buildTypeDeclarations,
|
|
13
|
-
type DeclarationResult,
|
|
14
|
-
} from "../utils/build";
|
|
12
|
+
import { buildTypeDeclarations } from "../utils/build";
|
|
15
13
|
import { i18nExtractPlugin, finalizeTranslations } from "../i18n";
|
|
14
|
+
import { compileAllCatalogs } from "../i18n/compile";
|
|
15
|
+
import {
|
|
16
|
+
isCacheHit,
|
|
17
|
+
updateCache,
|
|
18
|
+
type BuildCache,
|
|
19
|
+
} from "./build-cache";
|
|
20
|
+
import {
|
|
21
|
+
readInstalledVersion,
|
|
22
|
+
sha256Hex,
|
|
23
|
+
sha256OfDir,
|
|
24
|
+
sha256OfFiles,
|
|
25
|
+
sha256OfJson,
|
|
26
|
+
} from "./hash";
|
|
27
|
+
import { pAll } from "./parallel";
|
|
16
28
|
|
|
17
29
|
/** Re-export for internal CLI consumers (avoid direct platform dependency in consumers). */
|
|
18
30
|
export type { BuildManifest, ModuleDescriptor };
|
|
31
|
+
/** Re-export hash helpers for backward compatibility (deploy/remote-sync uses sha256OfFiles). */
|
|
32
|
+
export { sha256Hex, sha256OfFiles };
|
|
19
33
|
|
|
20
34
|
/** Clients that a context package is built for. */
|
|
21
35
|
const CONTEXT_CLIENTS = [
|
|
@@ -23,62 +37,6 @@ const CONTEXT_CLIENTS = [
|
|
|
23
37
|
{ name: "browser", target: "browser" as const, defines: { ONLY_SERVER: "false", ONLY_BROWSER: "true", ONLY_CLIENT: "true" } },
|
|
24
38
|
];
|
|
25
39
|
|
|
26
|
-
/**
|
|
27
|
-
* Build a context package (browser/server split) using Bun.build directly,
|
|
28
|
-
* then generate .d.ts declarations via tsgo (with tsc fallback).
|
|
29
|
-
*
|
|
30
|
-
* Returns declaration errors (if any) so the caller can display them.
|
|
31
|
-
*/
|
|
32
|
-
async function buildContextPackage(
|
|
33
|
-
pkg: WorkspacePackage,
|
|
34
|
-
): Promise<{ declarationErrors: string[] }> {
|
|
35
|
-
const entrypoint = pkg.entrypoint;
|
|
36
|
-
const outDir = join(pkg.path, "dist");
|
|
37
|
-
const peerDeps = Object.keys(pkg.packageJson.peerDependencies || {});
|
|
38
|
-
const deps = Object.keys(pkg.packageJson.dependencies || {});
|
|
39
|
-
const externals = [...peerDeps, ...deps];
|
|
40
|
-
const allDeclErrors: string[] = [];
|
|
41
|
-
|
|
42
|
-
for (const client of CONTEXT_CLIENTS) {
|
|
43
|
-
// JS bundle
|
|
44
|
-
const result = await Bun.build({
|
|
45
|
-
entrypoints: [entrypoint],
|
|
46
|
-
outdir: join(outDir, client.name, "main"),
|
|
47
|
-
target: client.target,
|
|
48
|
-
format: "esm",
|
|
49
|
-
naming: "index.[ext]",
|
|
50
|
-
external: externals,
|
|
51
|
-
define: client.defines,
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
if (!result.success) {
|
|
55
|
-
console.error(`Context ${client.name} build failed:`);
|
|
56
|
-
for (const log of result.logs) console.error(log);
|
|
57
|
-
throw new Error(`${client.name} build failed for ${pkg.name}`);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Type declarations — globals match the define values
|
|
61
|
-
const globalsContent = Object.entries(client.defines)
|
|
62
|
-
.map(([k, v]) => `declare const ${k}: ${v};`)
|
|
63
|
-
.join("\n");
|
|
64
|
-
|
|
65
|
-
const declResult = await buildTypeDeclarations(
|
|
66
|
-
[entrypoint],
|
|
67
|
-
join(outDir, client.name),
|
|
68
|
-
dirname(entrypoint),
|
|
69
|
-
globalsContent,
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
if (!declResult.success && declResult.errors.length > 0) {
|
|
73
|
-
allDeclErrors.push(
|
|
74
|
-
...declResult.errors.map((e) => `[${pkg.name}/${client.name}] ${e}`),
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return { declarationErrors: allDeclErrors };
|
|
80
|
-
}
|
|
81
|
-
|
|
82
40
|
/** Packages that shell provides — modules import them but don't bundle them. */
|
|
83
41
|
export const SHELL_EXTERNALS = [
|
|
84
42
|
"react",
|
|
@@ -104,26 +62,9 @@ export interface WorkspacePackage {
|
|
|
104
62
|
/** @deprecated use ModuleDescriptor from @arcote.tech/platform */
|
|
105
63
|
export type ModuleEntry = ModuleDescriptor;
|
|
106
64
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
export function sha256Hex(bytes: Uint8Array | ArrayBuffer | string): string {
|
|
111
|
-
const hasher = new Bun.CryptoHasher("sha256");
|
|
112
|
-
hasher.update(bytes as any);
|
|
113
|
-
return hasher.digest("hex");
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** Hash of concatenated file contents in a directory (stable order). */
|
|
117
|
-
export function sha256OfFiles(paths: readonly string[]): string {
|
|
118
|
-
const hasher = new Bun.CryptoHasher("sha256");
|
|
119
|
-
const sorted = [...paths].sort();
|
|
120
|
-
for (const p of sorted) {
|
|
121
|
-
if (!existsSync(p)) continue;
|
|
122
|
-
hasher.update(readFileSync(p));
|
|
123
|
-
hasher.update("\0");
|
|
124
|
-
}
|
|
125
|
-
return hasher.digest("hex");
|
|
126
|
-
}
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Discovery
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
127
68
|
|
|
128
69
|
/**
|
|
129
70
|
* Discover workspace packages from root package.json.
|
|
@@ -155,7 +96,6 @@ export function discoverPackages(rootDir: string): WorkspacePackage[] {
|
|
|
155
96
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
156
97
|
if (pkg.name?.startsWith("@arcote.tech/")) continue;
|
|
157
98
|
|
|
158
|
-
// Find entrypoint: src/index.ts, src/index.tsx, or root index.ts
|
|
159
99
|
const pkgDir = join(baseDir, entry);
|
|
160
100
|
const candidates = [
|
|
161
101
|
join(pkgDir, "src", "index.ts"),
|
|
@@ -192,147 +132,333 @@ export function isContextPackage(pkg: Record<string, any>): boolean {
|
|
|
192
132
|
return false;
|
|
193
133
|
}
|
|
194
134
|
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// Hash helpers (per-package source / dep version)
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
const sourceFilter = (rel: string): boolean => {
|
|
140
|
+
if (rel.startsWith("dist/") || rel.startsWith("dist")) return false;
|
|
141
|
+
if (rel.includes("/node_modules/") || rel.startsWith("node_modules")) return false;
|
|
142
|
+
if (rel.startsWith(".arc/") || rel.startsWith(".arc")) return false;
|
|
143
|
+
return true;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const tsxOnlyFilter = (rel: string): boolean => {
|
|
147
|
+
if (!sourceFilter(rel)) return false;
|
|
148
|
+
// Allow directories (we cannot tell from rel alone, but walk descends regardless;
|
|
149
|
+
// we only filter at file level via extension check)
|
|
150
|
+
if (/\/$/.test(rel)) return true;
|
|
151
|
+
// Files: keep only .ts / .tsx (other dirent kinds come through as files too)
|
|
152
|
+
if (/\.(ts|tsx)$/.test(rel)) return true;
|
|
153
|
+
// Directory entries (no extension) — let them through so walker can descend
|
|
154
|
+
if (!/\.[^/]+$/.test(rel)) return true;
|
|
155
|
+
return false;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
function pkgSourceHash(pkg: WorkspacePackage): string {
|
|
159
|
+
return sha256OfDir(join(pkg.path, "src"), sourceFilter);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function depVersionsHash(rootDir: string, pkg: WorkspacePackage): string {
|
|
163
|
+
const peerDeps = Object.keys(pkg.packageJson.peerDependencies ?? {});
|
|
164
|
+
const deps = Object.keys(pkg.packageJson.dependencies ?? {});
|
|
165
|
+
const versions: Record<string, string | null> = {};
|
|
166
|
+
for (const dep of [...peerDeps, ...deps].sort()) {
|
|
167
|
+
versions[dep] = readInstalledVersion(rootDir, dep);
|
|
168
|
+
}
|
|
169
|
+
return sha256OfJson(versions);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// Context package build — per (pkg × client) cacheable unit
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
interface ContextClientResult {
|
|
177
|
+
pkgName: string;
|
|
178
|
+
client: string;
|
|
179
|
+
declarationErrors: string[];
|
|
180
|
+
cached: boolean;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function buildContextClient(
|
|
184
|
+
pkg: WorkspacePackage,
|
|
185
|
+
rootDir: string,
|
|
186
|
+
client: typeof CONTEXT_CLIENTS[number],
|
|
187
|
+
cache: BuildCache,
|
|
188
|
+
noCache: boolean,
|
|
189
|
+
): Promise<ContextClientResult> {
|
|
190
|
+
const unitId = `context-pkg:${pkg.name}:${client.name}`;
|
|
191
|
+
const outDir = join(pkg.path, "dist", client.name);
|
|
192
|
+
|
|
193
|
+
const inputHash = sha256OfJson({
|
|
194
|
+
src: pkgSourceHash(pkg),
|
|
195
|
+
pkg: pkg.packageJson,
|
|
196
|
+
deps: depVersionsHash(rootDir, pkg),
|
|
197
|
+
client: client.name,
|
|
198
|
+
target: client.target,
|
|
199
|
+
defines: client.defines,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
if (!noCache && isCacheHit(cache, unitId, inputHash, [join(outDir, "main", "index.js")])) {
|
|
203
|
+
console.log(` ✓ cached: ${pkg.name} (${client.name})`);
|
|
204
|
+
return { pkgName: pkg.name, client: client.name, declarationErrors: [], cached: true };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
console.log(` building: ${pkg.name} (${client.name})`);
|
|
208
|
+
|
|
209
|
+
const peerDeps = Object.keys(pkg.packageJson.peerDependencies ?? {});
|
|
210
|
+
const deps = Object.keys(pkg.packageJson.dependencies ?? {});
|
|
211
|
+
const externals = [...peerDeps, ...deps];
|
|
212
|
+
|
|
213
|
+
const result = await Bun.build({
|
|
214
|
+
entrypoints: [pkg.entrypoint],
|
|
215
|
+
outdir: join(outDir, "main"),
|
|
216
|
+
target: client.target,
|
|
217
|
+
format: "esm",
|
|
218
|
+
naming: "index.[ext]",
|
|
219
|
+
external: externals,
|
|
220
|
+
define: client.defines,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
if (!result.success) {
|
|
224
|
+
console.error(`Context ${client.name} build failed:`);
|
|
225
|
+
for (const log of result.logs) console.error(log);
|
|
226
|
+
throw new Error(`${client.name} build failed for ${pkg.name}`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const globalsContent = Object.entries(client.defines)
|
|
230
|
+
.map(([k, v]) => `declare const ${k}: ${v};`)
|
|
231
|
+
.join("\n");
|
|
232
|
+
|
|
233
|
+
const declResult = await buildTypeDeclarations(
|
|
234
|
+
[pkg.entrypoint],
|
|
235
|
+
outDir,
|
|
236
|
+
dirname(pkg.entrypoint),
|
|
237
|
+
globalsContent,
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
const declarationErrors =
|
|
241
|
+
!declResult.success && declResult.errors.length > 0
|
|
242
|
+
? declResult.errors.map((e) => `[${pkg.name}/${client.name}] ${e}`)
|
|
243
|
+
: [];
|
|
244
|
+
|
|
245
|
+
const outputHash = sha256OfDir(outDir);
|
|
246
|
+
updateCache(cache, unitId, inputHash, { outputHash });
|
|
247
|
+
|
|
248
|
+
return { pkgName: pkg.name, client: client.name, declarationErrors, cached: false };
|
|
249
|
+
}
|
|
250
|
+
|
|
195
251
|
/**
|
|
196
|
-
* Build all
|
|
197
|
-
* Context packages get a wrapper that calls arc() to auto-register.
|
|
198
|
-
* Regular modules already call arc() in their source.
|
|
252
|
+
* Build all context packages in parallel — each (pkg × client) is an independent task.
|
|
199
253
|
*/
|
|
200
|
-
export async function
|
|
254
|
+
export async function buildContextPackages(
|
|
201
255
|
rootDir: string,
|
|
202
|
-
outDir: string,
|
|
203
256
|
packages: WorkspacePackage[],
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
// Build contexts first (server/browser split via Bun.build + declarations)
|
|
257
|
+
cache: BuildCache,
|
|
258
|
+
noCache: boolean,
|
|
259
|
+
): Promise<{ declarationErrors: string[] }> {
|
|
208
260
|
const contexts = packages.filter((p) => isContextPackage(p.packageJson));
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
261
|
+
if (contexts.length === 0) return { declarationErrors: [] };
|
|
262
|
+
|
|
263
|
+
const tasks = contexts.flatMap((pkg) =>
|
|
264
|
+
CONTEXT_CLIENTS.map((client) => () =>
|
|
265
|
+
buildContextClient(pkg, rootDir, client, cache, noCache),
|
|
266
|
+
),
|
|
267
|
+
);
|
|
215
268
|
|
|
216
|
-
|
|
269
|
+
const results = await pAll(tasks);
|
|
270
|
+
const declarationErrors = results.flatMap((r) => r.declarationErrors);
|
|
271
|
+
|
|
272
|
+
if (declarationErrors.length > 0) {
|
|
217
273
|
console.warn("\n\x1b[33mType declaration errors:\x1b[0m");
|
|
218
|
-
for (const err of
|
|
219
|
-
console.warn(` ${err}`);
|
|
220
|
-
}
|
|
274
|
+
for (const err of declarationErrors) console.warn(` ${err}`);
|
|
221
275
|
console.warn("");
|
|
222
276
|
}
|
|
223
277
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
mkdirSync(tmpDir, { recursive: true });
|
|
227
|
-
|
|
228
|
-
const entrypoints: string[] = [];
|
|
229
|
-
const fileToName = new Map<string, string>(); // safeName → module name
|
|
230
|
-
|
|
231
|
-
for (const pkg of packages) {
|
|
232
|
-
const safeName = pkg.path.split("/").pop()!;
|
|
233
|
-
// Module name: strip scope (e.g. @ndt/admin → admin)
|
|
234
|
-
const moduleName = pkg.name.includes("/") ? pkg.name.split("/").pop()! : pkg.name;
|
|
235
|
-
fileToName.set(safeName, moduleName);
|
|
236
|
-
|
|
237
|
-
// All packages get a simple re-export wrapper.
|
|
238
|
-
// Context packages that use module().build() self-register via side effects.
|
|
239
|
-
// Legacy packages that export appContext will be picked up by arc(() => appContext).
|
|
240
|
-
const wrapperFile = join(tmpDir, `${safeName}.ts`);
|
|
241
|
-
writeFileSync(wrapperFile, `export * from "${pkg.name}";\n`);
|
|
242
|
-
entrypoints.push(wrapperFile);
|
|
243
|
-
}
|
|
278
|
+
return { declarationErrors };
|
|
279
|
+
}
|
|
244
280
|
|
|
245
|
-
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
// Modules bundle — single Bun.build of all wrapper re-exports
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
246
284
|
|
|
247
|
-
|
|
248
|
-
|
|
285
|
+
interface ModulesBundleResult {
|
|
286
|
+
modules: ModuleDescriptor[];
|
|
287
|
+
cached: boolean;
|
|
288
|
+
}
|
|
249
289
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
290
|
+
/**
|
|
291
|
+
* Bundle all workspace packages into ESM chunks for the platform shell.
|
|
292
|
+
* Cached as a single unit — any source change in any package invalidates.
|
|
293
|
+
*/
|
|
294
|
+
export async function buildModulesBundle(
|
|
295
|
+
rootDir: string,
|
|
296
|
+
outDir: string,
|
|
297
|
+
packages: WorkspacePackage[],
|
|
298
|
+
cache: BuildCache,
|
|
299
|
+
noCache: boolean,
|
|
300
|
+
): Promise<ModulesBundleResult> {
|
|
301
|
+
mkdirSync(outDir, { recursive: true });
|
|
260
302
|
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
},
|
|
303
|
+
const unitId = "modules-bundle";
|
|
304
|
+
|
|
305
|
+
// Per-package source hash — gives us deterministic input identity even though
|
|
306
|
+
// we run a single Bun.build for the whole bundle (chunk sharing wins).
|
|
307
|
+
const pkgHashes = packages.map((p) => ({
|
|
308
|
+
name: p.name,
|
|
309
|
+
safeName: basename(p.path),
|
|
310
|
+
srcHash: pkgSourceHash(p),
|
|
311
|
+
}));
|
|
312
|
+
|
|
313
|
+
const inputHash = sha256OfJson({
|
|
314
|
+
pkgHashes,
|
|
315
|
+
externals: SHELL_EXTERNALS,
|
|
316
|
+
define: { ONLY_SERVER: "false", ONLY_BROWSER: "true", ONLY_CLIENT: "true" },
|
|
275
317
|
});
|
|
276
318
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
319
|
+
// Cache hit → reconstruct module descriptors from existing files in outDir.
|
|
320
|
+
if (!noCache && isCacheHit(cache, unitId, inputHash)) {
|
|
321
|
+
const existing = cache.units[unitId]?.outputHashes ?? {};
|
|
322
|
+
const modules: ModuleDescriptor[] = [];
|
|
323
|
+
for (const { safeName, name } of pkgHashes) {
|
|
324
|
+
const file = `${safeName}.js`;
|
|
325
|
+
const filePath = join(outDir, file);
|
|
326
|
+
if (!existsSync(filePath)) {
|
|
327
|
+
// Output missing despite hash match — fall through to rebuild.
|
|
328
|
+
console.log(` rebuilding modules-bundle: output ${file} missing`);
|
|
329
|
+
return await actuallyBuild();
|
|
330
|
+
}
|
|
331
|
+
modules.push({ file, name, hash: existing[safeName] ?? sha256Hex(readFileSync(filePath)) });
|
|
332
|
+
}
|
|
333
|
+
console.log(` ✓ cached: modules-bundle (${modules.length} module(s))`);
|
|
334
|
+
return { modules, cached: true };
|
|
281
335
|
}
|
|
282
336
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
const
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
337
|
+
return await actuallyBuild();
|
|
338
|
+
|
|
339
|
+
async function actuallyBuild(): Promise<ModulesBundleResult> {
|
|
340
|
+
console.log(` building: modules-bundle (${packages.length} package(s))`);
|
|
341
|
+
|
|
342
|
+
const tmpDir = join(outDir, "_entries");
|
|
343
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
344
|
+
|
|
345
|
+
const entrypoints: string[] = [];
|
|
346
|
+
const fileToName = new Map<string, string>();
|
|
347
|
+
|
|
348
|
+
for (const pkg of packages) {
|
|
349
|
+
const safeName = basename(pkg.path);
|
|
350
|
+
const moduleName = pkg.name.includes("/") ? pkg.name.split("/").pop()! : pkg.name;
|
|
351
|
+
fileToName.set(safeName, moduleName);
|
|
352
|
+
|
|
353
|
+
const wrapperFile = join(tmpDir, `${safeName}.ts`);
|
|
354
|
+
writeFileSync(wrapperFile, `export * from "${pkg.name}";\n`);
|
|
355
|
+
entrypoints.push(wrapperFile);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const i18nCollector = new Map<string, Set<string>>();
|
|
359
|
+
|
|
360
|
+
const arcExternalPlugin: import("bun").BunPlugin = {
|
|
361
|
+
name: "arc-external",
|
|
362
|
+
setup(build) {
|
|
363
|
+
build.onResolve({ filter: /^@arcote\.tech\// }, (args) => {
|
|
364
|
+
return { path: args.path, external: true };
|
|
365
|
+
});
|
|
366
|
+
},
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
const result = await Bun.build({
|
|
370
|
+
entrypoints,
|
|
371
|
+
outdir: outDir,
|
|
372
|
+
splitting: true,
|
|
373
|
+
format: "esm",
|
|
374
|
+
target: "browser",
|
|
375
|
+
external: SHELL_EXTERNALS,
|
|
376
|
+
plugins: [arcExternalPlugin, i18nExtractPlugin(i18nCollector, rootDir)],
|
|
377
|
+
naming: "[name].[ext]",
|
|
378
|
+
define: {
|
|
379
|
+
ONLY_SERVER: "false",
|
|
380
|
+
ONLY_BROWSER: "true",
|
|
381
|
+
ONLY_CLIENT: "true",
|
|
382
|
+
},
|
|
303
383
|
});
|
|
304
384
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
buildTime: new Date().toISOString(),
|
|
311
|
-
};
|
|
385
|
+
if (!result.success) {
|
|
386
|
+
console.error("Modules bundle build failed:");
|
|
387
|
+
for (const log of result.logs) console.error(log);
|
|
388
|
+
throw new Error("Module build failed");
|
|
389
|
+
}
|
|
312
390
|
|
|
313
|
-
|
|
314
|
-
join(outDir, "
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
391
|
+
// i18n extraction → write/update .po files (compileAllCatalogs handles .json).
|
|
392
|
+
await finalizeTranslations(rootDir, join(outDir, ".."), i18nCollector);
|
|
393
|
+
|
|
394
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
395
|
+
|
|
396
|
+
const outputHashes: Record<string, string> = {};
|
|
397
|
+
const modules: ModuleDescriptor[] = result.outputs
|
|
398
|
+
.filter((o) => o.kind === "entry-point")
|
|
399
|
+
.map((o) => {
|
|
400
|
+
const file = basename(o.path);
|
|
401
|
+
const safeName = file.replace(/\.js$/, "");
|
|
402
|
+
const bytes = readFileSync(o.path);
|
|
403
|
+
const hash = sha256Hex(bytes);
|
|
404
|
+
outputHashes[safeName] = hash;
|
|
405
|
+
return {
|
|
406
|
+
file,
|
|
407
|
+
name: fileToName.get(safeName) ?? safeName,
|
|
408
|
+
hash,
|
|
409
|
+
};
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
updateCache(cache, unitId, inputHash, { outputHashes });
|
|
413
|
+
return { modules, cached: false };
|
|
414
|
+
}
|
|
318
415
|
}
|
|
319
416
|
|
|
417
|
+
// ---------------------------------------------------------------------------
|
|
418
|
+
// Translations — compile .po → .json as a separate cache unit
|
|
419
|
+
// ---------------------------------------------------------------------------
|
|
420
|
+
|
|
320
421
|
/**
|
|
321
|
-
*
|
|
422
|
+
* Compile .po catalogs to .json. Independent cache unit — edits to .po
|
|
423
|
+
* regenerate .json without triggering a full module rebuild.
|
|
322
424
|
*/
|
|
323
|
-
export async function
|
|
425
|
+
export async function buildTranslations(
|
|
324
426
|
rootDir: string,
|
|
325
|
-
|
|
427
|
+
arcDir: string,
|
|
428
|
+
cache: BuildCache,
|
|
429
|
+
noCache: boolean,
|
|
326
430
|
): Promise<void> {
|
|
327
|
-
|
|
431
|
+
const localesDir = join(rootDir, "locales");
|
|
432
|
+
if (!existsSync(localesDir)) return;
|
|
433
|
+
|
|
434
|
+
const unitId = "translations";
|
|
435
|
+
const poFiles = readdirSync(localesDir)
|
|
436
|
+
.filter((f) => f.endsWith(".po"))
|
|
437
|
+
.map((f) => join(localesDir, f));
|
|
438
|
+
if (poFiles.length === 0) return;
|
|
328
439
|
|
|
329
|
-
const
|
|
330
|
-
const outputCss = join(outDir, "styles.css");
|
|
331
|
-
const rootRel = relative(outDir, rootDir).replace(/\\/g, "/");
|
|
440
|
+
const inputHash = sha256OfFiles(poFiles);
|
|
332
441
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
442
|
+
if (!noCache && isCacheHit(cache, unitId, inputHash, [join(arcDir, "locales")])) {
|
|
443
|
+
console.log(` ✓ cached: translations`);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
console.log(` building: translations (${poFiles.length} catalog(s))`);
|
|
448
|
+
compileAllCatalogs(localesDir, join(arcDir, "locales"));
|
|
449
|
+
|
|
450
|
+
const jsonFiles = readdirSync(join(arcDir, "locales"))
|
|
451
|
+
.filter((f) => f.endsWith(".json"))
|
|
452
|
+
.map((f) => join(arcDir, "locales", f));
|
|
453
|
+
const outputHash = sha256OfFiles(jsonFiles);
|
|
454
|
+
updateCache(cache, unitId, inputHash, { outputHash });
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// ---------------------------------------------------------------------------
|
|
458
|
+
// Styles (Tailwind) — cached unit
|
|
459
|
+
// ---------------------------------------------------------------------------
|
|
460
|
+
|
|
461
|
+
const TAILWIND_INPUT_TEMPLATE = (rootRel: string) => `@import "tailwindcss";
|
|
336
462
|
@import "tw-animate-css";
|
|
337
463
|
|
|
338
464
|
@source "${rootRel}/packages/*/*.{ts,tsx}";
|
|
@@ -390,12 +516,82 @@ export async function buildStyles(
|
|
|
390
516
|
min-height: 100vh;
|
|
391
517
|
}
|
|
392
518
|
}
|
|
393
|
-
|
|
394
|
-
|
|
519
|
+
`;
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Build styles + (optional) copy theme as a single cached unit.
|
|
523
|
+
* Theme.css is hashed as input so editing it invalidates the unit.
|
|
524
|
+
*/
|
|
525
|
+
export async function buildStyles(
|
|
526
|
+
rootDir: string,
|
|
527
|
+
arcDir: string,
|
|
528
|
+
packages: WorkspacePackage[],
|
|
529
|
+
themePath: string | undefined,
|
|
530
|
+
cache: BuildCache,
|
|
531
|
+
noCache: boolean,
|
|
532
|
+
): Promise<void> {
|
|
533
|
+
mkdirSync(arcDir, { recursive: true });
|
|
534
|
+
|
|
535
|
+
const inputCss = join(arcDir, "_input.css");
|
|
536
|
+
const outputCss = join(arcDir, "styles.css");
|
|
537
|
+
const themeOutput = join(arcDir, "theme.css");
|
|
538
|
+
const rootRel = relative(arcDir, rootDir).replace(/\\/g, "/");
|
|
539
|
+
const inputCssContent = TAILWIND_INPUT_TEMPLATE(rootRel);
|
|
540
|
+
|
|
541
|
+
// Hash inputs: every workspace package src/, framework src/ trees, theme content,
|
|
542
|
+
// input css template content.
|
|
543
|
+
const tsxFilter = (rel: string): boolean => {
|
|
544
|
+
if (!sourceFilter(rel)) return false;
|
|
545
|
+
if (/\.[^/]+$/.test(rel) && !/\.(ts|tsx)$/.test(rel)) return false;
|
|
546
|
+
return true;
|
|
547
|
+
};
|
|
395
548
|
|
|
396
|
-
|
|
549
|
+
const wsHashes: Record<string, string> = {};
|
|
550
|
+
for (const p of packages) {
|
|
551
|
+
wsHashes[p.name] = sha256OfDir(join(p.path, "src"), tsxFilter);
|
|
552
|
+
}
|
|
553
|
+
const platformSrc = join(rootDir, "node_modules", "@arcote.tech", "platform", "src");
|
|
554
|
+
const arcDsSrc = join(rootDir, "node_modules", "@arcote.tech", "arc-ds", "src");
|
|
555
|
+
const frameworkHashes = {
|
|
556
|
+
platform: sha256OfDir(platformSrc, tsxFilter),
|
|
557
|
+
arcDs: sha256OfDir(arcDsSrc, tsxFilter),
|
|
558
|
+
};
|
|
559
|
+
const themeContent =
|
|
560
|
+
themePath && existsSync(join(rootDir, themePath))
|
|
561
|
+
? readFileSync(join(rootDir, themePath))
|
|
562
|
+
: null;
|
|
563
|
+
const themeHash = themeContent ? sha256Hex(themeContent) : null;
|
|
564
|
+
|
|
565
|
+
const unitId = "styles";
|
|
566
|
+
const inputHash = sha256OfJson({
|
|
567
|
+
workspaces: wsHashes,
|
|
568
|
+
framework: frameworkHashes,
|
|
569
|
+
inputCss: inputCssContent,
|
|
570
|
+
themeHash,
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
const requiredOutputs = [outputCss];
|
|
574
|
+
if (themePath) requiredOutputs.push(themeOutput);
|
|
575
|
+
|
|
576
|
+
if (!noCache && isCacheHit(cache, unitId, inputHash, requiredOutputs)) {
|
|
577
|
+
console.log(` ✓ cached: styles`);
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
console.log(` building: styles`);
|
|
582
|
+
writeFileSync(inputCss, inputCssContent);
|
|
397
583
|
execSync(`bunx @tailwindcss/cli -i ${inputCss} -o ${outputCss} --minify`, {
|
|
398
584
|
cwd: rootDir,
|
|
399
585
|
stdio: "inherit",
|
|
400
586
|
});
|
|
587
|
+
|
|
588
|
+
// Copy theme.css if configured (output of the same unit).
|
|
589
|
+
if (themePath && themeContent) {
|
|
590
|
+
writeFileSync(themeOutput, themeContent);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const outFiles = [outputCss];
|
|
594
|
+
if (themePath && existsSync(themeOutput)) outFiles.push(themeOutput);
|
|
595
|
+
const outputHash = sha256OfFiles(outFiles);
|
|
596
|
+
updateCache(cache, unitId, inputHash, { outputHash });
|
|
401
597
|
}
|