Skip to content

Commit 35e4b6d

Browse files
authored
Merge pull request #372 from karthik2804/sourcemap_chaining
add ability to generate chained sourcemap for precompiled sources
2 parents 5d210a7 + 3222e92 commit 35e4b6d

File tree

10 files changed

+752
-624
lines changed

10 files changed

+752
-624
lines changed

packages/build-tools/package-lock.json

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

packages/build-tools/package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,14 @@
2929
"typescript": "^5.7.3"
3030
},
3131
"dependencies": {
32+
"@ampproject/remapping": "^2.3.0",
3233
"@bytecodealliance/componentize-js": "^0.18.1",
3334
"@bytecodealliance/jco": "^1.10.2",
34-
"yargs": "^17.7.2",
3535
"acorn-walk": "^8.3.4",
3636
"acron": "^1.0.5",
3737
"magic-string": "^0.30.17",
38-
"regexpu-core": "^6.2.0"
38+
"regexpu-core": "^6.2.0",
39+
"yargs": "^17.7.2"
3940
},
4041
"files": [
4142
"lib",
@@ -53,4 +54,4 @@
5354
}
5455
]
5556
}
56-
}
57+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import {
2+
getPackagesWithWasiDeps,
3+
processWasiDeps
4+
} from '../../dist/wasiDepsParser.js';
5+
6+
export async function SpinEsbuildPlugin() {
7+
const { getWitImports } = await import('../../lib/wit_tools.js');
8+
9+
// Step 1: Get WIT imports from dependencies
10+
const wasiDeps = getPackagesWithWasiDeps(process.cwd(), new Set(), true);
11+
const { witPaths, targetWorlds } = processWasiDeps(wasiDeps);
12+
const witImports = getWitImports(witPaths, targetWorlds);
13+
14+
// Store as a Set for fast lookup
15+
const externals = new Set(witImports);
16+
17+
return {
18+
name: 'spin-sdk-externals',
19+
setup(build) {
20+
build.onResolve({ filter: /.*/ }, args => {
21+
if (externals.has(args.path)) {
22+
return { path: args.path, external: true };
23+
}
24+
return null;
25+
});
26+
}
27+
};
28+
}

packages/build-tools/src/index.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import { version as componentizeVersion } from '@bytecodealliance/componentize-j
55
import { getPackagesWithWasiDeps, processWasiDeps } from './wasiDepsParser.js';
66
import {
77
calculateChecksum,
8+
chainSourceMaps,
9+
fileExists,
10+
getSourceMapFromFile,
811
saveBuildData,
912
} from './utils.js';
1013
import { getCliArgs } from './cli.js';
@@ -14,6 +17,8 @@ import { mergeWit } from '../lib/wit_tools.js';
1417
//@ts-ignore
1518
import { precompile } from "./precompile.js"
1619
import path from 'node:path'
20+
import { SourceMapInput } from '@ampproject/remapping';
21+
import { get } from 'node:http';
1722

1823
async function main() {
1924
try {
@@ -65,13 +70,20 @@ async function main() {
6570
console.log('Componentizing...');
6671

6772
const source = await readFile(src, 'utf8');
68-
const precompiledSource = precompile(source, src, true) as string;
73+
let { content: precompiledSource, sourceMap: precompiledSourceMap } = precompile(source, src, true, 'precompiled-source.js') as { content: string; sourceMap: SourceMapInput };
74+
// Check if input file has a source map because if we does, we need to chain it with the precompiled source map
75+
let inputSourceMap = await getSourceMapFromFile(src);
76+
if (inputSourceMap) {
77+
precompiledSourceMap = chainSourceMaps(precompiledSourceMap, { [src]: inputSourceMap }) as SourceMapInput;
78+
}
6979

70-
// Write precompiled source to disk for debugging purposes In the future we
71-
// will also write a source map to make debugging easier
80+
// Write precompiled source to disk for debugging purposes.
7281
let srcDir = path.dirname(src);
7382
let precompiledSourcePath = path.join(srcDir, 'precompiled-source.js');
7483
await writeFile(precompiledSourcePath, precompiledSource);
84+
if (precompiledSourceMap) {
85+
await writeFile(precompiledSourcePath + '.map', JSON.stringify(precompiledSourceMap, null, 2));
86+
}
7587

7688
const { component } = await componentize({
7789
sourcePath: precompiledSourcePath,

packages/build-tools/src/precompile.js

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const POSTAMBLE = '}';
1717
/// will intern regular expressions, duplicating them at the top level and testing them with both
1818
/// an ascii and utf8 string should ensure that they won't be re-compiled when run in the fetch
1919
/// handler.
20-
export function precompile(source, filename = '<input>', moduleMode = false) {
20+
export function precompile(source, filename = '<input>', moduleMode = false, precompiledFileName = 'precompiled-source.js') {
2121
const magicString = new MagicString(source, {
2222
filename,
2323
});
@@ -51,12 +51,22 @@ export function precompile(source, filename = '<input>', moduleMode = false) {
5151

5252
magicString.prepend(`${PREAMBLE}${precompileCalls.join('\n')}${POSTAMBLE}`);
5353

54-
// When we're ready to pipe in source maps:
55-
// const map = magicString.generateMap({
56-
// source: 'source.js',
57-
// file: 'converted.js.map',
58-
// includeContent: true
59-
// });
54+
const sourceMapRegex = /\/\/# sourceMappingURL=.*$/gm;
6055

61-
return magicString.toString();
56+
let match;
57+
while ((match = sourceMapRegex.exec(source))) {
58+
const start = match.index;
59+
const end = start + match[0].length;
60+
magicString.remove(start, end);
61+
}
62+
63+
const precompiledSource = magicString.toString() + `\n//# sourceMappingURL=${precompiledFileName}.map\n`;
64+
65+
const map = magicString.generateMap({
66+
source: filename,
67+
file: `${precompiledFileName}.map`,
68+
includeContent: true
69+
});
70+
71+
return { content: precompiledSource, sourceMap: map };
6272
}

packages/build-tools/src/utils.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,25 @@
11
import { readFile } from 'fs/promises';
22
import { createHash } from 'node:crypto';
33
import { access, writeFile } from 'node:fs/promises';
4+
import remapping, { SourceMapInput } from '@ampproject/remapping';
5+
import path from 'path';
46

7+
type FileName = string;
8+
type SourceMapLookup = Record<FileName, SourceMapInput>;
9+
10+
export function chainSourceMaps(finalMap: SourceMapInput, sourceMapLookup: SourceMapLookup) {
11+
return remapping(finalMap, (source) => {
12+
const sourceMap = sourceMapLookup[source];
13+
if (sourceMap) {
14+
return sourceMap;
15+
}
16+
// If not the source, we do not want to traverse it further so we return null.
17+
// This is because sometimes npm packages have their own source maps but do
18+
// not have the original source files.
19+
return null;
20+
});
21+
}
522

6-
// Function to calculate file checksum
723
export async function calculateChecksum(content: string | Buffer) {
824
try {
925
const hash = createHash('sha256');
@@ -58,3 +74,35 @@ export async function saveBuildData(
5874
throw error;
5975
}
6076
}
77+
78+
export async function getSourceMapFromFile(filePath: string): Promise<SourceMapInput | null> {
79+
try {
80+
const content = await readFile(filePath, 'utf8');
81+
82+
// Look for the sourceMappingURL comment
83+
const sourceMapRegex = /\/\/[#@]\s*sourceMappingURL=(.+)$/m;
84+
const match = content.match(sourceMapRegex);
85+
86+
if (!match) return null;
87+
88+
const sourceMapUrl = match[1].trim();
89+
90+
if (sourceMapUrl.startsWith('data:application/json;base64,')) {
91+
// Inline base64-encoded source map
92+
const base64 = sourceMapUrl.slice('data:application/json;base64,'.length);
93+
const rawMap = Buffer.from(base64, 'base64').toString('utf8');
94+
return JSON.parse(rawMap);
95+
} else {
96+
// External .map file
97+
const mapPath = path.resolve(path.dirname(filePath), sourceMapUrl);
98+
try {
99+
const rawMap = await readFile(mapPath, 'utf8');
100+
return JSON.parse(rawMap);
101+
} catch (e) {
102+
return null; // map file not found or invalid
103+
}
104+
}
105+
} catch (err) {
106+
return null; // file doesn't exist or can't be read
107+
}
108+
}

test/test-app/build.mjs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// build.mjs
2+
import { build } from 'esbuild';
3+
import path from 'path';
4+
import { SpinEsbuildPlugin } from "@spinframework/build-tools/plugins/esbuild/index.js";
5+
import fs from 'fs';
6+
7+
const spinPlugin = await SpinEsbuildPlugin();
8+
9+
// plugin to handle vendor files in node_modules that may not be bundled.
10+
// Instead of generating a real source map for these files, it appends a minimal
11+
// inline source map pointing to an empty source. This avoids errors and ensures
12+
// source maps exist even for unbundled vendor code.
13+
let SourceMapPlugin = {
14+
name: 'excludeVendorFromSourceMap',
15+
setup(build) {
16+
build.onLoad({ filter: /node_modules/ }, args => {
17+
return {
18+
contents: fs.readFileSync(args.path, 'utf8')
19+
+ '\n//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIiJdLCJtYXBwaW5ncyI6IkEifQ==',
20+
loader: 'default',
21+
}
22+
})
23+
},
24+
}
25+
26+
await build({
27+
entryPoints: ['./src/index.ts'],
28+
outfile: './build/bundle.js',
29+
bundle: true,
30+
format: 'esm',
31+
platform: 'node',
32+
sourcemap: true,
33+
minify: false,
34+
plugins: [spinPlugin, SourceMapPlugin],
35+
logLevel: 'error',
36+
loader: {
37+
'.ts': 'ts',
38+
'.tsx': 'tsx',
39+
},
40+
resolveExtensions: ['.ts', '.tsx', '.js'],
41+
// This prevents sourcemaps from traversing into node_modules
42+
sourceRoot: path.resolve(process.cwd(), 'src'),
43+
});

0 commit comments

Comments
 (0)