From fe8fde94ba70fc6ab7195ee354114d0a4f524edb Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 28 Jul 2026 05:39:46 +0000 Subject: [PATCH 01/10] feat: Cache the API blueprint on disk Building the blueprint costs around 700ms on every invocation, almost all of it evaluating @seamapi/types and running createBlueprint. Persist the result as JSON under the platform cache directory, which takes a full command from roughly 750ms to roughly 300ms. The blueprint is a pure function of @seamapi/types and @seamapi/blueprint, so their installed versions form the cache key together with a format version for changes to how the blueprint is built. Both packages are now imported dynamically, so a cache hit never pays to evaluate them; that also takes seam --version from roughly 420ms to roughly 260ms, since it no longer loads the API definitions to print a string. Remote definitions are never cached because they can change without any package version changing. Writes go through a rename so a concurrent invocation cannot read a partial file, stale entries are pruned when a new one is written, and any cache failure falls back to rebuilding rather than failing the command. SEAM_CLI_DISABLE_BLUEPRINT_CACHE bypasses it entirely. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01XoC26cdSXPRxxJ4cbk4rrj --- README.md | 29 ++++++++-- package-lock.json | 16 ++++++ package.json | 1 + src/lib/blueprint-cache.ts | 102 +++++++++++++++++++++++++++++++++++ src/lib/get-api-blueprint.ts | 47 +++++++++++++--- 5 files changed, 184 insertions(+), 11 deletions(-) create mode 100644 src/lib/blueprint-cache.ts diff --git a/README.md b/README.md index 5bb9a308..a33d9292 100644 --- a/README.md +++ b/README.md @@ -56,15 +56,36 @@ dependencies through npm: there is no bundling step. Earlier versions of the CLI bundled with `tsup` and `@vercel/ncc` and built binaries with `pkg`. That pipeline is gone. `pkg` was archived in 2024 in -favour of Node's single executable applications, and bundling measurably buys -little here: a single-file build starts in roughly 290ms against roughly -420ms unbundled, because most of the startup cost is parsing the Seam API -definitions rather than resolving modules. +favour of Node's single executable applications, and bundling buys little +here: most of the startup cost is parsing the Seam API definitions and +building the blueprint, not resolving modules, so it is addressed by the +blueprint cache below rather than by a bundler. If self-contained binaries are wanted later, `bun build --compile` and `deno compile` both cross-compile and both consume the ES modules this package already publishes, so nothing here needs to change first. +## Blueprint cache + +The command list is built from the Seam API definitions on startup, which +costs around 700ms: most of it evaluating `@seamapi/types` and running +`createBlueprint`. The result is cached as JSON under the platform cache +directory, which takes a full command invocation from roughly 750ms to +roughly 300ms. + +The blueprint is a pure function of `@seamapi/types` and `@seamapi/blueprint`, +so their installed versions are the cache key, alongside a format version for +changes to how the blueprint is built. Both packages are imported dynamically +and are never evaluated on a cache hit. Stale entries are removed when a new +one is written. + +Remote definitions, selected with `seam config use-remote-api-defs`, are never +cached: they can change without any package version changing. + +Set `SEAM_CLI_DISABLE_BLUEPRINT_CACHE` to a non-empty value to bypass the +cache in both directions. A cache that cannot be read or written is not an +error; the blueprint is simply rebuilt. + ## Development and Testing ### Quickstart diff --git a/package-lock.json b/package-lock.json index 0b573d70..78aceb76 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "chalk": "^6.0.0", "command-line-usage": "^7.0.4", "configstore": "^8.0.0", + "env-paths": "^4.0.0", "minimist": "^1.2.8", "nanospinner": "^1.2.2", "open": "^11.0.0", @@ -2819,6 +2820,21 @@ "node": ">=10.13.0" } }, + "node_modules/env-paths": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-4.0.0.tgz", + "integrity": "sha512-pxP8eL2SwwaTRi/KHYwLYXinDs7gL3jxFcBYmEdYfZmZXbaVDvdppd0XBU8qVz03rDfKZMXg1omHCbsJjZrMsw==", + "license": "MIT", + "dependencies": { + "is-safe-filename": "^0.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/es-abstract": { "version": "1.24.2", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", diff --git a/package.json b/package.json index f0b126db..f16a517b 100644 --- a/package.json +++ b/package.json @@ -109,6 +109,7 @@ "chalk": "^6.0.0", "command-line-usage": "^7.0.4", "configstore": "^8.0.0", + "env-paths": "^4.0.0", "minimist": "^1.2.8", "nanospinner": "^1.2.2", "open": "^11.0.0", diff --git a/src/lib/blueprint-cache.ts b/src/lib/blueprint-cache.ts new file mode 100644 index 00000000..1c56c5fc --- /dev/null +++ b/src/lib/blueprint-cache.ts @@ -0,0 +1,102 @@ +import { + mkdir, + readdir, + readFile, + rename, + rm, + writeFile, +} from 'node:fs/promises' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +import type { Blueprint } from '@seamapi/blueprint' +import envPaths from 'env-paths' + +// Bump when the cached representation changes in a way that older files cannot +// satisfy, e.g. when the options passed to createBlueprint change. +const cacheFormatVersion = 1 + +const cacheFilePrefix = 'blueprint-' + +const cacheDirectory = envPaths('seam-cli', { suffix: '' }).cache + +const isCacheDisabled = (): boolean => + (process.env['SEAM_CLI_DISABLE_BLUEPRINT_CACHE'] ?? '') !== '' + +// The Blueprint is derived entirely from these two packages, so their installed +// versions are the only thing that can invalidate a cached copy. +const versionedPackages = ['@seamapi/types/connect', '@seamapi/blueprint'] + +const readPackageVersion = async (specifier: string): Promise => { + let directory = dirname(fileURLToPath(import.meta.resolve(specifier))) + + for (;;) { + try { + const manifest = JSON.parse( + await readFile(join(directory, 'package.json'), 'utf8'), + ) as { version?: string } + if (manifest.version != null) return manifest.version + } catch { + // Keep walking up: this directory has no readable manifest. + } + + const parent = dirname(directory) + if (parent === directory) { + throw new Error(`Could not resolve the version of ${specifier}`) + } + directory = parent + } +} + +const getCacheFileName = async (): Promise => { + const versions = await Promise.all(versionedPackages.map(readPackageVersion)) + return `${cacheFilePrefix}${cacheFormatVersion}-${versions.join('-')}.json` +} + +export const readCachedBlueprint = async (): Promise => { + if (isCacheDisabled()) return null + + try { + const file = join(cacheDirectory, await getCacheFileName()) + return JSON.parse(await readFile(file, 'utf8')) as Blueprint + } catch { + // A missing, unreadable or truncated cache is not an error: rebuild. + return null + } +} + +export const writeCachedBlueprint = async ( + blueprint: Blueprint, +): Promise => { + if (isCacheDisabled()) return + + try { + const fileName = await getCacheFileName() + await mkdir(cacheDirectory, { recursive: true }) + + // Write to a process-private path and rename, so a concurrent invocation + // never reads a partially written file. + const file = join(cacheDirectory, fileName) + const temporaryFile = `${file}.${process.pid}.tmp` + await writeFile(temporaryFile, JSON.stringify(blueprint), 'utf8') + await rename(temporaryFile, file) + + await pruneStaleCacheFiles(fileName) + } catch { + // Caching is an optimization: failing to persist must not fail the command. + } +} + +const pruneStaleCacheFiles = async (currentFileName: string): Promise => { + const entries = await readdir(cacheDirectory) + await Promise.all( + entries + .filter( + (entry) => + entry.startsWith(cacheFilePrefix) && entry !== currentFileName, + ) + .map(async (entry) => { + await rm(join(cacheDirectory, entry), { force: true }) + }), + ) +} diff --git a/src/lib/get-api-blueprint.ts b/src/lib/get-api-blueprint.ts index 4e5d7c06..9aa17ee1 100644 --- a/src/lib/get-api-blueprint.ts +++ b/src/lib/get-api-blueprint.ts @@ -1,7 +1,6 @@ -import { type Blueprint, createBlueprint } from '@seamapi/blueprint' -import { getOpenapiSchema } from '@seamapi/http/connect' -import * as seamTypes from '@seamapi/types/connect' +import type { Blueprint } from '@seamapi/blueprint' +import { readCachedBlueprint, writeCachedBlueprint } from './blueprint-cache.js' import { getServer } from './get-server.js' export type ApiBlueprint = Blueprint @@ -9,9 +8,43 @@ export type ApiBlueprint = Blueprint export const getApiBlueprint = async ( useRemoteDefinitions: boolean, ): Promise => { - const typesModule = useRemoteDefinitions - ? { ...seamTypes, openapi: await getOpenapiSchema(getServer()) } - : seamTypes + // Remote definitions can change without any package version changing, so + // they are always built fresh. + if (useRemoteDefinitions) return await createRemoteBlueprint() - return createBlueprint(typesModule, { omitUndocumented: true }) + const cached = await readCachedBlueprint() + if (cached != null) return cached + + const blueprint = await createLocalBlueprint() + await writeCachedBlueprint(blueprint) + + return blueprint +} + +// @seamapi/types and @seamapi/blueprint are imported dynamically so that a +// cache hit never pays to evaluate them: between them, loading the API +// definitions and building the Blueprint dominates CLI startup. +const createLocalBlueprint = async (): Promise => { + const [seamTypes, { createBlueprint }] = await Promise.all([ + import('@seamapi/types/connect'), + import('@seamapi/blueprint'), + ]) + + return await createBlueprint(seamTypes, { omitUndocumented: true }) +} + +const createRemoteBlueprint = async (): Promise => { + const [seamTypes, { createBlueprint }, { getOpenapiSchema }] = + await Promise.all([ + import('@seamapi/types/connect'), + import('@seamapi/blueprint'), + import('@seamapi/http/connect'), + ]) + + const openapi = await getOpenapiSchema(getServer()) + + return await createBlueprint( + { ...seamTypes, openapi }, + { omitUndocumented: true }, + ) } From 24c4f450cf74333d02305bd59e79695405b06ba2 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 28 Jul 2026 05:48:15 +0000 Subject: [PATCH 02/10] feat: Generate the blueprint at build time instead of caching it Replace the runtime blueprint cache with a blueprint.json generated by scripts/generate-blueprint.ts and shipped with the package. Nothing builds a blueprint on the default path any more, so @seamapi/types becomes a development dependency and an optional peer rather than a runtime one. It is most of the install: 63MB unpacked, because it ships ESM, CJS, TypeScript sources and both flavours of declarations, with multi-MB generated route types and OpenAPI document in each. An install goes from around 81MB to around 21MB, which is what matters for Homebrew and the AUR. env-paths and the user-level cache directory are gone with it. Generation is idempotent: the file records the versions it was generated from and is rewritten only when they change, so the prebuild and prestart hooks are cheap to re-run. It is not committed, since each revision would add a few MB to the repository permanently. Two paths still build a blueprint and so still need @seamapi/types: remote definitions, which describe whatever the server is running and cannot be generated ahead of time, and a working copy where blueprint.json has yet to be generated. Neither occurs in a published install, and reaching one without the package installed reports what to install. Spell out CustomMetadata structurally so the published declarations do not depend on the optional peer either. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01XoC26cdSXPRxxJ4cbk4rrj --- .gitignore | 6 +- README.md | 47 ++++++----- eslint.config.ts | 2 +- package-lock.json | 19 +---- package.json | 16 +++- scripts/generate-blueprint.ts | 94 ++++++++++++++++++++++ src/lib/blueprint-cache.ts | 102 ------------------------ src/lib/blueprint-file.ts | 24 ++++++ src/lib/get-api-blueprint.ts | 39 +++++---- src/lib/interact-for-custom-metadata.ts | 5 +- src/lib/package-file.ts | 15 ++++ src/lib/version.ts | 19 ++--- tsconfig.json | 2 +- 13 files changed, 218 insertions(+), 172 deletions(-) create mode 100644 scripts/generate-blueprint.ts delete mode 100644 src/lib/blueprint-cache.ts create mode 100644 src/lib/blueprint-file.ts create mode 100644 src/lib/package-file.ts diff --git a/.gitignore b/.gitignore index c7cc4c62..1a7b689f 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,9 @@ !src/**/*.d.ts !ava.config.js +# Generated at build time from @seamapi/types +/blueprint.json + # Build directories package @@ -207,7 +210,8 @@ $RECYCLE.BIN/ .LSOverride # Icon must end with two \r -Icon +Icon + # Thumbnails ._* diff --git a/README.md b/README.md index a33d9292..90d393bf 100644 --- a/README.md +++ b/README.md @@ -57,34 +57,43 @@ dependencies through npm: there is no bundling step. Earlier versions of the CLI bundled with `tsup` and `@vercel/ncc` and built binaries with `pkg`. That pipeline is gone. `pkg` was archived in 2024 in favour of Node's single executable applications, and bundling buys little -here: most of the startup cost is parsing the Seam API definitions and -building the blueprint, not resolving modules, so it is addressed by the -blueprint cache below rather than by a bundler. +here: the startup cost was never module resolution, it was building the +blueprint, which the generated `blueprint.json` below removes. If self-contained binaries are wanted later, `bun build --compile` and `deno compile` both cross-compile and both consume the ES modules this package already publishes, so nothing here needs to change first. -## Blueprint cache +## The generated blueprint -The command list is built from the Seam API definitions on startup, which -costs around 700ms: most of it evaluating `@seamapi/types` and running -`createBlueprint`. The result is cached as JSON under the platform cache -directory, which takes a full command invocation from roughly 750ms to -roughly 300ms. +The command list is derived from the Seam API definitions. Doing that at +startup costs around 700ms, almost all of it evaluating `@seamapi/types` and +running `createBlueprint`, so it is done once at build time instead: +`scripts/generate-blueprint.ts` writes a `blueprint.json` that ships with the +package and is read in around 20ms. -The blueprint is a pure function of `@seamapi/types` and `@seamapi/blueprint`, -so their installed versions are the cache key, alongside a format version for -changes to how the blueprint is built. Both packages are imported dynamically -and are never evaluated on a cache hit. Stale entries are removed when a new -one is written. +Because nothing builds a blueprint at runtime, `@seamapi/types` is a +development dependency rather than a runtime one. That is most of the install: +it unpacks to 63MB, because it ships ESM, CJS, TypeScript sources and both +flavours of declarations, and the generated route types and OpenAPI document +are several MB in each. Dropping it takes an install from around 81MB to +around 21MB, which matters for Homebrew and the AUR. -Remote definitions, selected with `seam config use-remote-api-defs`, are never -cached: they can change without any package version changing. +Generation is idempotent. The file records the versions it was generated from, +and the script rewrites it only when they change, so the `prebuild` and +`prestart` hooks are cheap to re-run. `blueprint.json` is not committed, since +each revision would add a few MB to the repository forever. -Set `SEAM_CLI_DISABLE_BLUEPRINT_CACHE` to a non-empty value to bypass the -cache in both directions. A cache that cannot be read or written is not an -error; the blueprint is simply rebuilt. +Two paths still build a blueprint and so still need `@seamapi/types`, which is +declared as an optional peer dependency: + +- Remote definitions, selected with `seam config use-remote-api-defs`, which + describe whatever the server is currently running and therefore cannot be + generated ahead of time. +- A working copy where `blueprint.json` has not been generated yet. + +Neither happens in a published install. If one is reached without the package +present, the CLI says so and names what to install. ## Development and Testing diff --git a/eslint.config.ts b/eslint.config.ts index 49eafd02..2df3996e 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -19,7 +19,7 @@ export default [ { // The CLI writes its output to the console: that is the product. // Its identifiers mirror the snake_case parameter names of the Seam API. - files: ['src/cli.ts', 'src/lib/**/*.ts'], + files: ['src/cli.ts', 'src/lib/**/*.ts', 'scripts/**/*.ts'], rules: { camelcase: 'off', 'no-console': 'off', diff --git a/package-lock.json b/package-lock.json index 78aceb76..2c182523 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,11 +11,9 @@ "dependencies": { "@seamapi/blueprint": "^1.1.0", "@seamapi/http": "^2.0.0", - "@seamapi/types": "^1.983.0", "chalk": "^6.0.0", "command-line-usage": "^7.0.4", "configstore": "^8.0.0", - "env-paths": "^4.0.0", "minimist": "^1.2.8", "nanospinner": "^1.2.2", "open": "^11.0.0", @@ -25,6 +23,7 @@ "seam": "cli.js" }, "devDependencies": { + "@seamapi/types": "^1.983.0", "@types/command-line-usage": "^5.0.4", "@types/minimist": "^1.2.5", "@types/node": "^24.10.9", @@ -913,6 +912,7 @@ "version": "1.983.0", "resolved": "https://registry.npmjs.org/@seamapi/types/-/types-1.983.0.tgz", "integrity": "sha512-SMkfn1SVC70x67mtRAvLMJtpFh/0zaStLatb6LA+kz9n/rV1gBS/UlH8SzBFg7iStE22f/VPjkdltHTIY1paoA==", + "devOptional": true, "license": "MIT", "engines": { "node": ">=22.11.0", @@ -2820,21 +2820,6 @@ "node": ">=10.13.0" } }, - "node_modules/env-paths": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-4.0.0.tgz", - "integrity": "sha512-pxP8eL2SwwaTRi/KHYwLYXinDs7gL3jxFcBYmEdYfZmZXbaVDvdppd0XBU8qVz03rDfKZMXg1omHCbsJjZrMsw==", - "license": "MIT", - "dependencies": { - "is-safe-filename": "^0.1.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/es-abstract": { "version": "1.24.2", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", diff --git a/package.json b/package.json index f16a517b..77daefbd 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "email": "devops@getseam.com" }, "files": [ + "blueprint.json", "index.js", "index.js.map", "index.d.ts", @@ -48,7 +49,7 @@ ], "scripts": { "build": "npm run build:entrypoints", - "prebuild": "tsx src/index.ts", + "prebuild": "npm run generate:blueprint && tsx src/index.ts", "postbuild": "node ./index.js && node ./cli.js --version", "build:entrypoints": "npm run build:ts", "build:ts": "tsc --project tsconfig.build.json", @@ -63,12 +64,22 @@ "lint": "eslint .", "postlint": "prettier --check --ignore-path .gitignore .", "postversion": "git push --follow-tags", + "generate:blueprint": "tsx scripts/generate-blueprint.ts", "start": "tsx src/cli.ts", + "prestart": "npm run generate:blueprint", "start:inspect": "tsx --inspect src/cli.ts", "format": "prettier --write --ignore-path .gitignore .", "preformat": "eslint --fix .", "report": "c8 report" }, + "peerDependencies": { + "@seamapi/types": "^1.983.0" + }, + "peerDependenciesMeta": { + "@seamapi/types": { + "optional": true + } + }, "engines": { "node": ">=22.11.0", "npm": ">=10.9.4" @@ -84,6 +95,7 @@ } }, "devDependencies": { + "@seamapi/types": "^1.983.0", "@types/command-line-usage": "^5.0.4", "@types/minimist": "^1.2.5", "@types/node": "^24.10.9", @@ -105,11 +117,9 @@ "dependencies": { "@seamapi/blueprint": "^1.1.0", "@seamapi/http": "^2.0.0", - "@seamapi/types": "^1.983.0", "chalk": "^6.0.0", "command-line-usage": "^7.0.4", "configstore": "^8.0.0", - "env-paths": "^4.0.0", "minimist": "^1.2.8", "nanospinner": "^1.2.2", "open": "^11.0.0", diff --git a/scripts/generate-blueprint.ts b/scripts/generate-blueprint.ts new file mode 100644 index 00000000..2b582a49 --- /dev/null +++ b/scripts/generate-blueprint.ts @@ -0,0 +1,94 @@ +#!/usr/bin/env tsx + +// Generates the blueprint.json that the published package ships, so that +// @seamapi/types is not needed at runtime. Idempotent: regenerates only when +// the packages the blueprint is derived from have changed. + +import { readFile, rename, writeFile } from 'node:fs/promises' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +import { createBlueprint } from '@seamapi/blueprint' +import * as seamTypes from '@seamapi/types/connect' + +import { type BlueprintFile, blueprintFileName } from 'lib/blueprint-file.js' + +// The blueprint is a pure function of these packages. +const sourcePackages = ['@seamapi/types/connect', '@seamapi/blueprint'] + +const packageRoot = new URL('../', import.meta.url) + +const readPackageVersion = async (specifier: string): Promise => { + let directory = dirname(fileURLToPath(import.meta.resolve(specifier))) + + for (;;) { + try { + const manifest = JSON.parse( + await readFile(join(directory, 'package.json'), 'utf8'), + ) as { name?: string; version?: string } + if (manifest.version != null) return manifest.version + } catch { + // Keep walking up: this directory has no readable manifest. + } + + const parent = dirname(directory) + if (parent === directory) { + throw new Error(`Could not resolve the version of ${specifier}`) + } + directory = parent + } +} + +const getGeneratedFrom = async (): Promise> => { + const entries = await Promise.all( + sourcePackages.map(async (specifier) => { + const name = specifier.split('/').slice(0, 2).join('/') + return [name, await readPackageVersion(specifier)] as const + }), + ) + + return Object.fromEntries(entries) +} + +const readExisting = async (url: URL): Promise => { + try { + return JSON.parse(await readFile(url, 'utf8')) as BlueprintFile + } catch { + return null + } +} + +const isUpToDate = ( + existing: BlueprintFile | null, + generatedFrom: Record, +): boolean => { + if (existing?.blueprint == null) return false + + const previous = existing.generatedFrom ?? {} + const names = Object.keys(generatedFrom) + + return ( + names.length === Object.keys(previous).length && + names.every((name) => previous[name] === generatedFrom[name]) + ) +} + +const file = new URL(blueprintFileName, packageRoot) +const generatedFrom = await getGeneratedFrom() +const describe = Object.entries(generatedFrom) + .map(([name, version]) => `${name}@${version}`) + .join(', ') + +if (isUpToDate(await readExisting(file), generatedFrom)) { + console.log(`${blueprintFileName} is up to date with ${describe}`) +} else { + const blueprint = await createBlueprint(seamTypes, { omitUndocumented: true }) + const contents: BlueprintFile = { generatedFrom, blueprint } + + // Write and rename so a partial file is never left behind on failure. + const temporaryFile = new URL(`${blueprintFileName}.tmp`, packageRoot) + await writeFile(temporaryFile, `${JSON.stringify(contents)}\n`, 'utf8') + await rename(temporaryFile, file) + + console.log(`Generated ${blueprintFileName} from ${describe}`) +} diff --git a/src/lib/blueprint-cache.ts b/src/lib/blueprint-cache.ts deleted file mode 100644 index 1c56c5fc..00000000 --- a/src/lib/blueprint-cache.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { - mkdir, - readdir, - readFile, - rename, - rm, - writeFile, -} from 'node:fs/promises' -import { dirname, join } from 'node:path' -import { fileURLToPath } from 'node:url' - -import type { Blueprint } from '@seamapi/blueprint' -import envPaths from 'env-paths' - -// Bump when the cached representation changes in a way that older files cannot -// satisfy, e.g. when the options passed to createBlueprint change. -const cacheFormatVersion = 1 - -const cacheFilePrefix = 'blueprint-' - -const cacheDirectory = envPaths('seam-cli', { suffix: '' }).cache - -const isCacheDisabled = (): boolean => - (process.env['SEAM_CLI_DISABLE_BLUEPRINT_CACHE'] ?? '') !== '' - -// The Blueprint is derived entirely from these two packages, so their installed -// versions are the only thing that can invalidate a cached copy. -const versionedPackages = ['@seamapi/types/connect', '@seamapi/blueprint'] - -const readPackageVersion = async (specifier: string): Promise => { - let directory = dirname(fileURLToPath(import.meta.resolve(specifier))) - - for (;;) { - try { - const manifest = JSON.parse( - await readFile(join(directory, 'package.json'), 'utf8'), - ) as { version?: string } - if (manifest.version != null) return manifest.version - } catch { - // Keep walking up: this directory has no readable manifest. - } - - const parent = dirname(directory) - if (parent === directory) { - throw new Error(`Could not resolve the version of ${specifier}`) - } - directory = parent - } -} - -const getCacheFileName = async (): Promise => { - const versions = await Promise.all(versionedPackages.map(readPackageVersion)) - return `${cacheFilePrefix}${cacheFormatVersion}-${versions.join('-')}.json` -} - -export const readCachedBlueprint = async (): Promise => { - if (isCacheDisabled()) return null - - try { - const file = join(cacheDirectory, await getCacheFileName()) - return JSON.parse(await readFile(file, 'utf8')) as Blueprint - } catch { - // A missing, unreadable or truncated cache is not an error: rebuild. - return null - } -} - -export const writeCachedBlueprint = async ( - blueprint: Blueprint, -): Promise => { - if (isCacheDisabled()) return - - try { - const fileName = await getCacheFileName() - await mkdir(cacheDirectory, { recursive: true }) - - // Write to a process-private path and rename, so a concurrent invocation - // never reads a partially written file. - const file = join(cacheDirectory, fileName) - const temporaryFile = `${file}.${process.pid}.tmp` - await writeFile(temporaryFile, JSON.stringify(blueprint), 'utf8') - await rename(temporaryFile, file) - - await pruneStaleCacheFiles(fileName) - } catch { - // Caching is an optimization: failing to persist must not fail the command. - } -} - -const pruneStaleCacheFiles = async (currentFileName: string): Promise => { - const entries = await readdir(cacheDirectory) - await Promise.all( - entries - .filter( - (entry) => - entry.startsWith(cacheFilePrefix) && entry !== currentFileName, - ) - .map(async (entry) => { - await rm(join(cacheDirectory, entry), { force: true }) - }), - ) -} diff --git a/src/lib/blueprint-file.ts b/src/lib/blueprint-file.ts new file mode 100644 index 00000000..157e6e16 --- /dev/null +++ b/src/lib/blueprint-file.ts @@ -0,0 +1,24 @@ +import { readFile } from 'node:fs/promises' + +import type { Blueprint } from '@seamapi/blueprint' + +import { packageFileUrl } from './package-file.js' + +export const blueprintFileName = 'blueprint.json' + +export interface BlueprintFile { + generatedFrom: Record + blueprint: Blueprint +} + +export const readBlueprintFile = async (): Promise => { + try { + return JSON.parse( + await readFile(packageFileUrl(blueprintFileName), 'utf8'), + ) as BlueprintFile + } catch { + // The file is generated at build time, so treat a missing or unreadable + // one as absent and let the caller build the blueprint instead. + return null + } +} diff --git a/src/lib/get-api-blueprint.ts b/src/lib/get-api-blueprint.ts index 9aa17ee1..dce06f38 100644 --- a/src/lib/get-api-blueprint.ts +++ b/src/lib/get-api-blueprint.ts @@ -1,6 +1,7 @@ import type { Blueprint } from '@seamapi/blueprint' +import type * as SeamTypes from '@seamapi/types/connect' -import { readCachedBlueprint, writeCachedBlueprint } from './blueprint-cache.js' +import { blueprintFileName, readBlueprintFile } from './blueprint-file.js' import { getServer } from './get-server.js' export type ApiBlueprint = Blueprint @@ -8,25 +9,23 @@ export type ApiBlueprint = Blueprint export const getApiBlueprint = async ( useRemoteDefinitions: boolean, ): Promise => { - // Remote definitions can change without any package version changing, so - // they are always built fresh. + // Remote definitions describe whatever the server is currently running, so + // they are always built from the live schema. if (useRemoteDefinitions) return await createRemoteBlueprint() - const cached = await readCachedBlueprint() - if (cached != null) return cached + const file = await readBlueprintFile() + if (file != null) return file.blueprint - const blueprint = await createLocalBlueprint() - await writeCachedBlueprint(blueprint) - - return blueprint + return await createLocalBlueprint() } -// @seamapi/types and @seamapi/blueprint are imported dynamically so that a -// cache hit never pays to evaluate them: between them, loading the API -// definitions and building the Blueprint dominates CLI startup. +// The published package ships a generated blueprint.json, so @seamapi/types is +// not a runtime dependency: evaluating its API definitions and building the +// blueprint is what made startup slow. This path only runs in a working copy +// where blueprint.json has not been generated yet. const createLocalBlueprint = async (): Promise => { const [seamTypes, { createBlueprint }] = await Promise.all([ - import('@seamapi/types/connect'), + importSeamTypes(), import('@seamapi/blueprint'), ]) @@ -36,7 +35,7 @@ const createLocalBlueprint = async (): Promise => { const createRemoteBlueprint = async (): Promise => { const [seamTypes, { createBlueprint }, { getOpenapiSchema }] = await Promise.all([ - import('@seamapi/types/connect'), + importSeamTypes(), import('@seamapi/blueprint'), import('@seamapi/http/connect'), ]) @@ -48,3 +47,15 @@ const createRemoteBlueprint = async (): Promise => { { omitUndocumented: true }, ) } + +// @seamapi/types is an optional peer dependency: it is only needed to build a +// blueprint, which the published package does not do for the default path. +const importSeamTypes = async (): Promise => { + try { + return await import('@seamapi/types/connect') + } catch { + throw new Error( + `Building an API blueprint requires @seamapi/types, which is not installed. Install it alongside the CLI, or use the ${blueprintFileName} generated at build time.`, + ) + } +} diff --git a/src/lib/interact-for-custom-metadata.ts b/src/lib/interact-for-custom-metadata.ts index 4b356516..88e7dff4 100644 --- a/src/lib/interact-for-custom-metadata.ts +++ b/src/lib/interact-for-custom-metadata.ts @@ -1,6 +1,9 @@ -import type { CustomMetadata } from '@seamapi/types/connect' import prompts from 'prompts' +// Structurally the CustomMetadata of @seamapi/types, spelled out here so the +// published declarations do not depend on that optional peer dependency. +type CustomMetadata = Record + type UpdatedCustomMetadata = { [x: string]: string | boolean | null } diff --git a/src/lib/package-file.ts b/src/lib/package-file.ts new file mode 100644 index 00000000..ae3ed3f1 --- /dev/null +++ b/src/lib/package-file.ts @@ -0,0 +1,15 @@ +import { existsSync } from 'node:fs' + +// This module is emitted to lib/ in the published package and runs from +// src/lib/ during development, so the package root is one or two directories +// up depending on how the CLI was started. +const candidatePaths = ['../', '../../'] + +export const packageFileUrl = (name: string): URL => { + for (const path of candidatePaths) { + const url = new URL(`${path}${name}`, import.meta.url) + if (existsSync(url)) return url + } + + throw new Error(`Could not find ${name} in the package root`) +} diff --git a/src/lib/version.ts b/src/lib/version.ts index 0072243d..42aa55b7 100644 --- a/src/lib/version.ts +++ b/src/lib/version.ts @@ -1,23 +1,16 @@ import { readFileSync } from 'node:fs' -// This module resolves to lib/version.js in the published package and to -// src/lib/version.ts when running from source, so package.json sits one or -// two directories up depending on how the CLI was started. -const packageJsonUrls = [ - new URL('../package.json', import.meta.url), - new URL('../../package.json', import.meta.url), -] +import { packageFileUrl } from './package-file.js' -const readVersion = (url: URL): string | undefined => { +const readVersion = (): string | undefined => { try { - const { version } = JSON.parse(readFileSync(url, 'utf8')) as { - version?: string - } + const { version } = JSON.parse( + readFileSync(packageFileUrl('package.json'), 'utf8'), + ) as { version?: string } return version } catch { return undefined } } -export const version: string = - packageJsonUrls.map(readVersion).find((v) => v != null) ?? '0.0.0' +export const version: string = readVersion() ?? '0.0.0' diff --git a/tsconfig.json b/tsconfig.json index 6ddbf22a..1b26cbc3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -30,5 +30,5 @@ } }, "files": ["src/index.ts", "src/cli.ts"], - "include": ["src/**/*", "test/**/*", "eslint.config.ts"] + "include": ["src/**/*", "test/**/*", "scripts/**/*", "eslint.config.ts"] } From f0b06364d53d8efe8c53cbd52f234b6680e7c5d7 Mon Sep 17 00:00:00 2001 From: Seam Bot Date: Tue, 28 Jul 2026 05:48:38 +0000 Subject: [PATCH 03/10] ci: Generate code --- package-lock.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/package-lock.json b/package-lock.json index 2c182523..0aad0fbb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,6 +45,14 @@ "engines": { "node": ">=22.11.0", "npm": ">=10.9.4" + }, + "peerDependencies": { + "@seamapi/types": "^1.983.0" + }, + "peerDependenciesMeta": { + "@seamapi/types": { + "optional": true + } } }, "node_modules/@bcoe/v8-coverage": { From 5e1a4e28c709954094a29fd4dc0db2d242ea4f2a Mon Sep 17 00:00:00 2001 From: Evan Sosenko Date: Tue, 28 Jul 2026 12:43:06 -0500 Subject: [PATCH 04/10] Fix .gitignore --- .gitignore | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 811513a0..17431235 100644 --- a/.gitignore +++ b/.gitignore @@ -9,11 +9,9 @@ *.d.ts *.js *.js.map +blueprint.json !src/**/*.d.ts -# Generated at build time from @seamapi/types -/blueprint.json - # Build directories package @@ -209,8 +207,7 @@ $RECYCLE.BIN/ .LSOverride # Icon must end with two \r -Icon - +Icon # Thumbnails ._* From b9d1c9420ba1ab3da0cf357b1c97a12c590995e9 Mon Sep 17 00:00:00 2001 From: Evan Sosenko Date: Tue, 28 Jul 2026 16:25:37 -0500 Subject: [PATCH 05/10] Prepack the blueprint --- .gitignore | 1 - README.md | 48 +++++----- prepack.ts | 66 +++++++++---- scripts/generate-blueprint.ts | 96 +++---------------- src/lib/blueprint-file.ts | 24 ----- src/lib/blueprint.ts | 6 ++ src/lib/get-api-blueprint.ts | 45 +++++---- src/lib/package-file.ts | 15 --- ...nfig.version.json => tsconfig.prepack.json | 2 +- 9 files changed, 115 insertions(+), 188 deletions(-) delete mode 100644 src/lib/blueprint-file.ts create mode 100644 src/lib/blueprint.ts delete mode 100644 src/lib/package-file.ts rename tsconfig.version.json => tsconfig.prepack.json (66%) diff --git a/.gitignore b/.gitignore index 17431235..d53437c8 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,6 @@ *.d.ts *.js *.js.map -blueprint.json !src/**/*.d.ts # Build directories diff --git a/README.md b/README.md index 422dfcf3..834af090 100644 --- a/README.md +++ b/README.md @@ -38,32 +38,28 @@ $ seam --help The command list is derived from the Seam API definitions. Doing that at startup costs around 700ms, almost all of it evaluating `@seamapi/types` and -running `createBlueprint`, so it is done once at build time instead: -`scripts/generate-blueprint.ts` writes a `blueprint.json` that ships with the -package and is read in around 20ms. - -Because nothing builds a blueprint at runtime, `@seamapi/types` is a -development dependency rather than a runtime one. That is most of the install: -it unpacks to 63MB, because it ships ESM, CJS, TypeScript sources and both -flavours of declarations, and the generated route types and OpenAPI document -are several MB in each. Dropping it takes an install from around 81MB to -around 21MB, which matters for Homebrew and the AUR. - -Generation is idempotent. The file records the versions it was generated from, -and the script rewrites it only when they change, so the `prebuild` and -`preseam` hooks are cheap to re-run. `blueprint.json` is not committed, since -each revision would add a few MB to the repository forever. - -Two paths still build a blueprint and so still need `@seamapi/types`, which is -declared as an optional peer dependency: - -- Remote definitions, selected with `seam config use-remote-api-defs`, which - describe whatever the server is currently running and therefore cannot be - generated ahead of time. -- A working copy where `blueprint.json` has not been generated yet. - -Neither happens in a published install. If one is reached without the package -present, the CLI says so and names what to install. +running `createBlueprint`, so the blueprint is generated ahead of time. + +During development, `scripts/generate-blueprint.ts` writes +`tmp/blueprint.json`. The `prebuild` and `preseam` hooks regenerate it before it +is used. When the package is packed, `prepack.ts` injects that generated value +into `src/lib/blueprint.ts` and rebuilds the module, just as it injects the +package version into `src/lib/version.ts`. The published CLI therefore loads +the blueprint directly from JavaScript without shipping or reading a separate +JSON file. + +Because the default runtime path does not build a blueprint, `@seamapi/types` +is a development dependency rather than a runtime one. That is most of the +install: it unpacks to 63MB, because it ships ESM, CJS, TypeScript sources and +both flavours of declarations, and the generated route types and OpenAPI +document are several MB in each. Dropping it takes an install from around 81MB +to around 21MB, which matters for Homebrew and the AUR. + +Remote definitions, selected with `seam config use-remote-api-defs`, still +need `@seamapi/types` because they describe whatever the server is currently +running and cannot be generated ahead of time. It is declared as an optional +peer dependency, and the CLI reports what to install if that mode is selected +without it. ## Development and Testing diff --git a/prepack.ts b/prepack.ts index fffe73ef..751252e9 100644 --- a/prepack.ts +++ b/prepack.ts @@ -4,15 +4,22 @@ import { fileURLToPath } from 'node:url' import { $ } from 'execa' const versionFile = './src/lib/version.ts' +const blueprintFile = './src/lib/blueprint.ts' +const generatedBlueprintFile = './tmp/blueprint.json' const main = async (): Promise => { - const version = await injectVersion( - fileURLToPath(new URL(versionFile, import.meta.url)), - ) + const version = await injectVersion(resolveFile(versionFile)) // eslint-disable-next-line no-console console.log(`✓ Version ${version} injected into ${versionFile}`) - const { command } = await $`tsc --project tsconfig.version.json` + await injectBlueprint( + resolveFile(blueprintFile), + resolveFile(generatedBlueprintFile), + ) + // eslint-disable-next-line no-console + console.log(`✓ Blueprint injected into ${blueprintFile}`) + + const { command } = await $`tsc --project tsconfig.prepack.json` // eslint-disable-next-line no-console console.log(`✓ Rebuilt with '${command}'`) } @@ -24,25 +31,48 @@ const injectVersion = async (path: string): Promise => { throw new Error('Missing version in package.json') } - const buff = await readFile(path) - - const data = buff - .toString() - .replace( - "const seamapiCliVersion = '0.0.0'", - `const seamapiCliVersion = '${version}'`, - ) - - await writeFile(path, data) + await replaceInFile( + path, + "const seamapiCliVersion = '0.0.0'", + `const seamapiCliVersion = '${version}'`, + ) return version } -const readPackageJson = async (): Promise<{ version?: string }> => { - const pkgBuff = await readFile( - fileURLToPath(new URL('package.json', import.meta.url)), +const injectBlueprint = async ( + path: string, + generatedPath: string, +): Promise => { + const blueprint = JSON.parse(await readFile(generatedPath, 'utf8')) as unknown + + await replaceInFile( + path, + 'const seamapiBlueprint: Blueprint | null = null', + `const seamapiBlueprint: Blueprint | null = ${JSON.stringify(blueprint)} as unknown as Blueprint`, ) - return JSON.parse(pkgBuff.toString()) } +const replaceInFile = async ( + path: string, + placeholder: string, + replacement: string, +): Promise => { + const source = await readFile(path, 'utf8') + + if (!source.includes(placeholder)) { + throw new Error(`Missing generated-value placeholder in ${path}`) + } + + await writeFile(path, source.replace(placeholder, replacement), 'utf8') +} + +const resolveFile = (path: string): string => + fileURLToPath(new URL(path, import.meta.url)) + +const readPackageJson = async (): Promise<{ version?: string }> => + JSON.parse(await readFile(resolveFile('package.json'), 'utf8')) as { + version?: string + } + await main() diff --git a/scripts/generate-blueprint.ts b/scripts/generate-blueprint.ts index 2b582a49..42fa6c13 100644 --- a/scripts/generate-blueprint.ts +++ b/scripts/generate-blueprint.ts @@ -1,94 +1,20 @@ #!/usr/bin/env tsx -// Generates the blueprint.json that the published package ships, so that -// @seamapi/types is not needed at runtime. Idempotent: regenerates only when -// the packages the blueprint is derived from have changed. - -import { readFile, rename, writeFile } from 'node:fs/promises' -import { dirname, join } from 'node:path' -import { fileURLToPath } from 'node:url' +import { mkdir, rename, writeFile } from 'node:fs/promises' import { createBlueprint } from '@seamapi/blueprint' import * as seamTypes from '@seamapi/types/connect' -import { type BlueprintFile, blueprintFileName } from 'lib/blueprint-file.js' - -// The blueprint is a pure function of these packages. -const sourcePackages = ['@seamapi/types/connect', '@seamapi/blueprint'] - -const packageRoot = new URL('../', import.meta.url) - -const readPackageVersion = async (specifier: string): Promise => { - let directory = dirname(fileURLToPath(import.meta.resolve(specifier))) - - for (;;) { - try { - const manifest = JSON.parse( - await readFile(join(directory, 'package.json'), 'utf8'), - ) as { name?: string; version?: string } - if (manifest.version != null) return manifest.version - } catch { - // Keep walking up: this directory has no readable manifest. - } - - const parent = dirname(directory) - if (parent === directory) { - throw new Error(`Could not resolve the version of ${specifier}`) - } - directory = parent - } -} - -const getGeneratedFrom = async (): Promise> => { - const entries = await Promise.all( - sourcePackages.map(async (specifier) => { - const name = specifier.split('/').slice(0, 2).join('/') - return [name, await readPackageVersion(specifier)] as const - }), - ) - - return Object.fromEntries(entries) -} - -const readExisting = async (url: URL): Promise => { - try { - return JSON.parse(await readFile(url, 'utf8')) as BlueprintFile - } catch { - return null - } -} - -const isUpToDate = ( - existing: BlueprintFile | null, - generatedFrom: Record, -): boolean => { - if (existing?.blueprint == null) return false - - const previous = existing.generatedFrom ?? {} - const names = Object.keys(generatedFrom) - - return ( - names.length === Object.keys(previous).length && - names.every((name) => previous[name] === generatedFrom[name]) - ) -} - -const file = new URL(blueprintFileName, packageRoot) -const generatedFrom = await getGeneratedFrom() -const describe = Object.entries(generatedFrom) - .map(([name, version]) => `${name}@${version}`) - .join(', ') +const temporaryDirectory = new URL('../tmp/', import.meta.url) +const blueprintFile = new URL('blueprint.json', temporaryDirectory) +const temporaryFile = new URL('blueprint.json.tmp', temporaryDirectory) -if (isUpToDate(await readExisting(file), generatedFrom)) { - console.log(`${blueprintFileName} is up to date with ${describe}`) -} else { - const blueprint = await createBlueprint(seamTypes, { omitUndocumented: true }) - const contents: BlueprintFile = { generatedFrom, blueprint } +const blueprint = await createBlueprint(seamTypes, { + omitUndocumented: true, +}) - // Write and rename so a partial file is never left behind on failure. - const temporaryFile = new URL(`${blueprintFileName}.tmp`, packageRoot) - await writeFile(temporaryFile, `${JSON.stringify(contents)}\n`, 'utf8') - await rename(temporaryFile, file) +await mkdir(temporaryDirectory, { recursive: true }) +await writeFile(temporaryFile, `${JSON.stringify(blueprint)}\n`, 'utf8') +await rename(temporaryFile, blueprintFile) - console.log(`Generated ${blueprintFileName} from ${describe}`) -} +console.log('Generated tmp/blueprint.json') diff --git a/src/lib/blueprint-file.ts b/src/lib/blueprint-file.ts deleted file mode 100644 index 157e6e16..00000000 --- a/src/lib/blueprint-file.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { readFile } from 'node:fs/promises' - -import type { Blueprint } from '@seamapi/blueprint' - -import { packageFileUrl } from './package-file.js' - -export const blueprintFileName = 'blueprint.json' - -export interface BlueprintFile { - generatedFrom: Record - blueprint: Blueprint -} - -export const readBlueprintFile = async (): Promise => { - try { - return JSON.parse( - await readFile(packageFileUrl(blueprintFileName), 'utf8'), - ) as BlueprintFile - } catch { - // The file is generated at build time, so treat a missing or unreadable - // one as absent and let the caller build the blueprint instead. - return null - } -} diff --git a/src/lib/blueprint.ts b/src/lib/blueprint.ts new file mode 100644 index 00000000..ff9be545 --- /dev/null +++ b/src/lib/blueprint.ts @@ -0,0 +1,6 @@ +import type { Blueprint } from '@seamapi/blueprint' + +// Replaced with the generated blueprint when the package is packed. +const seamapiBlueprint: Blueprint | null = null + +export default seamapiBlueprint diff --git a/src/lib/get-api-blueprint.ts b/src/lib/get-api-blueprint.ts index dce06f38..4a1b334e 100644 --- a/src/lib/get-api-blueprint.ts +++ b/src/lib/get-api-blueprint.ts @@ -1,7 +1,9 @@ +import { readFile } from 'node:fs/promises' + import type { Blueprint } from '@seamapi/blueprint' import type * as SeamTypes from '@seamapi/types/connect' -import { blueprintFileName, readBlueprintFile } from './blueprint-file.js' +import seamapiBlueprint from './blueprint.js' import { getServer } from './get-server.js' export type ApiBlueprint = Blueprint @@ -13,23 +15,30 @@ export const getApiBlueprint = async ( // they are always built from the live schema. if (useRemoteDefinitions) return await createRemoteBlueprint() - const file = await readBlueprintFile() - if (file != null) return file.blueprint + if (seamapiBlueprint != null) return seamapiBlueprint - return await createLocalBlueprint() + return await readDevelopmentBlueprint() } -// The published package ships a generated blueprint.json, so @seamapi/types is -// not a runtime dependency: evaluating its API definitions and building the -// blueprint is what made startup slow. This path only runs in a working copy -// where blueprint.json has not been generated yet. -const createLocalBlueprint = async (): Promise => { - const [seamTypes, { createBlueprint }] = await Promise.all([ - importSeamTypes(), - import('@seamapi/blueprint'), - ]) - - return await createBlueprint(seamTypes, { omitUndocumented: true }) +const readDevelopmentBlueprint = async (): Promise => { + // This module runs from src/lib under tsx and from lib after a development + // build. The packed module never takes this path because its blueprint is + // injected by prepack.ts. + const candidates = ['../../tmp/blueprint.json', '../tmp/blueprint.json'] + + for (const candidate of candidates) { + try { + return JSON.parse( + await readFile(new URL(candidate, import.meta.url), 'utf8'), + ) as Blueprint + } catch { + // Try the path for the other execution mode. + } + } + + throw new Error( + 'Missing tmp/blueprint.json. Run `npm run generate:blueprint` to generate it.', + ) } const createRemoteBlueprint = async (): Promise => { @@ -48,14 +57,14 @@ const createRemoteBlueprint = async (): Promise => { ) } -// @seamapi/types is an optional peer dependency: it is only needed to build a -// blueprint, which the published package does not do for the default path. +// @seamapi/types is an optional peer dependency: it is only needed when remote +// definitions are enabled. The default blueprint is embedded in the package. const importSeamTypes = async (): Promise => { try { return await import('@seamapi/types/connect') } catch { throw new Error( - `Building an API blueprint requires @seamapi/types, which is not installed. Install it alongside the CLI, or use the ${blueprintFileName} generated at build time.`, + 'Remote API definitions require @seamapi/types, which is not installed. Install it alongside the CLI or disable remote API definitions.', ) } } diff --git a/src/lib/package-file.ts b/src/lib/package-file.ts deleted file mode 100644 index ae3ed3f1..00000000 --- a/src/lib/package-file.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { existsSync } from 'node:fs' - -// This module is emitted to lib/ in the published package and runs from -// src/lib/ during development, so the package root is one or two directories -// up depending on how the CLI was started. -const candidatePaths = ['../', '../../'] - -export const packageFileUrl = (name: string): URL => { - for (const path of candidatePaths) { - const url = new URL(`${path}${name}`, import.meta.url) - if (existsSync(url)) return url - } - - throw new Error(`Could not find ${name} in the package root`) -} diff --git a/tsconfig.version.json b/tsconfig.prepack.json similarity index 66% rename from tsconfig.version.json rename to tsconfig.prepack.json index 1478edc5..4905b4d6 100644 --- a/tsconfig.version.json +++ b/tsconfig.prepack.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/tsconfig", "extends": "./tsconfig.build.json", - "files": ["src/lib/version.ts"], + "files": ["src/lib/blueprint.ts", "src/lib/version.ts"], "exclude": ["**/*"] } From ca75bf0010d3226d983d7958ac28ca9cde4657ea Mon Sep 17 00:00:00 2001 From: Evan Sosenko Date: Tue, 28 Jul 2026 16:37:32 -0500 Subject: [PATCH 06/10] Remove scripts --- README.md | 15 ++++++------ eslint.config.ts | 2 +- package.json | 4 +-- prepack.ts | 9 +++---- scripts/generate-blueprint.ts | 20 --------------- src/lib/blueprint.ts | 46 ++++++++++++++++++++++++++++++++++- src/lib/get-api-blueprint.ts | 29 ++-------------------- tsconfig.json | 1 - 8 files changed, 61 insertions(+), 65 deletions(-) delete mode 100644 scripts/generate-blueprint.ts diff --git a/README.md b/README.md index 834af090..43f3e5fd 100644 --- a/README.md +++ b/README.md @@ -40,13 +40,14 @@ The command list is derived from the Seam API definitions. Doing that at startup costs around 700ms, almost all of it evaluating `@seamapi/types` and running `createBlueprint`, so the blueprint is generated ahead of time. -During development, `scripts/generate-blueprint.ts` writes -`tmp/blueprint.json`. The `prebuild` and `preseam` hooks regenerate it before it -is used. When the package is packed, `prepack.ts` injects that generated value -into `src/lib/blueprint.ts` and rebuilds the module, just as it injects the -package version into `src/lib/version.ts`. The published CLI therefore loads -the blueprint directly from JavaScript without shipping or reading a separate -JSON file. +`src/lib/blueprint.ts` contains the packaged blueprint. In a development +checkout its placeholder is empty, so it dynamically imports the API types, +builds the blueprint, and stores it in `tmp/blueprint.json` for subsequent +runs. When the package is packed, `prepack.ts` forces a fresh blueprint, +injects it into that placeholder, and rebuilds the module, just as it injects +the package version into `src/lib/version.ts`. The published CLI therefore +loads the blueprint directly from JavaScript without importing +`@seamapi/types` or reading a separate JSON file. Because the default runtime path does not build a blueprint, `@seamapi/types` is a development dependency rather than a runtime one. That is most of the diff --git a/eslint.config.ts b/eslint.config.ts index 013512ee..2ba7ae8f 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -17,7 +17,7 @@ export default [ }, }, { - files: ['src/bin/**/*.ts', 'src/lib/**/*.ts', 'scripts/**/*.ts'], + files: ['src/bin/**/*.ts', 'src/lib/**/*.ts'], rules: { // TODO: Rename the identifiers that mirror the snake_case parameter names // of the Seam API so that everything here is camelCase. diff --git a/package.json b/package.json index 1c0e6049..36194529 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ ], "scripts": { "build": "npm run build:entrypoints", - "prebuild": "npm run generate:blueprint && tsx src/index.ts", + "prebuild": "tsx src/index.ts", "postbuild": "concurrently --raw --group 'node ./index.js' 'node ./bin/cli.js --version'", "build:entrypoints": "npm run build:ts", "build:ts": "tsc --project tsconfig.build.json", @@ -62,8 +62,6 @@ "postlint": "prettier --check --ignore-path .gitignore .", "prepack": "tsx ./prepack.ts", "postversion": "git push --follow-tags", - "generate:blueprint": "tsx scripts/generate-blueprint.ts", - "preseam": "npm run generate:blueprint", "seam": "tsx src/bin/cli.ts", "inspect": "tsx --inspect src/bin/cli.ts", "format": "prettier --write --ignore-path .gitignore .", diff --git a/prepack.ts b/prepack.ts index 751252e9..47108539 100644 --- a/prepack.ts +++ b/prepack.ts @@ -3,9 +3,10 @@ import { fileURLToPath } from 'node:url' import { $ } from 'execa' +import getBlueprint from './src/lib/blueprint.js' + const versionFile = './src/lib/version.ts' const blueprintFile = './src/lib/blueprint.ts' -const generatedBlueprintFile = './tmp/blueprint.json' const main = async (): Promise => { const version = await injectVersion(resolveFile(versionFile)) @@ -14,7 +15,7 @@ const main = async (): Promise => { await injectBlueprint( resolveFile(blueprintFile), - resolveFile(generatedBlueprintFile), + await getBlueprint({ regenerate: true }), ) // eslint-disable-next-line no-console console.log(`✓ Blueprint injected into ${blueprintFile}`) @@ -42,10 +43,8 @@ const injectVersion = async (path: string): Promise => { const injectBlueprint = async ( path: string, - generatedPath: string, + blueprint: unknown, ): Promise => { - const blueprint = JSON.parse(await readFile(generatedPath, 'utf8')) as unknown - await replaceInFile( path, 'const seamapiBlueprint: Blueprint | null = null', diff --git a/scripts/generate-blueprint.ts b/scripts/generate-blueprint.ts deleted file mode 100644 index 42fa6c13..00000000 --- a/scripts/generate-blueprint.ts +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env tsx - -import { mkdir, rename, writeFile } from 'node:fs/promises' - -import { createBlueprint } from '@seamapi/blueprint' -import * as seamTypes from '@seamapi/types/connect' - -const temporaryDirectory = new URL('../tmp/', import.meta.url) -const blueprintFile = new URL('blueprint.json', temporaryDirectory) -const temporaryFile = new URL('blueprint.json.tmp', temporaryDirectory) - -const blueprint = await createBlueprint(seamTypes, { - omitUndocumented: true, -}) - -await mkdir(temporaryDirectory, { recursive: true }) -await writeFile(temporaryFile, `${JSON.stringify(blueprint)}\n`, 'utf8') -await rename(temporaryFile, blueprintFile) - -console.log('Generated tmp/blueprint.json') diff --git a/src/lib/blueprint.ts b/src/lib/blueprint.ts index ff9be545..50a95c77 100644 --- a/src/lib/blueprint.ts +++ b/src/lib/blueprint.ts @@ -3,4 +3,48 @@ import type { Blueprint } from '@seamapi/blueprint' // Replaced with the generated blueprint when the package is packed. const seamapiBlueprint: Blueprint | null = null -export default seamapiBlueprint +interface GetBlueprintOptions { + regenerate?: boolean +} + +const getBlueprint = async ( + options: GetBlueprintOptions = {}, +): Promise => { + if (seamapiBlueprint != null) return seamapiBlueprint + + const blueprintFile = new URL('../../tmp/blueprint.json', import.meta.url) + + if (options.regenerate !== true) { + const existing = await readBlueprint(blueprintFile) + if (existing != null) return existing + } + + const [{ createBlueprint }, seamTypes] = await Promise.all([ + import('@seamapi/blueprint'), + import('@seamapi/types/connect'), + ]) + const blueprint = await createBlueprint(seamTypes, { + omitUndocumented: true, + }) + + const { mkdir, rename, writeFile } = await import('node:fs/promises') + const temporaryDirectory = new URL('./', blueprintFile) + const temporaryFile = new URL('blueprint.json.tmp', temporaryDirectory) + + await mkdir(temporaryDirectory, { recursive: true }) + await writeFile(temporaryFile, `${JSON.stringify(blueprint)}\n`, 'utf8') + await rename(temporaryFile, blueprintFile) + + return blueprint +} + +const readBlueprint = async (file: URL): Promise => { + try { + const { readFile } = await import('node:fs/promises') + return JSON.parse(await readFile(file, 'utf8')) as Blueprint + } catch { + return null + } +} + +export default getBlueprint diff --git a/src/lib/get-api-blueprint.ts b/src/lib/get-api-blueprint.ts index 4a1b334e..4bf2279e 100644 --- a/src/lib/get-api-blueprint.ts +++ b/src/lib/get-api-blueprint.ts @@ -1,9 +1,7 @@ -import { readFile } from 'node:fs/promises' - import type { Blueprint } from '@seamapi/blueprint' import type * as SeamTypes from '@seamapi/types/connect' -import seamapiBlueprint from './blueprint.js' +import getBlueprint from './blueprint.js' import { getServer } from './get-server.js' export type ApiBlueprint = Blueprint @@ -15,30 +13,7 @@ export const getApiBlueprint = async ( // they are always built from the live schema. if (useRemoteDefinitions) return await createRemoteBlueprint() - if (seamapiBlueprint != null) return seamapiBlueprint - - return await readDevelopmentBlueprint() -} - -const readDevelopmentBlueprint = async (): Promise => { - // This module runs from src/lib under tsx and from lib after a development - // build. The packed module never takes this path because its blueprint is - // injected by prepack.ts. - const candidates = ['../../tmp/blueprint.json', '../tmp/blueprint.json'] - - for (const candidate of candidates) { - try { - return JSON.parse( - await readFile(new URL(candidate, import.meta.url), 'utf8'), - ) as Blueprint - } catch { - // Try the path for the other execution mode. - } - } - - throw new Error( - 'Missing tmp/blueprint.json. Run `npm run generate:blueprint` to generate it.', - ) + return await getBlueprint() } const createRemoteBlueprint = async (): Promise => { diff --git a/tsconfig.json b/tsconfig.json index 281f2a6a..ac990992 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -33,7 +33,6 @@ "include": [ "src/**/*", "test/**/*", - "scripts/**/*", "eslint.config.ts", "prepack.ts", "vitest.config.ts" From b32fa8fdaa4b47896d86c25d237a9526bffe51ad Mon Sep 17 00:00:00 2001 From: Evan Sosenko Date: Tue, 28 Jul 2026 16:40:37 -0500 Subject: [PATCH 07/10] Update lockfile --- package-lock.json | 813 ++++++++++++++++++++++++---------------------- 1 file changed, 421 insertions(+), 392 deletions(-) diff --git a/package-lock.json b/package-lock.json index e283ae1f..937f39a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1008,6 +1008,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1025,6 +1028,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1042,6 +1048,9 @@ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1059,6 +1068,9 @@ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1076,6 +1088,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1093,6 +1108,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1221,9 +1239,9 @@ "license": "MIT" }, "node_modules/@seamapi/blueprint": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@seamapi/blueprint/-/blueprint-1.1.0.tgz", - "integrity": "sha512-wX1HZkA/IK9hDQ6Qdxw5Mo+Ysfh82p9IEXQJafakO9VMbszW6n1U02eEhZHVY3CfzN/duk6t9h1veX0zRlhWBQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@seamapi/blueprint/-/blueprint-1.2.0.tgz", + "integrity": "sha512-+fomtVD/VGJavEpOtVZD+3ZxUYgTXAS4DreNqbsNl0R1qS0wiIdJ40AmE9dgLnY1cHCfPNBmf1z//FdvCqHhrw==", "license": "MIT", "dependencies": { "change-case": "^5.4.4", @@ -1231,7 +1249,7 @@ }, "engines": { "node": ">=22.11.0", - "npm": ">=10.9.4" + "npm": ">=10.0.0" } }, "node_modules/@seamapi/http": { @@ -1250,9 +1268,9 @@ } }, "node_modules/@seamapi/types": { - "version": "1.983.0", - "resolved": "https://registry.npmjs.org/@seamapi/types/-/types-1.983.0.tgz", - "integrity": "sha512-SMkfn1SVC70x67mtRAvLMJtpFh/0zaStLatb6LA+kz9n/rV1gBS/UlH8SzBFg7iStE22f/VPjkdltHTIY1paoA==", + "version": "1.985.0", + "resolved": "https://registry.npmjs.org/@seamapi/types/-/types-1.985.0.tgz", + "integrity": "sha512-3+aFZXav6zQ9mPsl6FLR0TuMBbu3u3kC16OoEjNHZbTASj1eOYTAjb2lUR8VjFAaR1iZi/rNcIPBRfX/xaWbfg==", "dev": true, "license": "MIT", "engines": { @@ -1320,6 +1338,19 @@ "eslint": ">=8.40.0" } }, + "node_modules/@stylistic/eslint-plugin/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", @@ -1632,6 +1663,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@typescript-eslint/utils": { "version": "8.65.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz", @@ -1763,16 +1807,6 @@ } } }, - "node_modules/@vitest/mocker/node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, "node_modules/@vitest/pretty-format": { "version": "4.1.10", "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", @@ -1842,9 +1876,9 @@ } }, "node_modules/acorn": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", - "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", "dev": true, "license": "MIT", "bin": { @@ -1864,6 +1898,18 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/ajv": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", @@ -1921,19 +1967,6 @@ "node": ">= 8" } }, - "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2142,23 +2175,6 @@ "js-tokens": "^10.0.0" } }, - "node_modules/ast-v8-to-istanbul/node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", - "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", - "dev": true, - "license": "MIT" - }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -2225,31 +2241,6 @@ "axios": "0.x || 1.x" } }, - "node_modules/axios/node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/axios/node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "license": "MIT", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2358,6 +2349,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -2426,6 +2427,18 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chalk-template/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/change-case": { "version": "5.4.4", "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", @@ -2485,24 +2498,6 @@ "node": ">=20" } }, - "node_modules/cliui/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2603,65 +2598,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/concurrently/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/concurrently/node_modules/supports-color": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", - "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/concurrently/node_modules/yargs": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", - "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^9.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "string-width": "^7.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^22.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=23" - } - }, - "node_modules/concurrently/node_modules/yargs-parser": { - "version": "22.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", - "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=23" - } - }, "node_modules/configstore": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/configstore/-/configstore-8.0.0.tgz", @@ -2901,63 +2837,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/del/node_modules/@sindresorhus/merge-streams": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", - "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/del/node_modules/globby": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", - "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sindresorhus/merge-streams": "^2.1.0", - "fast-glob": "^3.3.3", - "ignore": "^7.0.3", - "path-type": "^6.0.0", - "slash": "^5.1.0", - "unicorn-magic": "^0.3.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/del/node_modules/ignore": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", - "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/del/node_modules/unicorn-magic": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", - "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -3028,21 +2907,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/dot-prop/node_modules/type-fest": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.8.0.tgz", - "integrity": "sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==", - "license": "(MIT OR CC0-1.0)", - "dependencies": { - "tagged-tag": "^1.0.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -3065,9 +2929,9 @@ "license": "MIT" }, "node_modules/enhanced-resolve": { - "version": "5.24.3", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.3.tgz", - "integrity": "sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ==", + "version": "5.24.4", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.4.tgz", + "integrity": "sha512-GVoi+ICHocoOIU7qVVM48wOJziRsqrsyqlI0Ce0LdowRn6v3bcH2zUa9kp85ncx0nwIb9/HOCOLS3fdThDG/XQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3421,6 +3285,19 @@ "eslint": ">=6.0.0" } }, + "node_modules/eslint-compat-utils/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/eslint-import-resolver-node": { "version": "0.3.10", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz", @@ -3537,16 +3414,6 @@ "ms": "^2.1.1" } }, - "node_modules/eslint-plugin-import/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/eslint-plugin-n": { "version": "17.24.0", "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-17.24.0.tgz", @@ -3587,6 +3454,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/eslint-plugin-n/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/eslint-plugin-promise": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-7.3.0.tgz", @@ -3639,16 +3519,6 @@ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, - "node_modules/eslint-plugin-react/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/eslint-plugin-simple-import-sort": { "version": "12.1.1", "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-12.1.1.tgz", @@ -3738,6 +3608,19 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/espree": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", @@ -3792,6 +3675,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -3914,24 +3807,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, "node_modules/figures": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", @@ -4283,6 +4158,50 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/globby": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", + "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.3", + "path-type": "^6.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby/node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby/node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/globrex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", @@ -4405,6 +4324,19 @@ "dev": true, "license": "MIT" }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/human-signals": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", @@ -4442,16 +4374,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/import-fresh/node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -5086,6 +5008,19 @@ "node": ">=10" } }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/istanbul-reports": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", @@ -5129,9 +5064,9 @@ } }, "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", "dev": true, "license": "MIT" }, @@ -5384,6 +5319,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5405,6 +5343,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5426,6 +5367,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5447,6 +5391,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5538,6 +5485,13 @@ "loose-envify": "cli.js" } }, + "node_modules/loose-envify/node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -5576,6 +5530,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -5609,30 +5576,17 @@ } }, "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, "engines": { "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/mime-db": { @@ -5838,19 +5792,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/neostandard/node_modules/unicorn-magic": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", - "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/neostandard/node_modules/yocto-queue": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", @@ -5883,16 +5824,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/node-exports-info/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -5933,19 +5864,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/npm-run-path/node_modules/unicorn-magic": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", - "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -6198,16 +6116,6 @@ "node": ">=6" } }, - "node_modules/parent-module/node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/parse-ms": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", @@ -6286,13 +6194,13 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" + "node": ">=8.6" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" @@ -6512,19 +6420,6 @@ "node": ">=8.10.0" } }, - "node_modules/readdirp/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -6593,6 +6488,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -6750,16 +6655,13 @@ } }, "node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" } }, "node_modules/set-function-length": { @@ -7000,6 +6902,24 @@ "node": ">= 0.4" } }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/string.prototype.matchall": { "version": "4.0.12", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", @@ -7167,15 +7087,16 @@ "license": "MIT" }, "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, "node_modules/supports-preserve-symlinks-flag": { @@ -7264,6 +7185,37 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/tinyrainbow": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", @@ -7333,6 +7285,19 @@ "typescript": ">=4.0.0" } }, + "node_modules/ts-declaration-location/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/tsc-alias": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/tsc-alias/-/tsc-alias-1.9.1.tgz", @@ -7438,6 +7403,21 @@ "node": ">= 0.8.0" } }, + "node_modules/type-fest": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.8.0.tgz", + "integrity": "sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==", + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -7589,6 +7569,19 @@ "dev": true, "license": "MIT" }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -7677,6 +7670,19 @@ } } }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/vitest": { "version": "4.1.10", "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", @@ -7767,6 +7773,19 @@ } } }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/when-exit": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.5.tgz", @@ -7948,24 +7967,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/wsl-utils": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.3.1.tgz", @@ -8004,6 +8005,34 @@ "node": ">=10" } }, + "node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", From 1f3ef18d2c5d78f4bf85eedaebdb4962ddcf0a14 Mon Sep 17 00:00:00 2001 From: Evan Sosenko Date: Tue, 28 Jul 2026 16:41:05 -0500 Subject: [PATCH 08/10] Update README --- README.md | 33 --------------------------------- 1 file changed, 33 deletions(-) diff --git a/README.md b/README.md index ff0cca8a..2a057850 100644 --- a/README.md +++ b/README.md @@ -13,11 +13,6 @@ Every command is interactive: the CLI prompts for any missing required parameter with suggestions pulled from your workspace. Pass `-y` to take the first suggestion instead of being asked. -The command list is derived from the [Seam API blueprint], so the CLI exposes -every documented endpoint without needing a release of its own. - -[Seam API blueprint]: https://github.com/seamapi/blueprint - ## Installation Install the CLI globally using [npm] with @@ -69,34 +64,6 @@ seam access-codes create --code "1234" --name "My Code" seam access-codes list --device-id $MY_DOOR ``` -## The generated blueprint - -The command list is derived from the Seam API definitions. Doing that at -startup costs around 700ms, almost all of it evaluating `@seamapi/types` and -running `createBlueprint`, so the blueprint is generated ahead of time. - -`src/lib/blueprint.ts` contains the packaged blueprint. In a development -checkout its placeholder is empty, so it dynamically imports the API types, -builds the blueprint, and stores it in `tmp/blueprint.json` for subsequent -runs. When the package is packed, `prepack.ts` forces a fresh blueprint, -injects it into that placeholder, and rebuilds the module, just as it injects -the package version into `src/lib/version.ts`. The published CLI therefore -loads the blueprint directly from JavaScript without importing -`@seamapi/types` or reading a separate JSON file. - -Because the default runtime path does not build a blueprint, `@seamapi/types` -is a development dependency rather than a runtime one. That is most of the -install: it unpacks to 63MB, because it ships ESM, CJS, TypeScript sources and -both flavours of declarations, and the generated route types and OpenAPI -document are several MB in each. Dropping it takes an install from around 81MB -to around 21MB, which matters for Homebrew and the AUR. - -Remote definitions, selected with `seam config use-remote-api-defs`, still -need `@seamapi/types` because they describe whatever the server is currently -running and cannot be generated ahead of time. It is declared as an optional -peer dependency, and the CLI reports what to install if that mode is selected -without it. - ## Development and Testing ### Quickstart From bd3fecf417e2a12409eab648196e4e9e9faf0e06 Mon Sep 17 00:00:00 2001 From: Evan Sosenko Date: Tue, 28 Jul 2026 16:41:29 -0500 Subject: [PATCH 09/10] Remove peerDependencies --- package-lock.json | 8 -------- package.json | 8 -------- 2 files changed, 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index 937f39a6..f51ae73c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,14 +47,6 @@ "engines": { "node": ">=22.11.0", "npm": ">=10.0.0" - }, - "peerDependencies": { - "@seamapi/types": "^1.983.0" - }, - "peerDependenciesMeta": { - "@seamapi/types": { - "optional": true - } } }, "node_modules/@babel/helper-string-parser": { diff --git a/package.json b/package.json index 2d6eb456..5ddf40c5 100644 --- a/package.json +++ b/package.json @@ -68,14 +68,6 @@ "preformat": "eslint --fix .", "report": "vitest run --coverage" }, - "peerDependencies": { - "@seamapi/types": "^1.983.0" - }, - "peerDependenciesMeta": { - "@seamapi/types": { - "optional": true - } - }, "engines": { "node": ">=22.11.0", "npm": ">=10.0.0" From 9c026b59ae7797bc49f2ecacc8994ac96c602604 Mon Sep 17 00:00:00 2001 From: Evan Sosenko Date: Tue, 28 Jul 2026 16:49:13 -0500 Subject: [PATCH 10/10] Generate remote based on openapi not types --- src/lib/blueprint.ts | 11 +++++---- src/lib/get-api-blueprint.ts | 32 ++++++------------------- src/lib/interact-for-custom-metadata.ts | 2 +- 3 files changed, 15 insertions(+), 30 deletions(-) diff --git a/src/lib/blueprint.ts b/src/lib/blueprint.ts index 50a95c77..3ecf23f7 100644 --- a/src/lib/blueprint.ts +++ b/src/lib/blueprint.ts @@ -19,13 +19,16 @@ const getBlueprint = async ( if (existing != null) return existing } - const [{ createBlueprint }, seamTypes] = await Promise.all([ + // This branch only runs in a development checkout. Published packages have + // seamapiBlueprint injected above and never load @seamapi/types. + const [{ createBlueprint }, { openapi }] = await Promise.all([ import('@seamapi/blueprint'), import('@seamapi/types/connect'), ]) - const blueprint = await createBlueprint(seamTypes, { - omitUndocumented: true, - }) + const blueprint = await createBlueprint( + { openapi }, + { omitUndocumented: true }, + ) const { mkdir, rename, writeFile } = await import('node:fs/promises') const temporaryDirectory = new URL('./', blueprintFile) diff --git a/src/lib/get-api-blueprint.ts b/src/lib/get-api-blueprint.ts index 4bf2279e..8029fb52 100644 --- a/src/lib/get-api-blueprint.ts +++ b/src/lib/get-api-blueprint.ts @@ -1,5 +1,4 @@ import type { Blueprint } from '@seamapi/blueprint' -import type * as SeamTypes from '@seamapi/types/connect' import getBlueprint from './blueprint.js' import { getServer } from './get-server.js' @@ -10,36 +9,19 @@ export const getApiBlueprint = async ( useRemoteDefinitions: boolean, ): Promise => { // Remote definitions describe whatever the server is currently running, so - // they are always built from the live schema. + // build them directly from the server's OpenAPI document. This runtime path + // does not load @seamapi/types. if (useRemoteDefinitions) return await createRemoteBlueprint() return await getBlueprint() } const createRemoteBlueprint = async (): Promise => { - const [seamTypes, { createBlueprint }, { getOpenapiSchema }] = - await Promise.all([ - importSeamTypes(), - import('@seamapi/blueprint'), - import('@seamapi/http/connect'), - ]) - + const [{ createBlueprint }, { getOpenapiSchema }] = await Promise.all([ + import('@seamapi/blueprint'), + import('@seamapi/http/connect'), + ]) const openapi = await getOpenapiSchema(getServer()) - return await createBlueprint( - { ...seamTypes, openapi }, - { omitUndocumented: true }, - ) -} - -// @seamapi/types is an optional peer dependency: it is only needed when remote -// definitions are enabled. The default blueprint is embedded in the package. -const importSeamTypes = async (): Promise => { - try { - return await import('@seamapi/types/connect') - } catch { - throw new Error( - 'Remote API definitions require @seamapi/types, which is not installed. Install it alongside the CLI or disable remote API definitions.', - ) - } + return await createBlueprint({ openapi }, { omitUndocumented: true }) } diff --git a/src/lib/interact-for-custom-metadata.ts b/src/lib/interact-for-custom-metadata.ts index 88e7dff4..147a0f07 100644 --- a/src/lib/interact-for-custom-metadata.ts +++ b/src/lib/interact-for-custom-metadata.ts @@ -1,7 +1,7 @@ import prompts from 'prompts' // Structurally the CustomMetadata of @seamapi/types, spelled out here so the -// published declarations do not depend on that optional peer dependency. +// published declarations do not depend on a development-only package. type CustomMetadata = Record type UpdatedCustomMetadata = {