{"_id":"@aiquants/platform-packaging","name":"@aiquants/platform-packaging","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@aiquants/platform-packaging","version":"0.1.0","description":"Build tooling that turns a Go command into per-platform npm packages (esbuild/Biome-style os+cpu split with optionalDependencies, bin-field executable bits, and publish-order guards)","keywords":["go","binary","npm","platform","optionaldependencies","cross-compile","publish","packaging"],"type":"module","exports":{".":"./src/index.mjs"},"engines":{"node":">=20"},"license":"MIT","author":{"name":"fehde-k","url":"https://x.com/fehdek"},"repository":{"type":"git","url":"git+https://github.com/fehde-k/aiquants.git","directory":"packages/platform-packaging"},"homepage":"https://github.com/fehde-k/aiquants/tree/main/packages/platform-packaging#readme","bugs":{"url":"https://github.com/fehde-k/aiquants/issues"},"publishConfig":{"access":"public"},"scripts":{"build":"node --check src/build.mjs && node --check src/publish.mjs && node --check src/registry.mjs && node --check src/index.mjs","test":"node --test tests/*.test.mjs","lint":"biome check src","lint:fix":"biome check --write src","format":"biome format --write src","format:check":"biome format src","publish:patch":"pnpm run build && pnpm run test && pnpm version patch --no-git-tag-version --no-git-checks && pnpm publish --no-git-checks","publish:minor":"pnpm run build && pnpm run test && pnpm version minor --no-git-tag-version --no-git-checks && pnpm publish --no-git-checks","publish:major":"pnpm run build && pnpm run test && pnpm version major --no-git-tag-version --no-git-checks && pnpm publish --no-git-checks"},"_nodeVersion":"26.0.0","_id":"@aiquants/platform-packaging@0.1.0","dist":{"integrity":"sha512-qjE3sCwq5GWKSbhTPchTmIXLYPtTiPybj+FqBS5RiOjPT32gHjU07B4U79X3xiBacEt9X3vukQzGTU41rrGYsw==","shasum":"e991d87cd1105034e76e75ce0acf9bcc7a5655f7","tarball":"https://registry.npmjs.org/@aiquants/platform-packaging/-/platform-packaging-0.1.0.tgz","fileCount":7,"unpackedSize":41451,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDWK3VC4abKJSDjauIYFCndbwZtgDb1EE/rh6tAfvtuXgIgSeEHSsB7p4eb3zpbmvoaWisplgGp1evQXY2rs4AIhLU="}]},"_npmUser":{"name":"fehde","email":"genbu0498@gmail.com"},"directories":{},"maintainers":[{"name":"fehde-k","email":"owner@aiquants.co.jp"},{"name":"fehde","email":"genbu0498@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/platform-packaging_0.1.0_1785396788951_0.2072252470661171"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-30T07:33:08.733Z","0.1.0":"2026-07-30T07:33:09.092Z","modified":"2026-07-30T07:33:09.354Z"},"maintainers":[{"name":"fehde-k","email":"owner@aiquants.co.jp"},{"name":"fehde","email":"genbu0498@gmail.com"}],"description":"Build tooling that turns a Go command into per-platform npm packages (esbuild/Biome-style os+cpu split with optionalDependencies, bin-field executable bits, and publish-order guards)","homepage":"https://github.com/fehde-k/aiquants/tree/main/packages/platform-packaging#readme","keywords":["go","binary","npm","platform","optionaldependencies","cross-compile","publish","packaging"],"repository":{"type":"git","url":"git+https://github.com/fehde-k/aiquants.git","directory":"packages/platform-packaging"},"author":{"name":"fehde-k","url":"https://x.com/fehdek"},"bugs":{"url":"https://github.com/fehde-k/aiquants/issues"},"license":"MIT","readme":"# `@aiquants/platform-packaging`\n\nBuild tooling that turns a Go command into per-platform npm packages, using the\nsame distribution model as esbuild, Biome, and turbo:\n\n- one small parent package (JavaScript only, no binary),\n- one `<parent>-<platform>-<arch>` package per target holding exactly one binary,\n- the parent references them through `optionalDependencies`, so npm/pnpm/yarn\n  downloads only the package matching the installing machine.\n\n## Why this exists\n\nShipping a Go binary inside a single npm package forces every consumer to\ndownload every platform's binary, breaks the executable bit unless a\n`postinstall` chmod runs (which pnpm 10+ blocks by default), and pins the\npublished binary to whatever machine ran the publish. The platform-split model\nfixes all three, but doing it by hand is a minefield of publish-ordering and\nlifecycle-script pitfalls. This package encodes the model once — it drives\n`@aiquants/storage-api` and `@aiquants/markdown`, and works the same way in any\nrepository.\n\n## Usage\n\nAdd the dev dependency (`workspace:*` inside this monorepo, a normal version\nrange anywhere else):\n\n```bash\npnpm add -D @aiquants/platform-packaging\n```\n\nRequirements: Node >= 20 and a Go toolchain on PATH (the builder shells out to\n`go build` with per-target `GOOS`/`GOARCH` and `CGO_ENABLED=0`).\n\nThen create two thin scripts in the consuming package:\n\n```js\n// scripts/build-platform-packages.mjs\nimport { runPlatformPackaging } from \"@aiquants/platform-packaging\"\n\nrunPlatformPackaging({\n    packageRoot,             // the npm package root\n    goModuleDir,             // the Go module directory\n    goPackage: \"./cmd/foo\",  // go build target (use \".\" for a root main.go)\n    binaryBaseName: \"foo\",   // binary name; \".exe\" is appended on Windows\n})\n```\n\n```js\n// scripts/publish-platform-packages.mjs\nimport { publishPlatformPackages } from \"@aiquants/platform-packaging\"\n\npublishPlatformPackages({ packageRoot })\n```\n\nAnd wire the lifecycle in `package.json`:\n\n```jsonc\n{\n  \"scripts\": {\n    \"build:platforms\": \"node scripts/build-platform-packages.mjs\",\n    \"publish:platforms\": \"node scripts/publish-platform-packages.mjs\",\n    \"prepack\": \"node scripts/build-platform-packages.mjs --inject\",\n    \"postpack\": \"node scripts/build-platform-packages.mjs --restore\",\n    // MUST stay cheap. In a pnpm workspace with injectWorkspacePackages,\n    // every `pnpm install` runs prepublishOnly for injected packages\n    // (measured) — use a cached build command (turbo shown here; any\n    // fast/cached equivalent works) and NEVER put build:platforms\n    // (6 cross-compiles) here.\n    \"prepublishOnly\": \"pnpm exec turbo run build --filter=<pkg>\",\n    \"publish:patch\": \"pnpm run typecheck && pnpm run --if-present test && pnpm version patch --no-git-tag-version --no-git-checks && pnpm run build:platforms && pnpm run publish:platforms && pnpm publish --no-git-checks\"\n  }\n}\n```\n\n## Lifecycle facts (measured on pnpm 11)\n\nWhich scripts fire in which context decides where work and guards may live:\n\n| Context | Scripts that run | `npm_command` |\n| --- | --- | --- |\n| `pnpm install` (injected workspace dep, `dedupeInjectedDeps: false`) | `prepare`, `prepublishOnly` | — |\n| `pnpm pack` | `prepack`, `prepare`, `postpack` | `pack` |\n| `pnpm publish` (incl. `--dry-run`) | `prepublishOnly`, `prepack`, `prepare`, `postpack` | `publish` |\n| `pnpm --prod deploy` | none of the above | — |\n\nConsequences baked into this package:\n\n- `prepublishOnly` runs on **every install**, so it must be turbo-cached and must\n  never cross-compile. It exists so that install-time injection packs a *built*\n  package (turbo rebuilds later do not re-sync injected copies).\n- `prepack`/`postpack` do **not** run on install, so `--inject`/`--restore` and\n  their guards never slow down or break installs.\n- `--inject` distinguishes publish from pack via `npm_command`, enabling a\n  publish-only registry guard while `pnpm pack` (tarball E2E of unpublished\n  versions) keeps working.\n\n## Rules that are easy to get wrong\n\n- **Platform packages must be published before the parent.** The parent pins them\n  at its own version, so publishing the parent first leaves a window where\n  installs cannot resolve a binary. `publish:*` must call the platform publisher\n  first.\n- **`optionalDependencies` is never committed.** It points at a version that does\n  not exist yet; committing it dirties the lockfile and breaks\n  `--frozen-lockfile` CI. `--inject` writes it at `prepack`, `--restore` reverts\n  it at `postpack`.\n- **The executable bit comes from the `bin` field, not from `postinstall`.**\n  `npm pack` normalizes file modes to `0644`, but package managers restore `0755`\n  for `bin` targets. A `postinstall` `chmod` is additionally unreliable: pnpm 10+\n  refuses dependency lifecycle scripts unless the consumer allow-lists them.\n- **`GOOS`/`GOARCH` are pinned per target and `CGO_ENABLED=0` is forced.** Without\n  this the publisher's own machine decides what ships, and a dynamically linked\n  binary breaks on any host with an older glibc.\n\n## Editing this package (monorepo-internal note)\n\nExternal consumers can skip this section. This monorepo resolves workspace\ndependencies as *injected* hardlink copies\n(`injectWorkspacePackages: true`), so a consumer keeps using the copy it was\ninstalled with. After editing anything under `src/`, run `pnpm run build` in this\npackage — `syncInjectedDepsAfterScripts` then refreshes every consumer's copy.\nThe `build` script is a syntax gate (`node --check`) whose real purpose is to\ntrigger that sync; skipping it means consumers silently run the old script.\n\n## Modes\n\n| Command | Effect |\n| --- | --- |\n| `node scripts/build-platform-packages.mjs` | Cross-compiles all targets into `platforms/<platform>-<arch>/`. |\n| `... --inject` | Backs up `package.json`, adds `optionalDependencies` for every target. |\n| `... --restore` | Restores the backed-up `package.json` and deletes the backup. |\n| `node scripts/publish-platform-packages.mjs` | Publishes every generated platform package, skipping versions already on the registry. |\n\nEach mode fails loudly rather than doing something plausible-but-wrong:\n\n- An **unrecognized mode** exits 1 instead of falling through to a build. Otherwise a\n  typo in the `prepack` argument would publish a parent with no\n  `optionalDependencies`, and every consumer would get \"binary not found\".\n- A **leftover backup** (a previous pack/publish crashed before `postpack`, so the\n  manifest is probably still injected) is handled by version: same version →\n  self-heal (restore the pristine backup, then re-inject, with a warning);\n  different version → exit 1, because silently restoring would resurrect the old\n  version.\n- `--inject` **requires a non-empty `dist/`** — packing without a build would\n  publish a tarball whose entry points do not exist.\n- `--inject` runs a **publish-only registry guard**: when `npm_command === \"publish\"`,\n  every platform package must already exist on the registry at the parent's\n  version, or it exits 1. This makes even a raw `pnpm publish` unable to ship a\n  parent whose binaries are missing. `pnpm pack` is exempt (unpublished versions\n  are normal there). The guard is **tri-state**: a 404-class answer\n  (`ERR_PNPM_PACKAGE_NOT_FOUND` / `ERR_PNPM_FETCH_404`, measured on pnpm 11) means\n  *unpublished* and prints the publish-first instruction; any other query failure\n  (network down, auth, timeout) is reported separately as *could not verify* so an\n  outage is never misdiagnosed as \"not published\". Because `pnpm view` can keep\n  serving a **stale cached 404 right after a successful publish** (observed on the\n  real 0.3.0 publish, 2026-07-30), every non-published verdict is confirmed by a\n  **direct, cache-free registry fetch** before the guard concludes; only when that\n  fetch cannot answer does the pnpm verdict stand. `pnpm publish --dry-run` also\n  passes through this guard (dry-run is not detectable from lifecycle env —\n  measured); for that rehearsal, and for verified-but-unreachable emergencies, set\n  `PLATFORM_PACKAGES_SKIP_PUBLISH_CHECK=1`.\n- The publisher **waits for registry visibility** after publishing: npmjs is\n  eventually consistent, and on the real 0.3.0 publish the six packages took\n  ~60 s to become fetchable after the publish API returned success. Without the\n  wait, the parent's prepack guard fails on every first attempt. The publisher\n  polls the registry directly (default: 5 s interval, 180 s timeout — tunable via\n  `PLATFORM_PACKAGES_VISIBILITY_TIMEOUT_MS` / `PLATFORM_PACKAGES_VISIBILITY_INTERVAL_MS`)\n  and exits 1 if visibility is not reached, so the chain stops before the parent\n  rather than at it. `PLATFORM_PACKAGES_SKIP_PUBLISH_CHECK=1` skips the wait too.\n- Direct fetches honor `PLATFORM_PACKAGES_REGISTRY_URL` (tests point it at a\n  local stub), then `npm_config_registry`, then `https://registry.npmjs.org`.\n- The publisher **requires the `platforms/` directory names to match the target set\n  one-to-one** (`<platform>-<arch>` for every target, nothing extra) and exits 1\n  listing what is missing or unexpected. A bare count check would let \"one stray\n  leftover + one missing target\" slip through; the name check closes that, so a\n  skipped or partial `build:platforms` cannot let the parent ship ahead of its\n  binaries.\n\n## Tests\n\n`pnpm run test` runs a hermetic suite (`tests/guards.test.mjs`, `node --test`)\ncovering every guard above: mode validation, inject/restore byte-exact round\ntrip, crash-leftover self-heal (same version) vs refusal (version drift vs\ncorrupted backup), dist presence, all registry-guard outcomes (including the\nstale-pnpm-cache-but-actually-published case), and the publisher's visibility\nwait (success and timeout). Registry states are simulated by a fake `pnpm`\nplaced first on `PATH` (`tests/helpers/fake-bin/pnpm`) plus a metadata stub\nserver (`tests/helpers/registry-stub.mjs`) targeted via\n`PLATFORM_PACKAGES_REGISTRY_URL` — the stub runs as a **separate process**\nbecause `spawnSync` blocks the test's own event loop. The suite needs no\nnetwork and cannot be fooled by pnpm's metadata cache. Guards call `process.exit(1)`, so every case\nruns through a subprocess driver (`tests/helpers/*-driver.mjs`) and asserts exit\ncodes plus on-disk manifest state. Cross-compilation (`--build`) is out of scope\nhere — that is covered by the real-tarball E2E documented in the consuming\npackages' test specs.\n","readmeFilename":"","_rev":"1-8331a02b193930e6450c5f9f712379a4"}