@@ -51,6 +51,7 @@ export class WorkflowCodeBundler {
5151 public readonly workflowInterceptorModules : string [ ] ;
5252 protected readonly payloadConverterPath ?: string ;
5353 protected readonly failureConverterPath ?: string ;
54+ protected readonly preloadedModules : string [ ] ;
5455 protected readonly ignoreModules : string [ ] ;
5556 protected readonly webpackConfigHook : ( config : Configuration ) => Configuration ;
5657
@@ -60,6 +61,7 @@ export class WorkflowCodeBundler {
6061 payloadConverterPath,
6162 failureConverterPath,
6263 workflowInterceptorModules,
64+ preloadedModules,
6365 ignoreModules,
6466 webpackConfigHook,
6567 } : BundleOptions ) {
@@ -68,6 +70,7 @@ export class WorkflowCodeBundler {
6870 this . payloadConverterPath = payloadConverterPath ;
6971 this . failureConverterPath = failureConverterPath ;
7072 this . workflowInterceptorModules = workflowInterceptorModules ?? [ ] ;
73+ this . preloadedModules = preloadedModules ?? [ ] ;
7174 this . ignoreModules = ignoreModules ?? [ ] ;
7275 this . webpackConfigHook = webpackConfigHook ?? ( ( config ) => config ) ;
7376 }
@@ -149,23 +152,29 @@ export class WorkflowCodeBundler {
149152 . map ( ( v ) => `require(/* webpackMode: "eager" */ ${ JSON . stringify ( v ) } )` )
150153 . join ( ', \n' ) ;
151154
155+ const preloadedModulesImports = [ ...new Set ( this . preloadedModules ) ]
156+ . map ( ( v ) => `require(/* webpackMode: "eager" */ ${ JSON . stringify ( v ) } )` )
157+ . join ( ';\n' ) ;
158+
152159 const code = `
153- const api = require('@temporalio/workflow/lib/worker-interface.js');
154- exports.api = api;
160+ const api = require('@temporalio/workflow/lib/worker-interface.js');
161+ exports.api = api;
155162
156- const { overrideGlobals } = require('@temporalio/workflow/lib/global-overrides.js');
157- overrideGlobals();
163+ const { overrideGlobals } = require('@temporalio/workflow/lib/global-overrides.js');
164+ overrideGlobals();
158165
159- exports.importWorkflows = function importWorkflows() {
160- return require(/* webpackMode: "eager" */ ${ JSON . stringify ( this . workflowsPath ) } );
161- }
166+ ${ preloadedModulesImports }
162167
163- exports.importInterceptors = function importInterceptors() {
164- return [
165- ${ interceptorImports }
166- ];
167- }
168- ` ;
168+ exports.importWorkflows = function importWorkflows() {
169+ return require(/* webpackMode: "eager" */ ${ JSON . stringify ( this . workflowsPath ) } );
170+ }
171+
172+ exports.importInterceptors = function importInterceptors() {
173+ return [
174+ ${ interceptorImports }
175+ ];
176+ }
177+ ` ;
169178 try {
170179 vol . mkdirSync ( path . dirname ( target ) , { recursive : true } ) ;
171180 } catch ( err : any ) {
@@ -190,7 +199,9 @@ exports.importInterceptors = function importInterceptors() {
190199 : data . request ?? '' ;
191200
192201 if ( moduleMatches ( module , disallowedModules ) && ! moduleMatches ( module , this . ignoreModules ) ) {
193- this . foundProblematicModules . add ( module ) ;
202+ // this.foundProblematicModules.add(module);
203+ // // callback(new Error(`Import of disallowed module: '${module}'`));
204+ throw new Error ( `Import of disallowed module: '${ module } '` ) ;
194205 }
195206
196207 return undefined ;
@@ -207,6 +218,7 @@ exports.importInterceptors = function importInterceptors() {
207218 __temporal_custom_failure_converter$ : this . failureConverterPath ?? false ,
208219 ...Object . fromEntries ( [ ...this . ignoreModules , ...disallowedModules ] . map ( ( m ) => [ m , false ] ) ) ,
209220 } ,
221+ conditionNames : [ 'temporalio:workflow' , '...' ] ,
210222 } ,
211223 externals : captureProblematicModules ,
212224 module : {
@@ -247,7 +259,8 @@ exports.importInterceptors = function importInterceptors() {
247259 ignoreWarnings : [ / F a i l e d t o p a r s e s o u r c e m a p / ] ,
248260 } ;
249261
250- const compiler = webpack ( this . webpackConfigHook ( options ) ) ;
262+ const finalOptions = this . webpackConfigHook ( options ) ;
263+ const compiler = webpack ( finalOptions ) ;
251264
252265 // Cast to any because the type declarations are inaccurate
253266 compiler . inputFileSystem = inputFilesystem as any ;
@@ -259,22 +272,27 @@ exports.importInterceptors = function importInterceptors() {
259272 return await new Promise < string > ( ( resolve , reject ) => {
260273 compiler . run ( ( err , stats ) => {
261274 if ( stats !== undefined ) {
262- const hasError = stats . hasErrors ( ) ;
275+ let userStatsOptions : Parameters < typeof stats . toString > [ 0 ] ;
276+ switch ( typeof ( finalOptions . stats ?? undefined ) ) {
277+ case 'string' :
278+ case 'boolean' :
279+ userStatsOptions = { preset : finalOptions . stats as string | boolean } ;
280+ break ;
281+ case 'object' :
282+ userStatsOptions = finalOptions . stats as object ;
283+ break ;
284+ default :
285+ userStatsOptions = undefined ;
286+ }
287+
263288 // To debug webpack build:
264289 // const lines = stats.toString({ preset: 'verbose' }).split('\n');
265290 const webpackOutput = stats . toString ( {
266291 chunks : false ,
267292 colors : hasColorSupport ( this . logger ) ,
268293 errorDetails : true ,
294+ ...userStatsOptions ,
269295 } ) ;
270- this . logger [ hasError ? 'error' : 'info' ] ( webpackOutput ) ;
271- if ( hasError ) {
272- reject (
273- new Error (
274- "Webpack finished with errors, if you're unsure what went wrong, visit our troubleshooting page at https://docs.temporal.io/develop/typescript/debugging#webpack-errors"
275- )
276- ) ;
277- }
278296
279297 if ( this . foundProblematicModules . size ) {
280298 const err = new Error (
@@ -287,10 +305,22 @@ exports.importInterceptors = function importInterceptors() {
287305 ` • Make sure that activity code is not imported from workflow code. Use \`import type\` to import activity function signatures.\n` +
288306 ` • Move code that has non-deterministic behaviour to activities.\n` +
289307 ` • If you know for sure that a disallowed module will not be used at runtime, add its name to 'WorkerOptions.bundlerOptions.ignoreModules' in order to dismiss this warning.\n` +
290- `See also: https://typescript.temporal.io/api/namespaces/worker#workflowbundleoption and https://docs.temporal.io/typescript/determinism .`
308+ `See also: https://typescript.temporal.io/api/namespaces/worker#workflowbundleoption and https://docs.temporal.io/develop/ typescript/debugging#webpack-errors .`
291309 ) ;
292310
293311 reject ( err ) ;
312+ return ;
313+ }
314+
315+ if ( stats . hasErrors ( ) ) {
316+ this . logger . error ( webpackOutput ) ;
317+ reject (
318+ new Error (
319+ "Webpack finished with errors, if you're unsure what went wrong, visit our troubleshooting page at https://docs.temporal.io/develop/typescript/debugging#webpack-errors"
320+ )
321+ ) ;
322+ } else if ( finalOptions . stats !== 'none' ) {
323+ this . logger . info ( webpackOutput ) ;
294324 }
295325
296326 const outputFilename = Object . keys ( stats . compilation . assets ) [ 0 ] ;
@@ -315,36 +345,82 @@ export interface BundleOptions {
315345 * Path to look up workflows in, any function exported in this path will be registered as a Workflows when the bundle is loaded by a Worker.
316346 */
317347 workflowsPath : string ;
348+
318349 /**
319350 * List of modules to import Workflow interceptors from.
320351 *
321352 * Modules should export an `interceptors` variable of type {@link WorkflowInterceptorsFactory}.
322353 */
323354 workflowInterceptorModules ?: string [ ] ;
355+
324356 /**
325357 * Optional logger for logging Webpack output
326358 */
327359 logger ?: Logger ;
360+
328361 /**
329362 * Path to a module with a `payloadConverter` named export.
330363 * `payloadConverter` should be an instance of a class that implements {@link PayloadConverter}.
331364 */
332365 payloadConverterPath ?: string ;
366+
333367 /**
334368 * Path to a module with a `failureConverter` named export.
335369 * `failureConverter` should be an instance of a class that implements {@link FailureConverter}.
336370 */
337371 failureConverterPath ?: string ;
372+
338373 /**
339374 * List of modules to be excluded from the Workflows bundle.
340375 *
376+ * > WARN: This is an advanced option that should be used with care. Improper usage may result in
377+ * > runtime errors (e.g. "Cannot read properties of undefined") in Workflow code.
378+ *
341379 * Use this option when your Workflow code references an import that cannot be used in isolation,
342380 * e.g. a Node.js built-in module. Modules listed here **MUST** not be used at runtime.
343- *
344- * > NOTE: This is an advanced option that should be used with care.
345381 */
346382 ignoreModules ?: string [ ] ;
347383
384+ /**
385+ * List of modules to be preloaded into the Workflow sandbox execution context.
386+ *
387+ * > WARN: This is an advanced option that should be used with care. Improper usage may result in
388+ * > non-deterministic behaviors and/or context leaks across workflow executions.
389+ *
390+ * When the Worker is configured with `reuseV8Context: true`, a single v8 execution context is
391+ * reused by multiple Workflow executions. That is, a single v8 execution context is created at
392+ * launch time; the source code of the workflow bundle gets injected into that context, and some
393+ * modules get `require`d, which forces the actual loading of those modules (i.e. module code gets
394+ * parsed, module variables and functions objects get instantiated, module gets registered into
395+ * the `require` cache, etc). After that initial loading, the execution context's globals and all
396+ * cached loaded modules get frozen, to avoid further mutations that could result in context
397+ * leaks between workflow executions.
398+ *
399+ * Then, every time a workflow is started, the workflow sandbox is restored to its pristine state,
400+ * and the workflow module gets `require`d, which results in loading the workflow module and any
401+ * other modules imported from that one. Importantly, modules loaded at that point will be
402+ * per-workflow-instance, and will therefore honor workflow-specific isolation guarantees without
403+ * requirement of being frozen. That notably means that module-level variables will be distinct
404+ * between workflow executions.
405+ *
406+ * Use this option to force preloading of some modules during the preparation phase of the
407+ * workflow execution context. This may be done for two reasons:
408+ *
409+ * - Preloading modules may reduce the per-workflow runtime cost of those modules, notably in
410+ * terms memory footprint and workflow startup time.
411+ * - Preloading modules may be necessary if those modules need to modify global variables that
412+ * would get frozen after the preparation phase, such as polyfills.
413+ *
414+ * Be warned, however, that preloaded modules will themselves get frozen, and may therefore be
415+ * unable to use module-level variables in some ways. There are ways to work around the
416+ * limitations incurred by freezing modules (e.g. use of `Map` or `Set`, closures, ECMA
417+ * `#privateFields`, etc.), but doing so may result in code that exhibits non-deterministic
418+ * behaviors and/or that may leak context across workflow executions.
419+ *
420+ * This option will have no noticeable effect if `reuseV8Context` is disabled.
421+ */
422+ preloadedModules ?: string [ ] ;
423+
348424 /**
349425 * Before Workflow code is bundled with Webpack, `webpackConfigHook` is called with the Webpack
350426 * {@link https://webpack.js.org/configuration/ | configuration} object so you can modify it.
0 commit comments