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

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,15 @@ jobs:
- name: Verify generated types are in sync with schema
run: npm run schema-typegen-diff-check

package-resolution:
needs: install-and-cibuild
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: ./.github/actions/setup-workspace
- name: Verify the published package loads under Node
run: npm run test-node-resolve

# ============================================================
# Standalone jobs (no dependencies on install-and-cibuild)
# ============================================================
Expand Down
5 changes: 5 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,8 @@ stackgl_modules/node_modules
tasks
test
topojson

# Exclude the TypeScript files (but not declarations) because Node doesn't
# parse TS when installed in node_modules.
src/**/*.ts
!src/**/*.d.ts
1 change: 1 addition & 0 deletions draftlogs/8000_fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Compile TypeScript files under `src/` to JavaScript during packaging to fix Node resolution [[#8000](https://github.com/plotly/plotly.js/pull/8000)]
6 changes: 0 additions & 6 deletions lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,6 @@ export type {
YAxisName
} from '../src/types/core/layout';

// ---------------------------------------------------------------------------
// Trace data
// ---------------------------------------------------------------------------

export type { Data } from '../src/types/core/data';

// ---------------------------------------------------------------------------
// Configuration
// ---------------------------------------------------------------------------
Expand Down
9 changes: 4 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"test-syntax": "tsx tasks/test_syntax.js && npm run find-strings -- --no-output",
"test-bundle": "node tasks/test_bundle.js",
"test-plain-obj": "node tasks/test_plain_obj.mjs",
"test-node-resolve": "node tasks/test_node_resolve.mjs",
"test": "npm run test-jasmine -- --nowatch && npm run test-bundle && npm run test-image && npm run test-export && npm run test-syntax && npm run lint",
"b64": "python3 test/image/generate_b64_mocks.py && node devtools/test_dashboard/server.mjs",
"mathjax3": "node devtools/test_dashboard/server.mjs --mathjax3",
Expand All @@ -63,7 +64,9 @@
"preversion": "check-node-version --node 22 --npm 10 && npm-link-check && npm ls --prod --all",
"version": "npm run build && git add -A lib dist build src/version.js",
"postversion": "node -e \"console.log('Version bumped and committed. If ok, run: git push && git push --tags')\"",
"postpublish": "node tasks/sync_packages.js"
"postpublish": "node tasks/sync_packages.js",
"prepack": "tsc -b tsconfig.build.json --force",
"postpack": "tsc -b tsconfig.build.json --clean"
},
"dependencies": {
"@plotly/d3": "3.8.2",
Expand All @@ -73,6 +76,7 @@
"@turf/area": "^7.3.5",
"@turf/centroid": "^7.3.5",
"@turf/meta": "^7.3.5",
"@types/d3": "^3.5.53",
"base64-arraybuffer": "^1.0.2",
"country-iso-search": "^0.1.2",
"culori": "^4.0.2",
Expand Down Expand Up @@ -111,7 +115,6 @@
"@biomejs/biome": "^2.5.5",
"@plotly/mathjax-v3": "npm:mathjax@^3.2.2",
"@plotly/mathjax-v4": "npm:mathjax@^4.1.3",
"@types/d3": "3.5.34",
"@types/node": "^26.1.1",
"assert": "^2.1.0",
"buffer": "^6.0.3",
Expand Down
2 changes: 1 addition & 1 deletion src/types/core/data.internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
* properties. For public trace types, see data.d.ts.
*/

import type { Data } from '../generated/schema';
import type { Datum } from '../lib/common';
import type { Data } from './data';

/**
* Calculated trace data (internal).
Expand Down
78 changes: 78 additions & 0 deletions tasks/test_node_resolve.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';

import { pathToRoot } from './util/constants.js';

// Bundlers resolve a `.ts` extension, so `npm run build` hides a package that
// Node alone cannot load. This test packs the real tarball and loads it the way
// a Node consumer does: `require('plotly.js')` under the CommonJS resolver.
// See https://github.com/plotly/plotly.js/issues/7995.
//
// The package needs a browser, so the load always ends in a DOM error. That is
// the pass condition. Any resolution error is the regression.

// tsc overwrites a hand-written `foo.js` when a `foo.ts` sits beside it, and it
// reports no error. Such a pair is already ambiguous, because esbuild picks the
// `.ts` and the local build silently ignores the `.js`. Fail here instead.
const collisions = fs
.globSync('src/**/*.ts', { cwd: pathToRoot })
.filter((file) => !file.endsWith('.d.ts'))
.filter((file) => fs.existsSync(path.join(pathToRoot, file.replace(/\.ts$/, '.js'))));

if (collisions.length) {
throw new Error(
[
'A TypeScript source shares a basename with a JavaScript file:',
...collisions.map((file) => ' ' + file),
'The pack step would overwrite the JavaScript file. Rename one of the two.'
].join('\n')
);
}

const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'plotly-node-resolve-'));

try {
console.log('Packing the tarball');
const packed = execFileSync('npm', ['pack', '--pack-destination', tmp, '--silent'], {
cwd: pathToRoot,
encoding: 'utf8'
})
.trim()
.split('\n')
.pop();

// Install the tarball the way npm would, so that `require('plotly.js')`
// goes through the package name, the `main` field, and the published file
// layout.
const pkg = path.join(tmp, 'node_modules', 'plotly.js');

fs.mkdirSync(pkg, { recursive: true });
execFileSync('tar', ['-xzf', path.join(tmp, packed), '-C', pkg, '--strip-components=1']);

// The tarball carries no dependencies. Borrow the ones already installed.
fs.symlinkSync(path.join(pathToRoot, 'node_modules'), path.join(pkg, 'node_modules'), 'dir');

// The probe resolves from `tmp`, which is where the tarball is installed.
// It runs in its own process so that it starts with a clean module registry
// and its own globals.
const probe = path.join(pathToRoot, 'tasks', 'util', 'node_resolve_probe.js');
const result = execFileSync(process.execPath, [probe, tmp], { encoding: 'utf8' }).trim();

// A ReferenceError means every `require` in the graph resolved, and the
// package only then reached for a browser API.
if (result === 'LOADED' || result === 'RUNTIME:ReferenceError') {
console.log('OK: the published package resolves under Node (' + result + ')');
} else {
throw new Error(
[
'The published package does not resolve under Node: ' + result,
'Every src/**/*.ts needs a generated .js sibling in the tarball.',
'See tsconfig.build.json and the prepack script in package.json.'
].join('\n')
);
}
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
26 changes: 26 additions & 0 deletions tasks/util/node_resolve_probe.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Loads plotly.js the way a Node consumer does.
//
// Takes the directory holding a `node_modules` with the packed tarball in it.
// `createRequire` bases resolution there, so the require below behaves as if
// this file sat in that directory: it goes through the package name, the `main`
// field, and the published file layout.
//
// plotly.js needs a browser, so even a complete load ends in a DOM error. The
// caller reads the single line this prints on stdout.

const { createRequire } = require('node:module');
const path = require('node:path');

const consumerDir = process.argv[2];
const consumerRequire = createRequire(path.join(consumerDir, 'index.js'));

globalThis.self = globalThis;
globalThis.window = globalThis;

try {
consumerRequire('plotly.js');
console.log('LOADED');
} catch (err) {
console.log(err.code === undefined ? 'RUNTIME:' + err.name : 'CODE:' + err.code);
console.error(err.message.split('\n')[0]);
}
30 changes: 30 additions & 0 deletions tsconfig.build.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
// Emit configuration for the published package.
//
// The repository authors a growing share of `src/` in TypeScript, but the
// published package must contain only JavaScript. Node's CommonJS resolver
// never tries a `.ts` extension, and Node refuses to strip types from any
// file below `node_modules`. So the `prepack` script writes a `.js` sibling
// for each `.ts` source, and `postpack` deletes it again.
//
// No `outDir` is set, so each `.js` lands next to its `.ts`. That is what
// makes `require('./mod')` resolve in the tarball.
//
// Build mode drives both scripts. `tsc -b` emits, and `tsc -b --clean`
// removes every generated file. Build mode also writes a state file, which
// `tsBuildInfoFile` parks below `build/`, because `build/` is already
// ignored by both git and npm.
//
// Type errors are not reported here. `npm run typecheck` owns that job and
// reads the whole program, including the JavaScript files.
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": false,
"noCheck": true,
"allowJs": false,
"module": "commonjs",
"tsBuildInfoFile": "build/tsconfig.build.tsbuildinfo"
},
"include": ["src/**/*.ts"],
"exclude": ["src/types/**"]
}
Loading