Skip to content

Commit

Permalink
fix: decouple init & module error handling from runtime module (#868)
Browse files Browse the repository at this point in the history
* refactor: use expect-error intead of ignore

* fix: separate responsibilites for runtime modules in TargetPlugin

* fix: limit poluting the global scope with runtime global types

* fix: use primitives from compiler.webpack

* chore: add comments

* chore: add ReanimatedPlugin to webpack

* chore: add changeset
  • Loading branch information
jbroma authored Jan 15, 2025
1 parent 153d1d4 commit 96915f8
Show file tree
Hide file tree
Showing 15 changed files with 349 additions and 217 deletions.
5 changes: 5 additions & 0 deletions .changeset/pink-pants-vanish.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@callstack/repack": patch
---

Decouple init & module error handling from load script runtime module inside RepackTargetPlugin
2 changes: 2 additions & 0 deletions apps/tester-app/webpack.config.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createRequire } from 'node:module';
import path from 'node:path';
import * as Repack from '@callstack/repack';
import { ReanimatedPlugin } from '@callstack/repack-plugin-reanimated';
import TerserPlugin from 'terser-webpack-plugin';

const dirname = Repack.getDirname(import.meta.url);
Expand Down Expand Up @@ -196,6 +197,7 @@ export default (env) => {
},
],
}),
new ReanimatedPlugin(),
// new Repack.plugins.ChunksToHermesBytecodePlugin({
// enabled: mode === 'production' && !devServer,
// test: /\.(js)?bundle$/,
Expand Down
9 changes: 5 additions & 4 deletions packages/repack/src/modules/ScriptManager/Script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
NormalizedScriptLocatorHTTPMethod,
NormalizedScriptLocatorSignatureVerificationMode,
} from './NativeScriptManager.js';
import type { ScriptLocator, WebpackContext } from './types.js';
import type { ScriptLocator } from './types.js';

/**
* Representation of a Script to load and execute, used by {@link ScriptManager}.
Expand All @@ -24,7 +24,7 @@ export class Script {
* @param scriptId Id of the script.
*/
static getDevServerURL(scriptId: string) {
return (webpackContext: WebpackContext) =>
return (webpackContext: RepackRuntimeGlobals.WebpackRequire) =>
`${webpackContext.p}${webpackContext.u(scriptId)}`;
}

Expand All @@ -34,7 +34,7 @@ export class Script {
* @param scriptId Id of the script.
*/
static getFileSystemURL(scriptId: string) {
return (webpackContext: WebpackContext) =>
return (webpackContext: RepackRuntimeGlobals.WebpackRequire) =>
webpackContext.u(`file:///${scriptId}`);
}

Expand All @@ -55,7 +55,8 @@ export class Script {
return url;
}

return (webpackContext: WebpackContext) => webpackContext.u(url);
return (webpackContext: RepackRuntimeGlobals.WebpackRequire) =>
webpackContext.u(url);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,10 @@ jest.mock('../NativeScriptManager', () => ({

globalThis.__webpack_require__ = {
i: [],
l: () => {},
u: (id: string) => `${id}.chunk.bundle`,
p: () => '',
repack: {
loadScript: jest.fn(),
loadHotUpdate: jest.fn(),
shared: { scriptManager: undefined },
},
};
Expand Down
8 changes: 5 additions & 3 deletions packages/repack/src/modules/ScriptManager/federated.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { ScriptManager } from './ScriptManager.js';
import type { WebpackContext } from './types.js';

/**
* Namespace for runtime utilities for Module Federation.
Expand All @@ -14,7 +13,10 @@ export namespace Federated {
export type URLResolver = (
scriptId: string,
caller?: string
) => string | ((webpackContext: WebpackContext) => string) | undefined;
) =>
| string
| ((webpackContext: RepackRuntimeGlobals.WebpackRequire) => string)
| undefined;

/**
* @deprecated
Expand Down Expand Up @@ -206,7 +208,7 @@ export namespace Federated {
);

if (url.includes('[ext]')) {
return (webpackContext: WebpackContext) =>
return (webpackContext: RepackRuntimeGlobals.WebpackRequire) =>
webpackContext.u(url.replace(/\[ext\]/g, ''));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import type { WebpackContext } from './types.js';

/**
* Get Webpack runtime context form current JavaScript scope.
*
* __You likely don't need to use it.__
*/
export function getWebpackContext(): WebpackContext {
export function getWebpackContext(): RepackRuntimeGlobals.WebpackRequire {
return __webpack_require__;
}
26 changes: 1 addition & 25 deletions packages/repack/src/modules/ScriptManager/types.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,3 @@
type ModuleExports = Record<string | number | symbol, any>;

type ModuleObject = {
id: number;
loaded: boolean;
error?: any;
exports: ModuleExports;
};

export interface WebpackContext {
i: ((options: {
id: number;
factory: (
moduleObject: ModuleObject,
moduleExports: ModuleExports,
webpackRequire: WebpackContext
) => void;
module: ModuleObject;
require: WebpackContext;
}) => void)[];
p: () => string;
u: (id: string) => string;
}

/**
* Interface specifying how to fetch a script.
* It represents the output of {@link ScriptLocatorResolver} function used by {@link ScriptManager}.
Expand All @@ -38,7 +14,7 @@ export interface ScriptLocator {
*
* **Passing query params might lead to unexpected results. To pass query params use `query` field.**
*/
url: string | ((webpackContext: WebpackContext) => string);
url: string | ((webpackContext: typeof __webpack_require__) => string);

/**
* Query params to append when building the final URL.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type {
Compiler,
RuntimeModule as RuntimeModuleType,
} from '@rspack/core';

interface InitRuntimeModuleConfig {
globalObject: string;
}

// runtime module class is generated dynamically based on the compiler instance
// this way it's compatible with both webpack and rspack
export const makeInitRuntimeModule = (
compiler: Compiler,
moduleConfig: InitRuntimeModuleConfig
): RuntimeModuleType => {
const Template = compiler.webpack.Template;
const RuntimeModule = compiler.webpack.RuntimeModule;

const InitRuntimeModule = class extends RuntimeModule {
constructor(private config: InitRuntimeModuleConfig) {
super('repack/init', RuntimeModule.STAGE_BASIC);
}

generate() {
return Template.asString([
Template.getFunctionContent(
require('./implementation/init.js')
).replaceAll('$globalObject$', this.config.globalObject),
]);
}
};

return new InitRuntimeModule(moduleConfig);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type {
Compiler,
RuntimeModule as RuntimeModuleType,
} from '@rspack/core';

interface LoadScriptRuntimeModuleConfig {
chunkId: string | number | undefined;
hmrEnabled: boolean;
}

// runtime module class is generated dynamically based on the compiler instance
// this way it's compatible with both webpack and rspack
export const makeLoadScriptRuntimeModule = (
compiler: Compiler,
moduleConfig: LoadScriptRuntimeModuleConfig
): RuntimeModuleType => {
const Template = compiler.webpack.Template;
const RuntimeGlobals = compiler.webpack.RuntimeGlobals;
const RuntimeModule = compiler.webpack.RuntimeModule;

const LoadScriptRuntimeModule = class extends RuntimeModule {
constructor(private config: LoadScriptRuntimeModuleConfig) {
super('repack/load script', RuntimeModule.STAGE_BASIC);
}

generate() {
return Template.asString([
Template.getFunctionContent(require('./implementation/loadScript.js'))
.replaceAll('$caller$', `'${this.config.chunkId?.toString()}'`)
.replaceAll('$hmrEnabled$', `${this.config.hmrEnabled}`)
.replaceAll('$loadScript$', RuntimeGlobals.loadScript),
]);
}
};

return new LoadScriptRuntimeModule(moduleConfig);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type {
Compiler,
RuntimeModule as RuntimeModuleType,
} from '@rspack/core';

interface ModuleErrorHandlerRuntimeModuleConfig {
globalObject: string;
}

// runtime module class is generated dynamically based on the compiler instance
// this way it's compatible with both webpack and rspack
export const makeModuleErrorHandlerRuntimeModule = (
compiler: Compiler,
moduleConfig: ModuleErrorHandlerRuntimeModuleConfig
): RuntimeModuleType => {
const Template = compiler.webpack.Template;
const RuntimeGlobals = compiler.webpack.RuntimeGlobals;
const RuntimeModule = compiler.webpack.RuntimeModule;

const ModuleErrorHandlerRuntimeModule = class extends RuntimeModule {
constructor(private config: ModuleErrorHandlerRuntimeModuleConfig) {
super('repack/module error handler', RuntimeModule.STAGE_BASIC);
}

generate() {
return Template.asString([
Template.getFunctionContent(
require('./implementation/moduleErrorHandler.js')
)
.replaceAll('$globalObject$', this.config.globalObject)
.replaceAll(
'$interceptModuleExecution$',
RuntimeGlobals.interceptModuleExecution
),
]);
}
};

return new ModuleErrorHandlerRuntimeModule(moduleConfig);
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import path from 'node:path';
import type { Compilation, Compiler, RspackPluginInstance } from '@rspack/core';
import type { RuntimeModule as WebpackRuntimeModule } from 'webpack';
import { makeInitRuntimeModule } from './InitRuntimeModule.js';
import { makeLoadScriptRuntimeModule } from './LoadScriptRuntimeModule.js';
import { makeModuleErrorHandlerRuntimeModule } from './ModuleErrorHandlerRuntimeModule.js';

type RspackRuntimeModule = Parameters<
Compilation['hooks']['runtimeModule']['call']
Expand All @@ -27,7 +30,7 @@ export class RepackTargetPlugin implements RspackPluginInstance {
*
* @param config Plugin configuration options.
*/
constructor(private config?: RepackTargetPluginConfig) {}
constructor(private config: RepackTargetPluginConfig) {}

replaceRuntimeModule(
module: RspackRuntimeModule | WebpackRuntimeModule,
Expand All @@ -53,8 +56,6 @@ export class RepackTargetPlugin implements RspackPluginInstance {
* @param compiler Webpack compiler instance.
*/
apply(compiler: Compiler) {
const Template = compiler.webpack.Template;

const globalObject = 'self';
compiler.options.target = false;
compiler.options.output.chunkLoading = 'jsonp';
Expand All @@ -70,7 +71,7 @@ export class RepackTargetPlugin implements RspackPluginInstance {
new compiler.webpack.BannerPlugin({
raw: true,
entryOnly: true,
banner: Template.asString([
banner: compiler.webpack.Template.asString([
`/******/ var ${globalObject} = ${globalObject} || this || new Function("return this")() || ({}); // repackGlobal'`,
'/******/',
]),
Expand All @@ -84,9 +85,9 @@ export class RepackTargetPlugin implements RspackPluginInstance {
const context = path.dirname(request);
resource.request = request;
resource.context = context;
// @ts-ignore
// @ts-expect-error incomplete rspack types
resource.createData.resource = request;
// @ts-ignore
// @ts-expect-error incomplete rspack types
resource.createData.context = context;
}
).apply(compiler);
Expand All @@ -97,38 +98,41 @@ export class RepackTargetPlugin implements RspackPluginInstance {
require.resolve('../../modules/EmptyModule.js')
).apply(compiler);

compiler.hooks.compilation.tap('RepackTargetPlugin', (compilation) => {
compilation.hooks.additionalTreeRuntimeRequirements.tap(
'RepackTargetPlugin',
(chunk) => {
compilation.addRuntimeModule(
chunk,
makeInitRuntimeModule(compiler, { globalObject })
);

compilation.addRuntimeModule(
chunk,
makeModuleErrorHandlerRuntimeModule(compiler, { globalObject })
);
}
);
});

compiler.hooks.thisCompilation.tap('RepackTargetPlugin', (compilation) => {
compilation.hooks.runtimeModule.tap(
'RepackTargetPlugin',
(module, chunk) => {
/**
* We inject RePack's runtime modules only when load_script module is present.
* This module is injected when:
* 1. HMR is enabled
* 2. Dynamic import is used anywhere in the project
*/
if (module.name === 'load_script' || module.name === 'load script') {
const loadScriptGlobal = compiler.webpack.RuntimeGlobals.loadScript;
const loadScriptRuntimeModule = Template.asString([
Template.getFunctionContent(
require('./implementation/loadScript.js')
)
.replaceAll('$loadScript$', loadScriptGlobal)
.replaceAll('$caller$', `'${chunk.id?.toString()}'`),
]);

const initRuntimeModule = Template.asString([
'// Repack runtime initialization logic',
Template.getFunctionContent(require('./implementation/init.js'))
.replaceAll('$globalObject$', globalObject)
.replaceAll('$hmrEnabled$', `${this.config?.hmr ?? false}`),
]);

// combine both runtime modules
const repackRuntimeModule = `${loadScriptRuntimeModule}\n${initRuntimeModule}`;
const loadScriptRuntimeModule = makeLoadScriptRuntimeModule(
compiler,
{
chunkId: chunk.id ?? undefined,
hmrEnabled: this.config.hmr ?? false,
}
);

// inject runtime module
this.replaceRuntimeModule(module, repackRuntimeModule.toString());
this.replaceRuntimeModule(
module,
loadScriptRuntimeModule.generate()
);
}

// Remove CSS runtime modules
Expand Down
Loading

0 comments on commit 96915f8

Please sign in to comment.