Skip to content

Commit

Permalink
feat: initial support for sourcemaps
Browse files Browse the repository at this point in the history
  • Loading branch information
esroyo committed Jun 26, 2024
1 parent 85f75f5 commit 371d706
Show file tree
Hide file tree
Showing 7 changed files with 211 additions and 13 deletions.
7 changes: 7 additions & 0 deletions deno.lock

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

1 change: 1 addition & 0 deletions deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export { rollup, VERSION as rollupVersion } from 'npm:rollup@3.29.4';
export { loadSync as dotenvLoad } from 'jsr:@std/dotenv@0.218.2';
export { serve } from 'jsr:@std/http@0.224.0';
export { dirname, resolve } from 'jsr:@std/path@0.224.0';
export { dirname as urlDirname, join as urlJoin } from 'jsr:@std/url@0.224.0';
export { default as request } from 'npm:request@2.88.2';
export { get as kvGet, set as kvSet } from 'jsr:@kitsonk/kv-toolbox@0.9.0/blob';
export { getBuildTargetFromUA } from 'npm:esm-compat@0.0.2';
Expand Down
11 changes: 10 additions & 1 deletion src/create-request-handler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
buildSourceModule,
calcExpires,
cloneHeaders,
denyHeaders,
Expand Down Expand Up @@ -234,8 +235,16 @@ export function createRequestHandler(
let body = await upstreamResponse.text();
upstreamSpan.end();
if (!isRawRequest && isJsResponse(upstreamResponse)) {
const sourcemapSpan = tracer.startSpan('sourcemap');
const sourceModule = await buildSourceModule(
body,
upstreamUrlString,
);
sourcemapSpan.end();
const buildSpan = tracer.startSpan('build');
const buildResult = await toSystemjs(body, { banner: OUTPUT_BANNER }, config);
const buildResult = await toSystemjs(sourceModule, {
banner: OUTPUT_BANNER,
}, config);
body = replaceOrigin(buildResult.code);
buildSpan.end();
} else {
Expand Down
7 changes: 5 additions & 2 deletions src/to-systemjs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export const toSystemjsMain = async (
const outputOptions: OutputOptions = {
dir: 'out', // not really used
format: 'systemjs' as ModuleFormat,
sourcemap: !!mod.map,
sourcemap: mod.map ? 'inline' : false,
...rollupOutputOptions,
footer: `/* rollup@${rollupVersion}${
rollupOutputOptions.footer ? ` - ${rollupOutputOptions.footer}` : ''
Expand All @@ -47,7 +47,10 @@ export const toSystemjsMain = async (
await bundle.close();
return {
code: output[0].code,
map: output[1] && output[1].type === 'asset' && typeof output[1].source === 'string' ? output[1].source : undefined,
map: output[1] && output[1].type === 'asset' &&
typeof output[1].source === 'string'
? output[1].source
: undefined,
};
};

Expand Down
6 changes: 3 additions & 3 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,9 @@ export type PartialServerTimingSpanExporter = Pick<
>;

export interface SourceDescription {
code: string;
map?: string | ExistingRawSourceMap;
};
code: string;
map?: string | ExistingRawSourceMap;
}

export interface RollupVirtualOptions {
[id: string]: string | SourceDescription;
Expand Down
139 changes: 138 additions & 1 deletion src/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { assertEquals } from '../dev_deps.ts';
import { assertEquals, spy } from '../dev_deps.ts';
/*
Deno.test('buildUpstreamUrl', async (t) => {
const { buildUpstreamUrl } = await import('./utils.ts');
Expand Down Expand Up @@ -109,3 +109,140 @@ Deno.test('sanitizeUpstreamOrigin', async (t) => {
);
});
});

Deno.test('parseSourceMapUrl', async (t) => {
const { parseSourceMapUrl } = await import('./utils.ts');

await t.step('should parse an absolute URL', async () => {
assertEquals(
parseSourceMapUrl(`
console.log('foo');
//# sourceMappingURL=http://example.com/path/to/your/sourcemap.map
`),
'http://example.com/path/to/your/sourcemap.map',
);
});

await t.step('should parse a relative URL', async () => {
assertEquals(
parseSourceMapUrl(`
console.log('foo');
//# sourceMappingURL=your-sourcemap.map
`),
'your-sourcemap.map',
);
});

await t.step('should require "start of line" matching', async () => {
assertEquals(
parseSourceMapUrl(`
console.log('//# sourceMappingURL=potato.map');
//# sourceMappingURL=http://example.com/path/to/your/sourcemap.map
`),
'http://example.com/path/to/your/sourcemap.map',
);
});

await t.step(
'should complete a relative URL if a baseUrl is provided',
async () => {
assertEquals(
parseSourceMapUrl(
`
console.log('foo');
//# sourceMappingURL=source.js.map
`,
'http://example.com/path/to/your/source.js',
),
'http://example.com/path/to/your/source.js.map',
);
},
);
});

Deno.test('buildSourceModule', async (t) => {
const { buildSourceModule } = await import('./utils.ts');

await t.step(
'should return the same input when no sourceMapURL exists',
async () => {
const moduleUrl =
'https://esm.sh/stable/@vue/runtime-dom@3.4.30/es2022/runtime-dom.mjs';
const moduleCode = `
console.log('foo');
`;
assertEquals(
await buildSourceModule(moduleCode, moduleUrl),
moduleCode,
);
},
);

await t.step(
'should return a SourceModule object when sourceMapURL exists',
async () => {
const sourceMapContent = '{"mappings":[],"version":3}';
const fetchMock = spy(() =>
Promise.resolve(
new Response(sourceMapContent),
)
);
const moduleUrl =
'https://esm.sh/stable/@vue/runtime-dom@3.4.30/es2022/runtime-dom.mjs';
const moduleCode = `
console.log('foo');
//# sourceMappingURL=rumtime-dom.js.map
`;
assertEquals(
await buildSourceModule(moduleCode, moduleUrl, fetchMock),
{ code: moduleCode, map: sourceMapContent, name: moduleUrl },
);
},
);

await t.step(
'should return the same input when sourceMapURL fetching is not OK',
async () => {
const fetchMock = spy(() =>
Promise.resolve(
new Response(null, { status: 404 }),
)
);
const moduleUrl =
'https://esm.sh/stable/@vue/runtime-dom@3.4.30/es2022/runtime-dom.mjs';
const moduleCode = `
console.log('foo');
//# sourceMappingURL=rumtime-dom.js.map
`;
assertEquals(
await buildSourceModule(moduleCode, moduleUrl, fetchMock),
moduleCode,
);
},
);

await t.step(
'should return the same input upon network error',
async () => {
const fetchMock = spy(() =>
Promise.reject(
new TypeError('Network request failed'),
)
);
const moduleUrl =
'https://esm.sh/stable/@vue/runtime-dom@3.4.30/es2022/runtime-dom.mjs';
const moduleCode = `
console.log('foo');
//# sourceMappingURL=rumtime-dom.js.map
`;
assertEquals(
await buildSourceModule(moduleCode, moduleUrl, fetchMock),
moduleCode,
);
},
);
});
53 changes: 47 additions & 6 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import { request } from '../deps.ts';
import type { HttpZResponseModel } from './types.ts';
import { request, urlDirname, urlJoin } from '../deps.ts';
import type { HttpZResponseModel, SourceModule } from './types.ts';

export const nodeRequest = async (
url: string,
init: RequestInit,
init?: RequestInit,
): Promise<Response> => {
return new Promise<Response>((resolve, reject) => {
const headers = Object.fromEntries(new Headers(init.headers).entries());
const headers = Object.fromEntries(
new Headers(init?.headers ?? {}).entries(),
);
request(
{
method: init.method || 'GET',
method: init?.method || 'GET',
url,
followRedirect: !init.redirect || init.redirect === 'follow',
followRedirect: !init?.redirect || init.redirect === 'follow',
headers,
},
function (
Expand Down Expand Up @@ -143,3 +145,42 @@ export const sanitizeUpstreamOrigin = (
}
return `${url.origin}${url.pathname}`;
};

const sourceMapRegExp = /^\/\/# sourceMappingURL=(\S+)/m;
export const parseSourceMapUrl = (
input: string,
baseUrl?: string,
): string | undefined => {
const m = input.match(sourceMapRegExp);
if (!m) {
return undefined;
}
if (!baseUrl) {
return m[1];
}
return urlJoin(urlDirname(baseUrl), m[1]).toString();
};

export const buildSourceModule = async (
input: string,
baseUrl: string,
fetch = nodeRequest,
): Promise<string | SourceModule> => {
try {
const sourceMapUrl = parseSourceMapUrl(input, baseUrl);
if (!sourceMapUrl) {
return input;
}
const sourceMapResponse = await fetch(sourceMapUrl);
if (!sourceMapResponse.ok) {
return input;
}
return {
code: input,
map: await sourceMapResponse.text(),
name: baseUrl,
};
} catch (_) {
return input;
}
};

0 comments on commit 371d706

Please sign in to comment.