Page crash frames in the shared packages resolve to their source, not their dist (#576)

A page crash's frames inside `@text-to-cad/core` and `@text-to-cad/ui`
now resolve in PostHog to the packages' source (`packages/*/src/**`,
with its text), not to the compiled `dist/*.js` the pages import.

## Evidence
On cadgen 0.7.17, a page-crash probe frame in the CAD Viewer's entry
chunk (`index-CNGP9tWC.js`, line 3, column 12) resolved in PostHog
(`resolved: true`) to
`../../../../packages/core/dist/client/crash.js:41:31`. The release's
maps were found and used, but they ended at `dist`. The cause: Rolldown
reads no map a module names (`//# sourceMappingURL=`). It chains only a
map that a plugin's `load` hook returns. The packages' library build
already wrote a map, with `sourcesContent`, beside every `dist` module,
and no page used it.

## What changed
- **`@text-to-cad/core/source-maps`**
(`packages/core/scripts/source-maps.mjs`): a Vite plugin that both
pages' builds add for `@text-to-cad/core` and `@text-to-cad/ui`.
- Each of their `dist` modules loads with its own map, sources made
absolute. Rolldown then chains it under every later edit, so each
chunk's map ends in `src/`.
- A module the library build copies as it is (an `.mjs`; only
`common/stepTopology.mjs` reaches a page) has no map of its own. It
loads with an identity map to its twin in `src/`, column for column.
- A chunk whose map still ends in a package's compiled code fails the
build, so a regression shows up in CI, not in PostHog. Imported images
are exempt: no frame is ever in one.
- **The CAD app's worker inlining keeps its map.** It prepended an
import line and returned `map: null`, which put the six modules that
start workers one line off. It now edits with magic-string and returns
that map.
- No source aliases, and the packages are still consumed as compiled
exports. The maps stay in `dist`, out of the wheel: `_runtime` has no
`.map`. Every chunk's text is unchanged except its `//# debugId=` line.
The 28 viewer chunks and 26 CAD app chunks that hold package code get
new debug ids, because their maps changed. The rest keep the ids main
gives them: the same text with the same map.
- Docs: the core, web and CAD app READMEs, and a comment in
`scripts/build/library.mjs`, which writes the maps the pages now depend
on.

## Verification (resolved paths)
Resolved with the `source-map` package (scratch script, outside the
repo), positions 1-based:

| Page chunk:line:col | Before (main) | After |
| --- | --- | --- |
| viewer `index-CNGP9tWC.js:3:10`–`12` |
`packages/core/dist/client/crash.js:42:26` |
`packages/core/src/client/crash.js:63:26` |
| viewer `index-CNGP9tWC.js:3:13`–`14` |
`packages/core/dist/client/crash.js:42:32` |
`packages/core/src/client/crash.js:63:31` |
| CAD app `index-CpUzQROK.js:10:16` |
`packages/core/dist/client/crash.js:42:41` |
`packages/core/src/client/crash.js:63:39` |

All three land on `const chromium = lines.some(line => /^\s+at
/.test(line));` in `crashOf`, the crash-signature code, with
`sourcesContent`. PostHog's `41:31` is the 0-based form of main's
`42:32`.

Samples from chunks with package code, each a token found in the chunk
and resolved:
- core `.js`: viewer `GlbRenderer-DFf8XZ14.js:1:4193` `castShadow` →
`packages/core/src/lib/render/glbScene.js:37:12`
- core `.mjs`: viewer `surfaceMaterials-Biudp1of.js:171:3653`
`NON_MANIFOLD` → `packages/core/src/common/stepTopology.mjs:27:3`
- ui `.ts`: viewer `fileView-CrKf9XW9.js:12:5403`
`requestAnimationFrame` →
`packages/ui/src/renderers/kit/shell/liveBinding.ts:83:56`; CAD app
`drawing-CG2XVurH.js:158:39650` `getContext` →
`packages/ui/src/drawing/fill.ts:61:26`
- ui `.tsx`: viewer `drawing-CB1Cs64-.js:159:43985` `background` →
`packages/ui/src/drawing/index.tsx:201:25`
- ui `.jsx`: CAD app `RobotRenderer-CoPUU5HD.js:1:14398`
`cancelAnimationFrame` →
`packages/ui/src/renderers/robot/PositionControls.jsx:122:43`

Across every chunk a crash can name (the 174 viewer and 182 CAD app
chunks in `__cadChunkIds`), each map now names all 417 package code
files the page bundles, in `src/`. Main named them in `dist/`. By
extension: core 133 `.js` and 1 `.mjs`; ui 190 `.js`, 61 `.jsx`, 22
`.ts` and 10 `.tsx`. None lacks `sourcesContent`.

Every mapped package token of 10 or more word characters resolves onto a
source line that contains it:
- viewer: 14,284 of 14,284;
- CAD app: 14,229 of 14,247. The other 18 are `__cadImport`, the CAD
app's own rewrite of an `import(`, and they map to that `import(`.

`scripts/release/sourcemaps.py collect` passes: `viewer: 174 chunks,
cad-app: 182 chunks`.

## Accuracy (the CAD app)
#570's script isn't in the repo, so this uses a stated metric and runs
it on main and on this branch. Of the mapped positions where both the
chunk text and the source text start a word (an identifier, keyword or
number), it counts the share where the two words are equal:

| | main | this PR |
| --- | --- | --- |
| CAD app, shipped maps (edited chunks, composed) | 50.72% of 1,079,808
| **50.73%** of 1,076,746 |
| CAD app, Rolldown's own map of its unedited chunks | 50.45% | 50.39% |
| CAD app, package tokens only (shipped) | 46.08%, against `dist` text |
46.05%, against `src` text |
| Viewer, shipped maps | 50.66% | 50.65% |

The relation #570 reported still holds: the composed maps are at or
above Rolldown's own map of the same build. Package tokens are now
compared with source text rather than compiled text. A sample of the
positions that matched `dist` and no longer match `src` shows two kinds,
both on the right source line:
- most are tokens the compiler rewrote: `children` from JSX, `void 0`
for `undefined`, `3e4` for `30_000`;
- the rest land a few columns early, where esbuild maps a statement
rather than each of its tokens (`} else if`, `export async function`).

Without the worker-map fix, package tokens score 45.79%.

## Cost
- **Build time.** Medians of 5 warm runs on an M-series Mac:
  - the viewer build goes from 1.82 s to 1.99 s (+0.17 s);
  - the CAD app build goes from 4.54 s to 5.00 s (+0.46 s).
- **CI time.** The `mcp` job builds the CAD app once. `web` and
`packaging` run `bundle.sh`, which builds both pages once. `core-js`
gets 3 tests, about 10 ms. Against the 0.7.17 release PR's full run
(37711526983), this PR's run (37718026111) stayed within runner noise:
  - `web`'s bundle step: 84 s against 81 s;
  - `packaging`'s bundle step: 24 s against 29 s;
  - `mcp`'s test-and-build step: 19 s against 29 s.
- **CI selection.** The change touches `packages/core`, so this PR runs
every job, as any core change does.
- **Map size.** Named-chunk maps:
  - viewer: 27.9 MB to 28.4 MB;
  - CAD app: 28.7 MB to 29.1 MB;
  - the release's `cadgen-sourcemaps` zip: 21.9 MB to 22.5 MB;
  - the largest single map is unchanged (`vendor-three`, 3.4 MB).
- **Page size.** The pages are the same code. The CAD app's `index.html`
differs by 8 bytes, from gzipping the new debug ids.

## Tests
- New, `packages/core/scripts/source-maps.test.mjs`, which reads core's
built `dist`:
- a compiled module loads with a map that sends the crash signature's
position in `dist/client/crash.js` to its line and column in
`src/client/crash.js`, with the source text;
  - a copied `.mjs` maps column for column to its twin;
- the guard fails a chunk whose map ends in `dist` code. With the guard
disabled, this test fails.
- `scripts/test/test-js.sh`:
  - `--select core`: 1295 tests, 1 skipped, as on main;
  - `--select ui`: 751, plus 54 browser and 347 vitest;
  - `--select web`: 56;
  - `--select mcp`: 37, and its build.
- `scripts/test/test-global.sh`: 210 tests, 1 skipped.
- `scripts/bundle/bundle.sh --check`, the full `bundle.sh` and
`scripts/github-workflows/check-builds.sh` pass.

## Not changed
- **Worker bundles.** The viewer's worker bundles (`surfWorker`,
`glbMeshWorker` and others) still map to `dist`. They aren't in
`__cadChunkIds` and aren't uploaded, and a worker's crash never reaches
the page's reporter. The CAD app inlines them and drops their maps.
- **A column shift inside `dist`.** `library.mjs` rewrites relative
import specifiers after esbuild, for example `./X.jsx` to `./X.js`,
without updating esbuild's map. That moves only what follows a specifier
on its own line: a `;` or `)`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
earthtojake
2026-10-07 23:06:11 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent a6b4a2f5c1
commit af1e9aa435
9 changed files with 220 additions and 23 deletions
+4 -2
View File
@@ -118,8 +118,10 @@ reference host `basic-host` does.
gave, and the page's own (`main.tsx`, its frames named by the chunks the inline loader
made blob URLs of, and by each chunk's debug id), as their type and frames, never a message.
The build keeps a source map of each chunk as the page runs it (`dist/sourcemaps`, which a
release uploads to PostHog): every edit it makes to a chunk is folded into rolldown's map,
each import's placeholder is as wide as the blob URL that replaces it, and the chunk's debug
release uploads to PostHog), leading into the shared packages' own source, whose modules
load with their maps (`@text-to-cad/core/source-maps`): every edit the build makes to a
module or a chunk is folded into rolldown's map, each import's placeholder is as wide as the
blob URL that replaces it, and the chunk's debug
id is the one its edited text and map decide (`@text-to-cad/core/chunk-ids`), never the
id of the unedited chunk the CAD Viewer may ship. It is on by default once
a `cadgen` command has said so, once, and nothing asks. The app menu's **Share usage
+22 -13
View File
@@ -6,6 +6,7 @@ import MagicString from 'magic-string';
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { stampDebugId } from '@text-to-cad/core/chunk-ids';
import { packageSourceMaps } from '@text-to-cad/core/source-maps';
import { drawingAssetFiles, localizeDrawingFontFallback } from '@text-to-cad/ui/drawing-assets';
// An MCP App is one HTML resource with no asset directory behind it: the host renders it in a
@@ -33,19 +34,25 @@ function inlineDrawingFonts() {
}
// The shared packages start workers with `new Worker(new URL(file, import.meta.url))`. Here
// that URL would point at the sandbox, so each one becomes Vite's inline (blob) worker.
// that URL would point at the sandbox, so each one becomes Vite's inline (blob) worker. The
// edit comes with its map (its imports push the module's code down a line), so that code keeps
// its place in the page's map.
const WORKER = /new Worker\(\s*new URL\(\s*(["'])([^"']+)\1\s*,\s*import\.meta\.url\s*\)\s*,\s*\{\s*type:\s*(["'])module\3\s*\}\s*\)/g;
function inlineWorkers() {
return {
name: 'cad-app-inline-workers', enforce: 'pre',
transform(code, id) {
if (!/\.[cm]?jsx?$/.test(id) || !code.includes('new Worker')) return;
const edited = new MagicString(code);
const imports = [];
const result = code.replace(/new Worker\(\s*new URL\(\s*(["'])([^"']+)\1\s*,\s*import\.meta\.url\s*\)\s*,\s*\{\s*type:\s*(["'])module\3\s*\}\s*\)/g, (_, _quote, file) => {
for (const match of code.matchAll(WORKER)) {
const name = `__cadInlineWorker${imports.length}`;
imports.push(`import ${name} from ${JSON.stringify(`${file}?worker&inline`)};`);
return `new ${name}()`;
});
return imports.length ? { code: `${imports.join('\n')}\n${result}`, map: null } : undefined;
imports.push(`import ${name} from ${JSON.stringify(`${match[2]}?worker&inline`)};`);
edited.overwrite(match.index, match.index + match[0].length, `new ${name}()`);
}
if (!imports.length) return;
edited.prepend(`${imports.join('\n')}\n`);
return { code: edited.toString(), map: edited.generateMap({ hires: true }) };
},
};
}
@@ -62,12 +69,14 @@ function inlineWorkers() {
//
// A crash on the page names the chunk it ran in by its debug id (`IDS`, as `__cadChunkIds`; the
// crash reporter in @text-to-cad/core reads it), and the release uploads each chunk with its source
// map (`dist/sourcemaps`, scripts/release/sourcemaps.py), so PostHog can show the source. The map
// must fit the text the page runs: every edit here is made with its positions kept (magic-string)
// and folded into rolldown's map, and a placeholder is as wide as the blob URL that replaces it
// (`WIDTH`, padded with spaces a URL's parser drops), so swapping it moves no code. The id is the
// one that text and its map decide (@text-to-cad/core/chunk-ids), never rolldown's: rolldown names
// the code before these edits, which the CAD Viewer may ship unedited under the same id.
// map (`dist/sourcemaps`, scripts/release/sourcemaps.py), so PostHog can show the source -- the
// shared packages' own, since their modules load with their maps (@text-to-cad/core/source-maps)
// and rolldown chains them. The map must fit the text the page runs: every edit here is made with
// its positions kept (magic-string) and folded into rolldown's map, and a placeholder is as wide as
// the blob URL that replaces it (`WIDTH`, padded with spaces a URL's parser drops), so swapping it
// moves no code. The id is the one that text and its map decide (@text-to-cad/core/chunk-ids), never
// rolldown's: rolldown names the code before these edits, which the CAD Viewer may ship unedited
// under the same id.
const CHUNK = 'cad-chunk:';
const WIDTH = 192;
const DYNAMIC_IMPORT = /\bimport\(\s*([`"'])\.\/([^`"'\s]+\.js)\1\s*\)/g;
@@ -179,7 +188,7 @@ function inlineDocument() {
}
export default defineConfig({
plugins: [inlineWorkers(), inlineDrawingFonts(), react(), inlineDocument()],
plugins: [packageSourceMaps(['@text-to-cad/core', '@text-to-cad/ui']), inlineWorkers(), inlineDrawingFonts(), react(), inlineDocument()],
resolve: { dedupe: ['react', 'react-dom', 'three', 'lucide-react'] },
build: {
assetsInlineLimit: Infinity, cssCodeSplit: false, modulePreload: false,
+3 -1
View File
@@ -308,7 +308,9 @@ file shown is counted as it joins the library (`/__cad/recents`). A Quick Edit c
through the host's `usage`, and the page's own crashes -- an error nothing caught, or one a view's
boundary caught (`main.tsx`) -- are reported as core's `crashOf` makes them: the error's type and its
script frames, never its message, each once a page. The build gives every chunk a hidden source map and
a debug id its text and that map decide (`vite.config.mjs`, `@text-to-cad/core/chunk-ids`); the wheel
a debug id its text and that map decide (`vite.config.mjs`, `@text-to-cad/core/chunk-ids`). A map leads
into the shared packages' own source, not their compiled `dist`: their modules load with the maps their
builds wrote (`@text-to-cad/core/source-maps`). The wheel
leaves the maps out and a release uploads them to PostHog, and the page
carries each chunk's id by file name (`__cadChunkIds`), and a frame names its chunk by it. The server holds all of it as counts in memory
-- a file once a day, by its format, never its name -- and sends them every few minutes.
+6 -3
View File
@@ -5,6 +5,7 @@ import { fileURLToPath } from "node:url";
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { stampDebugId } from "@text-to-cad/core/chunk-ids";
import { packageSourceMaps } from "@text-to-cad/core/source-maps";
import { drawingAssetsPlugin } from "@text-to-cad/ui/drawing-assets";
import { resolveDirectoryRoot as resolveViewerDirectoryRoot } from "./scripts/directoryRoot.mjs";
@@ -185,9 +186,10 @@ function serverLifetimePlugin() {
// Each chunk's debug id, by file name, in the page before anything runs (`__cadChunkIds`): a crash
// report names the chunk each of its frames ran in by it (@text-to-cad/core's crash reporter), and
// the release uploads every chunk with its source map (scripts/release/sourcemaps.py) for PostHog to
// show the source. Each chunk goes by an id its text and its map decide (@text-to-cad/core/chunk-ids),
// never rolldown's, which names the code alone. The maps stay in dist, which the wheel leaves out
// (scripts/bundle).
// show the source -- the shared packages' own source, whose maps the build chains
// (@text-to-cad/core/source-maps), not their compiled dist. Each chunk goes by an id its text and its
// map decide (@text-to-cad/core/chunk-ids), never rolldown's, which names the code alone. The maps stay
// in dist, which the wheel leaves out (scripts/bundle).
function chunkIdsPlugin() {
return {
name: "cad-viewer-chunk-ids",
@@ -226,6 +228,7 @@ export default defineConfig(async ({ command }) => ({
drawingAssetsPlugin({ exclude: [/\/fonts\/Xiaolai\//] }),
react(),
serverLifetimePlugin(),
packageSourceMaps(["@text-to-cad/core", "@text-to-cad/ui"]),
chunkIdsPlugin(),
],
resolve: { alias: { "@": viewerClientRoot }, dedupe: ["react", "react-dom", "three", "lucide-react"] },
+7 -3
View File
@@ -169,8 +169,9 @@ src/
# parity-tested against cad_ref_syntax.py)
bin/ # node builders the bundler ships into _runtime/node:
# mesh-export.mjs (the ONE mesh path)
scripts/ # chunk-ids.mjs (the pages' builds: a chunk's debug id)
# beside the package's own build and test runners
scripts/ # chunk-ids.mjs (the pages' builds: a chunk's debug id) and
# source-maps.mjs (the pages' builds: maps into the packages'
# src/), beside the package's own build and test runners
docs/ # subsystem docs (the map below)
```
@@ -208,7 +209,10 @@ a frame in one of the page's own chunks also names its debug id, from the table
page's build wrote, `__cadChunkIds`) -- and sends each distinct one once a page, a few
at most. A page's build stamps each chunk with the id its text and its source map decide
(`@text-to-cad/core/chunk-ids`, a Node module the builds import), never the bundler's,
which names the code alone: PostHog keeps one map an id. `dispose()` stops polling, aborts
which names the code alone: PostHog keeps one map an id. That map leads into the shared
packages' `src/`, not their `dist/`: the build loads their modules with the maps their own
builds wrote (`@text-to-cad/core/source-maps`), and fails if a chunk's map still ends in
compiled code. `dispose()` stops polling, aborts
requests and disposes render sessions. The client lazily owns its cache provider
and bounded write-back queue; each render session borrows a cancellable cache
view and owns its abort signal and worker leases. A host whose transport
+3 -1
View File
@@ -25,7 +25,8 @@
"types": "./dist/drawing/index.d.ts",
"import": "./dist/drawing/index.js"
},
"./chunk-ids": "./scripts/chunk-ids.mjs"
"./chunk-ids": "./scripts/chunk-ids.mjs",
"./source-maps": "./scripts/source-maps.mjs"
},
"scripts": {
"test": "node scripts/run-tests.mjs",
@@ -51,6 +52,7 @@
"dist",
"bin",
"scripts/chunk-ids.mjs",
"scripts/source-maps.mjs",
"docs",
"README.md"
]
+114
View File
@@ -0,0 +1,114 @@
/**
* The shared packages' own source maps, for the CAD Viewer's and the CAD app's builds: a page's map leads a frame
* in a package's code to that package's source (`src/`), with the source's text, and not to the compiled file in
* `dist/` the page imports, so a crash there reaches PostHog as the source.
*
* A package's build writes a map beside each module in `dist/` (esbuild's, which keeps the source's text) and names
* it on the module's last line (`//# sourceMappingURL=`). Rolldown reads no map a module names; it chains only the
* map a plugin loads the module with. So a page's build loads each module of the packages it names with that map.
* A module the package's build copied as it is (an `.mjs`) names none, and is its source: it loads with a map that
* names the same line and column of its copy in `src/`. Once the chunks are made, a chunk whose map still ends in
* a package's compiled code fails the build.
*/
import fs from 'node:fs';
import { createRequire } from 'node:module';
import path from 'node:path';
const MAP_LINE = /\n\/\/# sourceMappingURL=(\S+)\s*$/;
const BASE64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
function vlq(value) {
let rest = value < 0 ? (-value << 1) | 1 : value << 1, text = '';
do {
const digit = rest & 31;
rest >>>= 5;
text += BASE64[rest ? digit | 32 : digit];
} while (rest);
return text;
}
// A map that sends every line and column of `code` to the same place in `source`, which `code` is a copy of.
function identityMap(code, source) {
let mappings = '', line = 0, column = 0; // where the previous segment points
for (const [index, text] of code.split('\n').entries()) {
if (index) mappings += ';';
for (let at = 0; at < text.length; at++) {
mappings += `${at ? ',' : ''}${vlq(at ? 1 : 0)}A${vlq(index - line)}${vlq(at - column)}`;
line = index;
column = at;
}
}
return { version: 3, sources: [source], sourcesContent: [code], names: [], mappings };
}
/**
* A module of a package's `dist/`, as the page's build loads it: its code, and the map that leads it to its source,
* the one its build wrote (its sources made absolute) or, for a copy of its source, the identity. `null` for a
* module that is neither.
*
* @param {string} file The module, an absolute path.
* @param {string} dist The package's `dist/`.
* @param {string} src The package's `src/`.
* @returns {{ code: string, map: object } | null}
*/
export function loadWithMap(file, dist, src) {
const code = fs.readFileSync(file, 'utf8');
const named = MAP_LINE.exec(code);
if (named) {
const mapFile = path.resolve(path.dirname(file), named[1]);
const { sourceRoot = '', ...map } = JSON.parse(fs.readFileSync(mapFile, 'utf8'));
const base = path.resolve(path.dirname(mapFile), sourceRoot);
return { code: code.slice(0, named.index + 1), map: { ...map, sources: map.sources.map(source => path.resolve(base, source)) } };
}
const twin = path.join(src, path.relative(dist, file));
return fs.existsSync(twin) && fs.readFileSync(twin, 'utf8') === code ? { code, map: identityMap(code, twin) } : null;
}
/**
* The Vite plugin a page's build adds, before any other that loads a module.
*
* @param {string[]} packages The packages whose maps the page chains, by name; the page resolves each.
*/
export function packageSourceMaps(packages) {
let roots = [];
const rootOf = file => roots.find(({ dist }) => file.startsWith(dist + path.sep));
return {
name: 'cad-package-source-maps',
apply: 'build',
enforce: 'pre',
configResolved(config) {
const require = createRequire(path.join(config.root, 'package.json'));
roots = packages.map(name => {
const root = fs.realpathSync(path.dirname(require.resolve(`${name}/package.json`)));
return { name, dist: path.join(root, 'dist'), src: path.join(root, 'src') };
});
},
load: {
filter: { id: /[\\/]dist[\\/][^?]*\.m?js$/ },
handler(id) {
const file = path.resolve(id);
const root = rootOf(file);
return root ? loadWithMap(file, root.dist, root.src) : null;
},
},
generateBundle: {
order: 'pre',
handler(options, bundle) {
const out = options.dir ?? path.dirname(options.file);
const compiled = [];
for (const chunk of Object.values(bundle)) {
if (chunk.type !== 'chunk' || !chunk.map) continue;
for (const source of chunk.map.sources) {
const file = path.resolve(out, path.dirname(chunk.fileName), source);
const root = rootOf(file);
// Code only: an image a module imports is named too, and no frame is ever in one.
if (root && /\.m?js$/.test(file)) {
compiled.push(`${chunk.fileName}: ${root.name}/${path.relative(path.dirname(root.dist), file).split(path.sep).join('/')}`);
}
}
}
if (compiled.length) this.error(`A chunk's map ends in a package's compiled code, not its source:\n ${compiled.join('\n ')}`);
},
},
};
}
@@ -0,0 +1,59 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { SourceMap } from 'node:module';
import path from 'node:path';
import test from 'node:test';
import { fileURLToPath } from 'node:url';
import { loadWithMap, packageSourceMaps } from './source-maps.mjs';
// This package as its build left it (the test runs after `npm run build`).
const root = fileURLToPath(new URL('..', import.meta.url));
const dist = path.join(root, 'dist');
const src = path.join(root, 'src');
// The line and column (0-based) at which `needle` starts in `text`.
function placeOf(text, needle) {
const before = text.slice(0, text.indexOf(needle)).split('\n');
return [before.length - 1, before.at(-1).length];
}
test('a compiled module loads with the map that sends it back to its source, with the source text', () => {
const source = path.join(src, 'client/crash.js');
const { code, map } = loadWithMap(path.join(dist, 'client/crash.js'), dist, src);
assert.doesNotMatch(code, /sourceMappingURL/);
assert.deepEqual(map.sources, [source]);
assert.equal(map.sourcesContent[0], fs.readFileSync(source, 'utf8'));
// The crash signature's test for a Chromium stack, the code a page crash's probe frame named in PostHog.
const signature = '/^\\s+at /';
const found = new SourceMap(map).findEntry(...placeOf(code, signature));
assert.deepEqual([found.originalSource, found.originalLine, found.originalColumn],
[source, ...placeOf(map.sourcesContent[0], signature)]);
});
test('a module the build copied as it is loads as its source, line for line and column for column', () => {
const source = path.join(src, 'common/stepTopology.mjs');
const { code, map } = loadWithMap(path.join(dist, 'common/stepTopology.mjs'), dist, src);
assert.equal(code, fs.readFileSync(source, 'utf8'));
const lines = code.trimEnd().split('\n');
const last = [lines.length - 1, lines.at(-1).length - 1];
for (const [line, column] of [[0, 0], placeOf(code, 'export function'), last]) {
const found = new SourceMap(map).findEntry(line, column);
assert.deepEqual([found.originalSource, found.originalLine, found.originalColumn], [source, line, column]);
}
});
test("a page's build fails when a chunk's map still ends in a package's compiled code", () => {
const plugin = packageSourceMaps(['@text-to-cad/core']);
plugin.configResolved({ root });
const out = path.join(root, 'tmp-out');
const chunk = sources => ({ type: 'chunk', fileName: 'assets/index-A.js', map: { sources } });
const fail = message => { throw new Error(message); };
const relative = file => path.relative(path.join(out, 'assets'), file).split(path.sep).join('/');
// Its source, and an image a module imports (no frame is ever in one), pass.
plugin.generateBundle.handler.call({ error: fail }, { dir: out },
{ 'index-A.js': chunk([relative(path.join(src, 'client/crash.js')), relative(path.join(dist, 'logo.svg'))]) });
assert.throws(() => plugin.generateBundle.handler.call({ error: fail }, { dir: out },
{ 'index-A.js': chunk([relative(path.join(dist, 'client/crash.js'))]) }),
/assets\/index-A\.js: @text-to-cad\/core\/dist\/client\/crash\.js/);
});
+2
View File
@@ -25,6 +25,8 @@ export async function buildLibrary(root, { css = false } = {}) {
}
await walk(src);
await fs.rm(dist, { recursive: true, force: true });
// Each module's map, with its source's text, is what a page's build chains to show a crash
// in this code as its source (@text-to-cad/core/source-maps).
await build({entryPoints: entries, outbase: src, outdir: dist, bundle: false,
format: 'esm', platform: 'neutral', target: 'es2022', jsx: 'automatic',
loader: { '.js': 'jsx' }, sourcemap: true, logLevel: 'warning'});