@@ -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 protected readonly plugins : BundlerPlugin [ ] ;
@@ -68,6 +69,7 @@ export class WorkflowCodeBundler {
6869 payloadConverterPath,
6970 failureConverterPath,
7071 workflowInterceptorModules,
72+ preloadedModules,
7173 ignoreModules,
7274 webpackConfigHook,
7375 } = options ;
@@ -76,6 +78,7 @@ export class WorkflowCodeBundler {
7678 this . payloadConverterPath = payloadConverterPath ;
7779 this . failureConverterPath = failureConverterPath ;
7880 this . workflowInterceptorModules = workflowInterceptorModules ?? [ ] ;
81+ this . preloadedModules = preloadedModules ?? [ ] ;
7982 this . ignoreModules = ignoreModules ?? [ ] ;
8083 this . webpackConfigHook = webpackConfigHook ?? ( ( config ) => config ) ;
8184 }
@@ -157,23 +160,29 @@ export class WorkflowCodeBundler {
157160 . map ( ( v ) => `require(/* webpackMode: "eager" */ ${ JSON . stringify ( v ) } )` )
158161 . join ( ', \n' ) ;
159162
163+ const preloadedModulesImports = [ ...new Set ( this . preloadedModules ) ]
164+ . map ( ( v ) => `require(/* webpackMode: "eager" */ ${ JSON . stringify ( v ) } )` )
165+ . join ( ';\n' ) ;
166+
160167 const code = `
161- const api = require('@temporalio/workflow/lib/worker-interface.js');
162- exports.api = api;
168+ const api = require('@temporalio/workflow/lib/worker-interface.js');
169+ exports.api = api;
163170
164- const { overrideGlobals } = require('@temporalio/workflow/lib/global-overrides.js');
165- overrideGlobals();
171+ const { overrideGlobals } = require('@temporalio/workflow/lib/global-overrides.js');
172+ overrideGlobals();
166173
167- exports.importWorkflows = function importWorkflows() {
168- return require(/* webpackMode: "eager" */ ${ JSON . stringify ( this . workflowsPath ) } );
169- }
174+ ${ preloadedModulesImports }
170175
171- exports.importInterceptors = function importInterceptors() {
172- return [
173- ${ interceptorImports }
174- ];
175- }
176- ` ;
176+ exports.importWorkflows = function importWorkflows() {
177+ return require(/* webpackMode: "eager" */ ${ JSON . stringify ( this . workflowsPath ) } );
178+ }
179+
180+ exports.importInterceptors = function importInterceptors() {
181+ return [
182+ ${ interceptorImports }
183+ ];
184+ }
185+ ` ;
177186 try {
178187 vol . mkdirSync ( path . dirname ( target ) , { recursive : true } ) ;
179188 } catch ( err : any ) {
@@ -198,7 +207,9 @@ exports.importInterceptors = function importInterceptors() {
198207 : data . request ?? '' ;
199208
200209 if ( moduleMatches ( module , disallowedModules ) && ! moduleMatches ( module , this . ignoreModules ) ) {
201- this . foundProblematicModules . add ( module ) ;
210+ // this.foundProblematicModules.add(module);
211+ // // callback(new Error(`Import of disallowed module: '${module}'`));
212+ throw new Error ( `Import of disallowed module: '${ module } '` ) ;
202213 }
203214
204215 return undefined ;
@@ -215,6 +226,7 @@ exports.importInterceptors = function importInterceptors() {
215226 __temporal_custom_failure_converter$ : this . failureConverterPath ?? false ,
216227 ...Object . fromEntries ( [ ...this . ignoreModules , ...disallowedModules ] . map ( ( m ) => [ m , false ] ) ) ,
217228 } ,
229+ conditionNames : [ 'temporalio:workflow' , '...' ] ,
218230 } ,
219231 externals : captureProblematicModules ,
220232 module : {
@@ -255,7 +267,8 @@ exports.importInterceptors = function importInterceptors() {
255267 ignoreWarnings : [ / F a i l e d t o p a r s e s o u r c e m a p / ] ,
256268 } ;
257269
258- const compiler = webpack ( this . webpackConfigHook ( options ) ) ;
270+ const finalOptions = this . webpackConfigHook ( options ) ;
271+ const compiler = webpack ( finalOptions ) ;
259272
260273 // Cast to any because the type declarations are inaccurate
261274 compiler . inputFileSystem = inputFilesystem as any ;
@@ -267,22 +280,27 @@ exports.importInterceptors = function importInterceptors() {
267280 return await new Promise < string > ( ( resolve , reject ) => {
268281 compiler . run ( ( err , stats ) => {
269282 if ( stats !== undefined ) {
270- const hasError = stats . hasErrors ( ) ;
283+ let userStatsOptions : Parameters < typeof stats . toString > [ 0 ] ;
284+ switch ( typeof ( finalOptions . stats ?? undefined ) ) {
285+ case 'string' :
286+ case 'boolean' :
287+ userStatsOptions = { preset : finalOptions . stats as string | boolean } ;
288+ break ;
289+ case 'object' :
290+ userStatsOptions = finalOptions . stats as object ;
291+ break ;
292+ default :
293+ userStatsOptions = undefined ;
294+ }
295+
271296 // To debug webpack build:
272297 // const lines = stats.toString({ preset: 'verbose' }).split('\n');
273298 const webpackOutput = stats . toString ( {
274299 chunks : false ,
275300 colors : hasColorSupport ( this . logger ) ,
276301 errorDetails : true ,
302+ ...userStatsOptions ,
277303 } ) ;
278- this . logger [ hasError ? 'error' : 'info' ] ( webpackOutput ) ;
279- if ( hasError ) {
280- reject (
281- new Error (
282- "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"
283- )
284- ) ;
285- }
286304
287305 if ( this . foundProblematicModules . size ) {
288306 const err = new Error (
@@ -295,10 +313,22 @@ exports.importInterceptors = function importInterceptors() {
295313 ` • Make sure that activity code is not imported from workflow code. Use \`import type\` to import activity function signatures.\n` +
296314 ` • Move code that has non-deterministic behaviour to activities.\n` +
297315 ` • 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` +
298- `See also: https://typescript.temporal.io/api/namespaces/worker#workflowbundleoption and https://docs.temporal.io/typescript/determinism .`
316+ `See also: https://typescript.temporal.io/api/namespaces/worker#workflowbundleoption and https://docs.temporal.io/develop/ typescript/debugging#webpack-errors .`
299317 ) ;
300318
301319 reject ( err ) ;
320+ return ;
321+ }
322+
323+ if ( stats . hasErrors ( ) ) {
324+ this . logger . error ( webpackOutput ) ;
325+ reject (
326+ new Error (
327+ "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"
328+ )
329+ ) ;
330+ } else if ( finalOptions . stats !== 'none' ) {
331+ this . logger . info ( webpackOutput ) ;
302332 }
303333
304334 const outputFilename = Object . keys ( stats . compilation . assets ) [ 0 ] ;
@@ -345,36 +375,82 @@ export interface BundleOptions {
345375 * 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.
346376 */
347377 workflowsPath : string ;
378+
348379 /**
349380 * List of modules to import Workflow interceptors from.
350381 *
351382 * Modules should export an `interceptors` variable of type {@link WorkflowInterceptorsFactory}.
352383 */
353384 workflowInterceptorModules ?: string [ ] ;
385+
354386 /**
355387 * Optional logger for logging Webpack output
356388 */
357389 logger ?: Logger ;
390+
358391 /**
359392 * Path to a module with a `payloadConverter` named export.
360393 * `payloadConverter` should be an instance of a class that implements {@link PayloadConverter}.
361394 */
362395 payloadConverterPath ?: string ;
396+
363397 /**
364398 * Path to a module with a `failureConverter` named export.
365399 * `failureConverter` should be an instance of a class that implements {@link FailureConverter}.
366400 */
367401 failureConverterPath ?: string ;
402+
368403 /**
369404 * List of modules to be excluded from the Workflows bundle.
370405 *
406+ * > WARN: This is an advanced option that should be used with care. Improper usage may result in
407+ * > runtime errors (e.g. "Cannot read properties of undefined") in Workflow code.
408+ *
371409 * Use this option when your Workflow code references an import that cannot be used in isolation,
372410 * e.g. a Node.js built-in module. Modules listed here **MUST** not be used at runtime.
373- *
374- * > NOTE: This is an advanced option that should be used with care.
375411 */
376412 ignoreModules ?: string [ ] ;
377413
414+ /**
415+ * List of modules to be preloaded into the Workflow sandbox execution context.
416+ *
417+ * > WARN: This is an advanced option that should be used with care. Improper usage may result in
418+ * > non-deterministic behaviors and/or context leaks across workflow executions.
419+ *
420+ * When the Worker is configured with `reuseV8Context: true`, a single v8 execution context is
421+ * reused by multiple Workflow executions. That is, a single v8 execution context is created at
422+ * launch time; the source code of the workflow bundle gets injected into that context, and some
423+ * modules get `require`d, which forces the actual loading of those modules (i.e. module code gets
424+ * parsed, module variables and functions objects get instantiated, module gets registered into
425+ * the `require` cache, etc). After that initial loading, the execution context's globals and all
426+ * cached loaded modules get frozen, to avoid further mutations that could result in context
427+ * leaks between workflow executions.
428+ *
429+ * Then, every time a workflow is started, the workflow sandbox is restored to its pristine state,
430+ * and the workflow module gets `require`d, which results in loading the workflow module and any
431+ * other modules imported from that one. Importantly, modules loaded at that point will be
432+ * per-workflow-instance, and will therefore honor workflow-specific isolation guarantees without
433+ * requirement of being frozen. That notably means that module-level variables will be distinct
434+ * between workflow executions.
435+ *
436+ * Use this option to force preloading of some modules during the preparation phase of the
437+ * workflow execution context. This may be done for two reasons:
438+ *
439+ * - Preloading modules may reduce the per-workflow runtime cost of those modules, notably in
440+ * terms memory footprint and workflow startup time.
441+ * - Preloading modules may be necessary if those modules need to modify global variables that
442+ * would get frozen after the preparation phase, such as polyfills.
443+ *
444+ * Be warned, however, that preloaded modules will themselves get frozen, and may therefore be
445+ * unable to use module-level variables in some ways. There are ways to work around the
446+ * limitations incurred by freezing modules (e.g. use of `Map` or `Set`, closures, ECMA
447+ * `#privateFields`, etc.), but doing so may result in code that exhibits non-deterministic
448+ * behaviors and/or that may leak context across workflow executions.
449+ *
450+ * This option will have no noticeable effect if `reuseV8Context` is disabled.
451+ */
452+ preloadedModules ?: string [ ] ;
453+
378454 /**
379455 * Before Workflow code is bundled with Webpack, `webpackConfigHook` is called with the Webpack
380456 * {@link https://webpack.js.org/configuration/ | configuration} object so you can modify it.
0 commit comments