From b94bcc98be854d937ee39014326fc9bae703cf5f Mon Sep 17 00:00:00 2001 From: killa Date: Fri, 1 May 2026 23:08:48 +0800 Subject: [PATCH 1/6] feat(bundler): patch turbopack import meta output --- tools/egg-bundler/src/lib/Bundler.ts | 147 ++++++++++++++++++++- tools/egg-bundler/test/integration.test.ts | 111 +++++++++++++++- 2 files changed, 255 insertions(+), 3 deletions(-) diff --git a/tools/egg-bundler/src/lib/Bundler.ts b/tools/egg-bundler/src/lib/Bundler.ts index 72ab9a999c..bfd22b075e 100644 --- a/tools/egg-bundler/src/lib/Bundler.ts +++ b/tools/egg-bundler/src/lib/Bundler.ts @@ -12,6 +12,16 @@ const debug = debuglog('egg/bundler/bundler'); const BUNDLE_MANIFEST_VERSION = 1; const BUNDLE_MANIFEST_FILENAME = 'bundle-manifest.json'; +const IMPORT_META_FALLBACK_FILENAME_EXPR = + '(() => { const entryArg = typeof process !== "undefined" && process.argv && process.argv[1] ? process.argv[1] : "worker.js"; if (/^(?:[A-Za-z]:[\\\\/]|\\\\\\\\|\\/)/.test(entryArg)) return entryArg; const cwd = typeof process !== "undefined" && process.cwd ? process.cwd() : "."; const raw = cwd + "/" + entryArg; const parts = []; for (const part of raw.replace(/\\\\/g, "/").split("/")) { if (!part || part === ".") continue; if (part === "..") parts.pop(); else parts.push(part); } return (raw.startsWith("/") ? "/" : "") + parts.join("/"); })()'; +const IMPORT_META_FILENAME_EXPR = `(typeof __filename === "string" ? __filename : ${IMPORT_META_FALLBACK_FILENAME_EXPR})`; +const IMPORT_META_URL_EXPR = `(() => { const u = new URL("file:///"); u.pathname = ${IMPORT_META_FILENAME_EXPR}.replace(/\\\\/g, "/"); return u.href; })()`; +const THROWING_IMPORT_META_URL = + /\(\(\)\s*=>\s*\{\s*throw\s+new\s+Error\(\s*['"][^'"]*import\.meta\.url[^'"]*['"]\s*\)\s*;?\s*\}\)\s*\(\)/g; +const TURBOPACK_IMPORT_META_OBJECT = + /\b(var|let|const)\s+([A-Za-z_$][\w$]*import\$2e\$meta__[A-Za-z0-9_$]*)\s*=\s*\{\s*get\s+url\s*\(\)\s*\{[\s\S]*?\}\s*\};?/g; +const LINE_SOURCE_MAP_URL = /(?:\r?\n)?\/\/# sourceMappingURL=([^\r\n]*)\s*$/; +const BLOCK_SOURCE_MAP_URL = /(?:\r?\n)?\/\*# sourceMappingURL=([\s\S]*?)\*\/\s*$/; interface BundleManifest { readonly version: number; @@ -90,6 +100,15 @@ export class Bundler { const packResult = await wrapStep('pack build', () => packRunner.run()); debug('pack produced %d files', packResult.files.length); + const patchResult = await wrapStep('patch import.meta output', () => + this.#patchImportMetaOutput(absOutputDir, packResult.files), + ); + debug( + 'patched %d import.meta output occurrences and removed %d sourcemaps', + patchResult.patchCount, + patchResult.deletedMapCount, + ); + // Merge project name into output package.json so the framework's // getAppname() finds it (it reads baseDir/package.json). const outputPkgPath = path.join(absOutputDir, 'package.json'); @@ -113,14 +132,14 @@ export class Bundler { framework, entries: [{ name: 'worker', source: entries.workerEntry }], externals: Object.keys(externalsMap).sort((a, b) => a.localeCompare(b)), - chunks: packResult.files, + chunks: patchResult.outputFiles, }; await wrapStep('write bundle-manifest', () => fs.writeFile(manifestPathAbs, JSON.stringify(bundleManifest, null, 2)), ); // Re-enumerate files so bundle-manifest.json is included in the result. - const finalRelFiles = new Set(packResult.files); + const finalRelFiles = new Set(patchResult.outputFiles); finalRelFiles.add(BUNDLE_MANIFEST_FILENAME); const files = Array.from(finalRelFiles) .map((rel) => path.join(absOutputDir, rel)) @@ -132,4 +151,128 @@ export class Bundler { manifestPath: manifestPathAbs, }; } + + async #patchImportMetaOutput( + outputDir: string, + inputFiles: readonly string[], + ): Promise<{ patchCount: number; deletedMapCount: number; outputFiles: readonly string[] }> { + let patchCount = 0; + let deletedMapCount = 0; + const files = inputFiles.map((rel) => this.#sanitizeOutputRelativePath(rel)).sort((a, b) => a.localeCompare(b)); + const deletedFiles = new Set(); + + for (const rel of files) { + if (!rel.endsWith('.js')) continue; + + const filepath = path.join(outputDir, rel); + const content = await fs.readFile(filepath, 'utf8'); + + let metaMatches = 0; + let patched = content.replace( + TURBOPACK_IMPORT_META_OBJECT, + (_match, declarationKind: string, metaName: string) => { + metaMatches++; + return this.#renderImportMetaObject(declarationKind, metaName); + }, + ); + + const urlMatches = patched.match(THROWING_IMPORT_META_URL); + patched = patched.replace(THROWING_IMPORT_META_URL, IMPORT_META_URL_EXPR); + + const patchesForFile = (urlMatches?.length ?? 0) + metaMatches; + if (patchesForFile === 0) continue; + + const stripped = this.#stripSourceMappingUrl(patched); + await fs.writeFile(filepath, stripped); + + patchCount += patchesForFile; + const staleMaps = await this.#deleteStaleSourceMaps(outputDir, filepath, content); + deletedMapCount += staleMaps.deletedCount; + for (const deleted of staleMaps.deletedFiles) deletedFiles.add(deleted); + debug('patched %d import.meta output occurrences in %s', patchesForFile, rel); + } + + const outputFiles = files.filter((rel) => !deletedFiles.has(rel)); + return { patchCount, deletedMapCount, outputFiles }; + } + + #renderImportMetaObject(declarationKind: string, metaName: string): string { + return `${declarationKind} ${metaName} = (() => { + const filename = ${IMPORT_META_FILENAME_EXPR}; + const dirname = typeof __dirname === "string" ? __dirname : filename.replace(/[\\\\/][^\\\\/]*$/, ""); + const url = (() => { const u = new URL("file:///"); u.pathname = filename.replace(/\\\\/g, "/"); return u.href; })(); + return { + get url () { + return url; + }, + get dirname () { + return dirname; + }, + get filename () { + return filename; + } +}; +})();`; + } + + #stripSourceMappingUrl(content: string): string { + return content.replace(LINE_SOURCE_MAP_URL, '').replace(BLOCK_SOURCE_MAP_URL, ''); + } + + async #deleteStaleSourceMaps( + outputDir: string, + filepath: string, + originalContent: string, + ): Promise<{ deletedCount: number; deletedFiles: readonly string[] }> { + const mapPaths = new Set([`${filepath}.map`]); + const sourceMapUrl = this.#extractSourceMappingUrl(originalContent); + if (sourceMapUrl && !sourceMapUrl.startsWith('data:')) { + const resolved = path.resolve(path.dirname(filepath), sourceMapUrl); + if (this.#isInsideDir(outputDir, resolved)) mapPaths.add(resolved); + } + + let deletedCount = 0; + const deletedFiles: string[] = []; + for (const mapPath of mapPaths) { + if (!this.#isInsideDir(outputDir, mapPath)) continue; + try { + await fs.unlink(mapPath); + deletedCount++; + deletedFiles.push( + this.#sanitizeOutputRelativePath(path.relative(outputDir, mapPath).split(path.sep).join('/')), + ); + } catch (err) { + if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err; + } + } + return { deletedCount, deletedFiles }; + } + + #extractSourceMappingUrl(content: string): string | undefined { + const lineMatch = content.match(LINE_SOURCE_MAP_URL); + if (lineMatch?.[1]) return lineMatch[1].trim(); + const blockMatch = content.match(BLOCK_SOURCE_MAP_URL); + if (blockMatch?.[1]) return blockMatch[1].trim(); + return undefined; + } + + #sanitizeOutputRelativePath(relativeName: string): string { + const normalized = relativeName.split(path.sep).join('/'); + const segments = normalized.split('/'); + if ( + !normalized || + path.posix.isAbsolute(normalized) || + segments.some((segment) => !segment || segment === '.' || segment === '..') || + normalized.includes('\0') || + /[\r\n\u2028\u2029]/u.test(normalized) + ) { + throw new Error(`Unsafe bundle output path: ${relativeName}`); + } + return normalized; + } + + #isInsideDir(dir: string, target: string): boolean { + const rel = path.relative(dir, target); + return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel)); + } } diff --git a/tools/egg-bundler/test/integration.test.ts b/tools/egg-bundler/test/integration.test.ts index 69dec64d81..ab0a76e5f4 100644 --- a/tools/egg-bundler/test/integration.test.ts +++ b/tools/egg-bundler/test/integration.test.ts @@ -1,7 +1,8 @@ import fs from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; -import { fileURLToPath } from 'node:url'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { runInNewContext } from 'node:vm'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; @@ -183,6 +184,114 @@ describe('bundle() integration — minimal-app (Phase 1: mocked @utoo/pack)', () expect(bm.externals).toContain('synthetic-force-ext'); }); + it('patches nested Turbopack import.meta chunks and removes stale sourcemaps', async () => { + const sourceMapToken = 'sourceMapping' + 'URL'; + const throwingMeta = `var __TURBOPACK__import$2e$meta__ = { + get url () { + return (() => { throw new Error("could not convert import.meta.url to filepath"); })(); + } +}; +globalThis.__patchedMeta = { + url: __TURBOPACK__import$2e$meta__.url, + dirname: __TURBOPACK__import$2e$meta__.dirname, + filename: __TURBOPACK__import$2e$meta__.filename +}; +//# ${sourceMapToken}=chunk #.js.map +`; + const urlOnlyMeta = `let __TURBOPACK__import$2e$meta__ = { get url () { return "file:///already-patched.js"; } }; +globalThis.__patchedMeta = { + url: __TURBOPACK__import$2e$meta__.url, + dirname: __TURBOPACK__import$2e$meta__.dirname, + filename: __TURBOPACK__import$2e$meta__.filename +}; +/*# ${sourceMapToken}=url-only.js.map */ +`; + + const buildFunc: BuildFunc = async () => { + await fs.writeFile(path.join(tmpOutput, 'worker.js'), '// mock worker entry\n'); + await fs.mkdir(path.join(tmpOutput, 'chunks'), { recursive: true }); + await fs.writeFile(path.join(tmpOutput, 'chunks/chunk #.js'), throwingMeta); + await fs.writeFile(path.join(tmpOutput, 'chunks/chunk #.js.map'), '{"version":3}'); + await fs.writeFile(path.join(tmpOutput, 'chunks/url-only.js'), urlOnlyMeta); + await fs.writeFile(path.join(tmpOutput, 'chunks/url-only.js.map'), '{"version":3}'); + }; + + const result = await bundle({ + baseDir: tmpApp, + outputDir: tmpOutput, + pack: { buildFunc }, + }); + const bm = JSON.parse(await fs.readFile(result.manifestPath, 'utf8')) as { chunks: string[] }; + + async function runPatchedChunk( + filepath: string, + options: { argv: string[]; filename?: string }, + ): Promise<{ url: string; dirname: string; filename: string }> { + interface SandboxProcess { + argv: string[]; + cwd: () => string; + } + interface Sandbox { + URL: typeof URL; + process: SandboxProcess; + globalThis: Sandbox; + __dirname?: string; + __filename?: string; + __patchedMeta?: { url: string; dirname: string; filename: string }; + } + const sandbox = { + URL, + process: { argv: options.argv, cwd: () => tmpOutput }, + } as unknown as Sandbox; + if (options.filename) { + sandbox.__filename = options.filename; + sandbox.__dirname = path.dirname(options.filename); + } + sandbox.globalThis = sandbox; + runInNewContext(await fs.readFile(filepath, 'utf8'), sandbox); + return sandbox.__patchedMeta!; + } + + function expectedFileUrl(filename: string): string { + return pathToFileURL(filename).href; + } + + const nestedFilename = path.join(tmpOutput, 'chunks/chunk #.js'); + const nestedMeta = await runPatchedChunk(nestedFilename, { argv: ['node', 'worker.js'], filename: nestedFilename }); + expect(nestedMeta).toEqual({ + url: expectedFileUrl(nestedFilename), + dirname: path.dirname(nestedFilename), + filename: nestedFilename, + }); + + const urlOnlyFilename = path.join(tmpOutput, 'chunks/url-only.js'); + const urlOnlyPatched = await fs.readFile(urlOnlyFilename, 'utf8'); + expect(urlOnlyPatched).not.toContain('already-patched.js'); + const urlOnlyMetaResult = await runPatchedChunk(urlOnlyFilename, { argv: ['node'], filename: urlOnlyFilename }); + expect(urlOnlyMetaResult).toEqual({ + url: expectedFileUrl(urlOnlyFilename), + dirname: path.dirname(urlOnlyFilename), + filename: urlOnlyFilename, + }); + + const fallbackFilename = path.join(tmpOutput, 'worker.js'); + const fallbackMetaResult = await runPatchedChunk(urlOnlyFilename, { argv: ['node', './worker.js'] }); + expect(fallbackMetaResult).toEqual({ + url: expectedFileUrl(fallbackFilename), + dirname: tmpOutput, + filename: fallbackFilename, + }); + + for (const name of ['chunks/chunk #.js', 'chunks/url-only.js']) { + const content = await fs.readFile(path.join(tmpOutput, name), 'utf8'); + expect(content).not.toContain(sourceMapToken); + } + await expect(fs.stat(path.join(tmpOutput, 'chunks/chunk #.js.map'))).rejects.toMatchObject({ code: 'ENOENT' }); + await expect(fs.stat(path.join(tmpOutput, 'chunks/url-only.js.map'))).rejects.toMatchObject({ code: 'ENOENT' }); + expect(result.files).not.toEqual(expect.arrayContaining([expect.stringContaining('.js.map')])); + expect(bm.chunks).not.toEqual(expect.arrayContaining([expect.stringContaining('.js.map')])); + }); + it('wraps a buildFunc failure under the "pack build" step with an identifiable prefix and preserves cause', async () => { const original = new Error('synthetic pack failure'); const buildFunc: BuildFunc = async () => { From e4d93fbe6123d8b67ce84de79b3e1059f509342e Mon Sep 17 00:00:00 2001 From: killa Date: Fri, 1 May 2026 23:37:17 +0800 Subject: [PATCH 2/6] fix(bundler): reject win32 absolute output paths --- tools/egg-bundler/src/lib/Bundler.ts | 29 +++++++++++++--------- tools/egg-bundler/test/integration.test.ts | 6 +++++ 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/tools/egg-bundler/src/lib/Bundler.ts b/tools/egg-bundler/src/lib/Bundler.ts index bfd22b075e..fee22461be 100644 --- a/tools/egg-bundler/src/lib/Bundler.ts +++ b/tools/egg-bundler/src/lib/Bundler.ts @@ -42,6 +42,22 @@ function wrapStep(step: string, fn: () => Promise): Promise { }); } +export function sanitizeBundleOutputRelativePath(relativeName: string): string { + const normalized = relativeName.split(path.sep).join('/'); + const segments = normalized.split('/'); + if ( + !normalized || + path.posix.isAbsolute(normalized) || + path.win32.isAbsolute(normalized) || + segments.some((segment) => !segment || segment === '.' || segment === '..') || + normalized.includes('\0') || + /[\r\n\u2028\u2029]/u.test(normalized) + ) { + throw new Error(`Unsafe bundle output path: ${relativeName}`); + } + return normalized; +} + export class Bundler { readonly #config: BundlerConfig; @@ -257,18 +273,7 @@ export class Bundler { } #sanitizeOutputRelativePath(relativeName: string): string { - const normalized = relativeName.split(path.sep).join('/'); - const segments = normalized.split('/'); - if ( - !normalized || - path.posix.isAbsolute(normalized) || - segments.some((segment) => !segment || segment === '.' || segment === '..') || - normalized.includes('\0') || - /[\r\n\u2028\u2029]/u.test(normalized) - ) { - throw new Error(`Unsafe bundle output path: ${relativeName}`); - } - return normalized; + return sanitizeBundleOutputRelativePath(relativeName); } #isInsideDir(dir: string, target: string): boolean { diff --git a/tools/egg-bundler/test/integration.test.ts b/tools/egg-bundler/test/integration.test.ts index ab0a76e5f4..63a524fe32 100644 --- a/tools/egg-bundler/test/integration.test.ts +++ b/tools/egg-bundler/test/integration.test.ts @@ -7,6 +7,7 @@ import { runInNewContext } from 'node:vm'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { bundle, type BuildFunc } from '../src/index.ts'; +import { sanitizeBundleOutputRelativePath } from '../src/lib/Bundler.ts'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const FIXTURE_BASE = path.join(__dirname, 'fixtures/apps/minimal-app'); @@ -292,6 +293,11 @@ globalThis.__patchedMeta = { expect(bm.chunks).not.toEqual(expect.arrayContaining([expect.stringContaining('.js.map')])); }); + it('rejects Windows drive-absolute output paths before resolving bundle files', () => { + expect(() => sanitizeBundleOutputRelativePath('C:/foo.js')).toThrow(/Unsafe bundle output path/); + expect(() => sanitizeBundleOutputRelativePath('C:\\foo.js')).toThrow(/Unsafe bundle output path/); + }); + it('wraps a buildFunc failure under the "pack build" step with an identifiable prefix and preserves cause', async () => { const original = new Error('synthetic pack failure'); const buildFunc: BuildFunc = async () => { From 8193aa66e67c13d418bee92d8ff955c065ff153c Mon Sep 17 00:00:00 2001 From: killa Date: Fri, 1 May 2026 23:50:24 +0800 Subject: [PATCH 3/6] fix(bundler): normalize output path separators --- tools/egg-bundler/src/lib/Bundler.ts | 2 +- tools/egg-bundler/test/integration.test.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/egg-bundler/src/lib/Bundler.ts b/tools/egg-bundler/src/lib/Bundler.ts index fee22461be..b1c4d08ac8 100644 --- a/tools/egg-bundler/src/lib/Bundler.ts +++ b/tools/egg-bundler/src/lib/Bundler.ts @@ -43,7 +43,7 @@ function wrapStep(step: string, fn: () => Promise): Promise { } export function sanitizeBundleOutputRelativePath(relativeName: string): string { - const normalized = relativeName.split(path.sep).join('/'); + const normalized = relativeName.replace(/\\/g, '/'); const segments = normalized.split('/'); if ( !normalized || diff --git a/tools/egg-bundler/test/integration.test.ts b/tools/egg-bundler/test/integration.test.ts index 63a524fe32..170d266206 100644 --- a/tools/egg-bundler/test/integration.test.ts +++ b/tools/egg-bundler/test/integration.test.ts @@ -296,6 +296,7 @@ globalThis.__patchedMeta = { it('rejects Windows drive-absolute output paths before resolving bundle files', () => { expect(() => sanitizeBundleOutputRelativePath('C:/foo.js')).toThrow(/Unsafe bundle output path/); expect(() => sanitizeBundleOutputRelativePath('C:\\foo.js')).toThrow(/Unsafe bundle output path/); + expect(() => sanitizeBundleOutputRelativePath('..\\foo.js')).toThrow(/Unsafe bundle output path/); }); it('wraps a buildFunc failure under the "pack build" step with an identifiable prefix and preserves cause', async () => { From a6150a882852b465eb70d7c3dbb4433d60b52f2c Mon Sep 17 00:00:00 2001 From: killa Date: Sat, 2 May 2026 00:02:40 +0800 Subject: [PATCH 4/6] fix(bundler): tighten import meta patch cleanup --- tools/egg-bundler/src/lib/Bundler.ts | 19 +++++++++++--- tools/egg-bundler/test/integration.test.ts | 29 +++++++++++++++++++--- 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/tools/egg-bundler/src/lib/Bundler.ts b/tools/egg-bundler/src/lib/Bundler.ts index b1c4d08ac8..c8578124af 100644 --- a/tools/egg-bundler/src/lib/Bundler.ts +++ b/tools/egg-bundler/src/lib/Bundler.ts @@ -12,8 +12,21 @@ const debug = debuglog('egg/bundler/bundler'); const BUNDLE_MANIFEST_VERSION = 1; const BUNDLE_MANIFEST_FILENAME = 'bundle-manifest.json'; -const IMPORT_META_FALLBACK_FILENAME_EXPR = - '(() => { const entryArg = typeof process !== "undefined" && process.argv && process.argv[1] ? process.argv[1] : "worker.js"; if (/^(?:[A-Za-z]:[\\\\/]|\\\\\\\\|\\/)/.test(entryArg)) return entryArg; const cwd = typeof process !== "undefined" && process.cwd ? process.cwd() : "."; const raw = cwd + "/" + entryArg; const parts = []; for (const part of raw.replace(/\\\\/g, "/").split("/")) { if (!part || part === ".") continue; if (part === "..") parts.pop(); else parts.push(part); } return (raw.startsWith("/") ? "/" : "") + parts.join("/"); })()'; +const IMPORT_META_FALLBACK_FILENAME_EXPR = [ + '(() => {', + 'const entryArg = typeof process !== "undefined" && process.argv && process.argv[1] ? process.argv[1] : "worker.js";', + 'if (/^(?:[A-Za-z]:[\\\\/]|\\\\\\\\|\\/)/.test(entryArg)) return entryArg;', + 'const cwd = typeof process !== "undefined" && process.cwd ? process.cwd() : ".";', + 'const sep = cwd.includes("\\\\") ? "\\\\" : "/";', + 'const raw = cwd + sep + entryArg;', + 'const slash = raw.replace(/\\\\/g, "/");', + 'const root = /^[A-Za-z]:\\//.test(slash) ? slash.slice(0, 2) : slash.startsWith("//") ? "//" : slash.startsWith("/") ? "/" : "";', + 'const body = root && root !== "/" ? slash.slice(root.length + (root === "//" ? 0 : 1)) : slash;', + 'const parts = [];', + 'for (const part of body.split("/")) { if (!part || part === ".") continue; if (part === "..") parts.pop(); else parts.push(part); }', + 'return root === "/" ? "/" + parts.join("/") : root === "//" ? (sep === "\\\\" ? "\\\\\\\\" : "//") + parts.join(sep) : root ? root + sep + parts.join(sep) : parts.join(sep);', + '})()', +].join(' '); const IMPORT_META_FILENAME_EXPR = `(typeof __filename === "string" ? __filename : ${IMPORT_META_FALLBACK_FILENAME_EXPR})`; const IMPORT_META_URL_EXPR = `(() => { const u = new URL("file:///"); u.pathname = ${IMPORT_META_FILENAME_EXPR}.replace(/\\\\/g, "/"); return u.href; })()`; const THROWING_IMPORT_META_URL = @@ -244,7 +257,7 @@ export class Bundler { const sourceMapUrl = this.#extractSourceMappingUrl(originalContent); if (sourceMapUrl && !sourceMapUrl.startsWith('data:')) { const resolved = path.resolve(path.dirname(filepath), sourceMapUrl); - if (this.#isInsideDir(outputDir, resolved)) mapPaths.add(resolved); + if (resolved.endsWith('.map') && this.#isInsideDir(outputDir, resolved)) mapPaths.add(resolved); } let deletedCount = 0; diff --git a/tools/egg-bundler/test/integration.test.ts b/tools/egg-bundler/test/integration.test.ts index 170d266206..41f32da951 100644 --- a/tools/egg-bundler/test/integration.test.ts +++ b/tools/egg-bundler/test/integration.test.ts @@ -206,6 +206,14 @@ globalThis.__patchedMeta = { filename: __TURBOPACK__import$2e$meta__.filename }; /*# ${sourceMapToken}=url-only.js.map */ +`; + const nonMapTargetMeta = `let __TURBOPACK__import$2e$meta__ = { get url () { return "file:///already-patched.js"; } }; +globalThis.__patchedMeta = { + url: __TURBOPACK__import$2e$meta__.url, + dirname: __TURBOPACK__import$2e$meta__.dirname, + filename: __TURBOPACK__import$2e$meta__.filename +}; +//# ${sourceMapToken}=not-a-map.txt `; const buildFunc: BuildFunc = async () => { @@ -215,6 +223,8 @@ globalThis.__patchedMeta = { await fs.writeFile(path.join(tmpOutput, 'chunks/chunk #.js.map'), '{"version":3}'); await fs.writeFile(path.join(tmpOutput, 'chunks/url-only.js'), urlOnlyMeta); await fs.writeFile(path.join(tmpOutput, 'chunks/url-only.js.map'), '{"version":3}'); + await fs.writeFile(path.join(tmpOutput, 'chunks/non-map-target.js'), nonMapTargetMeta); + await fs.writeFile(path.join(tmpOutput, 'chunks/not-a-map.txt'), 'keep me'); }; const result = await bundle({ @@ -226,7 +236,7 @@ globalThis.__patchedMeta = { async function runPatchedChunk( filepath: string, - options: { argv: string[]; filename?: string }, + options: { argv: string[]; filename?: string; cwd?: string }, ): Promise<{ url: string; dirname: string; filename: string }> { interface SandboxProcess { argv: string[]; @@ -242,7 +252,7 @@ globalThis.__patchedMeta = { } const sandbox = { URL, - process: { argv: options.argv, cwd: () => tmpOutput }, + process: { argv: options.argv, cwd: () => options.cwd ?? tmpOutput }, } as unknown as Sandbox; if (options.filename) { sandbox.__filename = options.filename; @@ -283,12 +293,25 @@ globalThis.__patchedMeta = { filename: fallbackFilename, }); - for (const name of ['chunks/chunk #.js', 'chunks/url-only.js']) { + const windowsFallbackMetaResult = await runPatchedChunk(urlOnlyFilename, { + argv: ['node', 'worker.js'], + cwd: 'C:\\app\\dist', + }); + expect(windowsFallbackMetaResult).toEqual({ + url: 'file:///C:/app/dist/worker.js', + dirname: 'C:\\app\\dist', + filename: 'C:\\app\\dist\\worker.js', + }); + + for (const name of ['chunks/chunk #.js', 'chunks/url-only.js', 'chunks/non-map-target.js']) { const content = await fs.readFile(path.join(tmpOutput, name), 'utf8'); expect(content).not.toContain(sourceMapToken); } await expect(fs.stat(path.join(tmpOutput, 'chunks/chunk #.js.map'))).rejects.toMatchObject({ code: 'ENOENT' }); await expect(fs.stat(path.join(tmpOutput, 'chunks/url-only.js.map'))).rejects.toMatchObject({ code: 'ENOENT' }); + await expect(fs.stat(path.join(tmpOutput, 'chunks/not-a-map.txt'))).resolves.toBeTruthy(); + expect(result.files).toEqual(expect.arrayContaining([path.join(tmpOutput, 'chunks/not-a-map.txt')])); + expect(bm.chunks).toContain('chunks/not-a-map.txt'); expect(result.files).not.toEqual(expect.arrayContaining([expect.stringContaining('.js.map')])); expect(bm.chunks).not.toEqual(expect.arrayContaining([expect.stringContaining('.js.map')])); }); From 8a1d086c7bfc231ec00287dc8626fb19470876a3 Mon Sep 17 00:00:00 2001 From: killa Date: Sat, 2 May 2026 00:44:12 +0800 Subject: [PATCH 5/6] test(bundler): cover import meta output edge cases --- tools/egg-bundler/test/EntryGenerator.test.ts | 25 +++++++++++++++++++ tools/egg-bundler/test/integration.test.ts | 19 ++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/tools/egg-bundler/test/EntryGenerator.test.ts b/tools/egg-bundler/test/EntryGenerator.test.ts index e694ecb27b..cdb25892a0 100644 --- a/tools/egg-bundler/test/EntryGenerator.test.ts +++ b/tools/egg-bundler/test/EntryGenerator.test.ts @@ -106,6 +106,31 @@ describe('EntryGenerator', () => { expect(imports.map((i) => i.index)).toEqual([0, 1, 2, 3]); }); + it('keeps absolute tegg decorated files only when they stay inside baseDir', async () => { + const unitPath = path.join(tmpDir, 'modules/foo'); + const manifest = makeManifest({ + extensions: { + tegg: { + moduleDescriptors: [ + { unitPath, decoratedFiles: ['FooController.ts', '../outside.ts'] }, + { unitPath: path.dirname(tmpDir), decoratedFiles: [path.basename(tmpDir) + '/app/Service.ts'] }, + { unitPath: path.dirname(tmpDir), decoratedFiles: ['other-app/ignored.ts'] }, + ], + }, + }, + }); + + const gen = new EntryGenerator({ baseDir: tmpDir, manifestLoader: createFakeLoader(manifest) }); + const result = await gen.generate(); + const worker = await fs.readFile(result.workerEntry, 'utf8'); + + expect(extractImports(worker).map((i) => i.specifier)).toEqual([ + '../../app/Service.ts', + '../../modules/foo/FooController.ts', + '../../modules/outside.ts', + ]); + }); + it('skips resolveCache entries whose value is null', async () => { const manifest = makeManifest({ resolveCache: { diff --git a/tools/egg-bundler/test/integration.test.ts b/tools/egg-bundler/test/integration.test.ts index 41f32da951..4bcc494b41 100644 --- a/tools/egg-bundler/test/integration.test.ts +++ b/tools/egg-bundler/test/integration.test.ts @@ -214,6 +214,13 @@ globalThis.__patchedMeta = { filename: __TURBOPACK__import$2e$meta__.filename }; //# ${sourceMapToken}=not-a-map.txt +`; + const noSourceMapMeta = `const __TURBOPACK__import$2e$meta__ = { get url () { return "file:///already-patched.js"; } }; +globalThis.__patchedMeta = { + url: __TURBOPACK__import$2e$meta__.url, + dirname: __TURBOPACK__import$2e$meta__.dirname, + filename: __TURBOPACK__import$2e$meta__.filename +}; `; const buildFunc: BuildFunc = async () => { @@ -225,6 +232,7 @@ globalThis.__patchedMeta = { await fs.writeFile(path.join(tmpOutput, 'chunks/url-only.js.map'), '{"version":3}'); await fs.writeFile(path.join(tmpOutput, 'chunks/non-map-target.js'), nonMapTargetMeta); await fs.writeFile(path.join(tmpOutput, 'chunks/not-a-map.txt'), 'keep me'); + await fs.writeFile(path.join(tmpOutput, 'chunks/no-sourcemap.js'), noSourceMapMeta); }; const result = await bundle({ @@ -285,6 +293,17 @@ globalThis.__patchedMeta = { filename: urlOnlyFilename, }); + const noSourceMapFilename = path.join(tmpOutput, 'chunks/no-sourcemap.js'); + const noSourceMapMetaResult = await runPatchedChunk(noSourceMapFilename, { + argv: ['node'], + filename: noSourceMapFilename, + }); + expect(noSourceMapMetaResult).toEqual({ + url: expectedFileUrl(noSourceMapFilename), + dirname: path.dirname(noSourceMapFilename), + filename: noSourceMapFilename, + }); + const fallbackFilename = path.join(tmpOutput, 'worker.js'); const fallbackMetaResult = await runPatchedChunk(urlOnlyFilename, { argv: ['node', './worker.js'] }); expect(fallbackMetaResult).toEqual({ From 1c956e4dfd46f375339d52b4e3f15274e9449ce9 Mon Sep 17 00:00:00 2001 From: killa Date: Sat, 2 May 2026 02:24:02 +0800 Subject: [PATCH 6/6] test(supertest): cover missing callback warning --- packages/supertest/test/supertest.test.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/supertest/test/supertest.test.ts b/packages/supertest/test/supertest.test.ts index 78d907dfe3..c069ce87cd 100644 --- a/packages/supertest/test/supertest.test.ts +++ b/packages/supertest/test/supertest.test.ts @@ -8,7 +8,7 @@ import path from 'node:path'; import bodyParser from 'body-parser'; import cookieParser from 'cookie-parser'; import express, { type Express } from 'express'; -import { describe, it, beforeEach, beforeAll, expect } from 'vitest'; +import { describe, it, beforeEach, beforeAll, expect, vi } from 'vitest'; import request, { Test } from '../src/index.ts'; import { throwError } from './throwError.ts'; @@ -68,6 +68,18 @@ describe('request(url)', () => { }); describe('.end(cb)', () => { + it('should warn when callback function is not provided', () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const test = new Test('http://127.0.0.1', 'get', '/'); + + try { + test.assert(null, { status: 200 } as any, undefined as any); + expect(warn).toHaveBeenCalledWith('[@eggjs/supertest] no callback function provided, fn: %s', 'undefined'); + } finally { + warn.mockRestore(); + } + }); + it('should set `this` to the test object when calling cb', async () => { const app = express();