UNPKG

@temporalio/worker

Version:
297 lines (294 loc) 14.1 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.WorkflowCodeBundler = exports.disallowedModules = exports.disallowedBuiltinModules = exports.allowedBuiltinModules = exports.defaultWorkflowInterceptorModules = void 0; exports.moduleMatches = moduleMatches; exports.bundleWorkflowCode = bundleWorkflowCode; const realFS = __importStar(require("node:fs")); const node_module_1 = require("node:module"); const node_path_1 = __importDefault(require("node:path")); const node_util_1 = __importDefault(require("node:util")); const unionfs = __importStar(require("unionfs")); const memfs = __importStar(require("memfs")); const webpack_1 = require("webpack"); const logger_1 = require("../logger"); const utils_1 = require("../utils"); exports.defaultWorkflowInterceptorModules = [require.resolve('../workflow-log-interceptor')]; exports.allowedBuiltinModules = ['assert', 'url', 'util']; exports.disallowedBuiltinModules = node_module_1.builtinModules.filter((module) => !exports.allowedBuiltinModules.includes(module)); exports.disallowedModules = [ ...exports.disallowedBuiltinModules, '@temporalio/activity', '@temporalio/client', '@temporalio/worker', '@temporalio/common/lib/internal-non-workflow', '@temporalio/interceptors-opentelemetry/lib/client', '@temporalio/interceptors-opentelemetry/lib/worker', '@temporalio/testing', '@temporalio/core-bridge', ]; function moduleMatches(userModule, modules) { return modules.some((module) => userModule === module || userModule.startsWith(`${module}/`)); } /** * Builds a V8 Isolate by bundling provided Workflows using webpack. * * @param workflowsPath all Workflows found in path will be put in the bundle * @param workflowInterceptorModules list of interceptor modules to register on Workflow creation */ class WorkflowCodeBundler { foundProblematicModules = new Set(); logger; workflowsPath; workflowInterceptorModules; payloadConverterPath; failureConverterPath; ignoreModules; webpackConfigHook; constructor({ logger, workflowsPath, payloadConverterPath, failureConverterPath, workflowInterceptorModules, ignoreModules, webpackConfigHook, }) { this.logger = logger ?? new logger_1.DefaultLogger('INFO'); this.workflowsPath = workflowsPath; this.payloadConverterPath = payloadConverterPath; this.failureConverterPath = failureConverterPath; this.workflowInterceptorModules = workflowInterceptorModules ?? []; this.ignoreModules = ignoreModules ?? []; this.webpackConfigHook = webpackConfigHook ?? ((config) => config); } /** * @return a {@link WorkflowBundle} containing bundled code, including inlined source map */ async createBundle() { const vol = new memfs.Volume(); const ufs = new unionfs.Union(); /** * readdir and exclude sourcemaps and d.ts files */ function readdir(...args) { // Help TS a bit because readdir has multiple signatures const callback = args.pop(); const newArgs = [ ...args, (err, files) => { if (err !== null) { callback(err, []); return; } callback(null, files.filter((f) => /\.[jt]s$/.test(node_path_1.default.extname(f)) && !f.endsWith('.d.ts'))); }, ]; return realFS.readdir(...newArgs); } // Cast because the type definitions are inaccurate const memoryFs = memfs.createFsFromVolume(vol); ufs.use(memoryFs).use({ ...realFS, readdir: readdir }); const distDir = '/dist'; const entrypointPath = this.makeEntrypointPath(ufs, this.workflowsPath); this.genEntrypoint(vol, entrypointPath); const bundleFilePath = await this.bundle(ufs, memoryFs, entrypointPath, distDir); let code = memoryFs.readFileSync(bundleFilePath, 'utf8'); // Replace webpack's module cache with an object injected by the runtime. // This is the key to reusing a single v8 context. code = code.replace('var __webpack_module_cache__ = {}', 'var __webpack_module_cache__ = globalThis.__webpack_module_cache__'); this.logger.info('Workflow bundle created', { size: `${(0, utils_1.toMB)(code.length)}MB` }); // Cast because the type definitions are inaccurate return { sourceMap: 'deprecated: this is no longer in use\n', code, }; } makeEntrypointPath(fs, workflowsPath) { const stat = fs.statSync(workflowsPath); if (stat.isFile()) { // workflowsPath is a file; make the entrypoint a sibling of that file const { root, dir, name } = node_path_1.default.parse(workflowsPath); return node_path_1.default.format({ root, dir, base: `${name}-autogenerated-entrypoint.cjs` }); } else { // workflowsPath is a directory; make the entrypoint a sibling of that directory const { root, dir, base } = node_path_1.default.parse(workflowsPath); return node_path_1.default.format({ root, dir, base: `${base}-autogenerated-entrypoint.cjs` }); } } /** * Creates the main entrypoint for the generated webpack library. * * Exports all detected Workflow implementations and some workflow libraries to be used by the Worker. */ genEntrypoint(vol, target) { const interceptorImports = [...new Set(this.workflowInterceptorModules)] .map((v) => `require(/* webpackMode: "eager" */ ${JSON.stringify(v)})`) .join(', \n'); const code = ` const api = require('@temporalio/workflow/lib/worker-interface.js'); exports.api = api; const { overrideGlobals } = require('@temporalio/workflow/lib/global-overrides.js'); overrideGlobals(); exports.importWorkflows = function importWorkflows() { return require(/* webpackMode: "eager" */ ${JSON.stringify(this.workflowsPath)}); } exports.importInterceptors = function importInterceptors() { return [ ${interceptorImports} ]; } `; try { vol.mkdirSync(node_path_1.default.dirname(target), { recursive: true }); } catch (err) { if (err.code !== 'EEXIST') throw err; } vol.writeFileSync(target, code); } /** * Run webpack */ async bundle(inputFilesystem, outputFilesystem, entry, distDir) { const captureProblematicModules = async (data, _callback) => { // Ignore the "node:" prefix if any. const module = data.request?.startsWith('node:') ? data.request.slice('node:'.length) : data.request ?? ''; if (moduleMatches(module, exports.disallowedModules) && !moduleMatches(module, this.ignoreModules)) { this.foundProblematicModules.add(module); } return undefined; }; const options = { resolve: { // https://webpack.js.org/configuration/resolve/#resolvemodules modules: [node_path_1.default.resolve(__dirname, 'module-overrides'), 'node_modules'], extensions: ['.ts', '.js'], extensionAlias: { '.js': ['.ts', '.js'] }, alias: { __temporal_custom_payload_converter$: this.payloadConverterPath ?? false, __temporal_custom_failure_converter$: this.failureConverterPath ?? false, ...Object.fromEntries([...this.ignoreModules, ...exports.disallowedModules].map((m) => [m, false])), }, }, externals: captureProblematicModules, module: { rules: [ { test: /\.js$/, enforce: 'pre', use: [require.resolve('source-map-loader')], }, { test: /\.ts$/, exclude: /node_modules/, use: { loader: require.resolve('swc-loader'), options: { sourceMap: true, jsc: { target: 'es2017', parser: { syntax: 'typescript', decorators: true, }, }, }, }, }, ], }, entry: [entry], mode: 'development', devtool: 'inline-source-map', output: { path: distDir, filename: 'workflow-bundle-[fullhash].js', devtoolModuleFilenameTemplate: '[absolute-resource-path]', library: '__TEMPORAL__', }, ignoreWarnings: [/Failed to parse source map/], }; const compiler = (0, webpack_1.webpack)(this.webpackConfigHook(options)); // Cast to any because the type declarations are inaccurate compiler.inputFileSystem = inputFilesystem; // Don't use ufs due to a strange bug on Windows: // https://github.com/temporalio/sdk-typescript/pull/554 compiler.outputFileSystem = outputFilesystem; try { return await new Promise((resolve, reject) => { compiler.run((err, stats) => { if (stats !== undefined) { const hasError = stats.hasErrors(); // To debug webpack build: // const lines = stats.toString({ preset: 'verbose' }).split('\n'); const webpackOutput = stats.toString({ chunks: false, colors: (0, logger_1.hasColorSupport)(this.logger), errorDetails: true, }); this.logger[hasError ? 'error' : 'info'](webpackOutput); if (hasError) { reject(new Error("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")); } if (this.foundProblematicModules.size) { const err = new Error(`Your Workflow code (or a library used by your Workflow code) is importing the following disallowed modules:\n` + Array.from(this.foundProblematicModules) .map((module) => ` - '${module}'\n`) .join('') + `These modules can't be used in workflow context as they might break determinism.` + `HINT: Consider the following options:\n` + ` • Make sure that activity code is not imported from workflow code. Use \`import type\` to import activity function signatures.\n` + ` • Move code that has non-deterministic behaviour to activities.\n` + ` • 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` + `See also: https://typescript.temporal.io/api/namespaces/worker#workflowbundleoption and https://docs.temporal.io/typescript/determinism.`); reject(err); } const outputFilename = Object.keys(stats.compilation.assets)[0]; if (!err) { resolve(node_path_1.default.join(distDir, outputFilename)); } } reject(err); }); }); } finally { await node_util_1.default.promisify(compiler.close).bind(compiler)(); } } } exports.WorkflowCodeBundler = WorkflowCodeBundler; /** * Create a bundle to pass to {@link WorkerOptions.workflowBundle}. Helpful for reducing Worker startup time in * production. * * When using with {@link Worker.runReplayHistory}, make sure to pass the same interceptors and payload converter used * when the history was generated. */ async function bundleWorkflowCode(options) { const bundler = new WorkflowCodeBundler(options); return await bundler.createBundle(); } //# sourceMappingURL=bundler.js.map