@pithy-sh/cli 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/LICENSE +21 -0
- package/package.json +72 -0
- package/scripts/templateManifest.ts +49 -0
- package/scripts/tsconfig.json +26 -0
- package/scripts/vendorTemplate.ts +84 -0
- package/scripts/verifyPack.ts +88 -0
- package/src/audit/cliAudit.ts +406 -0
- package/src/bin.ts +111 -0
- package/src/capabilities/add.ts +288 -0
- package/src/capabilities/addBootstrap.ts +275 -0
- package/src/capabilities/catalog.ts +175 -0
- package/src/capabilities/compose.ts +39 -0
- package/src/capabilities/configConstants.ts +74 -0
- package/src/capabilities/configImports.ts +397 -0
- package/src/capabilities/eject.ts +331 -0
- package/src/capabilities/emailProvisioner.ts +346 -0
- package/src/capabilities/entitlementGap.ts +70 -0
- package/src/capabilities/entryExports.ts +162 -0
- package/src/capabilities/flow.ts +550 -0
- package/src/capabilities/hostRegistry.ts +368 -0
- package/src/capabilities/loadFailure.ts +208 -0
- package/src/capabilities/manifests.ts +238 -0
- package/src/capabilities/mediaProvisioner.ts +471 -0
- package/src/capabilities/mintSecrets.ts +306 -0
- package/src/capabilities/paymentsProvisioner.ts +207 -0
- package/src/capabilities/prerequisites.ts +168 -0
- package/src/capabilities/r2Bucket.ts +113 -0
- package/src/capabilities/reconcile.ts +1483 -0
- package/src/capabilities/remove.ts +597 -0
- package/src/capabilities/requiredOptions.ts +92 -0
- package/src/capabilities/rotateSecrets.ts +305 -0
- package/src/capabilities/secrets.ts +178 -0
- package/src/capabilities/secretsDispatcher.ts +29 -0
- package/src/capabilities/secretsProvisioner.ts +389 -0
- package/src/capabilities/storageProvisioner.ts +414 -0
- package/src/capabilities/supportProvisioner.ts +515 -0
- package/src/capabilities/testersLoader.ts +52 -0
- package/src/capabilities/testersProvisioner.ts +236 -0
- package/src/capabilities/turnstileProvisioner.ts +347 -0
- package/src/capabilities/vectorProvisioner.ts +260 -0
- package/src/ci/fileModes.ts +223 -0
- package/src/ci/sourceFiles.ts +200 -0
- package/src/ci/workflowDrivers.ts +524 -0
- package/src/cloudflare/accountAnswer.ts +110 -0
- package/src/cloudflare/config.ts +685 -0
- package/src/cloudflare/storeId.ts +129 -0
- package/src/commands/add.ts +372 -0
- package/src/commands/alias.ts +205 -0
- package/src/commands/dashboard.ts +651 -0
- package/src/commands/deploy.ts +150 -0
- package/src/commands/dev.ts +37 -0
- package/src/commands/doctor.ts +2059 -0
- package/src/commands/email.ts +425 -0
- package/src/commands/env.ts +155 -0
- package/src/commands/feature.ts +359 -0
- package/src/commands/init.ts +538 -0
- package/src/commands/media.ts +303 -0
- package/src/commands/migrate.ts +129 -0
- package/src/commands/payments.ts +336 -0
- package/src/commands/provision.ts +368 -0
- package/src/commands/remove.ts +151 -0
- package/src/commands/secrets.ts +652 -0
- package/src/commands/seed.ts +229 -0
- package/src/commands/storage.ts +309 -0
- package/src/commands/support.ts +331 -0
- package/src/commands/testers.ts +1020 -0
- package/src/commands/token.ts +364 -0
- package/src/commands/turnstile.ts +271 -0
- package/src/commands/ui.ts +222 -0
- package/src/commands/upgrade.ts +517 -0
- package/src/commands/vector.ts +390 -0
- package/src/commands/worker.ts +295 -0
- package/src/dashboard/api.ts +323 -0
- package/src/dashboard/connect.ts +758 -0
- package/src/dashboard/contract.ts +289 -0
- package/src/dashboard/grant.ts +124 -0
- package/src/dashboard/registry.ts +519 -0
- package/src/dashboard/resolveTarget.ts +119 -0
- package/src/dev/delivery.ts +174 -0
- package/src/dev/devLogin.ts +155 -0
- package/src/dev/devLoginTargets.ts +91 -0
- package/src/dev/env.ts +206 -0
- package/src/dev/hostWorkers.ts +290 -0
- package/src/dev/keys.ts +111 -0
- package/src/dev/logging.ts +87 -0
- package/src/dev/openUrl.ts +75 -0
- package/src/dev/orchestrator.ts +1014 -0
- package/src/dev/ports.ts +220 -0
- package/src/dev/readyWatch.ts +142 -0
- package/src/dev/state.ts +90 -0
- package/src/devSecrets/bootstrapVars.ts +265 -0
- package/src/devSecrets/devVars.ts +240 -0
- package/src/devSecrets/edit.ts +256 -0
- package/src/devSecrets/file.ts +277 -0
- package/src/devSecrets/generate.ts +428 -0
- package/src/devSecrets/location.ts +80 -0
- package/src/devSecrets/mode.ts +71 -0
- package/src/devSecrets/records.ts +30 -0
- package/src/devSecrets/report.ts +99 -0
- package/src/devSecrets/seed.ts +344 -0
- package/src/devSecrets/store.ts +262 -0
- package/src/devSecrets/targets.ts +204 -0
- package/src/dispatch.ts +147 -0
- package/src/docs/catalog.ts +246 -0
- package/src/docs/writeCatalog.ts +45 -0
- package/src/doctor/cloudflare.ts +287 -0
- package/src/doctor/devPreferences.ts +155 -0
- package/src/doctor/devSecrets.ts +464 -0
- package/src/doctor/devVars.ts +414 -0
- package/src/doctor/devVarsLocal.ts +138 -0
- package/src/doctor/environments.ts +155 -0
- package/src/doctor/health.ts +354 -0
- package/src/doctor/localDelivery.ts +91 -0
- package/src/doctor/portsRegistry.ts +252 -0
- package/src/doctor/projectName.ts +584 -0
- package/src/doctor/secretBindings.ts +166 -0
- package/src/doctor/settings.ts +274 -0
- package/src/doctor/settingsSources.ts +202 -0
- package/src/doctor/workerName.ts +174 -0
- package/src/doctor/wranglerVars.ts +33 -0
- package/src/feature/bindings.ts +93 -0
- package/src/feature/create.ts +179 -0
- package/src/feature/destroy.ts +160 -0
- package/src/feature/devConfig.ts +201 -0
- package/src/feature/identity.ts +100 -0
- package/src/feature/manifest.ts +132 -0
- package/src/feature/ports.ts +615 -0
- package/src/feature/provision.ts +362 -0
- package/src/feature/sync.ts +148 -0
- package/src/feature/worktree.ts +282 -0
- package/src/help/groups.ts +47 -0
- package/src/help/rootUsage.ts +135 -0
- package/src/main.ts +73 -0
- package/src/migrations/ledger.ts +129 -0
- package/src/migrations/registry.ts +47 -0
- package/src/migrations/run.ts +1066 -0
- package/src/notifier/check.ts +129 -0
- package/src/notifier/installer.ts +48 -0
- package/src/notifier/notify.ts +152 -0
- package/src/notifier/state.ts +248 -0
- package/src/notifier/version.ts +59 -0
- package/src/platform/editor.ts +333 -0
- package/src/platform/rc.ts +118 -0
- package/src/platform/shell.ts +83 -0
- package/src/project/appBindings.ts +184 -0
- package/src/project/appWorkflows.ts +266 -0
- package/src/project/applyDomains.ts +166 -0
- package/src/project/askDomains.ts +220 -0
- package/src/project/atomic.ts +466 -0
- package/src/project/bindingEntries.ts +425 -0
- package/src/project/config.ts +701 -0
- package/src/project/dashboard.ts +118 -0
- package/src/project/deploy.ts +364 -0
- package/src/project/devVars.ts +113 -0
- package/src/project/domainPrompt.ts +191 -0
- package/src/project/domains.ts +386 -0
- package/src/project/envInventory.ts +356 -0
- package/src/project/environment.ts +125 -0
- package/src/project/extensions.ts +69 -0
- package/src/project/jsonc.ts +289 -0
- package/src/project/packageManager.ts +238 -0
- package/src/project/readOptionalFile.ts +342 -0
- package/src/project/rollback.ts +145 -0
- package/src/project/scaffold.ts +1088 -0
- package/src/project/templateFiles.ts +53 -0
- package/src/project/verifyDeploy.ts +230 -0
- package/src/project/versionMetadata.ts +77 -0
- package/src/project/workerAddress.ts +176 -0
- package/src/project/workerCommand.ts +564 -0
- package/src/project/workerIdentity.ts +50 -0
- package/src/project/workerManifest.ts +135 -0
- package/src/project/workerScaffold.ts +289 -0
- package/src/project/workerScope.ts +394 -0
- package/src/project/workers.ts +86 -0
- package/src/project/workflows.ts +281 -0
- package/src/project/wrangler.ts +168 -0
- package/src/provision/confirm.ts +86 -0
- package/src/provision/environment.ts +407 -0
- package/src/provision/featureConfig.ts +98 -0
- package/src/provision/mode.ts +62 -0
- package/src/provision/pendingSecrets.ts +96 -0
- package/src/provision/resources.ts +126 -0
- package/src/provision/secretBindings.ts +149 -0
- package/src/provision/store.ts +33 -0
- package/src/provision/unprovisioned.ts +114 -0
- package/src/provision/wranglerEnv.ts +220 -0
- package/src/rootFlags.ts +48 -0
- package/src/seed/drivers.ts +423 -0
- package/src/seed/media.ts +187 -0
- package/src/seed/plan.ts +137 -0
- package/src/seed/prepare.ts +224 -0
- package/src/seed/registry.ts +25 -0
- package/src/seed/run.ts +793 -0
- package/src/seed/safety.ts +206 -0
- package/src/terminal/logger.ts +42 -0
- package/src/terminal/output.ts +64 -0
- package/src/terminal/style.ts +132 -0
- package/src/test-utils/doctorHarness.ts +190 -0
- package/src/test-utils/migrateHarness.ts +126 -0
- package/src/test-utils/seedHarness.ts +173 -0
- package/src/test-utils/tempRepo.ts +45 -0
- package/src/tokens/config.ts +16 -0
- package/src/tokens/engine.ts +345 -0
- package/src/tokens/mintedTokens.ts +233 -0
- package/src/tokens/sinks.ts +84 -0
- package/src/ui/flow.ts +451 -0
- package/src/ui/react.ts +112 -0
- package/src/ui/routeAllowlist.ts +208 -0
- package/src/ui/scaffold.ts +113 -0
- package/src/ui/screenStyles.ts +127 -0
- package/src/ui/stubs.ts +135 -0
- package/src/ui/templates.ts +52 -0
- package/src/ui/wire.ts +311 -0
- package/src/ui/workerUi.ts +172 -0
- package/templates/starter/.dev.secrets.example.jsonc +43 -0
- package/templates/starter/.dev.vars.example +30 -0
- package/templates/starter/apps/api/package.json +22 -0
- package/templates/starter/apps/api/pithy.config.ts +65 -0
- package/templates/starter/apps/api/pithy.worker.jsonc +11 -0
- package/templates/starter/apps/api/src/bindings.workers.test.ts +18 -0
- package/templates/starter/apps/api/src/cloudflare-test.d.ts +11 -0
- package/templates/starter/apps/api/src/index.ts +8 -0
- package/templates/starter/apps/api/tsconfig.json +26 -0
- package/templates/starter/apps/api/wrangler.jsonc +68 -0
- package/templates/starter/biome.template.jsonc +75 -0
- package/templates/starter/gitignore +37 -0
- package/templates/starter/package.json +28 -0
- package/templates/starter/pithy.config.ts +67 -0
- package/templates/starter/plugins/no-console.grit +25 -0
- package/templates/starter/plugins/no-process-io.grit +25 -0
- package/templates/starter/tsconfig.json +14 -0
- package/templates/starter/tsconfig.tools.json +30 -0
- package/templates/starter/vitest.config.ts +124 -0
- package/templates/starter/vitest.workers.config.ts +26 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import { join, relative, sep } from "node:path";
|
|
5
|
+
import type { Capability } from "@pithy-sh/core/src/capability/capability";
|
|
6
|
+
import { gateCallSites } from "@pithy-sh/core/src/entitlement/gateScan";
|
|
7
|
+
import { readSource, sourcePaths } from "../ci/sourceFiles";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The entitlement composition check — the CLI half of the entitlement seam.
|
|
11
|
+
*
|
|
12
|
+
* The seam fails closed: with no provider composed, `c.var.entitlements` holds nothing and every
|
|
13
|
+
* `requireEntitlement()` denies. That is the right runtime behavior and the wrong developer
|
|
14
|
+
* experience, because the runtime cannot tell the two cases apart. A legitimately unentitled user and
|
|
15
|
+
* a Worker that forgot to compose `payments` produce the identical 403, so the second one ships,
|
|
16
|
+
* paywalls every paid route shut, and is diagnosed from support tickets.
|
|
17
|
+
*
|
|
18
|
+
* So the mistake is caught where it is still cheap: `pithy doctor` and `pithy dev` compare the gates in
|
|
19
|
+
* a Worker's own source against whether any capability it composes declares
|
|
20
|
+
* {@link Capability.providesEntitlements}. Runtime denial stays the backstop; this is the check.
|
|
21
|
+
*
|
|
22
|
+
* What is left here is the filesystem half: which files to read, and which to leave alone. Deciding
|
|
23
|
+
* whether a given source gates is `@pithy-sh/core/src/entitlement/gateScan` — pure, and importable from a
|
|
24
|
+
* Workers-typed program, which is the half an adopter asserting their own rules needs.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** Source extensions a Worker's routes can live in. `.tsx` is included — a route file may render. */
|
|
28
|
+
const SOURCE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mts", ".mjs"];
|
|
29
|
+
|
|
30
|
+
/** A file a Worker's routes could live in, by base name. */
|
|
31
|
+
function isWorkerSource(name: string): boolean {
|
|
32
|
+
return SOURCE_EXTENSIONS.some((extension) => name.endsWith(extension));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The Worker's own source files that gate a route on an entitlement, relative to the Worker directory
|
|
37
|
+
* and sorted. Only `src/` is scanned: `node_modules` holds the seam's own source, so scanning it would
|
|
38
|
+
* report a gap on every project that installed `@pithy-sh/core`.
|
|
39
|
+
*
|
|
40
|
+
* The traversal is `ci/sourceFiles.ts` — the one walk in this repository, which skips dependencies, build
|
|
41
|
+
* output and dotted directories, never descends a symlink, and treats a directory it cannot list and a file
|
|
42
|
+
* that vanished between the listing and the read as skipped rather than fatal. This is the one of the five
|
|
43
|
+
* private walks #202 found that was not a test: it runs inside `pithy doctor` and `pithy dev`, against a
|
|
44
|
+
* directory the adopter is editing while it runs, so a throw here is a command that fails on a file the
|
|
45
|
+
* adopter had just moved. Synchronous, like the walk it now shares; the signature stays async because the
|
|
46
|
+
* scan it feeds is the caller's contract.
|
|
47
|
+
*/
|
|
48
|
+
export async function entitlementGates(workerDir: string): Promise<string[]> {
|
|
49
|
+
const sources: Record<string, string> = {};
|
|
50
|
+
for (const path of sourcePaths(join(workerDir, "src"), { keep: isWorkerSource })) {
|
|
51
|
+
const text = readSource(path);
|
|
52
|
+
if (text !== null) sources[relative(workerDir, path).split(sep).join("/")] = text;
|
|
53
|
+
}
|
|
54
|
+
return gateCallSites(sources);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The composed capability that fills the entitlement seam, by name, or null when none does. */
|
|
58
|
+
export function entitlementProvider(capabilities: readonly Capability[]): string | null {
|
|
59
|
+
return capabilities.find((capability) => capability.providesEntitlements === true)?.name ?? null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The gap: the Worker's gating source files when nothing it composes provides entitlements. Empty
|
|
64
|
+
* otherwise — including when there are no gates at all, which is most projects, and when a provider is
|
|
65
|
+
* composed but nothing gates yet, which is a project mid-build rather than a mistake.
|
|
66
|
+
*/
|
|
67
|
+
export async function findEntitlementGap(workerDir: string, capabilities: readonly Capability[]): Promise<string[]> {
|
|
68
|
+
if (entitlementProvider(capabilities) !== null) return [];
|
|
69
|
+
return entitlementGates(workerDir);
|
|
70
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import type { BindingSpec } from "@pithy-sh/core/src/capability/bindings";
|
|
5
|
+
import { exportsName, findNamedBinding, namedReexports, withoutBinding } from "./configImports";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* **A Durable Object binding is two halves, and this is the one that lives in the adopter's code.**
|
|
9
|
+
*
|
|
10
|
+
* `pithy add` writes the config half — a `durable_objects.bindings` entry naming a `class_name`, and a
|
|
11
|
+
* `new_sqlite_classes` migration tag registering that class against the script. Neither says where the
|
|
12
|
+
* class *is*. wrangler resolves `class_name` against the module `main` names, and refuses the deploy
|
|
13
|
+
* when nothing there exports it:
|
|
14
|
+
*
|
|
15
|
+
* Your Worker depends on the following Durable Objects, which are not exported in your entrypoint
|
|
16
|
+
* file: MultiplayerSession.
|
|
17
|
+
*
|
|
18
|
+
* The scaffolded entry is `export default createEntrypoint(config);` and nothing else — `createEntrypoint`
|
|
19
|
+
* returns a value, and a value cannot add a named export to the module holding it. So the export was left
|
|
20
|
+
* to a human whose only prompt was a line of manifest prose, and `pithy add multiplayer` produced a
|
|
21
|
+
* project that did not deploy (#428).
|
|
22
|
+
*
|
|
23
|
+
* The class and the module it comes from are both the capability's to state (`BindingSpec.className` and
|
|
24
|
+
* `BindingSpec.classModule`), so the line is derived rather than known here: nothing in this file names a
|
|
25
|
+
* capability, a class, or a package.
|
|
26
|
+
*
|
|
27
|
+
* **Idempotent, and keyed on the name.** An export the adopter wrote themselves satisfies wrangler
|
|
28
|
+
* exactly as ours does, and a second statement exporting the same name is a duplicate export the build
|
|
29
|
+
* refuses — so a class already on the module is left alone, whoever put it there, in any spelling that
|
|
30
|
+
* survives to runtime. Taking one *out* asks the second question the specifier answers: only an export
|
|
31
|
+
* pointing at the capability's own code is ours to remove — its package, or the `capabilities/<cap>` fork
|
|
32
|
+
* `--eject` repoints it into — which is the rule `pithy remove` reads a config's imports by, and is the
|
|
33
|
+
* caller's to answer rather than this file's.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/** One Durable Object class an entry has to export, and the module it comes from. */
|
|
37
|
+
export interface DurableObjectExport {
|
|
38
|
+
/** The exported class — what `class_name` in `wrangler.jsonc` resolves against the entry. */
|
|
39
|
+
readonly className: string;
|
|
40
|
+
/** The module specifier the class is exported from, verbatim from the binding's `classModule`. */
|
|
41
|
+
readonly module: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The note the written block sits under, so the next person to open the entry knows what the lines are
|
|
46
|
+
* and why deleting one breaks a deploy rather than a feature.
|
|
47
|
+
*/
|
|
48
|
+
const NOTE = "// Durable Object classes this Worker binds. wrangler resolves each class_name against this module.";
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Every Durable Object export a set of bindings calls for, in declaration order, one line per class.
|
|
52
|
+
*
|
|
53
|
+
* Keyed on the binding **kind**, never on the presence of a `className`: a `workflow` binding carries one
|
|
54
|
+
* too, and its class runs in the capability's own host Worker rather than in the adopter's entry. A
|
|
55
|
+
* capability declaring two bindings backed by one class (a second namespace over the same actor) gets one
|
|
56
|
+
* export — two would be a duplicate the build refuses.
|
|
57
|
+
*/
|
|
58
|
+
export function durableObjectExports(bindings: readonly BindingSpec[]): DurableObjectExport[] {
|
|
59
|
+
const found = new Map<string, DurableObjectExport>();
|
|
60
|
+
for (const binding of bindings) {
|
|
61
|
+
if (binding.type !== "durable_object") continue;
|
|
62
|
+
if (binding.className === undefined || binding.classModule === undefined) continue;
|
|
63
|
+
if (!found.has(binding.className)) {
|
|
64
|
+
found.set(binding.className, { className: binding.className, module: binding.classModule });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return [...found.values()];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The statement itself — the one place this line is spelled. */
|
|
71
|
+
function render(entry: DurableObjectExport): string {
|
|
72
|
+
return `export { ${entry.className} } from "${entry.module}";`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Whether the module already puts this class on itself, in any spelling that survives to runtime.
|
|
77
|
+
*
|
|
78
|
+
* Every spelling, not only the one this file writes. It used to ask `findNamedBinding` — which reads
|
|
79
|
+
* `export { X } from "…"` and nothing else — so an adopter who declared their own `export class
|
|
80
|
+
* MultiplayerSession {}` in the entry got ours appended beside it, and a duplicate export the build
|
|
81
|
+
* refuses before wrangler is ever asked.
|
|
82
|
+
*/
|
|
83
|
+
function alreadyExported(source: string, entry: DurableObjectExport): boolean {
|
|
84
|
+
return exportsName(source, entry.className);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The source with every missing Durable Object export appended.
|
|
89
|
+
*
|
|
90
|
+
* Appended rather than inserted: an entry is the adopter's file, and the end of it is the one position
|
|
91
|
+
* that cannot land inside something they wrote. The block reads as one because the note is written once
|
|
92
|
+
* — a second capability's class joins under the same one.
|
|
93
|
+
*/
|
|
94
|
+
export function withDurableObjectExports(source: string, exports: readonly DurableObjectExport[]): string {
|
|
95
|
+
// Deduplicated by class name on the way in, not only by {@link durableObjectExports} on the way out.
|
|
96
|
+
// That one dedupes within a capability; `pithy upgrade` hands this every composed capability's classes
|
|
97
|
+
// at once, and two statements exporting one name is the duplicate export this whole function avoids.
|
|
98
|
+
const byName = new Map<string, DurableObjectExport>();
|
|
99
|
+
for (const entry of exports) if (!byName.has(entry.className)) byName.set(entry.className, entry);
|
|
100
|
+
const missing = [...byName.values()].filter((entry) => !alreadyExported(source, entry));
|
|
101
|
+
if (missing.length === 0) return source;
|
|
102
|
+
|
|
103
|
+
const body = source.endsWith("\n") || source === "" ? source : `${source}\n`;
|
|
104
|
+
// The blank line and the note open the block, once. A later capability's class appends straight onto
|
|
105
|
+
// the last line of it, so the block stays contiguous — which is what lets `remove` tell a note with
|
|
106
|
+
// something still under it from a note with nothing.
|
|
107
|
+
const note = body.includes(NOTE) ? "" : `\n${NOTE}\n`;
|
|
108
|
+
return `${body}${note}${missing.map(render).join("\n")}\n`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The source with the Durable Object exports **this capability's modules** put there taken out — the
|
|
113
|
+
* inverse `pithy remove` needs, since a re-export of a package about to be uninstalled is a Worker that
|
|
114
|
+
* no longer builds.
|
|
115
|
+
*
|
|
116
|
+
* An export of the same class from somewhere else is the adopter's: the name identifies the statement and
|
|
117
|
+
* the specifier decides what to do about it, exactly as `remove` reads an import. The note goes when the
|
|
118
|
+
* last of our lines does, so an entry that never had a Durable Object is byte-identical to how it started.
|
|
119
|
+
*
|
|
120
|
+
* **`ours` is the caller's question, not this file's.** The line `pithy add` writes points at the
|
|
121
|
+
* capability's package and the one `pithy add --eject` leaves points into `capabilities/<cap>`, and both
|
|
122
|
+
* are the capability's — a fact about how a capability is wired, which `capabilities/remove.ts` already
|
|
123
|
+
* answers for the config's import through `isCapabilityImport`. Nothing here names a capability, so it
|
|
124
|
+
* takes the predicate rather than growing a second answer.
|
|
125
|
+
*/
|
|
126
|
+
export function withoutDurableObjectExports(
|
|
127
|
+
source: string,
|
|
128
|
+
exports: readonly DurableObjectExport[],
|
|
129
|
+
ours: (specifier: string) => boolean,
|
|
130
|
+
): string {
|
|
131
|
+
let rest = source;
|
|
132
|
+
for (const entry of exports) {
|
|
133
|
+
for (;;) {
|
|
134
|
+
const found = findNamedBinding(rest, entry.className, "export");
|
|
135
|
+
if (!found || !ours(found.specifier)) break;
|
|
136
|
+
const next = withoutBinding(rest, found);
|
|
137
|
+
if (next === rest) break; // nothing was taken out; never spin on it
|
|
138
|
+
rest = next;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (!rest.includes(NOTE)) return rest;
|
|
142
|
+
|
|
143
|
+
// The note with none of the block left under it. **What counts as "under it" is a re-export and
|
|
144
|
+
// nothing else.** A sibling capability's class keeps the note; anything else does not. This asked
|
|
145
|
+
// whether the next line was *blank*, which reads the block as the last thing in the file — and the
|
|
146
|
+
// block is appended at the end, so a line written after it is the expected case: multiplayer's own
|
|
147
|
+
// scaffold step tells the adopter to call `registerGameModel(myGame)` in the entry. With one there,
|
|
148
|
+
// `pithy remove` left the note standing over code it has nothing to do with.
|
|
149
|
+
//
|
|
150
|
+
// Decided by the same scanner every other reader in this pair uses, so a commented-out export under
|
|
151
|
+
// the note is not an export — the fact `withDurableObjectExports` already turns on.
|
|
152
|
+
const lines = rest.split("\n");
|
|
153
|
+
const at = lines.findIndex((line) => line.trim() === NOTE);
|
|
154
|
+
if (at === -1) return rest;
|
|
155
|
+
const under = lines.slice(0, at + 1).reduce((offset, line) => offset + line.length + 1, 0);
|
|
156
|
+
if (namedReexports(rest).some((found) => found.start === under)) return rest;
|
|
157
|
+
// Its own line goes, and the blank line that separated the block from the entry above it — otherwise
|
|
158
|
+
// `remove` leaves a trailing gap `add` never wrote.
|
|
159
|
+
lines.splice(at, 1);
|
|
160
|
+
if ((lines[at - 1] ?? "").trim() === "") lines.splice(at - 1, 1);
|
|
161
|
+
return lines.join("\n");
|
|
162
|
+
}
|