@arcote.tech/arc-cli 0.3.0 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcote.tech/arc-cli",
3
- "version": "0.3.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=node ./src/index.ts --outdir=dist && chmod +x dist/index.js"
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,7 @@
1
+ import { buildAll, ok, resolveWorkspace } from "../platform/shared";
2
+
3
+ export async function platformBuild(): Promise<void> {
4
+ const ws = resolveWorkspace();
5
+ const manifest = await buildAll(ws);
6
+ ok(`Platform built — ${manifest.modules.length} module(s)`);
7
+ }
@@ -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
+ }