@devdogsuga/devtools 0.1.14 → 0.1.16

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,3 +1,7 @@
1
+ export interface App {
2
+ name: string;
3
+ script: string;
4
+ }
1
5
  /**
2
6
  * Whether to ask at all.
3
7
  *
@@ -31,6 +35,32 @@ export declare function extractFilters(args: readonly string[]): {
31
35
  filters: string[];
32
36
  rest: string[];
33
37
  };
38
+ export interface DevPlan {
39
+ /** Apps whose dev server is `vinext dev`, started directly, one each. */
40
+ vinext: string[];
41
+ /** The `pnpm` args for every other selected app's `dev`, or null for none. */
42
+ others: string[] | null;
43
+ }
44
+ /**
45
+ * Splits a `dev` run between apps whose dev server is `vinext dev` and the
46
+ * rest.
47
+ *
48
+ * A vinext app's server code runs in workerd, which only sees what
49
+ * `@cloudflare/vite-plugin` hands it: the app dir's own `.dev.vars`/`.env`, or
50
+ * with `CLOUDFLARE_INCLUDE_PROCESS_ENV` the whole `process.env`. The repo's
51
+ * `.env` is at the root, so through the app's own `with-env vinext dev` script
52
+ * the Worker gets nothing and env validation fails. So a vinext app skips its
53
+ * script and runs `pnpm --filter <app> exec vinext dev` with the env
54
+ * `scopedProcessEnv` builds, the same way `workflows run` starts one: the
55
+ * flag set, and every other app's variables left out. Its script's own
56
+ * `with-env` would load the whole `.env` back in, so it is not used.
57
+ *
58
+ * Everything else still runs its own `dev` script under one `pnpm --parallel`.
59
+ * `apps` is every app with a `dev` script; `filters` the selection (empty for
60
+ * all of them). Only apps are named here: no other package has a `dev`
61
+ * script.
62
+ */
63
+ export declare function planDev(apps: readonly App[], filters: readonly string[], rest: readonly string[]): DevPlan;
34
64
  export interface TierArgResult {
35
65
  /** The validated tier, or absent when `--tier` was not passed. */
36
66
  tier?: string;
package/dist/run/pick.js CHANGED
@@ -59,7 +59,7 @@
59
59
  * typecheck/lint/test/dev run has no business doing (see `passthroughApps`'s
60
60
  * doc comment for why that is more than just wasted work).
61
61
  */
62
- import { spawnSync } from "node:child_process";
62
+ import { spawn, spawnSync } from "node:child_process";
63
63
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, } from "node:fs";
64
64
  import { dirname, join } from "node:path";
65
65
  import { cancel, confirm, isCancel, multiselect } from "@clack/prompts";
@@ -336,6 +336,111 @@ function passthroughApps(task, filters, rest, extraEnv) {
336
336
  ]);
337
337
  passthrough(commands, extraEnv);
338
338
  }
339
+ // ── dev on vinext ────────────────────────────────────────────────────────────
340
+ /** A `dev` script that starts `vinext dev` (platform's `with-env vinext dev`). */
341
+ const VINEXT_DEV = /(^|[\s;&|])vinext\s+dev\b/;
342
+ /**
343
+ * Splits a `dev` run between apps whose dev server is `vinext dev` and the
344
+ * rest.
345
+ *
346
+ * A vinext app's server code runs in workerd, which only sees what
347
+ * `@cloudflare/vite-plugin` hands it: the app dir's own `.dev.vars`/`.env`, or
348
+ * with `CLOUDFLARE_INCLUDE_PROCESS_ENV` the whole `process.env`. The repo's
349
+ * `.env` is at the root, so through the app's own `with-env vinext dev` script
350
+ * the Worker gets nothing and env validation fails. So a vinext app skips its
351
+ * script and runs `pnpm --filter <app> exec vinext dev` with the env
352
+ * `scopedProcessEnv` builds, the same way `workflows run` starts one: the
353
+ * flag set, and every other app's variables left out. Its script's own
354
+ * `with-env` would load the whole `.env` back in, so it is not used.
355
+ *
356
+ * Everything else still runs its own `dev` script under one `pnpm --parallel`.
357
+ * `apps` is every app with a `dev` script; `filters` the selection (empty for
358
+ * all of them). Only apps are named here: no other package has a `dev`
359
+ * script.
360
+ */
361
+ export function planDev(apps, filters, rest) {
362
+ const selected = filters.length > 0
363
+ ? apps.filter((app) => filters.includes(app.name))
364
+ : [...apps];
365
+ const vinext = selected
366
+ .filter((app) => VINEXT_DEV.test(app.script))
367
+ .map((app) => app.name);
368
+ const otherFilters = filters.length > 0
369
+ ? filters.filter((f) => !vinext.includes(f))
370
+ : apps.map((app) => app.name).filter((name) => !vinext.includes(name));
371
+ const others = otherFilters.length > 0
372
+ ? [
373
+ "-r",
374
+ "--if-present",
375
+ "--parallel",
376
+ ...otherFilters.flatMap((f) => ["--filter", f]),
377
+ "run",
378
+ "dev",
379
+ ...rest,
380
+ ]
381
+ : null;
382
+ return { vinext, others };
383
+ }
384
+ /**
385
+ * `dev` when at least one selected app runs `vinext dev`: builds every
386
+ * selected app's workspace dependencies, then starts the vinext apps (each
387
+ * with its own scoped env, see `planDev`) and the rest side by side, and
388
+ * exits when the first of them does, stopping the others.
389
+ *
390
+ * The env is the session's: `launch.ts` has already entered the tier into
391
+ * `process.env` (or `--tier` loaded one into `tierEnv`).
392
+ */
393
+ async function runDev(plan, filters, rest, tierEnv) {
394
+ const depsOfTargets = filters.length > 0 ? filters : allAppNames();
395
+ runOne([
396
+ "-r",
397
+ "--if-present",
398
+ ...depsOfTargets.flatMap((f) => ["--filter", `${f}^...`]),
399
+ "run",
400
+ "build",
401
+ ], tierEnv, { final: false });
402
+ // Dynamic, like `loadEnvLoad`: the env registry is no business of a plain
403
+ // `pnpm build`.
404
+ const { scopedProcessEnv } = await import("../cf/local-env.js");
405
+ const environment = { ...process.env, ...tierEnv };
406
+ const tier = environment.DEPLOY_ENV ?? "development";
407
+ const children = [
408
+ ...(await Promise.all(plan.vinext.map(async (app) => ({
409
+ args: ["--filter", app, "exec", "vinext", "dev", ...rest],
410
+ env: await scopedProcessEnv(app, environment, tier),
411
+ })))),
412
+ ...(plan.others ? [{ args: plan.others, env: environment }] : []),
413
+ ].map(({ args, env }) => spawn("pnpm", args, {
414
+ cwd: findRepoRoot(),
415
+ stdio: "inherit",
416
+ env: { ...env, DEVDOGS_PICK: "0" },
417
+ }));
418
+ // Ctrl-C reaches every child through the terminal's process group; this
419
+ // process waits for them to finish rather than dying first.
420
+ const ignore = () => { };
421
+ process.on("SIGINT", ignore);
422
+ process.on("SIGTERM", ignore);
423
+ const first = await Promise.race(children.map((child) => new Promise((resolve) => {
424
+ child.once("error", () => resolve(1));
425
+ child.once("exit", (code, signal) => resolve(code ?? (signal ? 130 : 1)));
426
+ })));
427
+ for (const child of children) {
428
+ if (child.exitCode === null && child.signalCode === null) {
429
+ child.kill("SIGTERM");
430
+ }
431
+ }
432
+ process.exit(first);
433
+ }
434
+ /** `passthroughApps`, except that a `dev` selecting a vinext app goes to
435
+ * `runDev`. */
436
+ async function dispatch(task, filters, rest, tierEnv) {
437
+ if (task === "dev") {
438
+ const plan = planDev(appsWith("dev"), filters, rest);
439
+ if (plan.vinext.length > 0)
440
+ return runDev(plan, filters, rest, tierEnv);
441
+ }
442
+ passthroughApps(task, filters, rest, tierEnv);
443
+ }
339
444
  // ── Tier ─────────────────────────────────────────────────────────────────────
340
445
  /** The tiers a loaded env can select. `preflight` is not a deploy environment
341
446
  * (see `@devdogsuga/env`'s target table) and is refused here for the same
@@ -513,13 +618,13 @@ export async function runTask(argv) {
513
618
  // to the underlying script rather than treating it as its own flag.
514
619
  if (all || !shouldAsk(rest)) {
515
620
  const { filters, rest: cleanRest } = extractFilters(rest);
516
- passthroughApps(task, filters, cleanRest, tierEnv);
621
+ return dispatch(task, filters, cleanRest, tierEnv);
517
622
  }
518
623
  const apps = appsWith(task);
519
624
  // Nothing to choose between: one app, or none that define this task (pnpm
520
625
  // will say so better than a picker with a single option would).
521
626
  if (apps.length < 2)
522
- passthroughApps(task, [], rest, tierEnv);
627
+ return dispatch(task, [], rest, tierEnv);
523
628
  const previous = remembered(task);
524
629
  const chosen = await multiselect({
525
630
  // The `--all` escape is named in the message rather than offered as an
@@ -555,5 +660,5 @@ export async function runTask(argv) {
555
660
  process.exit(0);
556
661
  }
557
662
  remember(task, chosen);
558
- passthroughApps(task, chosen, rest, tierEnv);
663
+ return dispatch(task, chosen, rest, tierEnv);
559
664
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devdogsuga/devtools",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Contributor CLI for a DevDogsUGA checkout: database, env, images, emails, cron, deploy. Ships built JS; run via `pnpm dlx @devdogsuga/devtools` (or the `devtools`/`devtools-ci` bins once installed) from inside a DevDogsUGA clone.",
@@ -67,9 +67,9 @@
67
67
  "react": "^19.2.8",
68
68
  "vitest": "^4.1.11",
69
69
  "@devdogsuga/brand": "0.1.2",
70
- "@devdogsuga/env": "0.1.2",
71
- "@devdogsuga/db": "0.1.1",
72
70
  "@devdogsuga/config": "0.1.1",
71
+ "@devdogsuga/db": "0.1.1",
72
+ "@devdogsuga/env": "0.1.2",
73
73
  "@devdogsuga/events": "0.1.3"
74
74
  },
75
75
  "scripts": {