diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 96676bc..42c5368 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,7 +20,5 @@ jobs: - run: pnpm install --frozen-lockfile - name: Lint run: pnpm run lint - - name: Type Check - run: pnpm run build:types - - name: Test + - name: Build and test run: pnpm run test diff --git a/package.json b/package.json index 2213913..09a3cc8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@codex-data/sdk", - "version": "2.4.2", + "version": "2.4.3", "engines": { "node": ">=17.5.0" }, @@ -25,10 +25,11 @@ "prebuild": "pnpm run clean && pnpm run generate:configs", "clean": "rm -rf dist dist-esm", "build": "pnpm run copy:schema && pnpm run generate:graphql && pnpm run build:sdk && pnpm run codegen && pnpm run lint ./src/sdk --fix && pnpm run build:types", - "build:types": "pnpm run build:cjs && pnpm run build:esm && pnpm run build:rename-esm", + "build:types": "pnpm run build:cjs && pnpm run build:esm && pnpm run build:rename-esm && pnpm run build:edge", "build:cjs": "tsc", "build:esm": "tsc -p tsconfig.esm.json", "build:rename-esm": "find dist-esm -name '*.js' -exec sh -c 'mv \"$1\" \"${1%.js}.mjs\"' _ {} \\; && cp -r dist-esm/* dist/ && rm -rf dist-esm", + "build:edge": "esbuild src/edge.ts --bundle --format=esm --platform=neutral --packages=external --target=es2020 --outfile=dist/edge.mjs", "fetch:schema": "curl -s https://graph.codex.io/schema/latest.graphql --output src/resources/schema.graphql && cp src/resources/schema.graphql schema.graphql", "copy:schema": "cp src/resources/schema.graphql schema.graphql", "schema:diff": "tsx src/scripts/schemaDiff.ts", @@ -37,7 +38,7 @@ "build:sdk": "tsx src/scripts/buildSdk.ts", "codegen": "graphql-codegen --config codegen.ts", "lint": "eslint --ext .ts src", - "test": "jest", + "test": "pnpm run build:types && jest", "format": "prettier --write .", "prepublishOnly": "pnpm run build", "publish:beta": "pnpm publish --tag beta --access public", @@ -92,6 +93,7 @@ "chalk": "^4.1.2", "cli-spinners": "^2.9.2", "dotenv": "^16.4.5", + "esbuild": "^0.25.12", "eslint": "^8.50.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-import": "^2.28.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 71de304..f0079a9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -69,6 +69,9 @@ importers: dotenv: specifier: ^16.4.5 version: 16.6.1 + esbuild: + specifier: ^0.25.12 + version: 0.25.12 eslint: specifier: ^8.50.0 version: 8.57.1 @@ -113,7 +116,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.24))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.24))(typescript@5.9.3) tsx: specifier: ^4.19.1 version: 4.20.6 @@ -9914,7 +9917,7 @@ snapshots: dependencies: typescript: 5.9.3 - ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.24))(typescript@5.9.3): + ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.24))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 @@ -9932,6 +9935,7 @@ snapshots: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 babel-jest: 29.7.0(@babel/core@7.28.5) + esbuild: 0.25.12 jest-util: 29.7.0 ts-log@2.2.7: {} diff --git a/src/edge.bundle.test.ts b/src/edge.bundle.test.ts new file mode 100644 index 0000000..2b5d996 --- /dev/null +++ b/src/edge.bundle.test.ts @@ -0,0 +1,103 @@ +import { execFileSync } from "node:child_process"; +import { mkdirSync, mkdtempSync, rmSync, symlinkSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; + +import { build, type Metafile } from "esbuild"; + +const packageRoot = resolve(__dirname, ".."); +let fixture: string; +let bundleBytes: number; +let metafile: Metafile; + +beforeAll(async () => { + fixture = mkdtempSync(join(tmpdir(), "codex-sdk-edge-package-")); + const archive = join(fixture, "sdk.tgz"); + execFileSync( + "pnpm", + ["--config.ignore-scripts=true", "pack", "--out", archive], + { cwd: packageRoot, stdio: "pipe" }, + ); + const packageDir = join(fixture, "node_modules", "@codex-data", "sdk"); + mkdirSync(packageDir, { recursive: true }); + execFileSync("tar", [ + "-xzf", + archive, + "--strip-components=1", + "-C", + packageDir, + ]); + for (const dependency of ["graphql", "graphql-ws"]) { + symlinkSync( + join(packageRoot, "node_modules", dependency), + join(fixture, "node_modules", dependency), + "junction", + ); + } + const result = await build({ + stdin: { + contents: `export { createClient, createEdgeClient, DEFAULT_EDGE_URL, withEdge } from "@codex-data/sdk/edge";`, + resolveDir: fixture, + sourcefile: "consumer.js", + }, + bundle: true, + minify: true, + platform: "browser", + format: "esm", + write: false, + metafile: true, + }); + bundleBytes = result.outputFiles[0].contents.byteLength; + metafile = result.metafile; +}, 30_000); + +afterAll(() => { + rmSync(fixture, { recursive: true, force: true }); +}); + +it("keeps the packed edge browser bundle below 60 KiB", () => { + expect(bundleBytes).toBeLessThan(60 * 1024); +}); + +it("excludes GraphQL server code and generated SDK documents from the bundle", () => { + const retained = Object.values(metafile.outputs).flatMap((output) => + Object.entries(output.inputs) + .filter(([, input]) => input.bytesInOutput > 0) + .map(([file]) => file.replace(/\\/g, "/")), + ); + expect( + retained.filter( + (file) => + /\/graphql\/(execution|validation|type)\//.test(file) || + /\/graphql-ws\/lib\/server\./.test(file) || + file.includes("/sdk/generated/"), + ), + ).toEqual([]); +}); + +it.each(["import", "require"])( + "loads the packed public edge export with Node %s", + (mode) => { + const imports = + mode === "import" + ? `import assert from "node:assert/strict"; + import * as sdk from "@codex-data/sdk/edge"; + import { createClient } from "graphql-ws";` + : `const assert = require("node:assert/strict"); + const sdk = require("@codex-data/sdk/edge"); + const { createClient } = require("graphql-ws");`; + execFileSync( + process.execPath, + [ + `--input-type=${mode === "import" ? "module" : "commonjs"}`, + "--eval", + `${imports} + assert.equal(sdk.createClient, createClient); + assert.equal(typeof sdk.createEdgeClient, "function"); + assert.equal(typeof sdk.withEdge, "function"); + assert.equal(sdk.DEFAULT_EDGE_URL, "wss://fra.edge.codex.io/graphql");`, + ], + { cwd: fixture, stdio: "pipe" }, + ); + }, +);