@devdogsuga/devtools 0.1.0
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/README.md +44 -0
- package/bin/devtools-ci-bare.mjs +48 -0
- package/bin/devtools-ci.mjs +17 -0
- package/bin/devtools.mjs +27 -0
- package/dist/args.d.ts +26 -0
- package/dist/args.js +78 -0
- package/dist/bws/bw.d.ts +9 -0
- package/dist/bws/bw.js +49 -0
- package/dist/bws/client.d.ts +43 -0
- package/dist/bws/client.js +363 -0
- package/dist/bws/environments.d.ts +96 -0
- package/dist/bws/environments.js +111 -0
- package/dist/bws/pace.d.ts +27 -0
- package/dist/bws/pace.js +43 -0
- package/dist/bws/retry.d.ts +12 -0
- package/dist/bws/retry.js +39 -0
- package/dist/bws/store.d.ts +1 -0
- package/dist/bws/store.js +32 -0
- package/dist/bws/token.d.ts +31 -0
- package/dist/bws/token.js +88 -0
- package/dist/bws/vault.d.ts +29 -0
- package/dist/bws/vault.js +200 -0
- package/dist/catalog.d.ts +55 -0
- package/dist/catalog.js +81 -0
- package/dist/cf/commands.d.ts +1 -0
- package/dist/cf/commands.js +182 -0
- package/dist/cf/local-env.d.ts +44 -0
- package/dist/cf/local-env.js +148 -0
- package/dist/ci.d.ts +1 -0
- package/dist/ci.js +319 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.js +1077 -0
- package/dist/commands.d.ts +255 -0
- package/dist/commands.js +1279 -0
- package/dist/completions.d.ts +4 -0
- package/dist/completions.js +110 -0
- package/dist/cron/commands.d.ts +47 -0
- package/dist/cron/commands.js +372 -0
- package/dist/cron/discovery.d.ts +55 -0
- package/dist/cron/discovery.js +85 -0
- package/dist/cron/schema.d.ts +45 -0
- package/dist/cron/schema.js +35 -0
- package/dist/db/config-push.d.ts +8 -0
- package/dist/db/config-push.js +11 -0
- package/dist/db/connection.d.ts +79 -0
- package/dist/db/connection.js +114 -0
- package/dist/db/exec.d.ts +1 -0
- package/dist/db/exec.js +11 -0
- package/dist/db/generate-migration.d.ts +1 -0
- package/dist/db/generate-migration.js +67 -0
- package/dist/db/generate-types.d.ts +1 -0
- package/dist/db/generate-types.js +4 -0
- package/dist/db/introspect.d.ts +1 -0
- package/dist/db/introspect.js +113 -0
- package/dist/db/local-env.d.ts +65 -0
- package/dist/db/local-env.js +23 -0
- package/dist/db/new-migration.d.ts +1 -0
- package/dist/db/new-migration.js +8 -0
- package/dist/db/run.d.ts +70 -0
- package/dist/db/run.js +158 -0
- package/dist/db/seed-buckets.d.ts +2 -0
- package/dist/db/seed-buckets.js +4 -0
- package/dist/db/seed-production.d.ts +8 -0
- package/dist/db/seed-production.js +73 -0
- package/dist/db/seed-roles.d.ts +7 -0
- package/dist/db/seed-roles.js +31 -0
- package/dist/db/session-refresh.d.ts +43 -0
- package/dist/db/session-refresh.js +40 -0
- package/dist/deploy/migrations.d.ts +20 -0
- package/dist/deploy/migrations.js +68 -0
- package/dist/deploy/orphans.d.ts +20 -0
- package/dist/deploy/orphans.js +177 -0
- package/dist/deploy/preflight.d.ts +53 -0
- package/dist/deploy/preflight.js +189 -0
- package/dist/deploy/report.d.ts +26 -0
- package/dist/deploy/report.js +70 -0
- package/dist/deploy/require-planner.d.ts +2 -0
- package/dist/deploy/require-planner.js +60 -0
- package/dist/deploy/require-token.d.ts +15 -0
- package/dist/deploy/require-token.js +78 -0
- package/dist/deploy/secrets-file.d.ts +17 -0
- package/dist/deploy/secrets-file.js +167 -0
- package/dist/deploy/write-env.d.ts +40 -0
- package/dist/deploy/write-env.js +393 -0
- package/dist/docs/index-pages.d.ts +36 -0
- package/dist/docs/index-pages.js +184 -0
- package/dist/doctor.d.ts +62 -0
- package/dist/doctor.js +225 -0
- package/dist/emails/commands.d.ts +15 -0
- package/dist/emails/commands.js +130 -0
- package/dist/emails/email-types.d.ts +13 -0
- package/dist/emails/email-types.js +1 -0
- package/dist/emails/fixtures.d.ts +20 -0
- package/dist/emails/fixtures.js +20 -0
- package/dist/env/audit.d.ts +206 -0
- package/dist/env/audit.js +436 -0
- package/dist/env/cloudflare.d.ts +21 -0
- package/dist/env/cloudflare.js +61 -0
- package/dist/env/commands.d.ts +99 -0
- package/dist/env/commands.js +612 -0
- package/dist/env/discovery.d.ts +15 -0
- package/dist/env/discovery.js +226 -0
- package/dist/env/document.d.ts +119 -0
- package/dist/env/document.js +323 -0
- package/dist/env/example.d.ts +68 -0
- package/dist/env/example.js +603 -0
- package/dist/env/selection.d.ts +105 -0
- package/dist/env/selection.js +226 -0
- package/dist/environment.d.ts +92 -0
- package/dist/environment.js +206 -0
- package/dist/fingerprint.d.ts +10 -0
- package/dist/fingerprint.js +18 -0
- package/dist/gen/campus-map.d.ts +1 -0
- package/dist/gen/campus-map.js +473 -0
- package/dist/gen/commands.d.ts +1 -0
- package/dist/gen/commands.js +25 -0
- package/dist/gen/hypno.d.ts +1 -0
- package/dist/gen/hypno.js +134 -0
- package/dist/gh/client.d.ts +80 -0
- package/dist/gh/client.js +258 -0
- package/dist/gh/environments.d.ts +83 -0
- package/dist/gh/environments.js +178 -0
- package/dist/gh/rulesets/actors.d.ts +14 -0
- package/dist/gh/rulesets/actors.js +68 -0
- package/dist/gh/rulesets/api.d.ts +15 -0
- package/dist/gh/rulesets/api.js +99 -0
- package/dist/gh/rulesets/commands.d.ts +3 -0
- package/dist/gh/rulesets/commands.js +203 -0
- package/dist/gh/rulesets/desired.d.ts +112 -0
- package/dist/gh/rulesets/desired.js +247 -0
- package/dist/gh/rulesets/diff.d.ts +65 -0
- package/dist/gh/rulesets/diff.js +134 -0
- package/dist/gh/rulesets/types.d.ts +76 -0
- package/dist/gh/rulesets/types.js +13 -0
- package/dist/gh/settings/api.d.ts +40 -0
- package/dist/gh/settings/api.js +172 -0
- package/dist/gh/settings/commands.d.ts +1 -0
- package/dist/gh/settings/commands.js +245 -0
- package/dist/gh/settings/desired.d.ts +53 -0
- package/dist/gh/settings/desired.js +39 -0
- package/dist/gh/settings/diff.d.ts +57 -0
- package/dist/gh/settings/diff.js +156 -0
- package/dist/gh/settings/types.d.ts +92 -0
- package/dist/gh/settings/types.js +11 -0
- package/dist/gh/settings/workflows.d.ts +41 -0
- package/dist/gh/settings/workflows.js +124 -0
- package/dist/grantRoot.d.ts +42 -0
- package/dist/grantRoot.js +89 -0
- package/dist/help.d.ts +18 -0
- package/dist/help.js +169 -0
- package/dist/images/commands.d.ts +35 -0
- package/dist/images/commands.js +233 -0
- package/dist/images/events.d.ts +36 -0
- package/dist/images/events.js +152 -0
- package/dist/images/graphics.d.ts +70 -0
- package/dist/images/graphics.js +233 -0
- package/dist/images/open-graph-types.d.ts +75 -0
- package/dist/images/open-graph-types.js +1 -0
- package/dist/images/prompts.d.ts +34 -0
- package/dist/images/prompts.js +100 -0
- package/dist/images/render.d.ts +21 -0
- package/dist/images/render.js +57 -0
- package/dist/images/select.d.ts +64 -0
- package/dist/images/select.js +96 -0
- package/dist/instance.d.ts +67 -0
- package/dist/instance.js +129 -0
- package/dist/invocation.d.ts +49 -0
- package/dist/invocation.js +78 -0
- package/dist/launch-ci.d.ts +1 -0
- package/dist/launch-ci.js +86 -0
- package/dist/launch.d.ts +29 -0
- package/dist/launch.js +371 -0
- package/dist/menu.d.ts +31 -0
- package/dist/menu.js +277 -0
- package/dist/newsletter/commands.d.ts +16 -0
- package/dist/newsletter/commands.js +311 -0
- package/dist/newsletter/imap.d.ts +17 -0
- package/dist/newsletter/imap.js +127 -0
- package/dist/newsletter/loopback.d.ts +24 -0
- package/dist/newsletter/loopback.js +100 -0
- package/dist/newsletter/oauth.d.ts +26 -0
- package/dist/newsletter/oauth.js +130 -0
- package/dist/newsletter/smtp.d.ts +23 -0
- package/dist/newsletter/smtp.js +133 -0
- package/dist/oauth/browser.d.ts +1 -0
- package/dist/oauth/browser.js +13 -0
- package/dist/oauth/config.d.ts +24 -0
- package/dist/oauth/config.js +24 -0
- package/dist/oauth/connect-url.d.ts +17 -0
- package/dist/oauth/connect-url.js +23 -0
- package/dist/oauth/db.d.ts +37 -0
- package/dist/oauth/db.js +108 -0
- package/dist/oauth/device.d.ts +93 -0
- package/dist/oauth/device.js +190 -0
- package/dist/oauth/discovery.d.ts +28 -0
- package/dist/oauth/discovery.js +54 -0
- package/dist/oauth/env-file.d.ts +6 -0
- package/dist/oauth/env-file.js +35 -0
- package/dist/oauth/exchange.d.ts +43 -0
- package/dist/oauth/exchange.js +87 -0
- package/dist/oauth/label.d.ts +1 -0
- package/dist/oauth/label.js +25 -0
- package/dist/oauth/loopback.d.ts +52 -0
- package/dist/oauth/loopback.js +162 -0
- package/dist/oauth/pkce.d.ts +6 -0
- package/dist/oauth/pkce.js +34 -0
- package/dist/oauth/target.d.ts +54 -0
- package/dist/oauth/target.js +102 -0
- package/dist/oauth/transport.d.ts +41 -0
- package/dist/oauth/transport.js +32 -0
- package/dist/oauth/wizard.d.ts +33 -0
- package/dist/oauth/wizard.js +571 -0
- package/dist/pick.d.ts +6 -0
- package/dist/pick.js +64 -0
- package/dist/planner/checks.d.ts +26 -0
- package/dist/planner/checks.js +104 -0
- package/dist/planner/commands.d.ts +10 -0
- package/dist/planner/commands.js +367 -0
- package/dist/planner/db.d.ts +15 -0
- package/dist/planner/db.js +33 -0
- package/dist/planner/role.d.ts +33 -0
- package/dist/planner/role.js +68 -0
- package/dist/repo/peers.d.ts +24 -0
- package/dist/repo/peers.js +114 -0
- package/dist/repo/preflight.d.ts +109 -0
- package/dist/repo/preflight.js +266 -0
- package/dist/repo/resolve.d.ts +41 -0
- package/dist/repo/resolve.js +152 -0
- package/dist/repo/root.d.ts +23 -0
- package/dist/repo/root.js +91 -0
- package/dist/repo/source.d.ts +14 -0
- package/dist/repo/source.js +54 -0
- package/dist/repo/tsx-loader.d.ts +6 -0
- package/dist/repo/tsx-loader.js +41 -0
- package/dist/run/pick.d.ts +60 -0
- package/dist/run/pick.js +559 -0
- package/dist/setup.d.ts +1 -0
- package/dist/setup.js +152 -0
- package/dist/stack.d.ts +48 -0
- package/dist/stack.js +290 -0
- package/dist/telemetry.d.ts +34 -0
- package/dist/telemetry.js +93 -0
- package/dist/tier.d.ts +40 -0
- package/dist/tier.js +93 -0
- package/dist/tsconfig.typecheck.tsbuildinfo +1 -0
- package/dist/ui.d.ts +25 -0
- package/dist/ui.js +51 -0
- package/dist/workers.d.ts +5 -0
- package/dist/workers.js +35 -0
- package/dist/workflows/commands.d.ts +44 -0
- package/dist/workflows/commands.js +668 -0
- package/env.ts +158 -0
- package/package.json +80 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loads devtools' optional-peer `@devdogsuga/*` libraries — the ones
|
|
3
|
+
* published to npm and consumed by the TARGET repo, which devtools reads at
|
|
4
|
+
* runtime rather than bundling its own copy of (see this package's
|
|
5
|
+
* `package.json`: `peerDependencies` + `peerDependenciesMeta.optional`, and
|
|
6
|
+
* `FINDINGS.md` experiment 2).
|
|
7
|
+
*
|
|
8
|
+
* Every export here is memoized per module (one dynamic `import()` per
|
|
9
|
+
* process, not per call) — this is what makes the module-identity guarantee
|
|
10
|
+
* in FINDINGS experiment 3 hold: every devtools call site that needs
|
|
11
|
+
* `@devdogsuga/env`'s registry gets the exact same module instance the
|
|
12
|
+
* repo's own manifests populated via `declare()`/`define()`, because both
|
|
13
|
+
* resolve to the identical absolute file path and Node's ESM cache is keyed
|
|
14
|
+
* by resolved URL.
|
|
15
|
+
*/
|
|
16
|
+
import { pathToFileURL } from "node:url";
|
|
17
|
+
import { findRepoRoot, RepoNotFoundError } from "./root.js";
|
|
18
|
+
import { findDependent, resolveFromRepo } from "./resolve.js";
|
|
19
|
+
const moduleCache = new Map();
|
|
20
|
+
/**
|
|
21
|
+
* Dynamically imports `specifier` as the repo would resolve it, memoized so
|
|
22
|
+
* repeated calls in one process return the SAME module instance (required
|
|
23
|
+
* for the env-registry identity guarantee above).
|
|
24
|
+
*
|
|
25
|
+
* Falls back to a plain bare-specifier `import()` when devtools is not
|
|
26
|
+
* running inside a DevDogsUGA checkout at all (`findRepoRoot()` throws
|
|
27
|
+
* `RepoNotFoundError`). That is not a production affordance — under real
|
|
28
|
+
* `pnpm dlx` use, `specifier` is an optional peer devtools' own package
|
|
29
|
+
* never installs, so the fallback import fails with the ordinary
|
|
30
|
+
* `MODULE_NOT_FOUND` either way. It exists so devtools' OWN test suite
|
|
31
|
+
* (which has no target repo to discover, but does have every one of these
|
|
32
|
+
* peers as a real Backstage workspace package) exercises this code against
|
|
33
|
+
* the genuine package rather than needing every test to mock this module.
|
|
34
|
+
*/
|
|
35
|
+
function loadPeer(specifier) {
|
|
36
|
+
const cached = moduleCache.get(specifier);
|
|
37
|
+
if (cached)
|
|
38
|
+
return cached;
|
|
39
|
+
const promise = (async () => {
|
|
40
|
+
// See `findRepoRoot()`'s own comment: Backstage's test suite sets this
|
|
41
|
+
// so every peer resolves to the real Backstage-workspace copy instead
|
|
42
|
+
// of a nonexistent target repo, without a per-test mock.
|
|
43
|
+
if (process.env.DEVTOOLS_TEST_REPO_ROOT) {
|
|
44
|
+
return import(specifier);
|
|
45
|
+
}
|
|
46
|
+
let repoRoot;
|
|
47
|
+
try {
|
|
48
|
+
repoRoot = findRepoRoot();
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
if (err instanceof RepoNotFoundError) {
|
|
52
|
+
return import(specifier);
|
|
53
|
+
}
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
const resolutionBase = findDependent(repoRoot, specifier);
|
|
57
|
+
if (!resolutionBase) {
|
|
58
|
+
throw new Error(`Nothing in this repo depends on ${specifier} — expected an app or package under apps/*` +
|
|
59
|
+
` or packages/* to declare it (dependencies/devDependencies/peerDependencies).`);
|
|
60
|
+
}
|
|
61
|
+
const { resolvedPath } = resolveFromRepo(repoRoot, resolutionBase, specifier);
|
|
62
|
+
return import(pathToFileURL(resolvedPath).href);
|
|
63
|
+
})();
|
|
64
|
+
moduleCache.set(specifier, promise);
|
|
65
|
+
return promise;
|
|
66
|
+
}
|
|
67
|
+
/** Test-only: clears the memoized peer modules so a test can re-resolve with a fresh mock. */
|
|
68
|
+
export function resetPeerCacheForTests() {
|
|
69
|
+
moduleCache.clear();
|
|
70
|
+
}
|
|
71
|
+
let resolvedEnvModule;
|
|
72
|
+
export function loadEnv() {
|
|
73
|
+
return loadPeer("@devdogsuga/env").then((mod) => {
|
|
74
|
+
resolvedEnvModule = mod;
|
|
75
|
+
return mod;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The synchronous companion to `loadEnv()`, for the handful of call sites
|
|
80
|
+
* (`gh/environments.ts`'s `GITHUB_ENVIRONMENT_SPECS` getters) that cannot
|
|
81
|
+
* become async without changing their own callers' contract — they read a
|
|
82
|
+
* plain array off a getter, not a Promise. Safe ONLY after `loadEnv()` has
|
|
83
|
+
* resolved at least once; every caller of these getters already requires
|
|
84
|
+
* `env/discovery.ts`'s `loadRegistry()` (which calls `loadEnv()` itself) to
|
|
85
|
+
* have run first, via `assertRegistryLoaded()`'s own guard.
|
|
86
|
+
*/
|
|
87
|
+
export function getEnvSync() {
|
|
88
|
+
if (!resolvedEnvModule) {
|
|
89
|
+
throw new Error("getEnvSync() called before loadEnv() ever resolved — call loadRegistry() " +
|
|
90
|
+
"(env/discovery.ts) first.");
|
|
91
|
+
}
|
|
92
|
+
return resolvedEnvModule;
|
|
93
|
+
}
|
|
94
|
+
/** Test-only: clears the sync cache alongside `resetPeerCacheForTests()`. */
|
|
95
|
+
export function resetEnvSyncCacheForTests() {
|
|
96
|
+
resolvedEnvModule = undefined;
|
|
97
|
+
}
|
|
98
|
+
export function loadEnvLoad() {
|
|
99
|
+
return loadPeer("@devdogsuga/env/load");
|
|
100
|
+
}
|
|
101
|
+
export function loadEnvSession() {
|
|
102
|
+
return loadPeer("@devdogsuga/env/session");
|
|
103
|
+
}
|
|
104
|
+
export function loadDbTypegen() {
|
|
105
|
+
return loadPeer("@devdogsuga/db/typegen");
|
|
106
|
+
}
|
|
107
|
+
export function loadBrandEvent() {
|
|
108
|
+
return loadPeer("@devdogsuga/brand/event");
|
|
109
|
+
}
|
|
110
|
+
// `@devdogsuga/newsletter` used to be an optional peer here, read by
|
|
111
|
+
// devtools' own `newsletter` command. Wave 2, stage A2 moved that command to
|
|
112
|
+
// a plain Backstage script (`packages/newsletter-cli`) — see its README —
|
|
113
|
+
// since the content it exports lives in Backstage already; devtools has no
|
|
114
|
+
// reason to load it at all any more.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
declare const ManifestSchema: z.ZodObject<{
|
|
3
|
+
devtools: z.ZodObject<{
|
|
4
|
+
latest: z.ZodString;
|
|
5
|
+
minimum: z.ZodString;
|
|
6
|
+
}, z.core.$strip>;
|
|
7
|
+
}, z.core.$strip>;
|
|
8
|
+
export type DevtoolsMinimumsManifest = z.infer<typeof ManifestSchema>;
|
|
9
|
+
/** Exported so tests can point elsewhere without touching the real `~/.cache`. */
|
|
10
|
+
export declare function cacheFilePath(): string;
|
|
11
|
+
export interface ManifestResult {
|
|
12
|
+
manifest: DevtoolsMinimumsManifest | null;
|
|
13
|
+
source: "cache" | "network" | "fail-open";
|
|
14
|
+
error?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Fetches (or reads the disk cache for) the minimums manifest. Never
|
|
18
|
+
* throws: any failure — timeout, network error, non-200, invalid JSON,
|
|
19
|
+
* schema mismatch — comes back as `{ manifest: null, source: "fail-open" }`.
|
|
20
|
+
*/
|
|
21
|
+
export declare function fetchManifest(options?: {
|
|
22
|
+
url?: string;
|
|
23
|
+
skipCache?: boolean;
|
|
24
|
+
timeoutMs?: number;
|
|
25
|
+
}): Promise<ManifestResult>;
|
|
26
|
+
export type PreflightAction = {
|
|
27
|
+
action: "skip-preflight";
|
|
28
|
+
} | {
|
|
29
|
+
action: "dev-mode";
|
|
30
|
+
} | {
|
|
31
|
+
action: "fail-open";
|
|
32
|
+
reason: string;
|
|
33
|
+
} | {
|
|
34
|
+
action: "self-refresh";
|
|
35
|
+
from: string;
|
|
36
|
+
to: string;
|
|
37
|
+
source: ManifestResult["source"];
|
|
38
|
+
} | {
|
|
39
|
+
action: "loop-guard-tripped";
|
|
40
|
+
detail: string;
|
|
41
|
+
} | {
|
|
42
|
+
action: "nudge";
|
|
43
|
+
from: string;
|
|
44
|
+
to: string;
|
|
45
|
+
source: ManifestResult["source"];
|
|
46
|
+
} | {
|
|
47
|
+
action: "up-to-date";
|
|
48
|
+
source: ManifestResult["source"];
|
|
49
|
+
};
|
|
50
|
+
export interface PreflightOptions {
|
|
51
|
+
currentVersion: string;
|
|
52
|
+
argv: readonly string[];
|
|
53
|
+
skipPreflight: boolean;
|
|
54
|
+
refresh: boolean;
|
|
55
|
+
registry?: string;
|
|
56
|
+
manifestUrl?: string;
|
|
57
|
+
devMode?: boolean;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Runs the decision half of preflight. Does not itself re-exec or print —
|
|
61
|
+
* `runPreflight` below does both, so the pure decision stays independently
|
|
62
|
+
* testable (loop guard, fail-open, version comparisons) without spawning a
|
|
63
|
+
* real child process in every test.
|
|
64
|
+
*/
|
|
65
|
+
export declare function decidePreflight(options: PreflightOptions): Promise<PreflightAction>;
|
|
66
|
+
/**
|
|
67
|
+
* Re-execs as the exact pinned `version`, inheriting stdio and propagating
|
|
68
|
+
* the child's exit code. Never returns — the caller relies on this to end
|
|
69
|
+
* the process, matching `spawnSync`'s synchronous nature (there is nothing
|
|
70
|
+
* meaningful left to do in this process either way).
|
|
71
|
+
*
|
|
72
|
+
* Exported separately from `runPreflight` so tests can stub it instead of
|
|
73
|
+
* spawning a real `pnpm dlx`.
|
|
74
|
+
*/
|
|
75
|
+
export declare function selfRefresh(options: {
|
|
76
|
+
version: string;
|
|
77
|
+
argv: readonly string[];
|
|
78
|
+
registry?: string;
|
|
79
|
+
}): never;
|
|
80
|
+
/**
|
|
81
|
+
* The full preflight: decides, prints the single line each outcome calls
|
|
82
|
+
* for (or nothing, for the common `up-to-date`/`skip-preflight`/`dev-mode`
|
|
83
|
+
* cases — see each branch), and re-execs on `self-refresh`. Returns once
|
|
84
|
+
* for every other outcome so `launch()` can continue dispatching the
|
|
85
|
+
* original command.
|
|
86
|
+
*/
|
|
87
|
+
export declare function runPreflight(options: PreflightOptions & {
|
|
88
|
+
reexec?: typeof selfRefresh;
|
|
89
|
+
}): Promise<PreflightAction>;
|
|
90
|
+
/** The directory this module's compiled file lives in one level under —
|
|
91
|
+
* i.e. the devtools package's own root (`dist/repo/preflight.js` → up two).
|
|
92
|
+
* Exported for tests; memoized since it never changes within a process. */
|
|
93
|
+
export declare function ownPackageDir(): string;
|
|
94
|
+
/** Reads this build's own `package.json` — the version preflight compares
|
|
95
|
+
* against the manifest, and the value dev-mode detection inspects. */
|
|
96
|
+
export declare function ownVersion(): string;
|
|
97
|
+
/**
|
|
98
|
+
* True when this devtools is running from a workspace/dev checkout rather
|
|
99
|
+
* than a published install — a `pnpm build && node ./bin/devtools.mjs` run
|
|
100
|
+
* inside `Backstage/packages/devtools` itself, or any other case where the
|
|
101
|
+
* package's own directory is not sitting under a `node_modules` tree (a
|
|
102
|
+
* real install — via dlx's isolated cache or an ordinary `node_modules` —
|
|
103
|
+
* always puts it under one). `version: "0.0.0-dev"` is the explicit,
|
|
104
|
+
* unambiguous override for anything that wants to force this (a fixture,
|
|
105
|
+
* an integration test); the path heuristic is the sensible default so
|
|
106
|
+
* nobody has to remember to set it.
|
|
107
|
+
*/
|
|
108
|
+
export declare function isDevMode(version?: string, dir?: string): boolean;
|
|
109
|
+
export {};
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dlx-distribution preflight: fetches a tiny minimums manifest from
|
|
3
|
+
* DevDogsUGA's `main` branch, compares it against this build's own version,
|
|
4
|
+
* and decides whether to proceed, nudge, or self-refresh by re-exec'ing
|
|
5
|
+
* `pnpm dlx @devdogsuga/devtools@<latest>`.
|
|
6
|
+
*
|
|
7
|
+
* Ported from the `devtools-dlx` prototype
|
|
8
|
+
* (`/home/sloan/scratchpad/devdogs/prototypes/devtools-dlx/FINDINGS.md`,
|
|
9
|
+
* experiment 7 and its "Recommended contract" section) — read that file for
|
|
10
|
+
* why each design choice below is the one that survived testing rather than
|
|
11
|
+
* the first thing that seemed reasonable:
|
|
12
|
+
*
|
|
13
|
+
* - The re-exec spec MUST be an exact version (`@devdogsuga/devtools@0.4.2`),
|
|
14
|
+
* never a bare spec. Experiment 6 found TWO independent staleness
|
|
15
|
+
* mechanisms a bare spec (even with `dlx-cache-max-age=0`) does not
|
|
16
|
+
* reliably bypass: pnpm's dlx cache TTL, and pnpm 11's
|
|
17
|
+
* `minimumReleaseAge` cooldown on a just-published version. Only an
|
|
18
|
+
* exact pinned spec bypasses both.
|
|
19
|
+
* - `--config.registry=<url>`, not `--registry` after `dlx` — pnpm 11
|
|
20
|
+
* rejects the latter positioned there.
|
|
21
|
+
* - The manifest fetch fails open (log + proceed on the current version)
|
|
22
|
+
* on ANY error — dead host, timeout, malformed JSON, schema mismatch —
|
|
23
|
+
* so an offline contributor is never blocked by this check.
|
|
24
|
+
*
|
|
25
|
+
* `devtools-ci` (`launch-ci.ts`) never calls this module — CI pins an exact
|
|
26
|
+
* version on purpose (see the carve-out plan's §9 Wave 2 decisions), and a
|
|
27
|
+
* self-refreshing CI job would defeat that pin.
|
|
28
|
+
*/
|
|
29
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
30
|
+
import { spawnSync } from "node:child_process";
|
|
31
|
+
import { homedir } from "node:os";
|
|
32
|
+
import { dirname, join, sep } from "node:path";
|
|
33
|
+
import { fileURLToPath } from "node:url";
|
|
34
|
+
import { z } from "zod";
|
|
35
|
+
/** Env var set on the re-exec'd child so a still-below-minimum child refuses
|
|
36
|
+
* to loop a second time instead of re-exec'ing forever. */
|
|
37
|
+
const LOOP_GUARD_ENV = "DEVTOOLS_REEXEC_GUARD";
|
|
38
|
+
/** Default location of the manifest — a repo-root file in DevDogsUGA's own
|
|
39
|
+
* `main` branch, not buried inside a package, so it survives reorganization
|
|
40
|
+
* and is a trivial hotfix-PR edit. Overridable for tests and for use before
|
|
41
|
+
* the file exists on the real remote. */
|
|
42
|
+
const DEFAULT_MINIMUMS_URL = "https://raw.githubusercontent.com/DevDogsUGA/DevDogsUGA/main/devtools-minimums.json";
|
|
43
|
+
const FETCH_TIMEOUT_MS = 1500;
|
|
44
|
+
/**
|
|
45
|
+
* Deliberately short — 10 minutes, not the plan's original "a few hours" —
|
|
46
|
+
* so a mid-meeting hotfix (someone drops the `minimum` to force a
|
|
47
|
+
* self-refresh) propagates to every contributor's next command within one
|
|
48
|
+
* coffee break, not one workday. This is the one place stage A2 knowingly
|
|
49
|
+
* overrides §5's original wording; see the carve-out plan's §9 note.
|
|
50
|
+
*/
|
|
51
|
+
const CACHE_TTL_MS = 10 * 60 * 1000;
|
|
52
|
+
const ManifestSchema = z.object({
|
|
53
|
+
devtools: z.object({
|
|
54
|
+
latest: z.string().min(1),
|
|
55
|
+
minimum: z.string().min(1),
|
|
56
|
+
}),
|
|
57
|
+
});
|
|
58
|
+
function xdgCacheHome() {
|
|
59
|
+
const fromEnv = process.env.XDG_CACHE_HOME;
|
|
60
|
+
return fromEnv !== undefined && fromEnv !== "" ? fromEnv : join(homedir(), ".cache");
|
|
61
|
+
}
|
|
62
|
+
/** Exported so tests can point elsewhere without touching the real `~/.cache`. */
|
|
63
|
+
export function cacheFilePath() {
|
|
64
|
+
return join(xdgCacheHome(), "devdogsuga-devtools", "minimums.json");
|
|
65
|
+
}
|
|
66
|
+
function readCache(url) {
|
|
67
|
+
const file = cacheFilePath();
|
|
68
|
+
if (!existsSync(file))
|
|
69
|
+
return null;
|
|
70
|
+
try {
|
|
71
|
+
const entry = JSON.parse(readFileSync(file, "utf8"));
|
|
72
|
+
if (entry.url !== url)
|
|
73
|
+
return null;
|
|
74
|
+
if (Date.now() - entry.fetchedAt > CACHE_TTL_MS)
|
|
75
|
+
return null;
|
|
76
|
+
const parsed = ManifestSchema.safeParse(entry.manifest);
|
|
77
|
+
return parsed.success ? parsed.data : null;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function writeCache(url, manifest) {
|
|
84
|
+
try {
|
|
85
|
+
const file = cacheFilePath();
|
|
86
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
87
|
+
const entry = { fetchedAt: Date.now(), url, manifest };
|
|
88
|
+
writeFileSync(file, JSON.stringify(entry));
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// Best-effort. A cache write failure (read-only home dir, full disk,
|
|
92
|
+
// whatever) must never fail the command it is only trying to speed up
|
|
93
|
+
// next time.
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Fetches (or reads the disk cache for) the minimums manifest. Never
|
|
98
|
+
* throws: any failure — timeout, network error, non-200, invalid JSON,
|
|
99
|
+
* schema mismatch — comes back as `{ manifest: null, source: "fail-open" }`.
|
|
100
|
+
*/
|
|
101
|
+
export async function fetchManifest(options) {
|
|
102
|
+
const url = options?.url ?? process.env.DEVTOOLS_MINIMUMS_URL ?? DEFAULT_MINIMUMS_URL;
|
|
103
|
+
const timeoutMs = options?.timeoutMs ?? FETCH_TIMEOUT_MS;
|
|
104
|
+
if (options?.skipCache !== true) {
|
|
105
|
+
const cached = readCache(url);
|
|
106
|
+
if (cached)
|
|
107
|
+
return { manifest: cached, source: "cache" };
|
|
108
|
+
}
|
|
109
|
+
const controller = new AbortController();
|
|
110
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
111
|
+
try {
|
|
112
|
+
const res = await fetch(url, { signal: controller.signal });
|
|
113
|
+
if (!res.ok)
|
|
114
|
+
throw new Error(`manifest fetch: HTTP ${res.status}`);
|
|
115
|
+
const json = await res.json();
|
|
116
|
+
const parsed = ManifestSchema.safeParse(json);
|
|
117
|
+
if (!parsed.success) {
|
|
118
|
+
throw new Error(`manifest fetch: invalid shape (${parsed.error.message})`);
|
|
119
|
+
}
|
|
120
|
+
writeCache(url, parsed.data);
|
|
121
|
+
return { manifest: parsed.data, source: "network" };
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
return { manifest: null, source: "fail-open", error: errorText(err) };
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
clearTimeout(timer);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function errorText(err) {
|
|
131
|
+
return err instanceof Error ? err.message : String(err);
|
|
132
|
+
}
|
|
133
|
+
/** Three-part semver comparison; `<0` means `a` is older than `b`. Anything
|
|
134
|
+
* non-numeric in a segment sorts as `0`, which is permissive on purpose —
|
|
135
|
+
* this only ever compares two well-formed manifest/package versions. */
|
|
136
|
+
function compareVersions(a, b) {
|
|
137
|
+
const pa = a.split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
138
|
+
const pb = b.split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
139
|
+
for (let i = 0; i < 3; i++) {
|
|
140
|
+
const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
141
|
+
if (diff !== 0)
|
|
142
|
+
return diff;
|
|
143
|
+
}
|
|
144
|
+
return 0;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Runs the decision half of preflight. Does not itself re-exec or print —
|
|
148
|
+
* `runPreflight` below does both, so the pure decision stays independently
|
|
149
|
+
* testable (loop guard, fail-open, version comparisons) without spawning a
|
|
150
|
+
* real child process in every test.
|
|
151
|
+
*/
|
|
152
|
+
export async function decidePreflight(options) {
|
|
153
|
+
if (options.skipPreflight)
|
|
154
|
+
return { action: "skip-preflight" };
|
|
155
|
+
if (options.devMode === true)
|
|
156
|
+
return { action: "dev-mode" };
|
|
157
|
+
const { manifest, source, error } = await fetchManifest({
|
|
158
|
+
url: options.manifestUrl,
|
|
159
|
+
skipCache: options.refresh,
|
|
160
|
+
});
|
|
161
|
+
if (!manifest) {
|
|
162
|
+
return { action: "fail-open", reason: error ?? "unknown error" };
|
|
163
|
+
}
|
|
164
|
+
const { latest, minimum } = manifest.devtools;
|
|
165
|
+
if (compareVersions(options.currentVersion, minimum) < 0) {
|
|
166
|
+
if (process.env[LOOP_GUARD_ENV]) {
|
|
167
|
+
return {
|
|
168
|
+
action: "loop-guard-tripped",
|
|
169
|
+
detail: `already re-exec'd once (${LOOP_GUARD_ENV}=${process.env[LOOP_GUARD_ENV]}); refusing to loop again`,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return { action: "self-refresh", from: options.currentVersion, to: latest, source };
|
|
173
|
+
}
|
|
174
|
+
if (compareVersions(options.currentVersion, latest) < 0) {
|
|
175
|
+
return { action: "nudge", from: options.currentVersion, to: latest, source };
|
|
176
|
+
}
|
|
177
|
+
return { action: "up-to-date", source };
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Re-execs as the exact pinned `version`, inheriting stdio and propagating
|
|
181
|
+
* the child's exit code. Never returns — the caller relies on this to end
|
|
182
|
+
* the process, matching `spawnSync`'s synchronous nature (there is nothing
|
|
183
|
+
* meaningful left to do in this process either way).
|
|
184
|
+
*
|
|
185
|
+
* Exported separately from `runPreflight` so tests can stub it instead of
|
|
186
|
+
* spawning a real `pnpm dlx`.
|
|
187
|
+
*/
|
|
188
|
+
export function selfRefresh(options) {
|
|
189
|
+
const spec = `@devdogsuga/devtools@${options.version}`;
|
|
190
|
+
const registryArgs = options.registry !== undefined ? [`--config.registry=${options.registry}`] : [];
|
|
191
|
+
const result = spawnSync("pnpm", ["dlx", ...registryArgs, spec, ...options.argv], {
|
|
192
|
+
stdio: "inherit",
|
|
193
|
+
env: { ...process.env, [LOOP_GUARD_ENV]: "1" },
|
|
194
|
+
});
|
|
195
|
+
process.exit(result.status ?? 1);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* The full preflight: decides, prints the single line each outcome calls
|
|
199
|
+
* for (or nothing, for the common `up-to-date`/`skip-preflight`/`dev-mode`
|
|
200
|
+
* cases — see each branch), and re-execs on `self-refresh`. Returns once
|
|
201
|
+
* for every other outcome so `launch()` can continue dispatching the
|
|
202
|
+
* original command.
|
|
203
|
+
*/
|
|
204
|
+
export async function runPreflight(options) {
|
|
205
|
+
const decision = await decidePreflight(options);
|
|
206
|
+
const reexec = options.reexec ?? selfRefresh;
|
|
207
|
+
switch (decision.action) {
|
|
208
|
+
case "skip-preflight":
|
|
209
|
+
case "dev-mode":
|
|
210
|
+
case "up-to-date":
|
|
211
|
+
// Silent — see the module header: this runs on every command, and a
|
|
212
|
+
// line of noise on the overwhelmingly common "nothing to do" path
|
|
213
|
+
// would defeat the point of the check running unconditionally.
|
|
214
|
+
break;
|
|
215
|
+
case "fail-open":
|
|
216
|
+
process.stderr.write(`devtools: preflight check skipped (${decision.reason})\n`);
|
|
217
|
+
break;
|
|
218
|
+
case "loop-guard-tripped":
|
|
219
|
+
process.stderr.write(`devtools: still below the minimum version after one self-refresh attempt; ` +
|
|
220
|
+
`continuing on the current version instead of looping (${decision.detail})\n`);
|
|
221
|
+
break;
|
|
222
|
+
case "nudge":
|
|
223
|
+
process.stderr.write(`devtools: a newer version (${decision.to}) is available — you're on ${decision.from}. ` +
|
|
224
|
+
`Run with --refresh, or it'll catch up on its own shortly.\n`);
|
|
225
|
+
break;
|
|
226
|
+
case "self-refresh":
|
|
227
|
+
process.stderr.write(`devtools: ${decision.from} is below the minimum supported version — ` +
|
|
228
|
+
`relaunching as ${decision.to}...\n`);
|
|
229
|
+
reexec({ version: decision.to, argv: options.argv, registry: options.registry });
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
return decision;
|
|
233
|
+
}
|
|
234
|
+
let cachedOwnDir;
|
|
235
|
+
/** The directory this module's compiled file lives in one level under —
|
|
236
|
+
* i.e. the devtools package's own root (`dist/repo/preflight.js` → up two).
|
|
237
|
+
* Exported for tests; memoized since it never changes within a process. */
|
|
238
|
+
export function ownPackageDir() {
|
|
239
|
+
if (cachedOwnDir === undefined) {
|
|
240
|
+
cachedOwnDir = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
241
|
+
}
|
|
242
|
+
return cachedOwnDir;
|
|
243
|
+
}
|
|
244
|
+
/** Reads this build's own `package.json` — the version preflight compares
|
|
245
|
+
* against the manifest, and the value dev-mode detection inspects. */
|
|
246
|
+
export function ownVersion() {
|
|
247
|
+
const pkg = JSON.parse(readFileSync(join(ownPackageDir(), "package.json"), "utf8"));
|
|
248
|
+
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* True when this devtools is running from a workspace/dev checkout rather
|
|
252
|
+
* than a published install — a `pnpm build && node ./bin/devtools.mjs` run
|
|
253
|
+
* inside `Backstage/packages/devtools` itself, or any other case where the
|
|
254
|
+
* package's own directory is not sitting under a `node_modules` tree (a
|
|
255
|
+
* real install — via dlx's isolated cache or an ordinary `node_modules` —
|
|
256
|
+
* always puts it under one). `version: "0.0.0-dev"` is the explicit,
|
|
257
|
+
* unambiguous override for anything that wants to force this (a fixture,
|
|
258
|
+
* an integration test); the path heuristic is the sensible default so
|
|
259
|
+
* nobody has to remember to set it.
|
|
260
|
+
*/
|
|
261
|
+
export function isDevMode(version = ownVersion(), dir = ownPackageDir()) {
|
|
262
|
+
if (version === "0.0.0-dev")
|
|
263
|
+
return true;
|
|
264
|
+
const marker = `${sep}node_modules${sep}`;
|
|
265
|
+
return !dir.includes(marker);
|
|
266
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface ResolvedFromRepo {
|
|
2
|
+
/** Absolute path to the resolved module entry point. */
|
|
3
|
+
resolvedPath: string;
|
|
4
|
+
/** The version declared in the resolved package's own `package.json`. */
|
|
5
|
+
version: string;
|
|
6
|
+
/** Absolute path to the resolved package's `package.json`. */
|
|
7
|
+
pkgJsonPath: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The installable package name a (possibly subpath) specifier belongs to:
|
|
11
|
+
* `@devdogsuga/env/load` → `@devdogsuga/env`, `foo/bar` → `foo`. A repo's
|
|
12
|
+
* package.json declares the PACKAGE as a dependency, never a subpath, so
|
|
13
|
+
* `findDependent` has to search on this rather than the literal specifier a
|
|
14
|
+
* caller wants to import.
|
|
15
|
+
*/
|
|
16
|
+
export declare function packageNameOf(specifier: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Scans `apps/*` and `packages/*` under `repoRoot` for the first
|
|
19
|
+
* `package.json` whose `dependencies`/`devDependencies`/`peerDependencies`
|
|
20
|
+
* names `specifier`'s package (see `packageNameOf`). Returns its path
|
|
21
|
+
* relative to `repoRoot`, or `null` if nothing in the repo depends on it.
|
|
22
|
+
*
|
|
23
|
+
* Re-scans the filesystem on every call — fine for a short-lived CLI
|
|
24
|
+
* invocation calling this a handful of times; see FINDINGS item 5 if this
|
|
25
|
+
* is ever called in a hot loop.
|
|
26
|
+
*/
|
|
27
|
+
export declare function findDependent(repoRoot: string, specifier: string): string | null;
|
|
28
|
+
/**
|
|
29
|
+
* Resolves `specifier` as `resolutionBase` (a workspace-relative
|
|
30
|
+
* `package.json` path known to depend on it, from `findDependent()`) would
|
|
31
|
+
* see it.
|
|
32
|
+
*
|
|
33
|
+
* `opts.condition`, when given, is NOT passed through to Node's resolver —
|
|
34
|
+
* `require.resolve` ignores tsx's `register({ conditions })` entirely (see
|
|
35
|
+
* this module's header). Instead the target package's own
|
|
36
|
+
* `exports["."]` map is read directly and `exports["."][condition]` is
|
|
37
|
+
* picked, falling back to `.default`.
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolveFromRepo(repoRoot: string, resolutionBase: string, specifier: string, opts?: {
|
|
40
|
+
condition?: string;
|
|
41
|
+
}): ResolvedFromRepo;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves an `@devdogsuga/*` package FROM the target repo, not from
|
|
3
|
+
* devtools' own (dlx-isolated) `node_modules`.
|
|
4
|
+
*
|
|
5
|
+
* Ported from the `devtools-dlx` prototype
|
|
6
|
+
* (`/home/sloan/scratchpad/devdogs/prototypes/devtools-dlx/FINDINGS.md`,
|
|
7
|
+
* experiments 2 and 4 — read that file for the two gotchas this module
|
|
8
|
+
* exists to avoid). The short version:
|
|
9
|
+
*
|
|
10
|
+
* - `require.resolve(\`${specifier}/package.json\`)` throws
|
|
11
|
+
* `ERR_PACKAGE_PATH_NOT_EXPORTED` on packages (like `@devdogsuga/env`,
|
|
12
|
+
* `@devdogsuga/open-graph`) whose `exports` map does not list
|
|
13
|
+
* `"./package.json"`, even though the file is on disk.
|
|
14
|
+
* - A resolved path cannot be assumed to contain a literal
|
|
15
|
+
* `node_modules/<specifier>` segment — pnpm workspace-linked packages
|
|
16
|
+
* resolve to their REALPATH (e.g. `packages/open-graph/dist/index.js`),
|
|
17
|
+
* which has no such segment at all.
|
|
18
|
+
* - `require.resolve` ignores whatever `conditions` a caller has set on
|
|
19
|
+
* `tsx`'s `register()` — condition-based resolution (the
|
|
20
|
+
* `devdogs-source` condition) has to be done by hand, reading the
|
|
21
|
+
* target package's own `exports["."]` map.
|
|
22
|
+
*
|
|
23
|
+
* Both problems are solved the same way: walk up from the resolved file's
|
|
24
|
+
* directory to the nearest `package.json` whose own `"name"` field matches
|
|
25
|
+
* the specifier. That works for a real installed dependency AND a
|
|
26
|
+
* workspace symlink, uniformly.
|
|
27
|
+
*/
|
|
28
|
+
import { createRequire } from "node:module";
|
|
29
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
30
|
+
import { dirname, join } from "node:path";
|
|
31
|
+
/**
|
|
32
|
+
* The installable package name a (possibly subpath) specifier belongs to:
|
|
33
|
+
* `@devdogsuga/env/load` → `@devdogsuga/env`, `foo/bar` → `foo`. A repo's
|
|
34
|
+
* package.json declares the PACKAGE as a dependency, never a subpath, so
|
|
35
|
+
* `findDependent` has to search on this rather than the literal specifier a
|
|
36
|
+
* caller wants to import.
|
|
37
|
+
*/
|
|
38
|
+
export function packageNameOf(specifier) {
|
|
39
|
+
const parts = specifier.split("/");
|
|
40
|
+
return specifier.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Scans `apps/*` and `packages/*` under `repoRoot` for the first
|
|
44
|
+
* `package.json` whose `dependencies`/`devDependencies`/`peerDependencies`
|
|
45
|
+
* names `specifier`'s package (see `packageNameOf`). Returns its path
|
|
46
|
+
* relative to `repoRoot`, or `null` if nothing in the repo depends on it.
|
|
47
|
+
*
|
|
48
|
+
* Re-scans the filesystem on every call — fine for a short-lived CLI
|
|
49
|
+
* invocation calling this a handful of times; see FINDINGS item 5 if this
|
|
50
|
+
* is ever called in a hot loop.
|
|
51
|
+
*/
|
|
52
|
+
export function findDependent(repoRoot, specifier) {
|
|
53
|
+
const packageName = packageNameOf(specifier);
|
|
54
|
+
const groups = ["apps", "packages"];
|
|
55
|
+
for (const group of groups) {
|
|
56
|
+
const groupDir = join(repoRoot, group);
|
|
57
|
+
let entries;
|
|
58
|
+
try {
|
|
59
|
+
entries = readdirSync(groupDir);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
for (const entry of entries) {
|
|
65
|
+
const pkgJsonPath = join(groupDir, entry, "package.json");
|
|
66
|
+
if (!existsSync(pkgJsonPath))
|
|
67
|
+
continue;
|
|
68
|
+
let pkg;
|
|
69
|
+
try {
|
|
70
|
+
pkg = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const deps = {
|
|
76
|
+
...pkg.dependencies,
|
|
77
|
+
...pkg.devDependencies,
|
|
78
|
+
...pkg.peerDependencies,
|
|
79
|
+
};
|
|
80
|
+
if (packageName in deps) {
|
|
81
|
+
return join(group, entry, "package.json");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
/** Walks up from `startDir` to the nearest `package.json` whose `"name"` matches `specifier`. */
|
|
88
|
+
function findOwningPackageJson(startDir, specifier) {
|
|
89
|
+
const packageName = packageNameOf(specifier);
|
|
90
|
+
let dir = startDir;
|
|
91
|
+
for (;;) {
|
|
92
|
+
const candidate = join(dir, "package.json");
|
|
93
|
+
if (existsSync(candidate)) {
|
|
94
|
+
try {
|
|
95
|
+
const pkg = JSON.parse(readFileSync(candidate, "utf8"));
|
|
96
|
+
if (pkg.name === packageName)
|
|
97
|
+
return candidate;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// fall through and keep walking
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const parent = dirname(dir);
|
|
104
|
+
if (parent === dir) {
|
|
105
|
+
throw new Error(`resolveFromRepo: could not find ${specifier}'s own package.json by walking up from ${startDir}`);
|
|
106
|
+
}
|
|
107
|
+
dir = parent;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Resolves `specifier` as `resolutionBase` (a workspace-relative
|
|
112
|
+
* `package.json` path known to depend on it, from `findDependent()`) would
|
|
113
|
+
* see it.
|
|
114
|
+
*
|
|
115
|
+
* `opts.condition`, when given, is NOT passed through to Node's resolver —
|
|
116
|
+
* `require.resolve` ignores tsx's `register({ conditions })` entirely (see
|
|
117
|
+
* this module's header). Instead the target package's own
|
|
118
|
+
* `exports["."]` map is read directly and `exports["."][condition]` is
|
|
119
|
+
* picked, falling back to `.default`.
|
|
120
|
+
*/
|
|
121
|
+
export function resolveFromRepo(repoRoot, resolutionBase, specifier, opts) {
|
|
122
|
+
const baseFile = join(repoRoot, resolutionBase);
|
|
123
|
+
const require = createRequire(baseFile);
|
|
124
|
+
let resolvedPath;
|
|
125
|
+
if (opts?.condition) {
|
|
126
|
+
// Find the package's own package.json first (via a plain resolve of the
|
|
127
|
+
// specifier itself, which DOES work through node's default conditions),
|
|
128
|
+
// then read its exports map by hand.
|
|
129
|
+
const defaultEntry = require.resolve(specifier);
|
|
130
|
+
const pkgJsonPath = findOwningPackageJson(dirname(defaultEntry), specifier);
|
|
131
|
+
const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
|
|
132
|
+
const rootExport = pkg.exports?.["."];
|
|
133
|
+
const conditioned = rootExport && typeof rootExport === "object"
|
|
134
|
+
? (rootExport[opts.condition] ??
|
|
135
|
+
rootExport.default)
|
|
136
|
+
: undefined;
|
|
137
|
+
if (typeof conditioned !== "string") {
|
|
138
|
+
throw new Error(`resolveFromRepo: ${specifier} has no "${opts.condition}" (or "default") export in its exports["."] map`);
|
|
139
|
+
}
|
|
140
|
+
resolvedPath = join(dirname(pkgJsonPath), conditioned);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
resolvedPath = require.resolve(specifier);
|
|
144
|
+
}
|
|
145
|
+
const pkgJsonPath = findOwningPackageJson(dirname(resolvedPath), specifier);
|
|
146
|
+
const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
|
|
147
|
+
return {
|
|
148
|
+
resolvedPath,
|
|
149
|
+
version: pkg.version ?? "0.0.0",
|
|
150
|
+
pkgJsonPath,
|
|
151
|
+
};
|
|
152
|
+
}
|