Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/build-dist.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ on:
- main
paths:
- 'src/**'
- 'vite.config.ts'
- 'package.json'
- 'package-lock.json'

jobs:
build-dist:
Expand Down
11 changes: 10 additions & 1 deletion .github/workflows/ci-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,13 @@ jobs:
run: npm run lint

- name: Tests
run: npm run test
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
11 changes: 10 additions & 1 deletion .github/workflows/ci-windows.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,13 @@ jobs:
run: npm run lint

- name: Tests
run: npm run test
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
15 changes: 15 additions & 0 deletions docs/package-consumers.md
Original file line number Diff line number Diff line change
@@ -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.
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -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"],
Expand Down
108 changes: 108 additions & 0 deletions scripts/test-package-consumer.mjs
Original file line number Diff line number Diff line change
@@ -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<T> = 0 extends (1 & T) ? true : false;
const declarationsMustResolve: false = null as unknown as IsAny<typeof SparkRenderer>;
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;
}
4 changes: 2 additions & 2 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down