A packaged application installs an Official on-demand bundle as `name@<running DSH version>`, and the plugin manager asks a custom registry alone, so the unsigned test DMG cannot install a locally built provider before its release reaches npm: each packed manifest still names the rest of the repository by version, and ordinary bundle tarballs therefore resolve against a registry that does not have them yet. `pnpm run dev:bundle-registry` validates the Official catalog and the workspace inventory, builds the official artifacts, packs the catalog's complete local dependency closure with the release families' own payload and manifest checks, records the run in `dist/test-bundles/<run-id>/bundle-registry.json` as its completion marker, and only then serves it from 127.0.0.1 on an allocated port under a fresh namespace. Repository-owned names are answered from this run or refused locally; third-party packages keep npm's real metadata and bytes through a fixed redirect. The run publishes nothing, changes no global npm/pnpm configuration, and leaves DMG packaging untouched. The packed installer rehearsal now installs that closure into an isolated profile with no `file:` override, checkout link, ignore list, or scoped registry standing in for a package the registry should answer, and it asserts a transitive repository package served only by this run.
172 lines
6.9 KiB
TypeScript
172 lines
6.9 KiB
TypeScript
/**
|
|
* Build, pack, and serve the Official on-demand bundles' local dependency
|
|
* closure as a loopback registry, so a packaged test app can install them
|
|
* before their release reaches npm.
|
|
*
|
|
* The run is a build/test utility, not a Harness application launcher: it
|
|
* starts no agent and needs no credentials. It shares only the artifact
|
|
* transport with the release path; packing, manifests, and validation stay with
|
|
* the owners that already perform them.
|
|
*/
|
|
|
|
import { mkdirSync } from 'node:fs'
|
|
import { randomUUID } from 'node:crypto'
|
|
import { join, resolve } from 'node:path'
|
|
import { execa } from 'execa'
|
|
import { getDshRuntimeVersion } from '../packages/boot/app-boot/src/plugin-compatibility.ts'
|
|
import { generateOfficialBundleCatalog } from './gen-official-bundle-catalog.ts'
|
|
import {
|
|
REGISTRY_MANIFEST_FILE,
|
|
REGISTRY_OUTPUT_ROOT,
|
|
createRegistryManifest,
|
|
hostPlatform,
|
|
packBundleArtifacts,
|
|
pnpmInvocationFor,
|
|
readReleaseMembers,
|
|
readWorkspacePackages,
|
|
selectBundleDependencies,
|
|
sourceEvidence,
|
|
startBundleRegistry,
|
|
verifyBuiltBundles,
|
|
verifyReleaseVersions,
|
|
writeRegistryManifest,
|
|
type BundleArtifact,
|
|
} from './local-bundle-registry.ts'
|
|
|
|
/** The preparation step a signal stopped, so the failure is not reported as a command's. */
|
|
class Interrupted extends Error {
|
|
/**
|
|
* @param stage - the step that was running when the signal arrived.
|
|
*/
|
|
constructor(readonly stage: string) {
|
|
super(`interrupted during ${stage}`)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Run one package script in the repository root, propagating its exit status.
|
|
* @param root - repository root the command runs in.
|
|
* @param args - pnpm arguments.
|
|
* @param signal - abort signal that stops the child on SIGINT/SIGTERM.
|
|
* @param label - step name used in the failure message.
|
|
*/
|
|
async function runStep(root: string, args: readonly string[], signal: AbortSignal, label: string): Promise<void> {
|
|
const invocation = pnpmInvocationFor(args)
|
|
let result
|
|
try {
|
|
result = await execa(invocation.command, invocation.args, {
|
|
cwd: root, stdio: 'inherit', reject: false, killDescendants: true, cancelSignal: signal,
|
|
})
|
|
} catch (error) {
|
|
if (signal.aborted) throw new Interrupted(label)
|
|
throw error
|
|
}
|
|
if (result.isCanceled || signal.aborted) throw new Interrupted(label)
|
|
if (result.exitCode !== 0) throw new Error(`${label} exited with ${String(result.exitCode ?? result.signal)}`)
|
|
}
|
|
|
|
/**
|
|
* Print the installation summary the developer copies from.
|
|
* @param options - run directory, registry URL, packed artifacts, source evidence, and foreign-target members.
|
|
*/
|
|
function printSummary(options: {
|
|
readonly directory: string
|
|
readonly url: string
|
|
readonly artifacts: readonly BundleArtifact[]
|
|
readonly source: ReturnType<typeof sourceEvidence>
|
|
readonly foreignTargets: readonly string[]
|
|
}): void {
|
|
process.stdout.write(`Source: ${options.source.commit}${options.source.dirty ? ' (dirty worktree)' : ''}, DSH ${options.source.dshVersion}\n`)
|
|
if (options.foreignTargets.length > 0) {
|
|
process.stdout.write(`Other platforms: ${options.foreignTargets.join(', ')} (served; pnpm skips them on this host)\n`)
|
|
}
|
|
process.stdout.write(`Artifacts: ${options.directory}\n`)
|
|
process.stdout.write(`Registry: ${options.url}\n`)
|
|
process.stdout.write('Packages:\n')
|
|
for (const artifact of options.artifacts) process.stdout.write(` ${artifact.name}@${artifact.version}\n`)
|
|
process.stdout.write('In Plugins > Add plugin, use an exact package above and this custom registry URL.\n')
|
|
process.stdout.write('Keep this process running during package operations. Ctrl+C stops the registry.\n')
|
|
}
|
|
|
|
/** Build one run directory name: sortable by time and unique per invocation. */
|
|
function runId(): string {
|
|
return `${new Date().toISOString().replace(/[:.]/gu, '-')}-${randomUUID().slice(0, 8)}`
|
|
}
|
|
|
|
/** Build, pack, and serve the local bundle closure until a signal stops the run. */
|
|
async function main(): Promise<void> {
|
|
const root = resolve(import.meta.dirname, '..')
|
|
if (!process.features.typescript) {
|
|
throw new Error('dev:bundle-registry: Node.js TypeScript type stripping is unavailable; remove --no-experimental-strip-types from NODE_OPTIONS')
|
|
}
|
|
const abort = new AbortController()
|
|
// A persistent listener, so a second signal during shutdown cannot fall back to the default kill;
|
|
// the second signal is an explicit forced exit instead.
|
|
let stopping = false
|
|
const onSignal = (signal: NodeJS.Signals): void => {
|
|
if (stopping) {
|
|
process.exit(130)
|
|
return
|
|
}
|
|
stopping = true
|
|
abort.abort(signal)
|
|
}
|
|
process.on('SIGINT', onSignal)
|
|
process.on('SIGTERM', onSignal)
|
|
|
|
let registry
|
|
try {
|
|
// 1. The catalog is the only root list; the inventory is how a local package is distinguished from an external one.
|
|
const entries = generateOfficialBundleCatalog(root).map(entry => entry.packageName)
|
|
const packages = readWorkspacePackages(root)
|
|
const selection = selectBundleDependencies({ entries, packages, platform: hostPlatform() })
|
|
const release = readReleaseMembers(root)
|
|
verifyReleaseVersions(root)
|
|
|
|
// 2. Current official artifacts, so the run never serves an earlier build.
|
|
await runStep(root, ['run', 'build:official'], abort.signal, 'build:official')
|
|
verifyBuiltBundles({ entries, packages })
|
|
|
|
// 3. One immutable, run-owned directory; a later run never overwrites these bytes.
|
|
const buildId = runId()
|
|
const directory = join(root, REGISTRY_OUTPUT_ROOT, buildId)
|
|
mkdirSync(directory, { recursive: true })
|
|
const artifacts = await packBundleArtifacts({
|
|
root, destination: directory, members: selection.members, release, signal: abort.signal,
|
|
})
|
|
|
|
// 4. The manifest is the completion marker: only a fully packed run has one.
|
|
const source = sourceEvidence(root, getDshRuntimeVersion())
|
|
const manifest = createRegistryManifest({
|
|
buildId, source, entries, workspaceNames: selection.workspaceNames, artifacts,
|
|
})
|
|
await writeRegistryManifest(join(directory, REGISTRY_MANIFEST_FILE), manifest)
|
|
|
|
// 5. Serve, then announce: the summary is never printed for a registry that did not listen.
|
|
registry = await startBundleRegistry({
|
|
directory, artifacts, workspaceNames: selection.workspaceNames,
|
|
})
|
|
printSummary({ directory, url: registry.url, artifacts, source, foreignTargets: selection.foreignTargets })
|
|
await new Promise<void>((resolveStop) => {
|
|
if (abort.signal.aborted) resolveStop()
|
|
else abort.signal.addEventListener('abort', () => { resolveStop() }, { once: true })
|
|
})
|
|
await registry.close()
|
|
registry = undefined
|
|
process.stdout.write('Registry stopped. Artifacts and installed packages stay on disk.\n')
|
|
} catch (error) {
|
|
if (registry !== undefined) await registry.close()
|
|
if (error instanceof Interrupted) {
|
|
process.stderr.write(`dev:bundle-registry: ${error.message}\n`)
|
|
process.exitCode = 130
|
|
return
|
|
}
|
|
throw error
|
|
} finally {
|
|
process.off('SIGINT', onSignal)
|
|
process.off('SIGTERM', onSignal)
|
|
}
|
|
}
|
|
|
|
await main()
|