diff --git a/.changeset/bitter-sheep-cry.md b/.changeset/bitter-sheep-cry.md new file mode 100644 index 000000000..814fc9ba5 --- /dev/null +++ b/.changeset/bitter-sheep-cry.md @@ -0,0 +1,6 @@ +--- +"@callstack/repack-dev-server": patch +"@callstack/repack": patch +--- + +Fix opening stack frame source file from LogBox diff --git a/.changeset/chatty-taxes-hug.md b/.changeset/chatty-taxes-hug.md new file mode 100644 index 000000000..b7556c979 --- /dev/null +++ b/.changeset/chatty-taxes-hug.md @@ -0,0 +1,5 @@ +--- +"@callstack/repack": minor +--- + +Handle displaying relative paths to sourcefiles in DevTools similarly to Metro diff --git a/.changeset/evil-geckos-play.md b/.changeset/evil-geckos-play.md new file mode 100644 index 000000000..a7b7b1ee9 --- /dev/null +++ b/.changeset/evil-geckos-play.md @@ -0,0 +1,5 @@ +--- +"@callstack/repack": patch +--- + +Don't include 3rd party lib sourcemaps by default into the final sourcemaps diff --git a/apps/tester-app/package.json b/apps/tester-app/package.json index 6e4ab80cf..799629896 100644 --- a/apps/tester-app/package.json +++ b/apps/tester-app/package.json @@ -3,9 +3,9 @@ "version": "0.0.1", "private": true, "scripts": { - "android": "react-native run-android --appId com.testerapp", + "android": "react-native run-android --appId com.testerapp --no-packager", "android:release": "node ./scripts/release.js android", - "ios": "react-native run-ios", + "ios": "react-native run-ios --no-packager", "ios:release": "node ./scripts/release.js ios", "pods": "(cd ios && bundle install && (bundle exec pod install || bundle exec pod update))", "pods:update": "(cd ios && bundle install && bundle exec pod update)", diff --git a/apps/tester-federation-v2/package.json b/apps/tester-federation-v2/package.json index 2c79549ca..bf49a7be8 100644 --- a/apps/tester-federation-v2/package.json +++ b/apps/tester-federation-v2/package.json @@ -3,8 +3,8 @@ "version": "0.0.1", "private": true, "scripts": { - "android": "react-native run-android --appId com.tester.federationV2", - "ios": "react-native run-ios", + "android": "react-native run-android --appId com.tester.federationV2 --no-packager", + "ios": "react-native run-ios --no-packager", "pods": "(cd ios && bundle install && (bundle exec pod install || bundle exec pod update))", "pods:update": "(cd ios && bundle install && bundle exec pod update)", "start:hostapp": "react-native webpack-start --config config.host-app.mts", diff --git a/apps/tester-federation/package.json b/apps/tester-federation/package.json index c7350564e..91b712a9a 100644 --- a/apps/tester-federation/package.json +++ b/apps/tester-federation/package.json @@ -3,8 +3,8 @@ "version": "0.0.1", "private": true, "scripts": { - "android": "react-native run-android --appId com.tester.federation", - "ios": "react-native run-ios", + "android": "react-native run-android --appId com.tester.federation --no-packager", + "ios": "react-native run-ios --no-packager", "pods": "(cd ios && bundle install && (bundle exec pod install || bundle exec pod update))", "pods:update": "(cd ios && bundle install && bundle exec pod update)", "start:hostapp": "react-native webpack-start --config config.host-app.mts", diff --git a/packages/dev-server/src/createServer.ts b/packages/dev-server/src/createServer.ts index 166310bd8..5ab29b36e 100644 --- a/packages/dev-server/src/createServer.ts +++ b/packages/dev-server/src/createServer.ts @@ -138,7 +138,7 @@ export async function createServer(config: Server.Config) { delegate, }); await instance.register(devtoolsPlugin, { - rootDir: options.rootDir, + delegate, }); await instance.register(symbolicatePlugin, { delegate, diff --git a/packages/dev-server/src/plugins/compiler/compilerPlugin.ts b/packages/dev-server/src/plugins/compiler/compilerPlugin.ts index a34fb091b..5555aeae4 100644 --- a/packages/dev-server/src/plugins/compiler/compilerPlugin.ts +++ b/packages/dev-server/src/plugins/compiler/compilerPlugin.ts @@ -21,10 +21,10 @@ async function compilerPlugin( }, }, handler: async (request, reply) => { - const filename = (request.params as { '*'?: string })['*']; + const filepath = (request.params as { '*'?: string })['*']; let { platform } = request.query as { platform?: string }; - if (!filename) { + if (!filepath) { // This technically should never happen - this route should not be called if file is missing. request.log.debug('File was not provided'); return reply.notFound('File was not provided'); @@ -49,12 +49,12 @@ async function compilerPlugin( try { const asset = await delegate.compiler.getAsset( - filename, + filepath, platform, sendProgress ); const mimeType = delegate.compiler.getMimeType( - filename, + filepath, platform, asset ); diff --git a/packages/dev-server/src/plugins/devtools/devtoolsPlugin.ts b/packages/dev-server/src/plugins/devtools/devtoolsPlugin.ts index 5644a46e1..7ffe06063 100644 --- a/packages/dev-server/src/plugins/devtools/devtoolsPlugin.ts +++ b/packages/dev-server/src/plugins/devtools/devtoolsPlugin.ts @@ -2,6 +2,7 @@ import type { FastifyInstance } from 'fastify'; import fastifyPlugin from 'fastify-plugin'; import launchEditor from 'launch-editor'; import open from 'open'; +import type { Server } from '../../types.js'; interface OpenURLRequestBody { url: string; @@ -18,7 +19,10 @@ function parseRequestBody(body: unknown): T { throw new Error(`Unsupported body type: ${typeof body}`); } -async function devtoolsPlugin(instance: FastifyInstance) { +async function devtoolsPlugin( + instance: FastifyInstance, + { delegate }: { delegate: Server.Delegate } +) { // reference implementation in `@react-native-community/cli-server-api`: // https://github.com/react-native-community/cli/blob/46436a12478464752999d34ed86adf3212348007/packages/cli-server-api/src/openURLMiddleware.ts instance.route({ @@ -40,8 +44,8 @@ async function devtoolsPlugin(instance: FastifyInstance) { const { file, lineNumber } = parseRequestBody( request.body ); - // TODO fix rewriting of `webpack://` to rootDir of the project - launchEditor(`${file}:${lineNumber}`, process.env.REACT_EDITOR); + const filepath = delegate.devTools?.resolveProjectPath(file) ?? file; + launchEditor(`${filepath}:${lineNumber}`, process.env.REACT_EDITOR); reply.send('OK'); }, }); diff --git a/packages/dev-server/src/plugins/symbolicate/Symbolicator.ts b/packages/dev-server/src/plugins/symbolicate/Symbolicator.ts index 05ec13fc6..bdb022209 100644 --- a/packages/dev-server/src/plugins/symbolicate/Symbolicator.ts +++ b/packages/dev-server/src/plugins/symbolicate/Symbolicator.ts @@ -162,17 +162,23 @@ export class Symbolicator { }; } - const lookup = consumer.originalPositionFor({ + let lookup = consumer.originalPositionFor({ line: frame.lineNumber, column: frame.column, bias: SourceMapConsumer.LEAST_UPPER_BOUND, }); - // If lookup fails, we get the same shape object, but with - // all values set to null if (!lookup.source) { - // It is better to gracefully return the original frame - // than to throw an exception + // fallback to GREATEST_LOWER_BOUND + lookup = consumer.originalPositionFor({ + line: frame.lineNumber, + column: frame.column, + bias: SourceMapConsumer.GREATEST_LOWER_BOUND, + }); + } + + // return the original frame when both lookups fail + if (!lookup.source) { return { ...frame, collapse: false, diff --git a/packages/dev-server/src/types.ts b/packages/dev-server/src/types.ts index 083d37c56..ab76392b3 100644 --- a/packages/dev-server/src/types.ts +++ b/packages/dev-server/src/types.ts @@ -77,6 +77,9 @@ export namespace Server { /** A compiler delegate. */ compiler: CompilerDelegate; + /** A DevTools delegate. */ + devTools?: DevToolsDelegate; + /** A symbolicator delegate. */ symbolicator: SymbolicatorDelegate; @@ -139,6 +142,19 @@ export namespace Server { onMessage: (log: any) => void; } + /** + * Delegate with implementation for dev tools functions. + */ + export interface DevToolsDelegate { + /** + * Resolve the project filepath with [projectRoot] prefix. + * + * @param filepath The filepath to resolve. + * @returns The resolved project path. + */ + resolveProjectPath: (filepath: string) => string; + } + /** * Delegate with implementation for messages used in route handlers. */ diff --git a/packages/repack/babel.config.js b/packages/repack/babel.config.js index ba56551cc..c64d92ff5 100644 --- a/packages/repack/babel.config.js +++ b/packages/repack/babel.config.js @@ -6,6 +6,10 @@ module.exports = { include: ['./src/**/implementation'], comments: false, }, + { + include: ['./src/**/implementation', './src/modules'], + sourceMaps: false, + }, { exclude: ['./src/**/implementation', './src/modules'], presets: [ diff --git a/packages/repack/package.json b/packages/repack/package.json index a371dfdbe..a2bd72ef7 100644 --- a/packages/repack/package.json +++ b/packages/repack/package.json @@ -47,7 +47,7 @@ "access": "public" }, "scripts": { - "build:js": "babel src --out-dir dist --extensions \".js,.ts\" --source-maps --ignore \"**/__tests__/**\" --delete-dir-on-start", + "build:js": "babel src --out-dir dist --extensions \".js,.ts\" --ignore \"**/__tests__/**\" --delete-dir-on-start", "build:ts": "tsc -p tsconfig.build.json --emitDeclarationOnly", "build": "pnpm run \"/^build:.*/\"", "test": "jest", diff --git a/packages/repack/src/commands/common/__tests__/parseUrl.test.ts b/packages/repack/src/commands/common/__tests__/parseUrl.test.ts new file mode 100644 index 000000000..6ad96c9ea --- /dev/null +++ b/packages/repack/src/commands/common/__tests__/parseUrl.test.ts @@ -0,0 +1,101 @@ +import { parseUrl } from '../parseUrl.js'; + +describe('parseUrl', () => { + const expectParsed = ( + url: string, + expected: { resourcePath: string; platform?: string }, + platformList = ['ios', 'android', 'web'] + ) => { + expect(parseUrl(url, platformList)).toEqual(expected); + }; + + it('should parse URLs with platform from query parameters', () => { + expectParsed('src/index.js?platform=ios', { + resourcePath: 'src/index.js', + platform: 'ios', + }); + expectParsed('components/Button.tsx?platform=android', { + resourcePath: 'components/Button.tsx', + platform: 'android', + }); + expectParsed('/absolute/path/file.js?platform=web', { + resourcePath: 'absolute/path/file.js', + platform: 'web', + }); + }); + + it('should parse URLs with platform from pathname', () => { + expectParsed('/ios/src/index.js', { + resourcePath: 'src/index.js', + platform: 'ios', + }); + expectParsed('/android/components/Button.tsx', { + resourcePath: 'components/Button.tsx', + platform: 'android', + }); + expectParsed('/web/utils/helper.js', { + resourcePath: 'utils/helper.js', + platform: 'web', + }); + }); + + it('should parse URLs with platform from file extension', () => { + expectParsed('src/index.ios.js', { + resourcePath: 'src/index.ios.js', + platform: 'ios', + }); + expectParsed('components/Button.android.tsx', { + resourcePath: 'components/Button.android.tsx', + platform: 'android', + }); + expectParsed('styles/theme.web.css', { + resourcePath: 'styles/theme.web.css', + platform: 'web', + }); + }); + + it('should handle URLs without platform detection', () => { + expectParsed('src/index.js', { + resourcePath: 'src/index.js', + platform: undefined, + }); + expectParsed('/components/Button.tsx', { + resourcePath: 'components/Button.tsx', + platform: undefined, + }); + expectParsed('utils/helper.unknown.js', { + resourcePath: 'utils/helper.unknown.js', + platform: undefined, + }); + }); + + it('should prioritize query parameter over pathname and extension', () => { + expectParsed('/android/src/index.ios.js?platform=web', { + resourcePath: 'android/src/index.ios.js', + platform: 'web', + }); + expectParsed('/ios/components/Button.android.tsx?platform=web', { + resourcePath: 'ios/components/Button.android.tsx', + platform: 'web', + }); + }); + + it('should work with different platform lists', () => { + expectParsed( + '/react-native/src/index.js', + { + resourcePath: 'src/index.js', + platform: 'react-native', + }, + ['react-native', 'macos'] + ); + expectParsed( + 'app.macos.js', + { + resourcePath: 'app.macos.js', + platform: 'macos', + }, + ['react-native', 'macos'] + ); + }); +}); diff --git a/packages/repack/src/commands/common/__tests__/resolveProjectPath.test.ts b/packages/repack/src/commands/common/__tests__/resolveProjectPath.test.ts new file mode 100644 index 000000000..89d2e06c2 --- /dev/null +++ b/packages/repack/src/commands/common/__tests__/resolveProjectPath.test.ts @@ -0,0 +1,44 @@ +import { resolveProjectPath } from '../resolveProjectPath.js'; + +describe('resolveProjectPath', () => { + const expectResolved = ( + input: string, + expected: string, + root = '/project/root' + ) => { + expect(resolveProjectPath(input, root)).toBe(expected); + }; + + it('should resolve [projectRoot] prefix correctly', () => { + expectResolved('[projectRoot]/src/index.js', '/project/root/src/index.js'); + expectResolved( + '[projectRoot]/build/output.js', + '/apps/my-app/build/output.js', + '/apps/my-app' + ); + expectResolved( + '[projectRoot]/special-file@2x.png', + '/project/root/special-file@2x.png' + ); + expectResolved( + '[projectRoot]/file with spaces.txt', + '/project/root/file with spaces.txt' + ); + }); + + it('should resolve [projectRoot^N] prefix with up-level navigation', () => { + expectResolved('[projectRoot^1]/src/index.js', '/project/src/index.js'); + expectResolved('[projectRoot^2]/shared/utils.js', '/shared/utils.js'); + expectResolved('[projectRoot^3]/global/config.json', '/global/config.json'); + expectResolved( + '[projectRoot^2]/utils/helper.js', + '/deep/nested/utils/helper.js', + '/deep/nested/project/folder' + ); + expectResolved( + '[projectRoot^5]/very/deep/file.js', + '/a/very/deep/file.js', + '/a/b/c/d/e/f' + ); + }); +}); diff --git a/packages/repack/src/commands/common/index.ts b/packages/repack/src/commands/common/index.ts index 2242a8f1a..af36e22e8 100644 --- a/packages/repack/src/commands/common/index.ts +++ b/packages/repack/src/commands/common/index.ts @@ -1,7 +1,8 @@ export * from './adaptFilenameToPlatform.js'; export * from './getMimeType.js'; +export * from './resolveProjectPath.js'; export * from './runAdbReverse.js'; -export * from './parseFileUrl.js'; +export * from './parseUrl.js'; export * from './resetPersistentCache.js'; export * from './setupInteractions.js'; export * from './setupStatsWriter.js'; diff --git a/packages/repack/src/commands/common/parseFileUrl.ts b/packages/repack/src/commands/common/parseFileUrl.ts deleted file mode 100644 index a059ce251..000000000 --- a/packages/repack/src/commands/common/parseFileUrl.ts +++ /dev/null @@ -1,29 +0,0 @@ -export function parseFileUrl(fileUrl: string, base?: string) { - const url = new URL(fileUrl, base); - const { pathname, searchParams } = url; - - let platform = searchParams.get('platform'); - let filename = pathname; - - if (!platform) { - const pathArray = pathname.split('/'); - const platformFromPath = pathArray[1]; - - if (platformFromPath === 'ios' || platformFromPath === 'android') { - platform = platformFromPath; - filename = pathArray.slice(2).join('/'); - } - } - - if (!platform) { - const [, platformOrName, name] = filename.split('.').reverse(); - if (name !== undefined) { - platform = platformOrName; - } - } - - return { - filename: filename.replace(/^\//, ''), - platform: platform || undefined, - }; -} diff --git a/packages/repack/src/commands/common/parseUrl.ts b/packages/repack/src/commands/common/parseUrl.ts new file mode 100644 index 000000000..06b840d89 --- /dev/null +++ b/packages/repack/src/commands/common/parseUrl.ts @@ -0,0 +1,28 @@ +export function parseUrl(url: string, platforms: string[], base = 'file:///') { + const { pathname, searchParams } = new URL(url, base); + + let path = pathname; + let platform = searchParams.get('platform'); + + if (!platform) { + const pathArray = pathname.split('/'); + const platformFromPath = pathArray[1]; + + if (platforms.includes(platformFromPath)) { + platform = platformFromPath; + path = pathArray.slice(2).join('/'); + } + } + + if (!platform) { + const [, platformOrName, name] = path.split('.').reverse(); + if (name !== undefined && platforms.includes(platformOrName)) { + platform = platformOrName; + } + } + + return { + resourcePath: path.replace(/^\//, ''), + platform: platform || undefined, + }; +} diff --git a/packages/repack/src/commands/common/resolveProjectPath.ts b/packages/repack/src/commands/common/resolveProjectPath.ts new file mode 100644 index 000000000..f5c293aa9 --- /dev/null +++ b/packages/repack/src/commands/common/resolveProjectPath.ts @@ -0,0 +1,19 @@ +import path from 'node:path'; + +const projectRootPattern = /^\[projectRoot(?:\^(\d+))?\]$/; + +function isProjectPath(filepath: string) { + const root = filepath.split('/')[0]; + return root.match(projectRootPattern); +} + +// Resolve [projectRoot] and [projectRoot^N] prefixes +export function resolveProjectPath(filepath: string, rootDir: string) { + const match = isProjectPath(filepath); + if (!match) return filepath; + + const [prefix, upLevels] = match; + const upPath = '../'.repeat(Number(upLevels ?? 0)); + const rootPath = path.join(rootDir, upPath); + return path.resolve(filepath.replace(prefix, rootPath)); +} diff --git a/packages/repack/src/commands/rspack/Compiler.ts b/packages/repack/src/commands/rspack/Compiler.ts index 50a007c9e..129b8789c 100644 --- a/packages/repack/src/commands/rspack/Compiler.ts +++ b/packages/repack/src/commands/rspack/Compiler.ts @@ -241,7 +241,9 @@ export class Compiler { } try { - const filePath = path.join(this.rootDir, filename); + const filePath = path.isAbsolute(filename) + ? filename + : path.join(this.rootDir, filename); const source = await fs.promises.readFile(filePath, 'utf8'); return source; } catch { diff --git a/packages/repack/src/commands/rspack/start.ts b/packages/repack/src/commands/rspack/start.ts index 4d46d22d6..814f9562b 100644 --- a/packages/repack/src/commands/rspack/start.ts +++ b/packages/repack/src/commands/rspack/start.ts @@ -11,8 +11,9 @@ import { CLIError } from '../common/cliError.js'; import { makeCompilerConfig } from '../common/config/makeCompilerConfig.js'; import { getMimeType, - parseFileUrl, + parseUrl, resetPersistentCache, + resolveProjectPath, setupInteractions, } from '../common/index.js'; import { runAdbReverse } from '../common/index.js'; @@ -41,12 +42,14 @@ export async function start( throw new CLIError(`Unrecognized platform: ${args.platform}`); } + const platforms = args.platform ? [args.platform] : detectedPlatforms; + const configs = await makeCompilerConfig({ args: args, bundler: 'rspack', command: 'start', rootDir: cliConfig.root, - platforms: args.platform ? [args.platform] : detectedPlatforms, + platforms: platforms, reactNativePath: cliConfig.reactNativePath, }); @@ -132,24 +135,32 @@ export async function start( return { compiler: { - getAsset: (filename, platform) => { - const parsedUrl = parseFileUrl(filename, 'file:///'); - return compiler.getSource(parsedUrl.filename, platform); + getAsset: (url, platform) => { + const { resourcePath } = parseUrl(url, platforms); + return compiler.getSource(resourcePath, platform); + }, + getMimeType: (filename) => { + return getMimeType(filename); }, - getMimeType: (filename) => getMimeType(filename), - inferPlatform: (uri) => { - const { platform } = parseFileUrl(uri, 'file:///'); + inferPlatform: (url) => { + const { platform } = parseUrl(url, platforms); return platform; }, }, + devTools: { + resolveProjectPath: (filepath) => { + return resolveProjectPath(filepath, cliConfig.root); + }, + }, symbolicator: { - getSource: (fileUrl) => { - const { filename, platform } = parseFileUrl(fileUrl); - return compiler.getSource(filename, platform); + getSource: (url) => { + let { resourcePath, platform } = parseUrl(url, platforms); + resourcePath = resolveProjectPath(resourcePath, cliConfig.root); + return compiler.getSource(resourcePath, platform); }, - getSourceMap: (fileUrl) => { - const { filename, platform } = parseFileUrl(fileUrl); - return compiler.getSourceMap(filename, platform); + getSourceMap: (url) => { + const { resourcePath, platform } = parseUrl(url, platforms); + return compiler.getSourceMap(resourcePath, platform); }, shouldIncludeFrame: (frame) => { // If the frame points to internal bootstrap/module system logic, skip the code frame. diff --git a/packages/repack/src/commands/webpack/start.ts b/packages/repack/src/commands/webpack/start.ts index b488dea62..b97747574 100644 --- a/packages/repack/src/commands/webpack/start.ts +++ b/packages/repack/src/commands/webpack/start.ts @@ -13,8 +13,9 @@ import { CLIError } from '../common/cliError.js'; import { makeCompilerConfig } from '../common/config/makeCompilerConfig.js'; import { getMimeType, - parseFileUrl, + parseUrl, resetPersistentCache, + resolveProjectPath, runAdbReverse, setupInteractions, } from '../common/index.js'; @@ -43,12 +44,14 @@ export async function start( throw new CLIError(`Unrecognized platform: ${args.platform}`); } + const platforms = args.platform ? [args.platform] : detectedPlatforms; + const configs = await makeCompilerConfig({ args: args, bundler: 'webpack', command: 'start', rootDir: cliConfig.root, - platforms: args.platform ? [args.platform] : detectedPlatforms, + platforms: platforms, reactNativePath: cliConfig.reactNativePath, }); @@ -171,28 +174,32 @@ export async function start( return { compiler: { - getAsset: (filename, platform, sendProgress) => { - const parsedUrl = parseFileUrl(filename, 'file:///'); - return compiler.getSource( - parsedUrl.filename, - platform, - sendProgress - ); + getAsset: (url, platform, sendProgress) => { + const { resourcePath } = parseUrl(url, platforms); + return compiler.getSource(resourcePath, platform, sendProgress); + }, + getMimeType: (filename) => { + return getMimeType(filename); }, - getMimeType: (filename) => getMimeType(filename), - inferPlatform: (uri) => { - const { platform } = parseFileUrl(uri, 'file:///'); + inferPlatform: (url) => { + const { platform } = parseUrl(url, platforms); return platform; }, }, + devTools: { + resolveProjectPath: (filepath) => { + return resolveProjectPath(filepath, cliConfig.root); + }, + }, symbolicator: { - getSource: (fileUrl) => { - const { filename, platform } = parseFileUrl(fileUrl); - return compiler.getSource(filename, platform); + getSource: (url) => { + let { resourcePath, platform } = parseUrl(url, platforms); + resourcePath = resolveProjectPath(resourcePath, cliConfig.root); + return compiler.getSource(resourcePath, platform); }, - getSourceMap: (fileUrl) => { - const { filename, platform } = parseFileUrl(fileUrl); - return compiler.getSourceMap(filename, platform); + getSourceMap: (url) => { + const { resourcePath, platform } = parseUrl(url, platforms); + return compiler.getSourceMap(resourcePath, platform); }, shouldIncludeFrame: (frame) => { // If the frame points to internal bootstrap/module system logic, skip the code frame. diff --git a/packages/repack/src/plugins/SourceMapPlugin.ts b/packages/repack/src/plugins/SourceMapPlugin.ts index 8c36eb405..b91d430a2 100644 --- a/packages/repack/src/plugins/SourceMapPlugin.ts +++ b/packages/repack/src/plugins/SourceMapPlugin.ts @@ -1,7 +1,69 @@ -import type { Compiler as RspackCompiler } from '@rspack/core'; +import path from 'node:path'; +import type { + Compiler as RspackCompiler, + SourceMapDevToolPluginOptions, +} from '@rspack/core'; import type { Compiler as WebpackCompiler } from 'webpack'; import { ConfigurationError } from './utils/ConfigurationError.js'; +type ModuleFilenameTemplate = + SourceMapDevToolPluginOptions['moduleFilenameTemplate']; +type ModuleFilenameTemplateFn = Exclude< + ModuleFilenameTemplate, + string | undefined +>; +type ModuleFilenameTemplateFnCtx = Parameters[0]; + +function devToolsmoduleFilenameTemplate( + namespace: string, + info: ModuleFilenameTemplateFnCtx +) { + // inlined modules + if (!info.identifier) { + return `${namespace}`; + } + + const [prefix, ...parts] = info.resourcePath.split('/'); + + // prefixed modules like React DevTools Backend + if (prefix !== '.' && prefix !== '..') { + const resourcePath = parts.filter((part) => part !== '..').join('/'); + return `webpack://${prefix}/${resourcePath}`; + } + + const hasValidAbsolutePath = path.isAbsolute(info.absoluteResourcePath); + + // project root + if (hasValidAbsolutePath && info.resourcePath.startsWith('./')) { + return `[projectRoot]${info.resourcePath.slice(1)}`; + } + + // outside of project root + if (hasValidAbsolutePath && info.resourcePath.startsWith('../')) { + const parts = info.resourcePath.split('/'); + const upLevel = parts.filter((part) => part === '..').length; + const restPath = parts.slice(parts.lastIndexOf('..') + 1).join('/'); + const rootRef = `[projectRoot^${upLevel}]`; + return `${rootRef}${restPath ? '/' + restPath : ''}`; + } + + return `[unknownOrigin]/${path.basename(info.identifier)}`; +} + +function defaultModuleFilenameTemplateHandler( + _: string, + info: ModuleFilenameTemplateFnCtx +) { + if (!info.absoluteResourcePath.startsWith('/')) { + // handle inlined modules + if (info.query || info.loaders || info.allLoaders) { + return `inlined-${info.hash}`; + } + } + // use absolute path for all other modules + return info.absoluteResourcePath; +} + interface SourceMapPluginConfig { platform?: string; } @@ -20,6 +82,18 @@ export class SourceMapPlugin { return; } + let moduleFilenameTemplateHandler: ModuleFilenameTemplateFn; + if (compiler.options.devServer) { + const host = compiler.options.devServer.host; + const port = compiler.options.devServer.port; + const namespace = `http://${host}:${port}`; + moduleFilenameTemplateHandler = (info: ModuleFilenameTemplateFnCtx) => + devToolsmoduleFilenameTemplate(namespace, info); + } else { + moduleFilenameTemplateHandler = (info: ModuleFilenameTemplateFnCtx) => + defaultModuleFilenameTemplateHandler('', info); + } + const format = compiler.options.devtool; // disable builtin sourcemap generation compiler.options.devtool = false; @@ -30,8 +104,8 @@ export class SourceMapPlugin { const devtoolNamespace = compiler.options.output.devtoolNamespace ?? compiler.options.output.uniqueName; - const devtoolModuleFilenameTemplate = - compiler.options.output.devtoolModuleFilenameTemplate; + // const devtoolModuleFilenameTemplate = + // compiler.options.output.devtoolModuleFilenameTemplate; const devtoolFallbackModuleFilenameTemplate = compiler.options.output.devtoolFallbackModuleFilenameTemplate; @@ -54,14 +128,10 @@ export class SourceMapPlugin { const moduleMaps = format.includes('module'); const noSources = format.includes('nosources'); - // TODO Fix sourcemap directory structure - // Right now its very messy and not every node module is inside of the node module - // like React Devtools backend etc or some symilinked module appear with relative path - // We should normalize this through a custom handler and provide an output similar to Metro new compiler.webpack.SourceMapDevToolPlugin({ test: /\.([cm]?jsx?|bundle)$/, filename: '[file].map', - moduleFilenameTemplate: devtoolModuleFilenameTemplate, + moduleFilenameTemplate: moduleFilenameTemplateHandler, fallbackModuleFilenameTemplate: devtoolFallbackModuleFilenameTemplate, append: hidden ? false diff --git a/packages/repack/src/utils/getJsTransformRules.ts b/packages/repack/src/utils/getJsTransformRules.ts index 49f6f7817..480d34754 100644 --- a/packages/repack/src/utils/getJsTransformRules.ts +++ b/packages/repack/src/utils/getJsTransformRules.ts @@ -85,15 +85,51 @@ export function getJsTransformRules(options?: GetJsTransformRulesOptions) { oneOf: [ { test: /jsx?$/, - use: { loader: 'builtin:swc-loader', options: jsRules }, + include: /node_modules/, + use: { + loader: 'builtin:swc-loader', + options: { ...jsRules, sourceMaps: false }, + }, + }, + { + test: /jsx?$/, + exclude: /node_modules/, + use: { + loader: 'builtin:swc-loader', + options: { ...jsRules, sourceMaps: true }, + }, }, { test: /ts$/, - use: { loader: 'builtin:swc-loader', options: tsRules }, + include: /node_modules/, + use: { + loader: 'builtin:swc-loader', + options: { ...tsRules, sourceMaps: false }, + }, + }, + { + test: /ts$/, + exclude: /node_modules/, + use: { + loader: 'builtin:swc-loader', + options: { ...tsRules, sourceMaps: true }, + }, + }, + { + test: /tsx$/, + include: /node_modules/, + use: { + loader: 'builtin:swc-loader', + options: { ...tsxRules, sourceMaps: false }, + }, }, { test: /tsx$/, - use: { loader: 'builtin:swc-loader', options: tsxRules }, + exclude: /node_modules/, + use: { + loader: 'builtin:swc-loader', + options: { ...tsxRules, sourceMaps: true }, + }, }, ], },