@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.
@@ -1,5 +1,5 @@
1
1
  import { spawn } from "child_process";
2
- import { readFileSync } from "fs";
2
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
3
3
  import { dirname, join } from "path";
4
4
  import type { ArcConfig, ContextInfo } from "./config";
5
5
  import { generateClientTypes } from "./config";
@@ -23,9 +23,6 @@ export interface BuildTask {
23
23
  watch: boolean;
24
24
  }
25
25
 
26
- /**
27
- * Read package.json and extract peer dependencies
28
- */
29
26
  function getPeerDependencies(configDir: string): string[] {
30
27
  try {
31
28
  const packageJsonPath = join(configDir, "package.json");
@@ -36,9 +33,6 @@ function getPeerDependencies(configDir: string): string[] {
36
33
  }
37
34
  }
38
35
 
39
- /**
40
- * Get external dependencies for a specific client
41
- */
42
36
  function getExternals(
43
37
  config: ArcConfig,
44
38
  configDir: string,
@@ -46,27 +40,20 @@ function getExternals(
46
40
  ): string[] {
47
41
  const peerDeps = getPeerDependencies(configDir);
48
42
  const externals = new Set<string>(peerDeps);
49
-
50
43
  if (config.externals?.common) {
51
44
  config.externals.common.forEach((ext) => externals.add(ext));
52
45
  }
53
-
54
46
  const clientExternals =
55
47
  config.externals?.[client as keyof typeof config.externals];
56
48
  if (Array.isArray(clientExternals)) {
57
49
  clientExternals.forEach((ext) => externals.add(ext));
58
50
  }
59
-
60
51
  return Array.from(externals);
61
52
  }
62
53
 
63
- /**
64
- * Build a single context for a specific client
65
- */
66
54
  export async function buildContext(task: BuildTask): Promise<BuildResult> {
67
55
  const startTime = Date.now();
68
56
  const { configPath, config, context, client, watch } = task;
69
-
70
57
  const result: BuildResult = {
71
58
  success: false,
72
59
  context: context.name,
@@ -77,11 +64,8 @@ export async function buildContext(task: BuildTask): Promise<BuildResult> {
77
64
  };
78
65
 
79
66
  try {
80
- // Build the JavaScript bundle
81
67
  await buildContextBundle(configPath, config, context, client, watch);
82
68
  result.jsBuilt = true;
83
-
84
- // Build declarations
85
69
  if (!watch) {
86
70
  const declResult = await buildContextDeclarations(
87
71
  configPath,
@@ -92,7 +76,6 @@ export async function buildContext(task: BuildTask): Promise<BuildResult> {
92
76
  result.declarationsBuilt = declResult.success;
93
77
  result.declarationErrors = declResult.errors;
94
78
  }
95
-
96
79
  result.success = result.jsBuilt;
97
80
  result.duration = Date.now() - startTime;
98
81
  return result;
@@ -103,9 +86,6 @@ export async function buildContext(task: BuildTask): Promise<BuildResult> {
103
86
  }
104
87
  }
105
88
 
106
- /**
107
- * Build JavaScript bundle for a context
108
- */
109
89
  export async function buildContextBundle(
110
90
  configPath: string,
111
91
  config: ArcConfig,
@@ -114,66 +94,98 @@ export async function buildContextBundle(
114
94
  watch: boolean = false,
115
95
  ): Promise<void> {
116
96
  const configDir = dirname(configPath);
117
-
118
97
  const defineValues: Record<string, string> = {};
119
-
120
98
  for (const c of config.clients) {
121
99
  const normalizedC = c.toUpperCase().replace(/[^A-Z0-9]/g, "_");
122
100
  defineValues[normalizedC] = c === client ? "true" : "false";
123
101
  defineValues[`NOT_ON_${normalizedC}`] = c === client ? "false" : "true";
124
102
  defineValues[`ONLY_${normalizedC}`] = c === client ? "true" : "false";
125
103
  }
126
-
127
104
  const defineArgs = Object.entries(defineValues)
128
105
  .map(([key, value]) => `--define="${key}=${value}"`)
129
106
  .join(" ");
130
-
131
107
  const externals = getExternals(config, configDir, client);
132
108
  const externalArgs = externals.map((dep) => `--external=${dep}`).join(" ");
133
-
134
109
  const buildTarget = client === "browser" ? "browser" : "bun";
135
-
110
+ // JS output: dist/{client}/{context}/ (matches TypeScript declaration output)
136
111
  const outDirPath = join(
137
112
  configDir,
138
113
  config.outDir,
139
- context.name,
140
114
  client.toLowerCase(),
115
+ context.name,
141
116
  );
142
-
143
117
  const command = `bun build ${context.fullPath} --target=${buildTarget} --outdir=${outDirPath} ${defineArgs} ${externalArgs}${watch ? " --watch" : ""}`;
144
118
 
145
119
  return new Promise((resolve, reject) => {
146
120
  const proc = spawn(command, { shell: true, cwd: configDir });
147
-
148
121
  let output = "";
149
122
  let errorOutput = "";
150
-
151
- proc.stdout.on("data", (data) => {
123
+ proc.stdout.on("data", (data: Buffer) => {
152
124
  output += data.toString();
153
125
  });
154
-
155
- proc.stderr.on("data", (data) => {
126
+ proc.stderr.on("data", (data: Buffer) => {
156
127
  errorOutput += data.toString();
157
128
  });
158
-
159
- proc.on("close", (code) => {
160
- if (code !== 0 && !watch) {
129
+ proc.on("close", (code: number) => {
130
+ if (code !== 0 && !watch)
161
131
  reject(new Error(`Build failed: ${errorOutput || output}`));
162
- } else if (!watch) {
163
- resolve();
164
- }
132
+ else if (!watch) resolve();
165
133
  });
166
134
  });
167
135
  }
168
136
 
169
- interface DeclarationResult {
137
+ export interface DeclarationResult {
170
138
  success: boolean;
171
139
  errors: string[];
172
140
  }
173
141
 
142
+ // ---------------------------------------------------------------------------
143
+ // Core declaration builder — reusable by both old arc.config flow and platform
144
+ // ---------------------------------------------------------------------------
145
+
146
+ /**
147
+ * Build type declarations using tsgo (with tsc fallback).
148
+ *
149
+ * @param files Entry .ts/.tsx files to generate declarations for
150
+ * @param outDir Output directory for .d.ts files
151
+ * @param rootDir Root directory for TypeScript (controls output structure)
152
+ * @param globalsContent Optional ambient declarations (ONLY_SERVER etc.) —
153
+ * written to a temp .d.ts and included in compilation.
154
+ */
155
+ export async function buildTypeDeclarations(
156
+ files: string[],
157
+ outDir: string,
158
+ rootDir: string,
159
+ globalsContent?: string,
160
+ ): Promise<DeclarationResult> {
161
+ const allFiles = [...files];
162
+
163
+ // Write temp globals .d.ts OUTSIDE source dir to avoid triggering file watchers
164
+ let globalsPath: string | null = null;
165
+ if (globalsContent) {
166
+ const tmpDir = join(outDir, "_build-types");
167
+ mkdirSync(tmpDir, { recursive: true });
168
+ globalsPath = join(tmpDir, "globals.d.ts");
169
+ writeFileSync(globalsPath, globalsContent);
170
+ allFiles.push(globalsPath);
171
+ }
172
+
173
+ let result = await runTsgo(allFiles, outDir, rootDir);
174
+ if (result === null) {
175
+ result = await runTsc(allFiles, outDir, rootDir);
176
+ }
177
+
178
+ // Clean up temp globals
179
+ if (globalsPath && existsSync(globalsPath)) {
180
+ unlinkSync(globalsPath);
181
+ }
182
+
183
+ return result;
184
+ }
185
+
174
186
  /**
175
- * Build declarations for a context using tsgo (TypeScript Go)
176
- * Falls back to regular tsc if tsgo is not available
187
+ * Build declarations (old arc.config flow).
188
+ * Output: dist/{client}/{context}/ (matches JS output and TypeScript's natural output)
177
189
  */
178
190
  export async function buildContextDeclarations(
179
191
  configPath: string,
@@ -182,61 +194,47 @@ export async function buildContextDeclarations(
182
194
  client: string,
183
195
  ): Promise<DeclarationResult> {
184
196
  const configDir = dirname(configPath);
185
-
186
- // Generate client types
187
197
  generateClientTypes(config, configDir, client);
188
198
 
189
- const outDir = join(
190
- configDir,
191
- config.outDir,
192
- context.name,
193
- client.toLowerCase(),
194
- );
199
+ const outDir = join(configDir, config.outDir, client.toLowerCase());
195
200
 
196
- // Try tsgo first, fall back to tsc
197
- const tsgoResult = await runTsgo(context.fullPath, outDir, configDir);
198
- if (tsgoResult !== null) {
199
- return tsgoResult;
200
- }
201
+ // Include arc.d.ts for global constants
202
+ const arcTypesPath = join(configDir, "arc.d.ts");
203
+ const files = existsSync(arcTypesPath)
204
+ ? [context.fullPath, arcTypesPath]
205
+ : [context.fullPath];
201
206
 
202
- // Fallback to tsc
203
- return runTsc(context.fullPath, outDir, configDir);
207
+ let result = await runTsgo(files, outDir, configDir);
208
+ if (result === null) {
209
+ result = await runTsc(files, outDir, configDir);
210
+ }
211
+ return result;
204
212
  }
205
213
 
206
- /**
207
- * Run tsgo CLI for declaration generation
208
- * Returns null if tsgo is not available
209
- */
214
+ // ---------------------------------------------------------------------------
215
+ // tsgo / tsc runners
216
+ // ---------------------------------------------------------------------------
217
+
210
218
  async function runTsgo(
211
- entryFile: string,
219
+ files: string[],
212
220
  outDir: string,
213
221
  cwd: string,
214
222
  ): Promise<DeclarationResult | null> {
215
- return new Promise((resolve) => {
216
- // Use npx to run tsgo from @typescript/native-preview
217
- // --ignoreConfig to skip tsconfig.json when files are specified on command line
218
- const command = `npx tsgo --declaration --emitDeclarationOnly --outDir "${outDir}" --skipLibCheck --moduleResolution bundler --module esnext --target esnext --ignoreConfig "${entryFile}"`;
223
+ const fileArgs = files.map((f) => `"${f}"`).join(" ");
224
+ const command = `npx tsgo ${fileArgs} --declaration --emitDeclarationOnly --outDir "${outDir}" --rootDir "${cwd}" --skipLibCheck --moduleResolution bundler --module esnext --target esnext --jsx react-jsx --ignoreConfig`;
219
225
 
226
+ return new Promise((resolve) => {
220
227
  const proc = spawn(command, { shell: true, cwd });
221
-
222
228
  let output = "";
223
229
  let errorOutput = "";
224
-
225
- proc.stdout.on("data", (data) => {
230
+ proc.stdout.on("data", (data: Buffer) => {
226
231
  output += data.toString();
227
232
  });
228
-
229
- proc.stderr.on("data", (data) => {
233
+ proc.stderr.on("data", (data: Buffer) => {
230
234
  errorOutput += data.toString();
231
235
  });
232
-
233
- proc.on("error", () => {
234
- // tsgo not available
235
- resolve(null);
236
- });
237
-
238
- proc.on("close", (code) => {
239
- // Check if tsgo was not found
236
+ proc.on("error", () => resolve(null));
237
+ proc.on("close", (code: number) => {
240
238
  if (
241
239
  errorOutput.includes("not found") ||
242
240
  errorOutput.includes("ENOENT") ||
@@ -245,64 +243,40 @@ async function runTsgo(
245
243
  resolve(null);
246
244
  return;
247
245
  }
248
-
249
- const allOutput = output + errorOutput;
250
- const errors = parseTypeScriptErrors(allOutput, cwd);
251
-
252
- resolve({
253
- success: code === 0 && errors.length === 0,
254
- errors,
255
- });
246
+ const errors = parseTypeScriptErrors(output + errorOutput, cwd);
247
+ resolve({ success: code === 0 && errors.length === 0, errors });
256
248
  });
257
249
  });
258
250
  }
259
251
 
260
- /**
261
- * Run tsc CLI for declaration generation (fallback)
262
- */
263
252
  async function runTsc(
264
- entryFile: string,
253
+ files: string[],
265
254
  outDir: string,
266
255
  cwd: string,
267
256
  ): Promise<DeclarationResult> {
268
- return new Promise((resolve) => {
269
- const command = `npx tsc --declaration --emitDeclarationOnly --outDir "${outDir}" --skipLibCheck --moduleResolution bundler --module esnext --target esnext "${entryFile}"`;
257
+ const fileArgs = files.map((f) => `"${f}"`).join(" ");
258
+ const command = `npx tsc ${fileArgs} --declaration --emitDeclarationOnly --outDir "${outDir}" --rootDir "${cwd}" --skipLibCheck --moduleResolution bundler --module esnext --target esnext --jsx react-jsx`;
270
259
 
260
+ return new Promise((resolve) => {
271
261
  const proc = spawn(command, { shell: true, cwd });
272
-
273
262
  let output = "";
274
263
  let errorOutput = "";
275
-
276
- proc.stdout.on("data", (data) => {
264
+ proc.stdout.on("data", (data: Buffer) => {
277
265
  output += data.toString();
278
266
  });
279
-
280
- proc.stderr.on("data", (data) => {
267
+ proc.stderr.on("data", (data: Buffer) => {
281
268
  errorOutput += data.toString();
282
269
  });
283
-
284
- proc.on("close", (code) => {
285
- const allOutput = output + errorOutput;
286
- const errors = parseTypeScriptErrors(allOutput, cwd);
287
-
288
- resolve({
289
- success: code === 0 && errors.length === 0,
290
- errors,
291
- });
270
+ proc.on("close", (code: number) => {
271
+ const errors = parseTypeScriptErrors(output + errorOutput, cwd);
272
+ resolve({ success: code === 0 && errors.length === 0, errors });
292
273
  });
293
274
  });
294
275
  }
295
276
 
296
- /**
297
- * Parse TypeScript CLI error output into structured errors
298
- */
299
277
  function parseTypeScriptErrors(output: string, cwd: string): string[] {
300
278
  const errors: string[] = [];
301
- const lines = output.split("\n");
302
-
303
- for (const line of lines) {
304
- // Match TypeScript error format: file(line,col): error TS1234: message
305
- // or: file:line:col - error TS1234: message
279
+ for (const line of output.split("\n")) {
306
280
  const match = line.match(
307
281
  /^(.+?)(?:\((\d+),(\d+)\)|:(\d+):(\d+))\s*[-:]\s*error\s+TS\d+:\s*(.+)$/,
308
282
  );
@@ -310,33 +284,28 @@ function parseTypeScriptErrors(output: string, cwd: string): string[] {
310
284
  const file = match[1].replace(cwd + "/", "");
311
285
  const lineNum = match[2] || match[4];
312
286
  const col = match[3] || match[5];
313
- const message = match[6];
314
- errors.push(`${file}:${lineNum}:${col} - ${message}`);
287
+ errors.push(`${file}:${lineNum}:${col} - ${match[6]}`);
315
288
  } else if (line.includes("error TS")) {
316
- // Catch any other error format
317
289
  errors.push(line.trim());
318
290
  }
319
291
  }
320
-
321
292
  return errors;
322
293
  }
323
294
 
324
- // Legacy functions for backward compatibility
295
+ // Legacy
325
296
  export async function buildClient(
326
297
  configPath: string,
327
298
  config: ArcConfig,
328
299
  client: string,
329
- watch: boolean = false,
300
+ watch = false,
330
301
  ): Promise<void> {
331
302
  const configDir = dirname(configPath);
332
-
333
303
  if (config.file) {
334
304
  const context: ContextInfo = {
335
305
  name: "main",
336
306
  entryFile: config.file,
337
307
  fullPath: join(configDir, config.file),
338
308
  };
339
-
340
309
  await buildContextBundle(configPath, config, context, client, watch);
341
310
  }
342
311
  }
@@ -347,14 +316,12 @@ export async function buildDeclarations(
347
316
  client: string,
348
317
  ): Promise<void> {
349
318
  const configDir = dirname(configPath);
350
-
351
319
  if (config.file) {
352
320
  const context: ContextInfo = {
353
321
  name: "main",
354
322
  entryFile: config.file,
355
323
  fullPath: join(configDir, config.file),
356
324
  };
357
-
358
325
  await buildContextDeclarations(configPath, config, context, client);
359
326
  }
360
327
  }
@@ -113,40 +113,29 @@ export function getContextsFromConfig(
113
113
 
114
114
  /**
115
115
  * Generate base types file for TypeScript intellisense
116
- * This creates a file with all constants defined as boolean type
116
+ * This creates a .d.ts file with all constants defined as boolean type
117
117
  */
118
118
  export function generateBaseTypes(config: ArcConfig, configDir: string): void {
119
119
  const { clients } = config;
120
120
 
121
- // Create .arc directory if it doesn't exist
122
- const arcDir = join(configDir, ".arc");
123
- if (!existsSync(arcDir)) {
124
- mkdirSync(arcDir, { recursive: true });
125
- }
126
-
127
- // Generate types.ts file with boolean declarations for IDE support
128
- let typesDefs = `declare global {\n`;
121
+ // Generate ambient declarations (no export {} so they're truly global)
122
+ let typesDefs = "";
129
123
 
130
124
  clients.forEach((client) => {
131
125
  const normalizedClient = normalizeClientName(client);
132
126
 
133
- typesDefs += ` const ${normalizedClient}: boolean;\n`;
134
- typesDefs += ` const NOT_ON_${normalizedClient}: boolean;\n`;
135
- typesDefs += ` const ONLY_${normalizedClient}: boolean;\n`;
127
+ typesDefs += `declare const ${normalizedClient}: boolean;\n`;
128
+ typesDefs += `declare const NOT_ON_${normalizedClient}: boolean;\n`;
129
+ typesDefs += `declare const ONLY_${normalizedClient}: boolean;\n`;
136
130
  });
137
131
 
138
- typesDefs += `}\n\nexport {};\n`;
139
-
140
- writeFileSync(join(arcDir, "types.ts"), typesDefs);
141
-
142
- // Also generate arc.types.ts in root for backward compatibility
143
- // Only generate at root level - contexts should reference the root file
144
- writeFileSync(join(configDir, "arc.types.ts"), typesDefs);
132
+ // Write as .d.ts file in the config directory for proper TypeScript pickup
133
+ writeFileSync(join(configDir, "arc.d.ts"), typesDefs);
145
134
  }
146
135
 
147
136
  /**
148
137
  * Generate client-specific types file for a build
149
- * This creates a file with concrete true/false values based on the current client
138
+ * This creates a .d.ts file with concrete true/false values based on the current client
150
139
  */
151
140
  export function generateClientTypes(
152
141
  config: ArcConfig,
@@ -154,33 +143,31 @@ export function generateClientTypes(
154
143
  client: string,
155
144
  ): string {
156
145
  const { clients } = config;
157
- const arcDir = join(configDir, ".arc");
158
146
 
159
- // Create client-specific types directory
147
+ // Create .arc/client-types directory
148
+ const arcDir = join(configDir, ".arc");
160
149
  const clientTypesDir = join(arcDir, "client-types");
161
150
  if (!existsSync(clientTypesDir)) {
162
151
  mkdirSync(clientTypesDir, { recursive: true });
163
152
  }
164
153
 
165
- // Generate client-specific types file
166
- let typesDefs = `declare global {\n`;
154
+ // Generate client-specific ambient declarations
155
+ let typesDefs = "";
167
156
 
168
157
  clients.forEach((c) => {
169
158
  const normalizedC = normalizeClientName(c);
170
159
  const isCurrentClient = normalizeClientName(client) === normalizedC;
171
160
 
172
161
  // Set concrete values based on the current client
173
- typesDefs += ` const ${normalizedC}: ${isCurrentClient ? "true" : "false"};\n`;
174
- typesDefs += ` const NOT_ON_${normalizedC}: ${isCurrentClient ? "false" : "true"};\n`;
175
- typesDefs += ` const ONLY_${normalizedC}: ${isCurrentClient ? "true" : "false"};\n`;
162
+ typesDefs += `declare const ${normalizedC}: ${isCurrentClient ? "true" : "false"};\n`;
163
+ typesDefs += `declare const NOT_ON_${normalizedC}: ${isCurrentClient ? "false" : "true"};\n`;
164
+ typesDefs += `declare const ONLY_${normalizedC}: ${isCurrentClient ? "true" : "false"};\n`;
176
165
  });
177
166
 
178
- typesDefs += `}\n\nexport {};\n`;
179
-
180
- // Write to a client-specific file
167
+ // Write as .d.ts file
181
168
  const typesPath = join(
182
169
  clientTypesDir,
183
- `${normalizeClientName(client).toLowerCase()}.ts`,
170
+ `${normalizeClientName(client).toLowerCase()}.d.ts`,
184
171
  );
185
172
  writeFileSync(typesPath, typesDefs);
186
173