@arcote.tech/arc-cli 0.3.1 → 0.4.1
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 +18071 -7175
- package/package.json +2 -2
- package/src/builder/module-builder.ts +348 -0
- package/src/commands/platform-build.ts +7 -0
- package/src/commands/platform-dev.ts +116 -0
- package/src/commands/platform-start.ts +56 -0
- package/src/i18n/catalog.ts +204 -0
- package/src/i18n/compile.ts +37 -0
- package/src/i18n/index.ts +77 -0
- package/src/i18n/plugin.ts +55 -0
- package/src/index.ts +24 -1
- package/src/platform/server.ts +353 -0
- package/src/platform/shared.ts +280 -0
- package/src/utils/build.ts +53 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arcote.tech/arc-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "CLI tool for Arc framework",
|
|
5
5
|
"module": "index.ts",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"arc": "./dist/index.js"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
|
-
"build": "bun build --target=
|
|
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 && chmod +x dist/index.js"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
15
|
"commander": "^11.1.0",
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from "fs";
|
|
9
|
+
import { dirname, join, relative } from "path";
|
|
10
|
+
import {
|
|
11
|
+
buildTypeDeclarations,
|
|
12
|
+
type DeclarationResult,
|
|
13
|
+
} from "../utils/build";
|
|
14
|
+
import { i18nExtractPlugin, finalizeTranslations } from "../i18n";
|
|
15
|
+
|
|
16
|
+
/** Clients that a context package is built for. */
|
|
17
|
+
const CONTEXT_CLIENTS = [
|
|
18
|
+
{ name: "server", target: "bun" as const, defines: { ONLY_SERVER: "true", ONLY_BROWSER: "false", ONLY_CLIENT: "false" } },
|
|
19
|
+
{ name: "browser", target: "browser" as const, defines: { ONLY_SERVER: "false", ONLY_BROWSER: "true", ONLY_CLIENT: "true" } },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Build a context package (browser/server split) using Bun.build directly,
|
|
24
|
+
* then generate .d.ts declarations via tsgo (with tsc fallback).
|
|
25
|
+
*
|
|
26
|
+
* Returns declaration errors (if any) so the caller can display them.
|
|
27
|
+
*/
|
|
28
|
+
async function buildContextPackage(
|
|
29
|
+
pkg: WorkspacePackage,
|
|
30
|
+
): Promise<{ declarationErrors: string[] }> {
|
|
31
|
+
const entrypoint = pkg.entrypoint;
|
|
32
|
+
const outDir = join(pkg.path, "dist");
|
|
33
|
+
const peerDeps = Object.keys(pkg.packageJson.peerDependencies || {});
|
|
34
|
+
const deps = Object.keys(pkg.packageJson.dependencies || {});
|
|
35
|
+
const externals = [...peerDeps, ...deps];
|
|
36
|
+
const allDeclErrors: string[] = [];
|
|
37
|
+
|
|
38
|
+
for (const client of CONTEXT_CLIENTS) {
|
|
39
|
+
// JS bundle
|
|
40
|
+
const result = await Bun.build({
|
|
41
|
+
entrypoints: [entrypoint],
|
|
42
|
+
outdir: join(outDir, client.name, "main"),
|
|
43
|
+
target: client.target,
|
|
44
|
+
format: "esm",
|
|
45
|
+
naming: "index.[ext]",
|
|
46
|
+
external: externals,
|
|
47
|
+
define: client.defines,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
if (!result.success) {
|
|
51
|
+
console.error(`Context ${client.name} build failed:`);
|
|
52
|
+
for (const log of result.logs) console.error(log);
|
|
53
|
+
throw new Error(`${client.name} build failed for ${pkg.name}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Type declarations — globals match the define values
|
|
57
|
+
const globalsContent = Object.entries(client.defines)
|
|
58
|
+
.map(([k, v]) => `declare const ${k}: ${v};`)
|
|
59
|
+
.join("\n");
|
|
60
|
+
|
|
61
|
+
const declResult = await buildTypeDeclarations(
|
|
62
|
+
[entrypoint],
|
|
63
|
+
join(outDir, client.name),
|
|
64
|
+
dirname(entrypoint),
|
|
65
|
+
globalsContent,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
if (!declResult.success && declResult.errors.length > 0) {
|
|
69
|
+
allDeclErrors.push(
|
|
70
|
+
...declResult.errors.map((e) => `[${pkg.name}/${client.name}] ${e}`),
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { declarationErrors: allDeclErrors };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Packages that shell provides — modules import them but don't bundle them. */
|
|
79
|
+
export const SHELL_EXTERNALS = [
|
|
80
|
+
"react",
|
|
81
|
+
"react-dom",
|
|
82
|
+
"react/jsx-runtime",
|
|
83
|
+
"react/jsx-dev-runtime",
|
|
84
|
+
"@arcote.tech/arc",
|
|
85
|
+
"@arcote.tech/arc-ds",
|
|
86
|
+
"@arcote.tech/arc-react",
|
|
87
|
+
"@arcote.tech/platform",
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
export interface WorkspacePackage {
|
|
91
|
+
name: string;
|
|
92
|
+
path: string;
|
|
93
|
+
entrypoint: string;
|
|
94
|
+
packageJson: Record<string, any>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface BuildManifest {
|
|
98
|
+
modules: string[];
|
|
99
|
+
buildTime: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Discover workspace packages from root package.json.
|
|
104
|
+
* Skips @arcote.tech/* packages (those are framework, not business).
|
|
105
|
+
*/
|
|
106
|
+
export function discoverPackages(rootDir: string): WorkspacePackage[] {
|
|
107
|
+
const rootPkg = JSON.parse(
|
|
108
|
+
readFileSync(join(rootDir, "package.json"), "utf-8"),
|
|
109
|
+
);
|
|
110
|
+
const workspaceGlobs: string[] = rootPkg.workspaces ?? [];
|
|
111
|
+
const results: WorkspacePackage[] = [];
|
|
112
|
+
|
|
113
|
+
for (const glob of workspaceGlobs) {
|
|
114
|
+
const base = glob.replace("/*", "");
|
|
115
|
+
const baseDir = join(rootDir, base);
|
|
116
|
+
if (!existsSync(baseDir)) continue;
|
|
117
|
+
|
|
118
|
+
let entries: string[];
|
|
119
|
+
try {
|
|
120
|
+
entries = readdirSync(baseDir);
|
|
121
|
+
} catch {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
for (const entry of entries) {
|
|
126
|
+
const pkgPath = join(baseDir, entry, "package.json");
|
|
127
|
+
if (!existsSync(pkgPath)) continue;
|
|
128
|
+
|
|
129
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
130
|
+
if (pkg.name?.startsWith("@arcote.tech/")) continue;
|
|
131
|
+
|
|
132
|
+
// Find entrypoint: src/index.ts, src/index.tsx, or root index.ts
|
|
133
|
+
const pkgDir = join(baseDir, entry);
|
|
134
|
+
const candidates = [
|
|
135
|
+
join(pkgDir, "src", "index.ts"),
|
|
136
|
+
join(pkgDir, "src", "index.tsx"),
|
|
137
|
+
join(pkgDir, "index.ts"),
|
|
138
|
+
join(pkgDir, "index.tsx"),
|
|
139
|
+
];
|
|
140
|
+
const entrypoint = candidates.find((c) => existsSync(c)) ?? null;
|
|
141
|
+
if (!entrypoint) continue;
|
|
142
|
+
|
|
143
|
+
results.push({
|
|
144
|
+
name: pkg.name,
|
|
145
|
+
path: join(baseDir, entry),
|
|
146
|
+
entrypoint,
|
|
147
|
+
packageJson: pkg,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return results;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Check if a package is a context (has browser/server exports split).
|
|
157
|
+
*/
|
|
158
|
+
export function isContextPackage(pkg: Record<string, any>): boolean {
|
|
159
|
+
const exports = pkg.exports ?? {};
|
|
160
|
+
for (const key of Object.keys(exports)) {
|
|
161
|
+
if (key.includes("/browser") || key.includes("/server")) return true;
|
|
162
|
+
const val = exports[key];
|
|
163
|
+
if (val && typeof val === "object" && ("browser" in val || "server" in val))
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Build all workspace packages as ESM chunks using Bun.build.
|
|
171
|
+
* Context packages get a wrapper that calls arc() to auto-register.
|
|
172
|
+
* Regular modules already call arc() in their source.
|
|
173
|
+
*/
|
|
174
|
+
export async function buildPackages(
|
|
175
|
+
rootDir: string,
|
|
176
|
+
outDir: string,
|
|
177
|
+
packages: WorkspacePackage[],
|
|
178
|
+
): Promise<BuildManifest> {
|
|
179
|
+
mkdirSync(outDir, { recursive: true });
|
|
180
|
+
|
|
181
|
+
// Build contexts first (server/browser split via Bun.build + declarations)
|
|
182
|
+
const contexts = packages.filter((p) => isContextPackage(p.packageJson));
|
|
183
|
+
const allDeclErrors: string[] = [];
|
|
184
|
+
for (const ctx of contexts) {
|
|
185
|
+
console.log(` Building context: ${ctx.name}`);
|
|
186
|
+
const { declarationErrors } = await buildContextPackage(ctx);
|
|
187
|
+
allDeclErrors.push(...declarationErrors);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (allDeclErrors.length > 0) {
|
|
191
|
+
console.warn("\n\x1b[33mType declaration errors:\x1b[0m");
|
|
192
|
+
for (const err of allDeclErrors) {
|
|
193
|
+
console.warn(` ${err}`);
|
|
194
|
+
}
|
|
195
|
+
console.warn("");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Generate wrapper entries in a tmp dir
|
|
199
|
+
const tmpDir = join(outDir, "_entries");
|
|
200
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
201
|
+
|
|
202
|
+
const entrypoints: string[] = [];
|
|
203
|
+
|
|
204
|
+
for (const pkg of packages) {
|
|
205
|
+
const safeName = pkg.path.split("/").pop()!;
|
|
206
|
+
|
|
207
|
+
// All packages get a simple re-export wrapper.
|
|
208
|
+
// Context packages that use module().build() self-register via side effects.
|
|
209
|
+
// Legacy packages that export appContext will be picked up by arc(() => appContext).
|
|
210
|
+
const wrapperFile = join(tmpDir, `${safeName}.ts`);
|
|
211
|
+
writeFileSync(wrapperFile, `export * from "${pkg.name}";\n`);
|
|
212
|
+
entrypoints.push(wrapperFile);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
console.log(` Bundling ${entrypoints.length} package(s)...`);
|
|
216
|
+
|
|
217
|
+
// i18n extraction — collect translatable strings during bundling
|
|
218
|
+
const i18nCollector = new Map<string, Set<string>>();
|
|
219
|
+
|
|
220
|
+
const result = await Bun.build({
|
|
221
|
+
entrypoints,
|
|
222
|
+
outdir: outDir,
|
|
223
|
+
splitting: true,
|
|
224
|
+
format: "esm",
|
|
225
|
+
target: "browser",
|
|
226
|
+
external: SHELL_EXTERNALS,
|
|
227
|
+
plugins: [i18nExtractPlugin(i18nCollector, rootDir)],
|
|
228
|
+
naming: "[name].[ext]",
|
|
229
|
+
define: {
|
|
230
|
+
ONLY_SERVER: "false",
|
|
231
|
+
ONLY_BROWSER: "true",
|
|
232
|
+
ONLY_CLIENT: "true",
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
if (!result.success) {
|
|
237
|
+
console.error("Build failed:");
|
|
238
|
+
for (const log of result.logs) console.error(log);
|
|
239
|
+
throw new Error("Module build failed");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Finalize translations — merge .po catalogs + compile .json
|
|
243
|
+
// outDir is modulesDir, translations JSON goes to parent (arcDir)
|
|
244
|
+
await finalizeTranslations(rootDir, join(outDir, ".."), i18nCollector);
|
|
245
|
+
|
|
246
|
+
// Clean tmp
|
|
247
|
+
const { rmSync } = await import("fs");
|
|
248
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
249
|
+
|
|
250
|
+
// Build manifest
|
|
251
|
+
const moduleFiles = result.outputs
|
|
252
|
+
.filter((o) => o.kind === "entry-point")
|
|
253
|
+
.map((o) => o.path.split("/").pop()!);
|
|
254
|
+
|
|
255
|
+
const manifest: BuildManifest = {
|
|
256
|
+
modules: moduleFiles,
|
|
257
|
+
buildTime: new Date().toISOString(),
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
writeFileSync(
|
|
261
|
+
join(outDir, "manifest.json"),
|
|
262
|
+
JSON.stringify(manifest, null, 2),
|
|
263
|
+
);
|
|
264
|
+
return manifest;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Build CSS using tailwind CLI.
|
|
269
|
+
*/
|
|
270
|
+
export async function buildStyles(
|
|
271
|
+
rootDir: string,
|
|
272
|
+
outDir: string,
|
|
273
|
+
): Promise<void> {
|
|
274
|
+
mkdirSync(outDir, { recursive: true });
|
|
275
|
+
|
|
276
|
+
const inputCss = join(outDir, "_input.css");
|
|
277
|
+
const outputCss = join(outDir, "styles.css");
|
|
278
|
+
const rootRel = relative(outDir, rootDir).replace(/\\/g, "/");
|
|
279
|
+
|
|
280
|
+
writeFileSync(
|
|
281
|
+
inputCss,
|
|
282
|
+
`@import "tailwindcss";
|
|
283
|
+
@import "tw-animate-css";
|
|
284
|
+
|
|
285
|
+
@source "${rootRel}/packages/*/*.{ts,tsx}";
|
|
286
|
+
@source "${rootRel}/packages/*/src/**/*.{ts,tsx}";
|
|
287
|
+
@source "${rootRel}/node_modules/@arcote.tech/platform/src/**/*.{ts,tsx}";
|
|
288
|
+
@source "${rootRel}/node_modules/@arcote.tech/arc-ds/src/**/*.{ts,tsx}";
|
|
289
|
+
|
|
290
|
+
@custom-variant dark (&:is(.dark *));
|
|
291
|
+
|
|
292
|
+
@theme inline {
|
|
293
|
+
--color-background: var(--background);
|
|
294
|
+
--color-foreground: var(--foreground);
|
|
295
|
+
--color-card: var(--card);
|
|
296
|
+
--color-card-foreground: var(--card-foreground);
|
|
297
|
+
--color-popover: var(--popover);
|
|
298
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
299
|
+
--color-primary: var(--primary);
|
|
300
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
301
|
+
--color-secondary: var(--secondary);
|
|
302
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
303
|
+
--color-muted: var(--muted);
|
|
304
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
305
|
+
--color-accent: var(--accent);
|
|
306
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
307
|
+
--color-destructive: var(--destructive);
|
|
308
|
+
--color-destructive-foreground: var(--destructive-foreground);
|
|
309
|
+
--color-border: var(--border);
|
|
310
|
+
--color-input: var(--input);
|
|
311
|
+
--color-ring: var(--ring);
|
|
312
|
+
--color-chart-1: var(--chart-1);
|
|
313
|
+
--color-chart-2: var(--chart-2);
|
|
314
|
+
--color-chart-3: var(--chart-3);
|
|
315
|
+
--color-chart-4: var(--chart-4);
|
|
316
|
+
--color-chart-5: var(--chart-5);
|
|
317
|
+
--color-sidebar: var(--sidebar);
|
|
318
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
319
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
320
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
321
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
322
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
323
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
324
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
325
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
326
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
327
|
+
--radius-lg: var(--radius);
|
|
328
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
@layer base {
|
|
332
|
+
* {
|
|
333
|
+
@apply border-border;
|
|
334
|
+
}
|
|
335
|
+
body {
|
|
336
|
+
@apply bg-background text-foreground;
|
|
337
|
+
min-height: 100vh;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
`,
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
console.log(" Building CSS...");
|
|
344
|
+
execSync(`bunx @tailwindcss/cli -i ${inputCss} -o ${outputCss} --minify`, {
|
|
345
|
+
cwd: rootDir,
|
|
346
|
+
stdio: "inherit",
|
|
347
|
+
});
|
|
348
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { existsSync, watch } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { compileAllCatalogs } from "../i18n/compile";
|
|
4
|
+
import { startPlatformServer } from "../platform/server";
|
|
5
|
+
import {
|
|
6
|
+
buildAll,
|
|
7
|
+
buildPackages,
|
|
8
|
+
buildStyles,
|
|
9
|
+
loadServerContext,
|
|
10
|
+
log,
|
|
11
|
+
ok,
|
|
12
|
+
resolveWorkspace,
|
|
13
|
+
} from "../platform/shared";
|
|
14
|
+
|
|
15
|
+
export async function platformDev(): Promise<void> {
|
|
16
|
+
const ws = resolveWorkspace();
|
|
17
|
+
const port = 5005;
|
|
18
|
+
|
|
19
|
+
// Build everything
|
|
20
|
+
let manifest = await buildAll(ws);
|
|
21
|
+
|
|
22
|
+
// Load server context
|
|
23
|
+
log("Loading server context...");
|
|
24
|
+
const context = await loadServerContext(ws.packages);
|
|
25
|
+
if (context) {
|
|
26
|
+
ok("Context loaded");
|
|
27
|
+
} else {
|
|
28
|
+
log("No context — server endpoints skipped");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Start server (dev mode = SSE reload + no-cache)
|
|
32
|
+
const platform = await startPlatformServer({
|
|
33
|
+
ws,
|
|
34
|
+
port,
|
|
35
|
+
manifest,
|
|
36
|
+
context,
|
|
37
|
+
dbPath: join(ws.rootDir, ".arc", "data", "dev.db"),
|
|
38
|
+
devMode: true,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
ok(`Server on http://localhost:${port}`);
|
|
42
|
+
if (platform.contextHandler) ok("Commands, queries, WebSocket — all on same port");
|
|
43
|
+
|
|
44
|
+
// Watch for changes
|
|
45
|
+
log("Watching for changes...");
|
|
46
|
+
let rebuildTimer: ReturnType<typeof setTimeout> | null = null;
|
|
47
|
+
let isRebuilding = false;
|
|
48
|
+
|
|
49
|
+
for (const pkg of ws.packages) {
|
|
50
|
+
const srcDir = join(pkg.path, "src");
|
|
51
|
+
if (!existsSync(srcDir)) continue;
|
|
52
|
+
|
|
53
|
+
watch(srcDir, { recursive: true }, (_event, filename) => {
|
|
54
|
+
// Ignore build artifacts and non-source files
|
|
55
|
+
if (
|
|
56
|
+
!filename ||
|
|
57
|
+
filename.includes(".arc") ||
|
|
58
|
+
filename.endsWith(".d.ts") ||
|
|
59
|
+
filename.includes("node_modules") ||
|
|
60
|
+
filename.includes("dist")
|
|
61
|
+
) return;
|
|
62
|
+
|
|
63
|
+
// Only rebuild for source file changes
|
|
64
|
+
if (!filename.endsWith(".ts") && !filename.endsWith(".tsx")) return;
|
|
65
|
+
|
|
66
|
+
if (rebuildTimer) clearTimeout(rebuildTimer);
|
|
67
|
+
rebuildTimer = setTimeout(async () => {
|
|
68
|
+
if (isRebuilding) return;
|
|
69
|
+
isRebuilding = true;
|
|
70
|
+
log("Rebuilding...");
|
|
71
|
+
try {
|
|
72
|
+
manifest = await buildPackages(
|
|
73
|
+
ws.rootDir,
|
|
74
|
+
ws.modulesDir,
|
|
75
|
+
ws.packages,
|
|
76
|
+
);
|
|
77
|
+
await buildStyles(ws.rootDir, ws.arcDir);
|
|
78
|
+
platform.setManifest(manifest);
|
|
79
|
+
platform.notifyReload(manifest);
|
|
80
|
+
ok(`Rebuilt ${manifest.modules.length} module(s)`);
|
|
81
|
+
} catch (e) {
|
|
82
|
+
console.error(`Rebuild failed: ${e}`);
|
|
83
|
+
} finally {
|
|
84
|
+
isRebuilding = false;
|
|
85
|
+
}
|
|
86
|
+
}, 300);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Watch locales/*.po for translation changes → recompile JSON
|
|
91
|
+
const localesDir = join(ws.rootDir, "locales");
|
|
92
|
+
if (existsSync(localesDir)) {
|
|
93
|
+
let poTimer: ReturnType<typeof setTimeout> | null = null;
|
|
94
|
+
watch(localesDir, { recursive: false }, (_event, filename) => {
|
|
95
|
+
if (!filename?.endsWith(".po")) return;
|
|
96
|
+
if (poTimer) clearTimeout(poTimer);
|
|
97
|
+
poTimer = setTimeout(async () => {
|
|
98
|
+
try {
|
|
99
|
+
compileAllCatalogs(localesDir, join(ws.arcDir, "locales"));
|
|
100
|
+
ok("Translations recompiled");
|
|
101
|
+
platform.notifyReload(manifest);
|
|
102
|
+
} catch (e) {
|
|
103
|
+
console.error(`Translation compile failed: ${e}`);
|
|
104
|
+
}
|
|
105
|
+
}, 200);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Cleanup
|
|
110
|
+
const cleanup = () => {
|
|
111
|
+
platform.stop();
|
|
112
|
+
process.exit(0);
|
|
113
|
+
};
|
|
114
|
+
process.on("SIGTERM", cleanup);
|
|
115
|
+
process.on("SIGINT", cleanup);
|
|
116
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { startPlatformServer } from "../platform/server";
|
|
4
|
+
import {
|
|
5
|
+
err,
|
|
6
|
+
loadServerContext,
|
|
7
|
+
log,
|
|
8
|
+
ok,
|
|
9
|
+
resolveWorkspace,
|
|
10
|
+
type BuildManifest,
|
|
11
|
+
} from "../platform/shared";
|
|
12
|
+
|
|
13
|
+
export async function platformStart(): Promise<void> {
|
|
14
|
+
const ws = resolveWorkspace();
|
|
15
|
+
const port = parseInt(process.env.PORT || "5005", 10);
|
|
16
|
+
|
|
17
|
+
// Read pre-built manifest
|
|
18
|
+
const manifestPath = join(ws.modulesDir, "manifest.json");
|
|
19
|
+
if (!existsSync(manifestPath)) {
|
|
20
|
+
err("No build found. Run `arc platform build` first.");
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
const manifest: BuildManifest = JSON.parse(
|
|
24
|
+
readFileSync(manifestPath, "utf-8"),
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
// Load server context
|
|
28
|
+
log("Loading server context...");
|
|
29
|
+
const context = await loadServerContext(ws.packages);
|
|
30
|
+
if (context) {
|
|
31
|
+
ok("Context loaded");
|
|
32
|
+
} else {
|
|
33
|
+
log("No context — server endpoints skipped");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Start server (production mode = no SSE reload, aggressive caching)
|
|
37
|
+
const platform = await startPlatformServer({
|
|
38
|
+
ws,
|
|
39
|
+
port,
|
|
40
|
+
manifest,
|
|
41
|
+
context,
|
|
42
|
+
dbPath: join(ws.rootDir, ".arc", "data", "prod.db"),
|
|
43
|
+
devMode: false,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
ok(`Server on http://localhost:${port}`);
|
|
47
|
+
if (platform.contextHandler) ok("Commands, queries, WebSocket — all on same port");
|
|
48
|
+
|
|
49
|
+
// Cleanup
|
|
50
|
+
const cleanup = () => {
|
|
51
|
+
platform.stop();
|
|
52
|
+
process.exit(0);
|
|
53
|
+
};
|
|
54
|
+
process.on("SIGTERM", cleanup);
|
|
55
|
+
process.on("SIGINT", cleanup);
|
|
56
|
+
}
|