diff --git a/packages/test-helpers/src/bundler.ts b/packages/test-helpers/src/bundler.ts index 928b696d15..b31c7b0265 100644 --- a/packages/test-helpers/src/bundler.ts +++ b/packages/test-helpers/src/bundler.ts @@ -14,6 +14,7 @@ export const baseBundlerIgnoreModules = [ '@temporalio/nexus', '@temporalio/worker', 'ava', + 'process', 'crypto', 'module', 'path', diff --git a/packages/test/src/mocks/workflows-with-process-global.ts b/packages/test/src/mocks/workflows-with-process-global.ts new file mode 100644 index 0000000000..e0f288fec8 --- /dev/null +++ b/packages/test/src/mocks/workflows-with-process-global.ts @@ -0,0 +1,3 @@ +export async function processGlobalWorkflow(): Promise { + return process.pid; +} diff --git a/packages/test/src/test-bundler.ts b/packages/test/src/test-bundler.ts index 4b523b5bc6..18524d5093 100644 --- a/packages/test/src/test-bundler.ts +++ b/packages/test/src/test-bundler.ts @@ -9,6 +9,7 @@ import { randomUUID } from 'crypto'; import type { ExecutionContext } from 'ava'; import test from 'ava'; import { moduleMatches } from '@temporalio/worker/lib/workflow/bundler'; +import { baseBundlerIgnoreModules } from '@temporalio/test-helpers'; import type { LogEntry, WorkerOptions } from '@temporalio/worker'; import { bundleWorkflowCode, DefaultLogger } from '@temporalio/worker'; import { WorkflowClient } from '@temporalio/client'; @@ -23,6 +24,27 @@ test('moduleMatches works', (t) => { t.false(moduleMatches('fs', ['foo'])); }); +test('An error is thrown when workflow references the process global', async (t) => { + await t.throwsAsync( + bundleWorkflowCode({ + workflowsPath: require.resolve('./mocks/workflows-with-process-global'), + }), + { + instanceOf: Error, + message: /is importing the following disallowed modules.*process/s, + } + ); +}); + +test('Workflow bundle can be created from code using process global with test helper ignoreModules', async (t) => { + await t.notThrowsAsync( + bundleWorkflowCode({ + workflowsPath: require.resolve('./mocks/workflows-with-process-global'), + ignoreModules: baseBundlerIgnoreModules, + }) + ); +}); + async function runPreloadSharedCounter( t: ExecutionContext, workerOptions: Pick diff --git a/packages/worker/src/workflow/bundler.ts b/packages/worker/src/workflow/bundler.ts index ae70695e4c..8997f888ad 100644 --- a/packages/worker/src/workflow/bundler.ts +++ b/packages/worker/src/workflow/bundler.ts @@ -4,7 +4,7 @@ import path from 'node:path'; import util from 'node:util'; import * as unionfs from 'unionfs'; import * as memfs from 'memfs'; -import type { Configuration } from 'webpack'; +import type { Configuration, javascript } from 'webpack'; import { webpack, NormalModuleReplacementPlugin } from 'webpack'; import type { Logger } from '../logger'; import { DefaultLogger, hasColorSupport } from '../logger'; @@ -58,6 +58,10 @@ export class WorkflowCodeBundler { protected readonly webpackConfigHook: (config: Configuration) => Configuration; protected readonly plugins: BundlerPlugin[]; + protected isIgnoredModule(module: string): boolean { + return moduleMatches(module, this.ignoreModules); + } + constructor(options: BundleOptions) { this.plugins = options.plugins ?? []; for (const plugin of this.plugins) { @@ -218,7 +222,7 @@ exports.importInterceptors = function importInterceptors() { ? data.request.slice('node:'.length) : data.request ?? ''; - if (moduleMatches(module, disallowedModules) && !moduleMatches(module, this.ignoreModules)) { + if (moduleMatches(module, disallowedModules) && !this.isIgnoredModule(module)) { this.foundProblematicModules.add(module); } @@ -238,6 +242,23 @@ exports.importInterceptors = function importInterceptors() { }, }, plugins: [ + { + apply: (compiler) => { + compiler.hooks.normalModuleFactory.tap('WorkflowCodeBundler', (normalModuleFactory) => { + for (const moduleType of ['javascript/auto', 'javascript/dynamic', 'javascript/esm'] as const) { + normalModuleFactory.hooks.parser + .for(moduleType) + .tap('WorkflowCodeBundler', (javascriptParser: javascript.JavascriptParser) => { + javascriptParser.hooks.expression.for('process').tap('WorkflowCodeBundler', () => { + if (!javascriptParser.isVariableDefined('process') && !this.isIgnoredModule('process')) { + this.foundProblematicModules.add('process'); + } + }); + }); + } + }); + }, + }, // `@temporalio/interceptors-opentelemetry` only requires `@temporalio/workflow` for interceptors that run in workflow context. // In order to keep `@temporalio/workflow` as an optional peer dependency for `@temporalio/interceptors-opentelemetry` // we use `workflow-imports` to reexport all required imports from `@temporalio/workflow`.