From 62901f68e21aa2cffe7f1d16bf2a07f34cd025d0 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 18 Jun 2026 09:10:33 +0900 Subject: [PATCH 1/2] build(sdk): replace tsdown bundler with tsgo file-by-file build Build with tsgo (file-by-file ESM emit) plus a postbuild script instead of the tsdown bundler. The postbuild renames to .mjs/.d.mts, rewrites module specifiers, emits YAML files as runtime modules, adds CLI shebangs (and the exec bit), injects the runtime-globals banner on configure/index.d.mts only, copies ERD viewer assets, and vendors the private @tailor-platform/tailor-proto package into dist/_proto so the unbundled output stays self-contained. Annotate initOperatorClient's return type so the declaration emit keeps OperatorClient instead of collapsing to Client under file-by-file emit. Remove tsdown and sonda, and drop the redundant measure:bundle metric with its sdk-metrics Bundle Size CI job: deployed function-bundle sizes are already guarded by example's bundled_execution size test. Add explicit knip entries that tsdown's plugins previously provided. Clean build ~6s (was ~22s). --- .changeset/claude-tsgo-build.md | 5 + .github/workflows/sdk-metrics.yml | 35 +--- .gitignore | 3 - packages/sdk/.gitignore | 1 - packages/sdk/.octocov.yml | 1 - packages/sdk/knip.json | 26 +++ packages/sdk/package.json | 5 +- packages/sdk/scripts/build-tsgo.mjs | 35 ++++ packages/sdk/scripts/postbuild-tsgo.mjs | 237 ++++++++++++++++++++++++ packages/sdk/src/cli/shared/client.ts | 2 +- packages/sdk/tsconfig.build.json | 34 ++++ packages/sdk/tsconfig.json | 1 - packages/sdk/tsdown.config.ts | 113 ----------- pnpm-lock.yaml | 29 --- scripts/measure-bundle-size-sonda.js | 95 ---------- 15 files changed, 340 insertions(+), 282 deletions(-) create mode 100644 .changeset/claude-tsgo-build.md create mode 100644 packages/sdk/scripts/build-tsgo.mjs create mode 100644 packages/sdk/scripts/postbuild-tsgo.mjs create mode 100644 packages/sdk/tsconfig.build.json delete mode 100644 packages/sdk/tsdown.config.ts delete mode 100644 scripts/measure-bundle-size-sonda.js diff --git a/.changeset/claude-tsgo-build.md b/.changeset/claude-tsgo-build.md new file mode 100644 index 0000000000..fccf13197e --- /dev/null +++ b/.changeset/claude-tsgo-build.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk": patch +--- + +Build the SDK with tsgo (file-by-file ESM emit) plus a postbuild script instead of the tsdown bundler. The published package is no longer a single bundle: modules are emitted per file and third-party dependencies resolve from the consumer's `node_modules` at runtime. The private `@tailor-platform/tailor-proto` workspace package is vendored into `dist/_proto` so the unbundled output stays self-contained. No public API changes. diff --git a/.github/workflows/sdk-metrics.yml b/.github/workflows/sdk-metrics.yml index dcb2c096bd..34aa1931cc 100644 --- a/.github/workflows/sdk-metrics.yml +++ b/.github/workflows/sdk-metrics.yml @@ -93,31 +93,6 @@ jobs: RUN_ID: ${{ github.run_id }} run: pnpm --filter @tailor-platform/sdk exec tsx scripts/cleanup-e2e-workspaces.ts --run-id="$RUN_ID" - bundle-size: - if: github.event.action != 'synchronize' || github.event.sender.login != 'tailor-pr-trigger[bot]' - name: Bundle Size - runs-on: ubuntu-latest - timeout-minutes: 10 - permissions: - contents: read - steps: - - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - persist-credentials: false - - - name: Install deps - uses: ./.github/actions/install-deps - - - name: Measure bundle size - run: pnpm --filter @tailor-platform/sdk measure:bundle - - - name: Upload results - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: bundle-size-results - path: packages/sdk/bundle-size.json - type-performance: if: github.event.action != 'synchronize' || github.event.sender.login != 'tailor-pr-trigger[bot]' name: Type Performance @@ -174,7 +149,7 @@ jobs: report-metrics: name: Report Metrics - needs: [coverage-shard, bundle-size, type-performance, runtime-performance] + needs: [coverage-shard, type-performance, runtime-performance] runs-on: ubuntu-latest timeout-minutes: 10 permissions: @@ -211,13 +186,6 @@ jobs: echo "See octocov report below for coverage details." echo "" - echo "## Bundle Size" - echo "" - echo "| Metric | Value |" - echo "|--------|------:|" - jq -r '.metrics[] | "| \(.name) | \(.value) \(.unit) |"' artifacts/bundle-size-results/bundle-size.json - echo "" - echo "## Type Check Performance" echo "" echo "| Feature | Instantiations | Types |" @@ -240,5 +208,4 @@ jobs: config: packages/sdk/.octocov.yml env: OCTOCOV_CUSTOM_METRICS_TYPE_PERFORMANCE: artifacts/type-perf-results/diagnostics.json - OCTOCOV_CUSTOM_METRICS_BUNDLE_SIZE: artifacts/bundle-size-results/bundle-size.json OCTOCOV_CUSTOM_METRICS_RUNTIME_PERFORMANCE: artifacts/runtime-perf-results/runtime-perf.json diff --git a/.gitignore b/.gitignore index 9ec659334f..8c887cac31 100644 --- a/.gitignore +++ b/.gitignore @@ -19,9 +19,6 @@ lerna-debug.log* # Eslint cache .eslintcache -# Bundle analysis -.sonda/ -bundle-size.json # Coverage directory used by tools like istanbul coverage/ diff --git a/packages/sdk/.gitignore b/packages/sdk/.gitignore index 671ff4234f..257a56f562 100644 --- a/packages/sdk/.gitignore +++ b/packages/sdk/.gitignore @@ -6,7 +6,6 @@ diagnostics.json diagnostics-summary.json runtime-perf.json runtime-perf-summary.json -bundle-size.json scripts/perf/tsconfig.temp.json # Integration test fixtures output diff --git a/packages/sdk/.octocov.yml b/packages/sdk/.octocov.yml index 4c7e48ccc4..fb1f201b2b 100644 --- a/packages/sdk/.octocov.yml +++ b/packages/sdk/.octocov.yml @@ -37,5 +37,4 @@ summary: if: true # Custom metrics are specified via environment variables: # - OCTOCOV_CUSTOM_METRICS_TYPE_PERFORMANCE: diagnostics.json -# - OCTOCOV_CUSTOM_METRICS_BUNDLE_SIZE: bundle-size.json # - OCTOCOV_CUSTOM_METRICS_RUNTIME_PERFORMANCE: runtime-perf.json diff --git a/packages/sdk/knip.json b/packages/sdk/knip.json index 467ac9e747..fd448e535d 100644 --- a/packages/sdk/knip.json +++ b/packages/sdk/knip.json @@ -2,6 +2,32 @@ "$schema": "https://unpkg.com/knip@6/schema.json", "ignoreExportsUsedInFile": true, "tags": ["-lintignore"], + "entry": [ + "src/configure/index.ts", + "src/cli/index.ts", + "src/cli/lib.ts", + "src/cli/skills.ts", + "src/utils/test/index.ts", + "src/kysely/index.ts", + "src/plugin/index.ts", + "src/plugin/builtin/kysely-type/index.ts", + "src/plugin/builtin/enum-constants/index.ts", + "src/plugin/builtin/file-utils/index.ts", + "src/plugin/builtin/seed/index.ts", + "src/seed/index.ts", + "src/vitest/index.ts", + "src/vitest/environment.ts", + "src/vitest/setup.ts", + "src/runtime/index.ts", + "src/runtime/globals.ts", + "src/runtime/iconv.ts", + "src/runtime/secretmanager.ts", + "src/runtime/authconnection.ts", + "src/runtime/idp.ts", + "src/runtime/workflow.ts", + "src/runtime/context.ts", + "src/runtime/file.ts" + ], "ignore": [ "scripts/**", "e2e/fixtures/**", diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 2eaebdba1b..267e470069 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -141,7 +141,7 @@ "test:coverage": "vitest --coverage", "docs:check": "vitest run --project=unit* src/cli/docs.test.ts", "docs:update": "POLITTY_DOCS_UPDATE=true vitest run --project=unit* src/cli/docs.test.ts", - "build": "tsdown && politty generate-worker --bin dist/cli/index.mjs --program tailor-sdk --shell zsh --verify", + "build": "node scripts/build-tsgo.mjs && politty generate-worker --bin dist/cli/index.mjs --program tailor-sdk --shell zsh --verify", "lint": "oxlint --type-aware .", "check:public-api-jsdoc": "tsx scripts/check-public-api-jsdoc.ts", "check:zod-isolation": "tsx scripts/check-zod-isolation.ts", @@ -153,7 +153,6 @@ "perf:runtime": "bash scripts/perf/runtime-perf.sh", "prepublish": "pnpm run build", "postinstall": "node postinstall.mjs", - "measure:bundle": "node ../../scripts/measure-bundle-size-sonda.js", "knip": "knip", "publint": "publint --strict", "generate": "zinfer && node -e \"const f=require('fs').readdirSync('src/types').filter(f=>f.endsWith('.generated.ts')).map(f=>'src/types/'+f);if(f.length)require('child_process').execSync('pnpm oxfmt --write '+f.join(' '),{stdio:'inherit'})\"" @@ -222,8 +221,6 @@ "oxfmt": "0.54.0", "oxlint": "1.69.0", "oxlint-tsgolint": "0.23.0", - "sonda": "0.13.0", - "tsdown": "0.22.2", "typescript": "6.0.3", "vitest": "4.1.8", "zinfer": "0.1.8" diff --git a/packages/sdk/scripts/build-tsgo.mjs b/packages/sdk/scripts/build-tsgo.mjs new file mode 100644 index 0000000000..c4b4529fc9 --- /dev/null +++ b/packages/sdk/scripts/build-tsgo.mjs @@ -0,0 +1,35 @@ +// Build entrypoint: run tsgo (file-by-file emit) then the postbuild rewrites. +// +// The build tsconfig needs `allowImportingTsExtensions` for the workspace +// `@tailor-proto` package to resolve under bundler module resolution, which in +// turn makes tsgo emit a benign TS5096 config diagnostic (and a couple of +// politty TS4023 "cannot be named" declaration-emit notes) and exit non-zero — +// even though it emits correct JS + declarations. Type correctness is gated by +// `pnpm typecheck` (tsc --noEmit), not by this build. So we run tsgo, ignore +// its exit code, and instead gate on the postbuild successfully finding and +// rewriting the expected output. tsgo is rerun with `clean`-like semantics by +// wiping dist first. +import { execFileSync } from "node:child_process"; +import { rmSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const pkgRoot = path.resolve(__dirname, ".."); + +rmSync(path.join(pkgRoot, "dist"), { recursive: true, force: true }); + +try { + execFileSync("tsgo", ["-p", "tsconfig.build.json"], { + cwd: pkgRoot, + stdio: "inherit", + }); +} catch { + // tsgo exits non-zero on the benign diagnostics described above; emission + // still happens. postbuild validates that the expected files exist. +} + +execFileSync("node", ["scripts/postbuild-tsgo.mjs"], { + cwd: pkgRoot, + stdio: "inherit", +}); diff --git a/packages/sdk/scripts/postbuild-tsgo.mjs b/packages/sdk/scripts/postbuild-tsgo.mjs new file mode 100644 index 0000000000..2a9d6fdbad --- /dev/null +++ b/packages/sdk/scripts/postbuild-tsgo.mjs @@ -0,0 +1,237 @@ +// Postbuild for the tsgo (file-by-file ESM) build. tsgo emits per-file JS + d.ts +// but leaves module specifiers exactly as written in source: extensionless +// relative imports, the `@/*` self-alias, and the `@tailor-proto/*` workspace +// alias. None of those are runnable as published ESM, so this script rewrites +// them and replicates the rest of what the old tsdown plugins did. +// +// Steps: +// 1. vendor the (already-compiled) @tailor-platform/tailor-proto sources into +// dist/_proto so the workspace-only package travels with the publish +// 2. rename .js -> .mjs, .d.ts -> .d.mts, .js.map -> .mjs.map (fix map refs) +// 3. emit `.mjs` runtime modules for imported YAML files (tsgo skips .yml) +// 4. rewrite every module specifier in dist: +// - `@/x` -> relative path into dist +// - `@tailor-proto/tailor/v1/x` -> relative path into dist/_proto +// - extensionless `./x`/`../x` -> `./x.mjs` or `./x/index.mjs` +// - `./x.js` / `./x.yml` -> `./x.mjs` / `./x.yml.mjs` +// directory specifiers resolve to `/index.mjs`. +// 5. prepend the node shebang to the CLI entrypoints +// 6. prepend the runtime/globals banner to configure/index.d.mts only +// 7. copy the ERD viewer assets into dist +import { + chmodSync, + cpSync, + existsSync, + mkdirSync, + readdirSync, + readFileSync, + renameSync, + rmSync, + statSync, + writeFileSync, +} from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const pkgRoot = path.resolve(__dirname, ".."); +const srcDir = path.join(pkgRoot, "src"); +const distDir = path.join(pkgRoot, "dist"); +const protoSrcDir = path.resolve(pkgRoot, "..", "tailor-proto", "src"); +const protoDistDir = path.join(distDir, "_proto"); + +/** + * Recursively collect every file path under a directory. + * @param dir - Directory to walk. + * @returns Absolute paths of all files found. + */ +function walk(dir) { + const out = []; + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) out.push(...walk(full)); + else if (entry.isFile()) out.push(full); + } + return out; +} + +// 1. Vendor tailor-proto. It is a private workspace package (not published), +// whose generated .js/.d.ts only import @bufbuild/protobuf + @connectrpc/connect +// (both SDK deps) plus its own relative files, so it can travel as-is. +function vendorProto() { + rmSync(protoDistDir, { recursive: true, force: true }); + mkdirSync(protoDistDir, { recursive: true }); + cpSync(protoSrcDir, protoDistDir, { + recursive: true, + filter: (s) => { + if (statSync(s).isDirectory()) return true; + return s.endsWith(".js") || s.endsWith(".d.ts") || s.endsWith(".js.map"); + }, + }); +} + +// 2. Rename emitted files to the .mjs / .d.mts extensions, fixing sourcemap refs. +function renameEmitted(root) { + for (const file of walk(root)) { + if (file.endsWith(".js.map")) { + const map = JSON.parse(readFileSync(file, "utf-8")); + if (typeof map.file === "string") map.file = map.file.replace(/\.js$/, ".mjs"); + writeFileSync(file, JSON.stringify(map), "utf-8"); + renameSync(file, `${file.slice(0, -".js.map".length)}.mjs.map`); + } + } + for (const file of walk(root)) { + if (file.endsWith(".d.ts")) { + renameSync(file, `${file.slice(0, -".d.ts".length)}.d.mts`); + } else if (file.endsWith(".js")) { + let content = readFileSync(file, "utf-8"); + content = content.replace(/(\/\/# sourceMappingURL=)(.*?)\.js\.map(\s*)$/, "$1$2.mjs.map$3"); + writeFileSync(file, content, "utf-8"); + renameSync(file, `${file.slice(0, -".js".length)}.mjs`); + } + } +} + +// 3. YAML runtime modules: mirror every src/*.yml|*.yaml into dist as `.mjs`. +function emitYamlModules() { + for (const file of walk(srcDir)) { + if (!file.endsWith(".yml") && !file.endsWith(".yaml")) continue; + const rel = path.relative(srcDir, file); + const target = path.join(distDir, `${rel}.mjs`); + mkdirSync(path.dirname(target), { recursive: true }); + writeFileSync( + target, + `export default ${JSON.stringify(readFileSync(file, "utf-8"))};\n`, + "utf-8", + ); + } +} + +// Resolve a now-extensionless / .mjs target path to an existing dist file, +// preferring `.mjs` then `/index.mjs`. Returns the posix relative +// specifier (with leading ./) or null when nothing matches. +function resolveSpecifier(fromFile, base) { + const abs = path.resolve(path.dirname(fromFile), base); + const candidates = [`${abs}.mjs`, path.join(abs, "index.mjs")]; + const isDecl = fromFile.endsWith(".d.mts"); + if (isDecl) { + candidates.unshift(`${abs}.d.mts`, path.join(abs, "index.d.mts")); + } + for (const cand of candidates) { + if (existsSync(cand)) { + let chosen = cand; + // For .d.mts resolution we still emit the runtime `.mjs` specifier. + if (chosen.endsWith(".d.mts")) chosen = `${chosen.slice(0, -".d.mts".length)}.mjs`; + let rel = path.relative(path.dirname(fromFile), chosen).split(path.sep).join("/"); + if (!rel.startsWith(".")) rel = `./${rel}`; + return rel; + } + } + return null; +} + +// 4. Rewrite all module specifiers across dist (.mjs and .d.mts). +function rewriteSpecifiers() { + // Captures the leading keyword/paren, the quote, and the specifier. + const re = /((?:from|import|export)\s*(?:\(\s*)?)(["'])([^"']+)\2/g; + for (const file of walk(distDir)) { + if (!file.endsWith(".mjs") && !file.endsWith(".d.mts")) continue; + const content = readFileSync(file, "utf-8"); + const next = content.replace(re, (match, lead, quote, spec) => { + const rewritten = rewriteSpec(file, spec); + return rewritten === null ? match : `${lead}${quote}${rewritten}${quote}`; + }); + if (next !== content) writeFileSync(file, next, "utf-8"); + } +} + +function rewriteSpec(file, spec) { + // @tailor-proto/tailor/v1/X -> dist/_proto/tailor/v1/X (resolve in vendored tree) + if (spec.startsWith("@tailor-proto/")) { + const sub = spec.slice("@tailor-proto/".length); // e.g. tailor/v1/service_pb + const abs = path.join(protoDistDir, sub); + return ( + resolveSpecifier(file, path.relative(path.dirname(file), abs).split(path.sep).join("/")) ?? + toRel(file, `${abs}.mjs`) + ); + } + // @/X -> dist/X + if (spec.startsWith("@/")) { + const abs = path.join(distDir, spec.slice("@/".length)); + return ( + resolveSpecifier(file, path.relative(path.dirname(file), abs).split(path.sep).join("/")) ?? + toRel(file, `${abs}.mjs`) + ); + } + // Relative specifiers, including the bare `.`/`..` directory forms. + if (spec === "." || spec === ".." || spec.startsWith("./") || spec.startsWith("../")) { + if (spec.endsWith(".yml")) return `${spec}.mjs`; + if (spec.endsWith(".yaml")) return `${spec}.mjs`; + const base = spec.endsWith(".js") ? spec.slice(0, -3) : spec; + if (spec.endsWith(".json")) return null; + return resolveSpecifier(file, base) ?? (spec.endsWith(".js") ? `${base}.mjs` : `${spec}.mjs`); + } + return null; +} + +function toRel(fromFile, abs) { + let rel = path.relative(path.dirname(fromFile), abs).split(path.sep).join("/"); + if (!rel.startsWith(".")) rel = `./${rel}`; + return rel; +} + +// 5. Shebang + executable bit for CLI entrypoints (bin targets in package.json). +function addShebangs() { + const shebang = "#!/usr/bin/env node\n"; + for (const rel of ["cli/index.mjs", "cli/skills.mjs"]) { + const file = path.join(distDir, rel); + const content = readFileSync(file, "utf-8"); + if (!content.startsWith("#!")) writeFileSync(file, shebang + content, "utf-8"); + chmodSync(file, 0o755); + } +} + +// 6. Banner on configure/index.d.mts only. +function addBanner() { + const banner = '/// \n'; + const file = path.join(distDir, "configure/index.d.mts"); + const content = readFileSync(file, "utf-8"); + if (!content.startsWith(banner)) writeFileSync(file, banner + content, "utf-8"); +} + +// 7. ERD viewer assets. +function copyErdAssets() { + const source = path.join(srcDir, "cli/commands/tailordb/erd/viewer-assets"); + const target = path.join(distDir, "cli/erd-viewer-assets"); + rmSync(target, { recursive: true, force: true }); + cpSync(source, target, { recursive: true }); +} + +// Entry .js files tsgo must have emitted; their absence means tsgo failed to +// emit (as opposed to merely exiting non-zero on benign diagnostics). +const REQUIRED_ENTRIES = [ + "configure/index.js", + "cli/index.js", + "cli/skills.js", + "runtime/index.js", +]; + +function main() { + if (!existsSync(distDir) || !statSync(distDir).isDirectory()) { + throw new Error(`dist not found at ${distDir}; tsgo did not emit`); + } + for (const rel of REQUIRED_ENTRIES) { + if (!existsSync(path.join(distDir, rel))) { + throw new Error(`expected tsgo output missing: dist/${rel}; tsgo emit failed`); + } + } + vendorProto(); + renameEmitted(distDir); + emitYamlModules(); + rewriteSpecifiers(); + addShebangs(); + addBanner(); + copyErdAssets(); +} + +main(); diff --git a/packages/sdk/src/cli/shared/client.ts b/packages/sdk/src/cli/shared/client.ts index 94c54da632..4eb67c571a 100644 --- a/packages/sdk/src/cli/shared/client.ts +++ b/packages/sdk/src/cli/shared/client.ts @@ -42,7 +42,7 @@ export type OperatorClient = Client; * @param accessToken - Access token for authentication * @returns Configured Operator client */ -export async function initOperatorClient(accessToken: string) { +export async function initOperatorClient(accessToken: string): Promise { const { createTracingInterceptor } = await import("@/cli/telemetry/interceptor"); const interceptors: Interceptor[] = [ diff --git a/packages/sdk/tsconfig.build.json b/packages/sdk/tsconfig.build.json new file mode 100644 index 0000000000..70e0318abe --- /dev/null +++ b/packages/sdk/tsconfig.build.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "preserve", + "moduleResolution": "bundler", + "esModuleInterop": true, + "skipLibCheck": true, + "strict": true, + "noUncheckedIndexedAccess": true, + "resolveJsonModule": true, + "isolatedModules": true, + "allowImportingTsExtensions": true, + "declaration": true, + "sourceMap": true, + "noEmit": false, + "noEmitOnError": false, + "outDir": "./dist", + "rootDir": "./src", + "paths": { + "@/*": ["./src/*"], + "@tailor-proto/*": ["../tailor-proto/src/tailor/v1/*"] + } + }, + "include": ["src/**/*.ts"], + "exclude": [ + "node_modules", + "dist", + "**/*.test.ts", + "**/*.spec.ts", + "**/__test_fixtures__/**", + "**/viewer-assets/**", + "e2e" + ] +} diff --git a/packages/sdk/tsconfig.json b/packages/sdk/tsconfig.json index f0715f03d3..b9630d0816 100644 --- a/packages/sdk/tsconfig.json +++ b/packages/sdk/tsconfig.json @@ -17,7 +17,6 @@ "./scripts/**/*.ts", "./eslint-rules/**/*.ts", "./e2e/**/*.ts", - "./tsdown.config.ts", "./vitest.config.ts", "./zinfer.config.ts" ], diff --git a/packages/sdk/tsdown.config.ts b/packages/sdk/tsdown.config.ts deleted file mode 100644 index f4c0b9a905..0000000000 --- a/packages/sdk/tsdown.config.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { cpSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; -import path from "node:path"; -import Sonda from "sonda/rolldown"; -import { defineConfig, type TsdownPluginOption } from "tsdown"; -import { loadYamlText } from "./scripts/yaml-text-plugin.mjs"; - -// `banner.dts` injects the triple-slash into every emitted d.mts. Keep it only -// on `configure/index.d.mts` (the `@tailor-platform/sdk` main entry) so that -// the legacy ambient globals stay active for that import path through v2.0. -// Strip it from every other `.d.mts` so subpath imports -// (`@tailor-platform/sdk/runtime`, `/vitest`, /plugin`, etc.) stay self-contained. -function stripBannerExceptConfigureEntry(outDir: string): void { - const pattern = /^\/\/\/ \r?\n/; - const root = path.resolve(outDir); - const keep = path.join(root, "configure", "index.d.mts"); - const walk = (dir: string): void => { - for (const entry of readdirSync(dir, { withFileTypes: true })) { - const full = path.join(dir, entry.name); - if (entry.isDirectory()) { - walk(full); - } else if (entry.isFile() && entry.name.endsWith(".d.mts") && full !== keep) { - const content = readFileSync(full, "utf-8"); - const cleaned = content.replace(pattern, ""); - if (cleaned !== content) writeFileSync(full, cleaned, "utf-8"); - } - } - }; - walk(root); -} - -function copyErdViewerAssets(outDir: string): void { - const source = path.resolve("src/cli/commands/tailordb/erd/viewer-assets"); - const target = path.resolve(outDir, "cli/erd-viewer-assets"); - rmSync(target, { recursive: true, force: true }); - cpSync(source, target, { recursive: true }); -} - -function yamlText() { - return { - name: "yaml-text", - load(id: string) { - const result = loadYamlText(id); - return result ? { code: result } : undefined; - }, - }; -} - -// Annotate as TsdownPluginOption[] to work around a tsgo TS2321 caused by -// rolldown's Plugin type appearing under two paths in node_modules (root -// rc.17 from tsdown's pin, packages/sdk rc.18 from our direct dep). tsc -// handles this fine; tsgo's recursive Plugin comparison gets stuck. -const plugins: TsdownPluginOption[] = [ - yamlText(), - Sonda({ - open: false, - format: "json", - filename: "bundle-analysis.json", - deep: true, - }) as TsdownPluginOption, -]; - -export default defineConfig({ - entry: [ - "src/configure/index.ts", - "src/cli/index.ts", - "src/cli/lib.ts", - "src/cli/skills.ts", - "src/utils/test/index.ts", - "src/kysely/index.ts", - "src/plugin/index.ts", - "src/plugin/builtin/kysely-type/index.ts", - "src/plugin/builtin/enum-constants/index.ts", - "src/plugin/builtin/file-utils/index.ts", - "src/plugin/builtin/seed/index.ts", - "src/seed/index.ts", - "src/vitest/index.ts", - "src/vitest/environment.ts", - "src/vitest/setup.ts", - "src/runtime/index.ts", - "src/runtime/globals.ts", - "src/runtime/iconv.ts", - "src/runtime/secretmanager.ts", - "src/runtime/authconnection.ts", - "src/runtime/idp.ts", - "src/runtime/workflow.ts", - "src/runtime/context.ts", - "src/runtime/file.ts", - ], - format: ["esm"], - target: "node22", - platform: "node", - clean: true, - dts: true, - outDir: "dist", - tsconfig: "./tsconfig.json", - minify: false, - outExtensions: () => ({ - js: ".mjs", - dts: ".d.mts", - }), - // Remove in v2.0. - banner: { - dts: '/// ', - }, - // peer dependencies: prevent bundling, resolve at runtime - deps: { neverBundle: ["vite", "vitest"] }, - sourcemap: true, - plugins, - onSuccess: (config) => { - stripBannerExceptConfigureEntry(config.outDir); - copyErdViewerAssets(config.outDir); - }, -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9b1fac8d03..502cd710aa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -558,12 +558,6 @@ importers: oxlint-tsgolint: specifier: 0.23.0 version: 0.23.0 - sonda: - specifier: 0.13.0 - version: 0.13.0 - tsdown: - specifier: 0.22.2 - version: 0.22.2(@typescript/native-preview@7.0.0-dev.20260614.1)(oxc-resolver@11.20.0)(publint@0.3.21)(tsx@4.22.4)(typescript@6.0.3) typescript: specifier: 6.0.3 version: 6.0.3 @@ -1184,9 +1178,6 @@ packages: '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} - '@jridgewell/remapping@2.3.5': - resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} - '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -3525,11 +3516,6 @@ packages: resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==} engines: {node: '>= 18'} - sonda@0.13.0: - resolution: {integrity: sha512-rV41e4LDL00jcDijXrP7jhkbpCYMiDKynl9TWq2hv1yzC6v2fJiFMaOpUiipmJmhe1Gf360gwwUqSwPQ0ear1A==} - engines: {node: '>=22.12'} - hasBin: true - source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -3615,9 +3601,6 @@ packages: resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} engines: {node: '>=6'} - tiny-open@1.3.0: - resolution: {integrity: sha512-GUFS8yjJZq0oWqCKCJVHcBgMpmi2WEGXY1le3E5ncR0DsgTII5uUyxtfk8/vGyDDGBE42UYHR6Ocvlk8mkXSRg==} - tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -4354,11 +4337,6 @@ snapshots: '@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/trace-mapping': 0.3.31 - '@jridgewell/remapping@2.3.5': - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/sourcemap-codec@1.5.5': {} @@ -6306,11 +6284,6 @@ snapshots: smol-toml@1.6.1: {} - sonda@0.13.0: - dependencies: - '@jridgewell/remapping': 2.3.5 - tiny-open: 1.3.0 - source-map-js@1.2.1: {} source-map@0.6.1: @@ -6385,8 +6358,6 @@ snapshots: tapable@2.3.3: {} - tiny-open@1.3.0: {} - tinybench@2.9.0: {} tinyexec@1.2.4: {} diff --git a/scripts/measure-bundle-size-sonda.js b/scripts/measure-bundle-size-sonda.js deleted file mode 100644 index a70503fafc..0000000000 --- a/scripts/measure-bundle-size-sonda.js +++ /dev/null @@ -1,95 +0,0 @@ -#!/usr/bin/env node -import fs from "fs"; -import path from "path"; -import { fileURLToPath } from "url"; -import madge from "madge"; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const sdkDir = path.join(__dirname, "../packages/sdk"); -const sondaOutput = path.join(sdkDir, ".sonda/bundle-analysis.json"); - -function getChunkSize(report, distPath) { - const normalized = distPath.startsWith("dist/") ? distPath : `dist/${distPath}`; - - const entry = report.resources.find( - (r) => r.kind === "asset" && path.normalize(r.name) === path.normalize(normalized), - ); - return entry?.uncompressed ?? 0; -} - -async function collectDependencies(entryFile) { - const result = await madge(entryFile, { - fileExtensions: ["mjs", "js"], - excludeRegExp: [/node_modules/], - baseDir: sdkDir, - }); - - const dependencyObj = result.obj(); - const visited = new Set(); - const deps = []; - - function traverse(file) { - if (visited.has(file)) return; - visited.add(file); - - const fileDeps = dependencyObj[file] || []; - for (const dep of fileDeps) { - const absolutePath = path.resolve(sdkDir, dep); - deps.push(absolutePath); - traverse(dep); - } - } - - const relativeEntry = path.relative(sdkDir, entryFile); - traverse(relativeEntry); - - return deps; -} - -async function main() { - if (!fs.existsSync(sondaOutput)) { - console.error(`Error: Sonda output not found at ${sondaOutput}`); - console.error("Please run 'pnpm build' first"); - process.exit(1); - } - - const report = JSON.parse(fs.readFileSync(sondaOutput, "utf8")); - - const entryPath = path.join(sdkDir, "dist/configure/index.mjs"); - const configureSize = getChunkSize(report, "dist/configure/index.mjs"); - - const deps = await collectDependencies(entryPath); - const depsSize = deps.reduce((sum, dep) => { - const relativePath = path.relative(sdkDir, dep); - return sum + getChunkSize(report, relativePath); - }, 0); - - const totalSize = configureSize + depsSize; - - const output = { - key: "bundle-size", - name: "SDK Configure Bundle Size", - metrics: [ - { - key: "configure-index-size", - value: +(configureSize / 1024).toFixed(2), - unit: "KB", - }, - { - key: "dependency-chunks-size", - value: +(depsSize / 1024).toFixed(2), - unit: "KB", - }, - { - key: "total-bundle-size", - value: +(totalSize / 1024).toFixed(2), - unit: "KB", - }, - ], - }; - - const outputPath = path.join(sdkDir, "bundle-size.json"); - fs.writeFileSync(outputPath, JSON.stringify(output, null, 2) + "\n"); -} - -main(); From fcc0c6b808dba0f1b18bce77e5265cdcf1e64605 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 18 Jun 2026 22:45:14 +0900 Subject: [PATCH 2/2] fix(sdk): rewrite dist specifiers via AST in tsgo postbuild MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The postbuild specifier rewrite matched raw text, so import-like text inside string literals and comments was rewritten too — corrupting the generated migration-script template (`./db` -> `./db.mjs`). Locate specifiers via the oxc AST (static import/export, dynamic import, and type-level `import()`), leaving template/comment text untouched. Also harden the build: - derive the post-emit existence check from every package.json exports/bin entry instead of four hand-listed ones, so a broken emit of any published entry is caught despite the ignored tsgo exit code - align the tsconfig.build.json `@tailor-proto` path alias with the canonical tsconfig.json mapping (was a doubled `tailor/v1` segment) - anchor the sourceMappingURL rewrite per-line so trailing content cannot leave a stale `.js.map` reference --- packages/sdk/scripts/postbuild-tsgo.mjs | 100 +++++++++++++++++++----- packages/sdk/tsconfig.build.json | 2 +- 2 files changed, 82 insertions(+), 20 deletions(-) diff --git a/packages/sdk/scripts/postbuild-tsgo.mjs b/packages/sdk/scripts/postbuild-tsgo.mjs index 2a9d6fdbad..47228c8393 100644 --- a/packages/sdk/scripts/postbuild-tsgo.mjs +++ b/packages/sdk/scripts/postbuild-tsgo.mjs @@ -9,7 +9,8 @@ // dist/_proto so the workspace-only package travels with the publish // 2. rename .js -> .mjs, .d.ts -> .d.mts, .js.map -> .mjs.map (fix map refs) // 3. emit `.mjs` runtime modules for imported YAML files (tsgo skips .yml) -// 4. rewrite every module specifier in dist: +// 4. rewrite every module specifier in dist (located via the oxc AST, so +// import-like text inside string literals/comments is never touched): // - `@/x` -> relative path into dist // - `@tailor-proto/tailor/v1/x` -> relative path into dist/_proto // - extensionless `./x`/`../x` -> `./x.mjs` or `./x/index.mjs` @@ -32,6 +33,7 @@ import { } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; +import { parseSync } from "oxc-parser"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const pkgRoot = path.resolve(__dirname, ".."); @@ -85,7 +87,7 @@ function renameEmitted(root) { renameSync(file, `${file.slice(0, -".d.ts".length)}.d.mts`); } else if (file.endsWith(".js")) { let content = readFileSync(file, "utf-8"); - content = content.replace(/(\/\/# sourceMappingURL=)(.*?)\.js\.map(\s*)$/, "$1$2.mjs.map$3"); + content = content.replace(/(\/\/# sourceMappingURL=)(.*?)\.js\.map$/m, "$1$2.mjs.map"); writeFileSync(file, content, "utf-8"); renameSync(file, `${file.slice(0, -".js".length)}.mjs`); } @@ -130,18 +132,67 @@ function resolveSpecifier(fromFile, base) { return null; } -// 4. Rewrite all module specifiers across dist (.mjs and .d.mts). +// Collect the string-literal source spans of type-level `import("x")` queries +// (TSImportType), which the module record does not report but `.d.mts` files +// rely on heavily. +function collectImportTypeSpans(node, out) { + if (!node || typeof node !== "object") return; + if (Array.isArray(node)) { + for (const child of node) collectImportTypeSpans(child, out); + return; + } + if (node.type === "TSImportType" && node.source && typeof node.source.start === "number") { + out.push(node.source); + } + for (const key of Object.keys(node)) { + if (key !== "type") collectImportTypeSpans(node[key], out); + } +} + +// Collect the quote-inclusive source spans of every real module specifier +// (static import/export, re-export, runtime dynamic import, and type-level +// `import("x")`) via the oxc AST, deduped by start offset and sorted descending +// so splicing keeps offsets valid. +function specifierSpans(program, moduleRecord) { + const byStart = new Map(); + const add = (mr) => { + if (mr && typeof mr.start === "number") byStart.set(mr.start, mr); + }; + for (const s of moduleRecord.staticImports ?? []) add(s.moduleRequest); + for (const s of moduleRecord.staticExports ?? []) { + for (const e of s.entries ?? []) add(e.moduleRequest); + } + for (const s of moduleRecord.dynamicImports ?? []) add(s.moduleRequest); + const importTypes = []; + collectImportTypeSpans(program.body, importTypes); + for (const src of importTypes) add(src); + return [...byStart.values()].toSorted((a, b) => b.start - a.start); +} + +// 4. Rewrite all module specifiers across dist (.mjs and .d.mts). Specifiers are +// located via the parsed AST, so import-like text inside template strings or +// comments (e.g. code-generator templates) is left untouched. function rewriteSpecifiers() { - // Captures the leading keyword/paren, the quote, and the specifier. - const re = /((?:from|import|export)\s*(?:\(\s*)?)(["'])([^"']+)\2/g; for (const file of walk(distDir)) { if (!file.endsWith(".mjs") && !file.endsWith(".d.mts")) continue; - const content = readFileSync(file, "utf-8"); - const next = content.replace(re, (match, lead, quote, spec) => { + const source = readFileSync(file, "utf-8"); + const { program, module: moduleRecord, errors } = parseSync(file, source); + if (errors.length) { + throw new Error( + `failed to parse emitted ${path.relative(distDir, file)}: ${errors[0].message}`, + ); + } + let next = source; + for (const mr of specifierSpans(program, moduleRecord)) { + const quote = source[mr.start]; + // Skip template-literal or otherwise non-string-literal specifiers. + if (quote !== '"' && quote !== "'") continue; + const spec = source.slice(mr.start + 1, mr.end - 1); const rewritten = rewriteSpec(file, spec); - return rewritten === null ? match : `${lead}${quote}${rewritten}${quote}`; - }); - if (next !== content) writeFileSync(file, next, "utf-8"); + if (rewritten === null) continue; + next = `${next.slice(0, mr.start)}${quote}${rewritten}${quote}${next.slice(mr.end)}`; + } + if (next !== source) writeFileSync(file, next, "utf-8"); } } @@ -207,20 +258,31 @@ function copyErdAssets() { cpSync(source, target, { recursive: true }); } -// Entry .js files tsgo must have emitted; their absence means tsgo failed to -// emit (as opposed to merely exiting non-zero on benign diagnostics). -const REQUIRED_ENTRIES = [ - "configure/index.js", - "cli/index.js", - "cli/skills.js", - "runtime/index.js", -]; +// Pre-rename `.js` files tsgo must have emitted, derived from every published +// `package.json` exports/bin target (~24 entries). The build wrapper ignores +// tsgo's non-zero exit, so without this a broken emit of any published entry — +// not just the four hand-listed ones — would ship silently. +function requiredEntries() { + const pkg = JSON.parse(readFileSync(path.join(pkgRoot, "package.json"), "utf-8")); + const entries = new Set(); + const add = (target) => { + if (typeof target === "string" && target.startsWith("./dist/") && target.endsWith(".mjs")) { + entries.add(`${target.slice("./dist/".length, -".mjs".length)}.js`); + } + }; + for (const condition of Object.values(pkg.exports ?? {})) { + if (typeof condition === "string") add(condition); + else for (const target of Object.values(condition ?? {})) add(target); + } + for (const target of Object.values(pkg.bin ?? {})) add(target); + return [...entries]; +} function main() { if (!existsSync(distDir) || !statSync(distDir).isDirectory()) { throw new Error(`dist not found at ${distDir}; tsgo did not emit`); } - for (const rel of REQUIRED_ENTRIES) { + for (const rel of requiredEntries()) { if (!existsSync(path.join(distDir, rel))) { throw new Error(`expected tsgo output missing: dist/${rel}; tsgo emit failed`); } diff --git a/packages/sdk/tsconfig.build.json b/packages/sdk/tsconfig.build.json index 70e0318abe..77dd7ad663 100644 --- a/packages/sdk/tsconfig.build.json +++ b/packages/sdk/tsconfig.build.json @@ -18,7 +18,7 @@ "rootDir": "./src", "paths": { "@/*": ["./src/*"], - "@tailor-proto/*": ["../tailor-proto/src/tailor/v1/*"] + "@tailor-proto/*": ["../tailor-proto/src/*"] } }, "include": ["src/**/*.ts"],