// @stago/sdk - Dist Versioning Helper // Pure filesystem logic used by stagoConfigPlugin's `versionedDist` option. // Kept in its own module so it can be unit tested without a full Vite build. import { join } from 'path'; import { existsSync, mkdirSync, readdirSync, rmSync, cpSync, writeFileSync } from 'fs'; /** * Server-only build artifacts that must stay in `outDir` for npm packaging / * the stago API host (dynamic `import` of dist/api.js), but must NOT be copied * into the versioned CDN nest — that tree is uploaded to public object storage * for MF `remoteEntry` / chunks, and shipping the Elysia plugin bundle there * would leak backend business logic. */ const CDN_EXCLUDED_ENTRIES = new Set(['api.js', 'api.js.map']); /** * When nesting under a dedicated CDN upload subdir (e.g. `cdn`), npm only needs * registration metadata + the API host bundle — browser assets are served from * OSS. `.npmignore` uses gitignore allowlist semantics (`*` then `!…`). */ const NPM_KEEP_WHEN_CDN_SUBDIR = ['module.json', 'api.js', 'api.js.map'] as const; /** * Copies browser-facing build output into `outDir//` * (or `outDir///` when `subdir` is given), leaving the * original flat files untouched on disk (so local inspection / accidental * non-CDN use still sees a full `outDir`; npm packing is narrowed separately). * * Used to support CI/CD pipelines (e.g. 云效内置 OSS 上传组件) that always * upload a fixed local directory to a fixed OSS prefix as-is, with no way * to inject the module version into the upload path per-run. Whatever * relative structure exists *inside* that local directory is preserved 1:1 * on the object storage side — so nesting a versioned copy of the build * output *inside* the directory the pipeline uploads is the only way to * make the version show up in the resulting remote path. * * Without `subdir`, that uploaded directory is `outDir` itself — so the * pipeline also re-uploads `outDir`'s own flat root files on every deploy, * wasting bandwidth/storage. Passing a fixed `subdir` name (e.g. `'cdn'`) * avoids this: point the pipeline's local-directory setting at * `outDir/` instead — a name fixed once at pipeline-setup time, * independent of the version — and only the versioned copy ever lives there. * * With `{ nestVersion: false }` (requires `subdir`), copies flat into * `outDir//` with no `/` segment — for api-only modules whose * CDN deploy overwrites a fixed OSS prefix on every release. * * A `.npmignore` is written at `outDir` root: * - with `subdir`: allowlist `module.json` / `api.js` / `api.js.map` only * (frontend goes to CDN; do not rely on `/module-assets` for such packages) * - without `subdir`: ignore only the nested `/` entry (full flat * `outDir` still publishes, keeping the non-CDN `/module-assets` fallback) * * Prefer outDir-root `.npmignore` over one inside the upload tree so OSS * stays clean. npm only honors a package-root `.npmignore` when * `package.json#files` is unset, but it *does* honor one placed under an * included directory even when `files` is set * (https://docs.npmjs.com/cli/configuring-npm/package-json#files). * Regenerated on every build, so it doesn't need to be committed. * * Assumes `build.emptyOutDir` is left at its default (`true`) so `outDir` * only ever contains the current build's output — otherwise a version * directory left over from a previous build would get copied into the new * one as well. */ export type NestVersionedDistOptions = { /** @default true — when false, copy flat into outDir// (subdir required) */ nestVersion?: boolean; }; export function nestVersionedDistCopy( outDir: string, version: string, subdir?: string, options?: NestVersionedDistOptions, ): void { if (!existsSync(outDir)) { throw new Error(`outDir does not exist: ${outDir}`); } const nestVersion = options?.nestVersion ?? true; if (!nestVersion && !subdir) { throw new Error('nestVersion: false requires a subdir (e.g. "cdn")'); } const nestRoot = subdir ? join(outDir, subdir) : outDir; const targetDir = nestVersion ? join(nestRoot, version) : nestRoot; // Snapshot entries (from outDir, always the source of truth) before // creating the version dir, excluding whichever of `subdir`/`version` // is the entry we're about to (re)create, so we never recurse into it. // Also skip server-only artifacts — they stay on the flat outDir for npm // / the API host, but must not land on the public CDN upload tree. const excludeEntry = subdir ?? version; const entries = readdirSync(outDir).filter( entry => entry !== excludeEntry && !CDN_EXCLUDED_ENTRIES.has(entry), ); if (existsSync(targetDir)) { rmSync(targetDir, { recursive: true, force: true }); } mkdirSync(targetDir, { recursive: true }); for (const entry of entries) { cpSync(join(outDir, entry), join(targetDir, entry), { recursive: true }); } writeFileSync(join(outDir, '.npmignore'), buildNpmignore(excludeEntry, Boolean(subdir))); } /** @internal exported for unit tests */ export function buildNpmignore(excludeEntry: string, slimForCdn: boolean): string { if (!slimForCdn) { return `${excludeEntry}\n`; } // Allowlist: keep registration + API host artifacts; drop MF frontend. const lines = ['*', ...NPM_KEEP_WHEN_CDN_SUBDIR.map(name => `!${name}`)]; return `${lines.join('\n')}\n`; }