Skip to content

Commit 9513b8b

Browse files
Merge pull request #272 from pmndrs/feat/published-source-maps
fix(glyph): publish resolvable hidden source maps
2 parents b6ec800 + dcec79c commit 9513b8b

9 files changed

Lines changed: 119 additions & 3 deletions

File tree

‎.agents/docs/attestations/2026-10-09-glyph-bb87af1e.md‎

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎.agents/docs/log/2026-10-09-published-source-maps.md‎

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎.agents/docs/packages/glyph.md‎

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/glyph/package.json‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
"dist",
2727
"src",
2828
"!dist/.tsbuildinfo",
29-
"!dist/**/*.map",
3029
"!dist/internal/raster-baker-profile.d.ts",
3130
"!dist/internal/raster-baker-profile.js"
3231
],

‎packages/glyph/scripts/build.mjs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { basename, join } from 'node:path';
55
import { fileURLToPath, pathToFileURL } from 'node:url';
66

77
import { captureCommand } from './support/capture-command.mjs';
8+
import { rewritePublishedSourceMaps } from './support/published-source-maps.mjs';
89
import { writeGeneratedTypescriptAbi } from './support/generated-typescript-abi.mjs';
910
import { reproducibleRustEnvironment } from './support/reproducible-rust-env.mjs';
1011

@@ -280,6 +281,7 @@ await run(tsc, [
280281
// private test seams out of the public module graph while the published subpaths share
281282
// package-owned chunks and TypeGPU metadata compiled at publish time.
282283
await run(tsdown, ['--out-dir', fileURLToPath(stagingDirectory), '--no-clean']);
284+
await rewritePublishedSourceMaps(stagingPath, fileURLToPath(distributionDirectory), join(packageRoot, 'src'));
283285
await run(wasmOpt, [
284286
'--enable-bulk-memory',
285287
'--enable-nontrapping-float-to-int',
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { readFile, readdir, writeFile } from 'node:fs/promises';
2+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
3+
4+
/** Rebase staged maps onto the source tree that ships beside dist, without changing their mappings. */
5+
export async function rewritePublishedSourceMaps(stagingDirectory, distributionDirectory, sourceDirectory) {
6+
for (const entry of await readdir(stagingDirectory, { recursive: true })) {
7+
if (!entry.endsWith('.map')) continue;
8+
const stagedFile = join(stagingDirectory, entry);
9+
const map = JSON.parse(await readFile(stagedFile, 'utf8'));
10+
map.sources = map.sources.map((source) => {
11+
const sourcePath = resolve(dirname(stagedFile), map.sourceRoot ?? '', source);
12+
const sourceRelative = relative(sourceDirectory, sourcePath);
13+
if (isAbsolute(sourceRelative) || sourceRelative === '..' || sourceRelative.startsWith(`..${sep}`)) {
14+
throw new Error(`Source map ${entry} references a source outside the published tree: ${source}`);
15+
}
16+
return relative(dirname(join(distributionDirectory, entry)), sourcePath)
17+
.split(sep)
18+
.join('/');
19+
});
20+
delete map.sourceRoot;
21+
await writeFile(stagedFile, JSON.stringify(map));
22+
}
23+
}

‎packages/glyph/tests/package/packed-package.test.mjs‎

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import assert from 'node:assert/strict';
22
import { execFileSync, spawnSync } from 'node:child_process';
33
import { copyFile, mkdtemp, mkdir, readFile, readdir, rename, rm, stat, writeFile } from 'node:fs/promises';
44
import { tmpdir } from 'node:os';
5-
import { dirname, join } from 'node:path';
5+
import { dirname, join, resolve } from 'node:path';
66
import { fileURLToPath, pathToFileURL } from 'node:url';
77
import test from 'node:test';
88
import { build } from 'vite';
@@ -58,6 +58,32 @@ test('the packed package exposes every ESM subpath and no CommonJS entry', async
5858
'package.json',
5959
'src',
6060
]);
61+
const maps = packedFiles.filter((path) => path.startsWith('dist/') && path.endsWith('.map'));
62+
assert.ok(
63+
maps.some((path) => path.endsWith('.js.map')),
64+
'JavaScript maps must ship',
65+
);
66+
assert.ok(
67+
maps.some((path) => path.endsWith('.d.ts.map')),
68+
'declaration maps must ship',
69+
);
70+
for (const path of packedFiles.filter((entry) => entry.startsWith('dist/') && entry.endsWith('.js'))) {
71+
assert.doesNotMatch(await readFile(join(installedDirectory, path), 'utf8'), /sourceMappingURL=/, path);
72+
}
73+
for (const path of packedFiles.filter((entry) => entry.startsWith('dist/') && entry.endsWith('.d.ts'))) {
74+
const declaration = await readFile(join(installedDirectory, path), 'utf8');
75+
for (const [, map] of declaration.matchAll(/sourceMappingURL=([^\s]+)/g)) {
76+
assert.ok(packedFiles.includes(join(dirname(path), map)), `${path} must reference a packed map`);
77+
}
78+
}
79+
for (const path of maps) {
80+
const map = JSON.parse(await readFile(join(installedDirectory, path), 'utf8'));
81+
for (const source of map.sources) {
82+
const sourcePath = resolve(dirname(join(installedDirectory, path)), map.sourceRoot ?? '', source);
83+
assert.ok(sourcePath.startsWith(`${join(installedDirectory, 'src')}/`), `${path}: ${source}`);
84+
assert.ok((await stat(sourcePath)).isFile(), `${path} must resolve to a packed source: ${source}`);
85+
}
86+
}
6187
for (const peer of ['typegpu', '@typegpu/gl', '@typegpu/three']) {
6288
assert.ok(manifest.peerDependencies[peer], `${peer} must be a declared peer`);
6389
assert.equal(manifest.peerDependenciesMeta[peer]?.optional, true, `${peer} must remain optional`);
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import assert from 'node:assert/strict';
2+
import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
3+
import { tmpdir } from 'node:os';
4+
import { join, relative, resolve } from 'node:path';
5+
import test from 'node:test';
6+
import { rewritePublishedSourceMaps } from '../../scripts/support/published-source-maps.mjs';
7+
8+
test('staged JS and declaration maps resolve from the published tree and preserve mappings', async (context) => {
9+
const root = await mkdtemp(join(tmpdir(), 'glyph-source-maps-'));
10+
context.after(() => rm(root, { recursive: true, force: true }));
11+
const staging = join(root, '.cache/build/staging/internal');
12+
const source = join(root, 'src/internal');
13+
await mkdir(staging, { recursive: true });
14+
await mkdir(source, { recursive: true });
15+
const original = {
16+
version: 3,
17+
sources: ['module.ts'],
18+
sourceRoot: relative(staging, source),
19+
names: ['value'],
20+
mappings: 'AAAA',
21+
sourcesContent: ['export const value = 1;'],
22+
};
23+
for (const suffix of ['js', 'd.ts']) await writeFile(join(staging, `module.${suffix}.map`), JSON.stringify(original));
24+
await rewritePublishedSourceMaps(join(root, '.cache/build/staging'), join(root, 'dist'), join(root, 'src'));
25+
for (const suffix of ['js', 'd.ts']) {
26+
const map = JSON.parse(await readFile(join(staging, `module.${suffix}.map`), 'utf8'));
27+
assert.equal(resolve(root, 'dist/internal', map.sources[0]), join(source, 'module.ts'));
28+
const { sourceRoot: _sourceRoot, ...unchanged } = original;
29+
assert.deepEqual(map, { ...unchanged, sources: ['../../src/internal/module.ts'] });
30+
}
31+
});
32+
33+
test('source maps cannot escape the packaged source tree', async (context) => {
34+
const root = await mkdtemp(join(tmpdir(), 'glyph-source-map-escape-'));
35+
context.after(() => rm(root, { recursive: true, force: true }));
36+
await mkdir(join(root, 'stage'));
37+
await writeFile(join(root, 'stage/module.js.map'), JSON.stringify({ version: 3, sources: ['../../outside.ts'] }));
38+
await assert.rejects(
39+
rewritePublishedSourceMaps(join(root, 'stage'), join(root, 'dist'), join(root, 'src')),
40+
/outside the published tree/,
41+
);
42+
});

‎packages/glyph/tsdown.config.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const shared = defineConfig({
2323
neverBundle: true,
2424
},
2525
exports: false,
26-
// The package excludes the map files, so the emitted modules must not reference them.
26+
// Ship maps for external tooling while keeping browser modules free of map references.
2727
sourcemap: 'hidden',
2828
report: false,
2929
// Auto-naming wraps constructor calls outside their PURE annotations, preventing

0 commit comments

Comments
 (0)