From 4bbf7ac361dc82e0ce8bfa8a4ade155e0a815499 Mon Sep 17 00:00:00 2001 From: Brandon Corbett Date: Sat, 12 Sep 2026 19:19:46 -0700 Subject: [PATCH] fix(verify): pack the client core alongside react for a workspace checkout The client SDK repo is becoming an npm workspace that publishes @seamless-auth/client next to @seamless-auth/react, and the react tarball depends on the client one. Packing only the root of such a checkout produces the private root package, and installing the react tarball alone goes to the registry for a client version that is not published yet. The harness reads workspaces from the checkout's package.json, packs both packages when it finds one, and the react image installs every tarball in one npm install so the dependency resolves from the sibling file. A checkout that predates the workspace is packed as before. --- .changeset/verify-pack-client-workspace.md | 16 +++++++++ src/commands/verify.test.ts | 39 ++++++++++++++++++++++ src/commands/verify.ts | 31 ++++++++++++++--- verify/docker-compose.verify.yml | 10 +++--- 4 files changed, 88 insertions(+), 8 deletions(-) create mode 100644 .changeset/verify-pack-client-workspace.md diff --git a/.changeset/verify-pack-client-workspace.md b/.changeset/verify-pack-client-workspace.md new file mode 100644 index 0000000..4ef40e8 --- /dev/null +++ b/.changeset/verify-pack-client-workspace.md @@ -0,0 +1,16 @@ +--- +'seamless-cli': patch +--- + +`seamless verify --local` packs `@seamless-auth/client` alongside `@seamless-auth/react` +when the React SDK checkout is an npm workspace. + +The client SDK repo is becoming a workspace that publishes a framework-agnostic +`@seamless-auth/client` next to `@seamless-auth/react`, and the react tarball depends on +the client one. Packing only the root of such a checkout would produce the private root +package, and installing the react tarball alone would go to the registry for a client +version that is not published yet. The harness now reads `workspaces` from the checkout's +`package.json`, packs both packages when it finds one, and the react image installs every +tarball in one `npm install` so the dependency resolves from the sibling file. A checkout +that predates the workspace is packed as before. The version line reported for +`@seamless-auth/react` reads the react package's own manifest in a workspace. diff --git a/src/commands/verify.test.ts b/src/commands/verify.test.ts index 1e9cb78..35cec11 100644 --- a/src/commands/verify.test.ts +++ b/src/commands/verify.test.ts @@ -281,6 +281,45 @@ describe("runVerify — local mode", () => { expect(exitSpy).not.toHaveBeenCalled(); }); + it("packs the react SDK as a single package when the checkout has no workspaces", async () => { + await runVerify(["--local"]); + + const packs = callsFor("npm").filter((a) => a[0] === "pack"); + expect(packs).toEqual([["pack", "--pack-destination", expect.stringContaining("react-vendor")]]); + }); + + it("packs the client core alongside react when the checkout is a workspace", async () => { + vi.mocked(fs.readFileSync).mockImplementation((p: never) => { + const s = String(p); + if (s.endsWith("registry.json")) return REGISTRY_JSON as never; + if (s.endsWith("template.json")) + return JSON.stringify({ verify: { flows: ["oauth"] } }) as never; + if (s === "/fake/reactsdk/package.json") + return JSON.stringify({ private: true, workspaces: ["packages/*"] }) as never; + if (s === "/fake/reactsdk/packages/react/package.json") + return JSON.stringify({ version: "0.13.0" }) as never; + return PKG_JSON as never; + }); + + await runVerify(["--local"]); + + const packs = callsFor("npm").filter((a) => a[0] === "pack"); + expect(packs).toEqual([ + [ + "pack", + "-w", + "@seamless-auth/client", + "-w", + "@seamless-auth/react", + "--pack-destination", + expect.stringContaining("react-vendor"), + ], + ]); + // The version line reads the react package's manifest, not the private root. + expect(logSpy.mock.calls.flat().join("\n")).toContain("@seamless-auth/react"); + expect(logSpy.mock.calls.flat().join("\n")).toContain("0.13.0"); + }); + it("falls back to sibling checkouts when the dir env vars are unset", async () => { // Exercises the default path.resolve(...) branches for the templates root and // the react SDK dir; everything "exists" so resolution succeeds. diff --git a/src/commands/verify.ts b/src/commands/verify.ts index e712d71..34dd797 100644 --- a/src/commands/verify.ts +++ b/src/commands/verify.ts @@ -178,13 +178,31 @@ function cleanVendor(): void { } } -// Build + pack the local @seamless-auth/react into ./react-vendor so the react -// service installs it over the published version (--local browser runs). +// The client SDK repo became an npm workspace publishing @seamless-auth/client +// alongside @seamless-auth/react. The react tarball depends on the client one, +// so both are packed into the vendor dir and the image installs them together. +// A checkout that predates the workspace still packs its single root package. +function reactSdkWorkspaces(sdkDir: string): string[] { + const workspaces = readJson(path.join(sdkDir, "package.json"))?.workspaces; + return Array.isArray(workspaces) && workspaces.length > 0 + ? ["@seamless-auth/client", "@seamless-auth/react"] + : []; +} + +// Build + pack the local @seamless-auth/react (and its client core when the +// checkout is a workspace) into ./react-vendor so the react service installs it +// over the published version (--local browser runs). async function packLocalReactSdk(env: NodeJS.ProcessEnv): Promise { const sdkDir = resolveReactSdkDir(); console.log(kleur.cyan("→ Building & packing local @seamless-auth/react…")); await runCommand("npm", ["run", "build"], sdkDir, env); - await runCommand("npm", ["pack", "--pack-destination", REACT_VENDOR_DIR], sdkDir, env); + const workspaceArgs = reactSdkWorkspaces(sdkDir).flatMap((pkg) => ["-w", pkg]); + await runCommand( + "npm", + ["pack", ...workspaceArgs, "--pack-destination", REACT_VENDOR_DIR], + sdkDir, + env, + ); } // Each adapter image installs core plus its own framework package, so the @@ -299,7 +317,12 @@ function collectPackageVersions( } if (opts.react) { try { - push("@seamless-auth/react", readPkgVersion(path.join(resolveReactSdkDir(), "package.json"))); + const sdkDir = resolveReactSdkDir(); + const reactPkg = + reactSdkWorkspaces(sdkDir).length > 0 + ? path.join(sdkDir, "packages", "react", "package.json") + : path.join(sdkDir, "package.json"); + push("@seamless-auth/react", readPkgVersion(reactPkg)); } catch { // React SDK checkout unavailable. } diff --git a/verify/docker-compose.verify.yml b/verify/docker-compose.verify.yml index 622c75c..fec8eec 100644 --- a/verify/docker-compose.verify.yml +++ b/verify/docker-compose.verify.yml @@ -154,8 +154,10 @@ services: # Gated behind the `react` profile so the api/adapter-only runs skip the build. # # The inline Dockerfile mirrors the starter's own Dockerfile, but additionally - # installs a locally-built @seamless-auth/react tarball when present (--local - # mode packs it into ./react-vendor; that dir is empty otherwise → published SDK). + # installs locally-built @seamless-auth/react (and @seamless-auth/client) + # tarballs when present (--local mode packs them into ./react-vendor; that dir + # is empty otherwise → published SDK). Installed in one command so npm resolves + # react's client dependency from the sibling tarball rather than the registry. react: profiles: ['react'] build: @@ -169,8 +171,8 @@ services: RUN npm ci COPY . . COPY --from=sdk . /tmp/sdk/ - RUN if ls /tmp/sdk/seamless-auth-react-*.tgz >/dev/null 2>&1; then \ - npm install /tmp/sdk/seamless-auth-react-*.tgz; \ + RUN if ls /tmp/sdk/*.tgz >/dev/null 2>&1; then \ + npm install /tmp/sdk/*.tgz; \ fi RUN npm run build FROM nginx:alpine