diff --git a/.gitignore b/.gitignore index f610f8be3..18d3978a2 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,12 @@ npm-debug.log* .idea # MacOs -.DS_Store \ No newline at end of file +.DS_Store + +# Yarn +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions \ No newline at end of file diff --git a/.prettierignore b/.prettierignore index d6c166b8e..89caf2903 100644 --- a/.prettierignore +++ b/.prettierignore @@ -3,4 +3,6 @@ .serverless .serverless-offline .webpack -coverage \ No newline at end of file +coverage +.pnp.cjs +yarn-4.1.1.cjs \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 2be2053a8..4f7bed214 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "serverless-offline", - "version": "14.3.0", + "version": "14.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "serverless-offline", - "version": "14.3.0", + "version": "14.3.1", "license": "MIT", "dependencies": { "@aws-sdk/client-lambda": "^3.636.0", diff --git a/src/lambda/handler-runner/worker-thread-runner/WorkerThreadRunner.js b/src/lambda/handler-runner/worker-thread-runner/WorkerThreadRunner.js index 4bf5d966a..b3cf0d0f9 100644 --- a/src/lambda/handler-runner/worker-thread-runner/WorkerThreadRunner.js +++ b/src/lambda/handler-runner/worker-thread-runner/WorkerThreadRunner.js @@ -1,25 +1,75 @@ import { MessageChannel, Worker } from "node:worker_threads" import { join } from "desm" +import { pathToFileURL } from "node:url" +import { versions, env as nodeEnv } from "node:process" +import { createRequire } from "node:module" +import { statSync } from "node:fs" + +const IS_NODE_20 = Number(versions.node.split(".")[0]) >= 20 + +const isFile = (path) => { + try { + return !!statSync(path, { throwIfNoEntry: false })?.isFile() + } catch { + /* istanbul ignore next */ + return false + } +} export default class WorkerThreadRunner { #workerThread = null constructor(funOptions, env) { const { codeDir, functionKey, handler, servicePath, timeout } = funOptions + // Resolve the PnP loader path if Yarn PnP is in use + let pnpLoaderPath + let execArgv + if (versions.pnp) { + const nodeOptions = nodeEnv.NODE_OPTIONS?.split(/\s+/) + const cjsRequire = createRequire(import.meta.url) + let pnpApiPath + try { + /** @see https://github.com/facebook/jest/issues/9543 */ + pnpApiPath = cjsRequire.resolve("pnpapi") + } catch { + /* empty */ + } + if ( + pnpApiPath && + !nodeOptions?.some( + (option, index) => + ["-r", "--require"].includes(option) && + pnpApiPath === cjsRequire.resolve(nodeOptions[index + 1]), + ) + ) { + execArgv = ["-r", pnpApiPath] + // eslint-disable-next-line no-underscore-dangle + const _pnpLoaderPath = join(pnpApiPath, "../.pnp.loader.mjs") + if (isFile(_pnpLoaderPath)) { + pnpLoaderPath = pathToFileURL(_pnpLoaderPath).toString() + if (!IS_NODE_20) { + execArgv = ["--experimental-loader", pnpLoaderPath, ...execArgv] + } + } + } + } + + const workerOptions = { + env, + execArgv, + workerData: { + codeDir, + functionKey, + handler, + pnpLoaderPath, + servicePath, + timeout, + }, + } this.#workerThread = new Worker( join(import.meta.url, "workerThreadHelper.js"), - { - // don't pass process.env from the main process! - env, - workerData: { - codeDir, - functionKey, - handler, - servicePath, - timeout, - }, - }, + workerOptions, ) } diff --git a/src/lambda/handler-runner/worker-thread-runner/workerThreadHelper.js b/src/lambda/handler-runner/worker-thread-runner/workerThreadHelper.js index ca1d84432..335da75c3 100644 --- a/src/lambda/handler-runner/worker-thread-runner/workerThreadHelper.js +++ b/src/lambda/handler-runner/worker-thread-runner/workerThreadHelper.js @@ -1,8 +1,16 @@ -import { env } from "node:process" +import { env, versions } from "node:process" import { parentPort, workerData } from "node:worker_threads" +import { register } from "node:module" import InProcessRunner from "../in-process-runner/index.js" -const { codeDir, functionKey, handler, servicePath, timeout } = workerData +const IS_NODE_20 = Number(versions.node.split(".")[0]) >= 20 + +const { codeDir, functionKey, handler, servicePath, timeout, pnpLoaderPath } = + workerData + +if (pnpLoaderPath && IS_NODE_20) { + register(pnpLoaderPath) +} const inProcessRunner = new InProcessRunner( { diff --git a/tests/installYarnModules.js b/tests/installYarnModules.js new file mode 100644 index 000000000..90d32a116 --- /dev/null +++ b/tests/installYarnModules.js @@ -0,0 +1,8 @@ +import { execa } from "execa" + +export default function installYarnModules(dirPath) { + return execa("yarn", ["--immutable"], { + cwd: dirPath, + stdio: "inherit", + }) +}