diff --git a/.github/workflows/build-dist.yml b/.github/workflows/build-dist.yml index 4ee5ff9a..a748397b 100644 --- a/.github/workflows/build-dist.yml +++ b/.github/workflows/build-dist.yml @@ -6,6 +6,9 @@ on: - main paths: - 'src/**' + - 'vite.config.ts' + - 'package.json' + - 'package-lock.json' jobs: build-dist: diff --git a/.github/workflows/ci-linux.yml b/.github/workflows/ci-linux.yml index aece6334..57c42e93 100644 --- a/.github/workflows/ci-linux.yml +++ b/.github/workflows/ci-linux.yml @@ -31,4 +31,13 @@ jobs: run: npm run lint - name: Tests - run: npm run test \ No newline at end of file + run: npm run test + + - name: Build WASM + run: npm run build:wasm + + - name: Build package + run: npm run build + + - name: Packed package consumers + run: npm run test:package diff --git a/.github/workflows/ci-windows.yaml b/.github/workflows/ci-windows.yaml index 413eaa4e..ecd4332b 100644 --- a/.github/workflows/ci-windows.yaml +++ b/.github/workflows/ci-windows.yaml @@ -37,4 +37,13 @@ jobs: run: npm run lint - name: Tests - run: npm run test \ No newline at end of file + run: npm run test + + - name: Build WASM + run: npm run build:wasm + + - name: Build package + run: npm run build + + - name: Packed package consumers + run: npm run test:package diff --git a/docs/package-consumers.md b/docs/package-consumers.md new file mode 100644 index 00000000..4e533358 --- /dev/null +++ b/docs/package-consumers.md @@ -0,0 +1,15 @@ +# CommonJS and ESM package consumers + +The package declares `type: module`. CommonJS output must therefore use a `.cjs` extension. Publishing CommonJS statements in `spark.cjs.js` caused Node to load that entry as an ES module: a CommonJS consumer could fail or receive no Spark exports. The `main` and `exports.require` entries now point to `dist/spark.cjs`; the ESM/CDN entry stays `dist/spark.module.js`. + +Build before checking consumers: + +```sh +npm run build:wasm +npm run build +npm run test:package +``` + +The test creates an `npm pack` tarball and installs it in a temporary consumer project. It checks a real `require()` call from a `.cjs` file, an ESM import, and the existing synchronous WebGL API in TypeScript. It tests Three.js 0.180.0 and pinned 0.185.1, uses bundler resolution for browser TypeScript consumers, and rejects declarations that silently resolve to `any`. A failed consumer project is retained for reproduction. + +Linux and Windows CI build the package before running this check. The distribution workflow also watches the package/build configuration so this fix produces the new CommonJS artifact after merging. Generated distribution files are not included in the source contribution. diff --git a/package.json b/package.json index 52f2dc7f..ae4f19ab 100644 --- a/package.json +++ b/package.json @@ -4,14 +4,14 @@ "description": "An advanced 3D Gaussian Splatting renderer for THREE.js", "homepage": "https://sparkjs.dev/", "type": "module", - "main": "./dist/spark.cjs.js", + "main": "./dist/spark.cjs", "module": "./dist/spark.module.js", "types": "./dist/types/index.d.ts", "exports": { ".": { "types": "./dist/types/index.d.ts", "import": "./dist/spark.module.js", - "require": "./dist/spark.cjs.js" + "require": "./dist/spark.cjs" } }, "scripts": { @@ -38,7 +38,8 @@ "site:deploy": "npm run site:build && node scripts/deploy-site.js", "site:serve": "node scripts/serve-site.js site", "start": "npm run dev", - "test": "node --no-warnings --loader ts-node/esm --test test/**/*.test.ts" + "test": "node --no-warnings --loader ts-node/esm --test test/**/*.test.ts", + "test:package": "node scripts/test-package-consumer.mjs" }, "repository": "sparkjs-dev/spark", "files": ["dist"], diff --git a/scripts/test-package-consumer.mjs b/scripts/test-package-consumer.mjs new file mode 100644 index 00000000..1edd2fb4 --- /dev/null +++ b/scripts/test-package-consumer.mjs @@ -0,0 +1,108 @@ +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = fileURLToPath(new URL("..", import.meta.url)); +const directory = mkdtempSync(join(tmpdir(), "spark-package-consumer-")); +const npmCli = process.env.npm_execpath; +if (!npmCli) throw new Error("Run this check via npm run test:package"); +function run(executable, args, cwd = directory) { + return execFileSync(executable, args, { + cwd, + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }); +} +try { + const [archive] = JSON.parse( + run( + process.execPath, + [npmCli, "pack", "--json", "--pack-destination", directory], + root, + ), + ); + writeFileSync( + join(directory, "package.json"), + JSON.stringify({ + name: "spark-package-consumer", + private: true, + type: "module", + }), + ); + writeFileSync( + join(directory, "consumer.cjs"), + `const assert = require('node:assert/strict'); +const { SparkRenderer, utils } = require('@sparkjsdev/spark'); +assert.equal(typeof SparkRenderer, 'function'); +assert.equal(utils.toHalf(1), 15360); +`, + ); + writeFileSync( + join(directory, "consumer.mjs"), + `import assert from 'node:assert/strict'; +import { SparkRenderer, utils } from '@sparkjsdev/spark'; +assert.equal(typeof SparkRenderer, 'function'); +assert.equal(utils.toHalf(1), 15360); +`, + ); + writeFileSync( + join(directory, "consumer.ts"), + `import * as THREE from 'three'; +import { SparkRenderer, type SparkRendererOptions } from '@sparkjsdev/spark'; +type IsAny = 0 extends (1 & T) ? true : false; +const declarationsMustResolve: false = null as unknown as IsAny; +export function render(renderer: THREE.WebGLRenderer, scene: THREE.Scene, camera: THREE.Camera) { + const options: SparkRendererOptions = { renderer }; + const spark: SparkRenderer = new SparkRenderer(options); + const context: WebGLRenderingContext | WebGL2RenderingContext = spark.renderer.getContext(); + const result: void = spark.render(scene, camera); + return { context, result }; +} +`, + ); + for (const [three, types] of [ + ["0.180.0", "0.180.0"], + ["0.185.1", "0.185.4"], + ]) { + run(process.execPath, [ + npmCli, + "install", + "--ignore-scripts", + "--no-audit", + "--no-fund", + "--save-exact", + resolve(directory, archive.filename), + `three@${three}`, + `@types/three@${types}`, + "typescript@5.8.3", + ]); + run(process.execPath, ["consumer.cjs"]); + run(process.execPath, ["consumer.mjs"]); + run(process.execPath, [ + "node_modules/typescript/bin/tsc", + "--noEmit", + "--strict", + "--target", + "ES2020", + "--module", + "ESNext", + "--moduleResolution", + "Bundler", + "--skipLibCheck", + "consumer.ts", + ]); + console.log( + `Packed CommonJS, ESM and TypeScript consumer passed: Three.js ${three}; Node ${process.version}`, + ); + } + assert(archive.filename); + rmSync(directory, { recursive: true, force: true }); +} catch (error) { + console.error(`Consumer reproduction retained at ${directory}`); + if (error.stdout) console.error(error.stdout); + if (error.stderr) console.error(error.stderr); + throw error; +} diff --git a/vite.config.ts b/vite.config.ts index e49bcf15..06ca48a4 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -93,8 +93,8 @@ export default defineConfig(({ mode }) => { name: "spark", formats: ["es", "cjs"], fileName: (format) => { - const base = format === "es" ? "spark.module" : `spark.${format}`; - return isMinify ? `${base}.min.js` : `${base}.js`; + if (format === "cjs") return isMinify ? "spark.min.cjs" : "spark.cjs"; + return isMinify ? "spark.module.min.js" : "spark.module.js"; }, }, sourcemap: true,