Repository navigation
fix: Compile TS to JS during packaging #8000
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
d1e6d7d
fa80f38
f195179
66ad262
e1089f8
f945b67
8035cf9
c1d910f
d0454f7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
| 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 }); | ||
| } |
| 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'); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Claude took me down this path as well while I was testing this out. To me, it feels a bit "one step removed" from a real-world validation. Could we instead write a more idiomatic
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was all an attempt to run without |
||
|
|
||
| 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]); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| { | ||
| // 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", | ||
| "declaration": false, | ||
| "isolatedModules": false, | ||
| "tsBuildInfoFile": "build/ts-build-info.json" | ||
| }, | ||
| "include": ["src/**/*.ts"], | ||
| "exclude": ["src/types/**"] | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a particular reason you write this as a util here?
To me, the file reads like a github action written in javascript. And it's doing some bizarre setup along the way that make the assertions seem a bit artificial.
Meanwhile, what we actually care about:
And that seems like an ideal case for a GH action or even just a unit test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right. I liked having the test locally, but it does fit better in GHA. I'll remove this file.