diff --git a/packages/php-wasm/node-builds/7-2/src/index.ts b/packages/php-wasm/node-builds/7-2/src/index.ts index 3cdb609972..018558acf2 100644 --- a/packages/php-wasm/node-builds/7-2/src/index.ts +++ b/packages/php-wasm/node-builds/7-2/src/index.ts @@ -20,12 +20,33 @@ const packageDir = existsSync(join(currentDirPath, 'jspi')) : dirname(currentDirPath); export async function getPHPLoaderModule(): Promise { - if (await jspi()) { - // @ts-ignore - return await import('../jspi/php_7_2.js'); + // Detect Jest environment and use require() to avoid dynamic import() issues + // For all other environments (ESM, modern CommonJS, Vitest), use dynamic import() + // which works in Node.js 12.20+ regardless of whether the code is running in ESM or CJS. + // Jest runs tests in a VM context that doesn't support dynamic import() without + // the --experimental-vm-modules flag. Jest sets JEST_WORKER_ID when running tests. + // Error without flag: "A dynamic import callback was invoked without --experimental-vm-modules" + // See: https://jestjs.io/docs/ecmascript-modules + const shouldUseRequire = process.env && process.env['JEST_WORKER_ID']; + + if (shouldUseRequire) { + // Use require() specifically for Jest + if (await jspi()) { + // @ts-ignore + return require('../jspi/php_7_2.js'); + } else { + // @ts-ignore + return require('../asyncify/php_7_2.js'); + } } else { - // @ts-ignore - return await import('../asyncify/php_7_2.js'); + // Use dynamic import() for all other environments + if (await jspi()) { + // @ts-ignore + return await import('../jspi/php_7_2.js'); + } else { + // @ts-ignore + return await import('../asyncify/php_7_2.js'); + } } } diff --git a/packages/php-wasm/node-builds/7-3/src/index.ts b/packages/php-wasm/node-builds/7-3/src/index.ts index 4c0296c338..7780cd8ce0 100644 --- a/packages/php-wasm/node-builds/7-3/src/index.ts +++ b/packages/php-wasm/node-builds/7-3/src/index.ts @@ -20,12 +20,33 @@ const packageDir = existsSync(join(currentDirPath, 'jspi')) : dirname(currentDirPath); export async function getPHPLoaderModule(): Promise { - if (await jspi()) { - // @ts-ignore - return await import('../jspi/php_7_3.js'); + // Detect Jest environment and use require() to avoid dynamic import() issues + // For all other environments (ESM, modern CommonJS, Vitest), use dynamic import() + // which works in Node.js 12.20+ regardless of whether the code is running in ESM or CJS. + // Jest runs tests in a VM context that doesn't support dynamic import() without + // the --experimental-vm-modules flag. Jest sets JEST_WORKER_ID when running tests. + // Error without flag: "A dynamic import callback was invoked without --experimental-vm-modules" + // See: https://jestjs.io/docs/ecmascript-modules + const shouldUseRequire = process.env && process.env['JEST_WORKER_ID']; + + if (shouldUseRequire) { + // Use require() specifically for Jest + if (await jspi()) { + // @ts-ignore + return require('../jspi/php_7_3.js'); + } else { + // @ts-ignore + return require('../asyncify/php_7_3.js'); + } } else { - // @ts-ignore - return await import('../asyncify/php_7_3.js'); + // Use dynamic import() for all other environments + if (await jspi()) { + // @ts-ignore + return await import('../jspi/php_7_3.js'); + } else { + // @ts-ignore + return await import('../asyncify/php_7_3.js'); + } } } diff --git a/packages/php-wasm/node-builds/7-4/src/index.ts b/packages/php-wasm/node-builds/7-4/src/index.ts index 643b9939e3..081a117def 100644 --- a/packages/php-wasm/node-builds/7-4/src/index.ts +++ b/packages/php-wasm/node-builds/7-4/src/index.ts @@ -20,12 +20,33 @@ const packageDir = existsSync(join(currentDirPath, 'jspi')) : dirname(currentDirPath); export async function getPHPLoaderModule(): Promise { - if (await jspi()) { - // @ts-ignore - return await import('../jspi/php_7_4.js'); + // Detect Jest environment and use require() to avoid dynamic import() issues + // For all other environments (ESM, modern CommonJS, Vitest), use dynamic import() + // which works in Node.js 12.20+ regardless of whether the code is running in ESM or CJS. + // Jest runs tests in a VM context that doesn't support dynamic import() without + // the --experimental-vm-modules flag. Jest sets JEST_WORKER_ID when running tests. + // Error without flag: "A dynamic import callback was invoked without --experimental-vm-modules" + // See: https://jestjs.io/docs/ecmascript-modules + const shouldUseRequire = process.env && process.env['JEST_WORKER_ID']; + + if (shouldUseRequire) { + // Use require() specifically for Jest + if (await jspi()) { + // @ts-ignore + return require('../jspi/php_7_4.js'); + } else { + // @ts-ignore + return require('../asyncify/php_7_4.js'); + } } else { - // @ts-ignore - return await import('../asyncify/php_7_4.js'); + // Use dynamic import() for all other environments + if (await jspi()) { + // @ts-ignore + return await import('../jspi/php_7_4.js'); + } else { + // @ts-ignore + return await import('../asyncify/php_7_4.js'); + } } } diff --git a/packages/php-wasm/node-builds/8-0/src/index.ts b/packages/php-wasm/node-builds/8-0/src/index.ts index 089463235b..8d689af8c0 100644 --- a/packages/php-wasm/node-builds/8-0/src/index.ts +++ b/packages/php-wasm/node-builds/8-0/src/index.ts @@ -20,12 +20,33 @@ const packageDir = existsSync(join(currentDirPath, 'jspi')) : dirname(currentDirPath); export async function getPHPLoaderModule(): Promise { - if (await jspi()) { - // @ts-ignore - return await import('../jspi/php_8_0.js'); + // Detect Jest environment and use require() to avoid dynamic import() issues + // For all other environments (ESM, modern CommonJS, Vitest), use dynamic import() + // which works in Node.js 12.20+ regardless of whether the code is running in ESM or CJS. + // Jest runs tests in a VM context that doesn't support dynamic import() without + // the --experimental-vm-modules flag. Jest sets JEST_WORKER_ID when running tests. + // Error without flag: "A dynamic import callback was invoked without --experimental-vm-modules" + // See: https://jestjs.io/docs/ecmascript-modules + const shouldUseRequire = process.env && process.env['JEST_WORKER_ID']; + + if (shouldUseRequire) { + // Use require() specifically for Jest + if (await jspi()) { + // @ts-ignore + return require('../jspi/php_8_0.js'); + } else { + // @ts-ignore + return require('../asyncify/php_8_0.js'); + } } else { - // @ts-ignore - return await import('../asyncify/php_8_0.js'); + // Use dynamic import() for all other environments + if (await jspi()) { + // @ts-ignore + return await import('../jspi/php_8_0.js'); + } else { + // @ts-ignore + return await import('../asyncify/php_8_0.js'); + } } } diff --git a/packages/php-wasm/node-builds/8-1/src/index.ts b/packages/php-wasm/node-builds/8-1/src/index.ts index 1e2a561c2e..e23bf6a07f 100644 --- a/packages/php-wasm/node-builds/8-1/src/index.ts +++ b/packages/php-wasm/node-builds/8-1/src/index.ts @@ -20,12 +20,33 @@ const packageDir = existsSync(join(currentDirPath, 'jspi')) : dirname(currentDirPath); export async function getPHPLoaderModule(): Promise { - if (await jspi()) { - // @ts-ignore - return await import('../jspi/php_8_1.js'); + // Detect Jest environment and use require() to avoid dynamic import() issues + // For all other environments (ESM, modern CommonJS, Vitest), use dynamic import() + // which works in Node.js 12.20+ regardless of whether the code is running in ESM or CJS. + // Jest runs tests in a VM context that doesn't support dynamic import() without + // the --experimental-vm-modules flag. Jest sets JEST_WORKER_ID when running tests. + // Error without flag: "A dynamic import callback was invoked without --experimental-vm-modules" + // See: https://jestjs.io/docs/ecmascript-modules + const shouldUseRequire = process.env && process.env['JEST_WORKER_ID']; + + if (shouldUseRequire) { + // Use require() specifically for Jest + if (await jspi()) { + // @ts-ignore + return require('../jspi/php_8_1.js'); + } else { + // @ts-ignore + return require('../asyncify/php_8_1.js'); + } } else { - // @ts-ignore - return await import('../asyncify/php_8_1.js'); + // Use dynamic import() for all other environments + if (await jspi()) { + // @ts-ignore + return await import('../jspi/php_8_1.js'); + } else { + // @ts-ignore + return await import('../asyncify/php_8_1.js'); + } } } diff --git a/packages/php-wasm/node-builds/8-2/src/index.ts b/packages/php-wasm/node-builds/8-2/src/index.ts index fde6a15925..10675b6a99 100644 --- a/packages/php-wasm/node-builds/8-2/src/index.ts +++ b/packages/php-wasm/node-builds/8-2/src/index.ts @@ -20,12 +20,33 @@ const packageDir = existsSync(join(currentDirPath, 'jspi')) : dirname(currentDirPath); export async function getPHPLoaderModule(): Promise { - if (await jspi()) { - // @ts-ignore - return await import('../jspi/php_8_2.js'); + // Detect Jest environment and use require() to avoid dynamic import() issues + // For all other environments (ESM, modern CommonJS, Vitest), use dynamic import() + // which works in Node.js 12.20+ regardless of whether the code is running in ESM or CJS. + // Jest runs tests in a VM context that doesn't support dynamic import() without + // the --experimental-vm-modules flag. Jest sets JEST_WORKER_ID when running tests. + // Error without flag: "A dynamic import callback was invoked without --experimental-vm-modules" + // See: https://jestjs.io/docs/ecmascript-modules + const shouldUseRequire = process.env && process.env['JEST_WORKER_ID']; + + if (shouldUseRequire) { + // Use require() specifically for Jest + if (await jspi()) { + // @ts-ignore + return require('../jspi/php_8_2.js'); + } else { + // @ts-ignore + return require('../asyncify/php_8_2.js'); + } } else { - // @ts-ignore - return await import('../asyncify/php_8_2.js'); + // Use dynamic import() for all other environments + if (await jspi()) { + // @ts-ignore + return await import('../jspi/php_8_2.js'); + } else { + // @ts-ignore + return await import('../asyncify/php_8_2.js'); + } } } diff --git a/packages/php-wasm/node-builds/8-3/src/index.ts b/packages/php-wasm/node-builds/8-3/src/index.ts index a3fd4b20cd..7631421b6f 100644 --- a/packages/php-wasm/node-builds/8-3/src/index.ts +++ b/packages/php-wasm/node-builds/8-3/src/index.ts @@ -20,12 +20,34 @@ const packageDir = existsSync(join(currentDirPath, 'jspi')) : dirname(currentDirPath); export async function getPHPLoaderModule(): Promise { - if (await jspi()) { - // @ts-ignore - return await import('../jspi/php_8_3.js'); + // Detect Jest environment and use require() instead of dynamic import() + // Jest runs tests in a VM context that doesn't support dynamic import() without + // the --experimental-vm-modules flag. Jest sets JEST_WORKER_ID when running tests. + // For all other environments (ESM, modern CommonJS, Vitest), dynamic import() works fine. + // See: https://jestjs.io/docs/ecmascript-modules + const isJest = + typeof process !== 'undefined' && + process.env && + process.env['JEST_WORKER_ID']; + + if (isJest) { + // Use require() for Jest + if (await jspi()) { + // @ts-ignore + return require('../jspi/php_8_3.js'); + } else { + // @ts-ignore + return require('../asyncify/php_8_3.js'); + } } else { - // @ts-ignore - return await import('../asyncify/php_8_3.js'); + // Use dynamic import() for all other environments + if (await jspi()) { + // @ts-ignore + return await import('../jspi/php_8_3.js'); + } else { + // @ts-ignore + return await import('../asyncify/php_8_3.js'); + } } } diff --git a/packages/php-wasm/node-builds/8-4/src/index.ts b/packages/php-wasm/node-builds/8-4/src/index.ts index 80ea13248d..86e45a47e8 100644 --- a/packages/php-wasm/node-builds/8-4/src/index.ts +++ b/packages/php-wasm/node-builds/8-4/src/index.ts @@ -20,12 +20,33 @@ const packageDir = existsSync(join(currentDirPath, 'jspi')) : dirname(currentDirPath); export async function getPHPLoaderModule(): Promise { - if (await jspi()) { - // @ts-ignore - return await import('../jspi/php_8_4.js'); + // Detect Jest environment and use require() to avoid dynamic import() issues + // For all other environments (ESM, modern CommonJS, Vitest), use dynamic import() + // which works in Node.js 12.20+ regardless of whether the code is running in ESM or CJS. + // Jest runs tests in a VM context that doesn't support dynamic import() without + // the --experimental-vm-modules flag. Jest sets JEST_WORKER_ID when running tests. + // Error without flag: "A dynamic import callback was invoked without --experimental-vm-modules" + // See: https://jestjs.io/docs/ecmascript-modules + const shouldUseRequire = process.env && process.env['JEST_WORKER_ID']; + + if (shouldUseRequire) { + // Use require() specifically for Jest + if (await jspi()) { + // @ts-ignore + return require('../jspi/php_8_4.js'); + } else { + // @ts-ignore + return require('../asyncify/php_8_4.js'); + } } else { - // @ts-ignore - return await import('../asyncify/php_8_4.js'); + // Use dynamic import() for all other environments + if (await jspi()) { + // @ts-ignore + return await import('../jspi/php_8_4.js'); + } else { + // @ts-ignore + return await import('../asyncify/php_8_4.js'); + } } } diff --git a/packages/php-wasm/node-builds/8-5/src/index.ts b/packages/php-wasm/node-builds/8-5/src/index.ts index 9545fa7fa0..02abd5a2fd 100644 --- a/packages/php-wasm/node-builds/8-5/src/index.ts +++ b/packages/php-wasm/node-builds/8-5/src/index.ts @@ -20,12 +20,33 @@ const packageDir = existsSync(join(currentDirPath, 'jspi')) : dirname(currentDirPath); export async function getPHPLoaderModule(): Promise { - if (await jspi()) { - // @ts-ignore - return await import('../jspi/php_8_5.js'); + // Detect Jest environment and use require() to avoid dynamic import() issues + // For all other environments (ESM, modern CommonJS, Vitest), use dynamic import() + // which works in Node.js 12.20+ regardless of whether the code is running in ESM or CJS. + // Jest runs tests in a VM context that doesn't support dynamic import() without + // the --experimental-vm-modules flag. Jest sets JEST_WORKER_ID when running tests. + // Error without flag: "A dynamic import callback was invoked without --experimental-vm-modules" + // See: https://jestjs.io/docs/ecmascript-modules + const shouldUseRequire = process.env && process.env['JEST_WORKER_ID']; + + if (shouldUseRequire) { + // Use require() specifically for Jest + if (await jspi()) { + // @ts-ignore + return require('../jspi/php_8_5.js'); + } else { + // @ts-ignore + return require('../asyncify/php_8_5.js'); + } } else { - // @ts-ignore - return await import('../asyncify/php_8_5.js'); + // Use dynamic import() for all other environments + if (await jspi()) { + // @ts-ignore + return await import('../jspi/php_8_5.js'); + } else { + // @ts-ignore + return await import('../asyncify/php_8_5.js'); + } } } diff --git a/packages/php-wasm/node/src/lib/get-php-loader-module.ts b/packages/php-wasm/node/src/lib/get-php-loader-module.ts index f8f60b4e5d..06568411a5 100644 --- a/packages/php-wasm/node/src/lib/get-php-loader-module.ts +++ b/packages/php-wasm/node/src/lib/get-php-loader-module.ts @@ -4,18 +4,60 @@ import type { PHPLoaderModule, SupportedPHPVersion } from '@php-wasm/universal'; /** * Loads the PHP loader module for the given PHP version. * - * Each PHP version is packaged separately to reduce bundle size: - * - @php-wasm/node-8-5 - * - @php-wasm/node-8-4 - * - @php-wasm/node-8-3 - * - etc. - * - * @param version The PHP version to load. - * @returns The PHP loader module. + * Uses dynamic import() by default (works in ESM, modern CommonJS, and Vitest). + * Only uses require() in Jest environments where dynamic import() isn't supported + * without --experimental-vm-modules. */ export async function getPHPLoaderModule( version: SupportedPHPVersion = LatestSupportedPHPVersion ): Promise { + // Detect Jest environment and use require() instead of dynamic import() + // Jest runs tests in a VM context that doesn't support dynamic import() without + // the --experimental-vm-modules flag. Jest sets JEST_WORKER_ID when running tests. + // For all other environments (ESM, modern CommonJS, Vitest), dynamic import() works fine. + // See: https://jestjs.io/docs/ecmascript-modules + const isJest = + typeof process !== 'undefined' && + process.env && + process.env['JEST_WORKER_ID']; + + if (isJest) { + // Use require() for Jest + switch (version) { + case '8.5': + // @ts-ignore + return require('@php-wasm/node-8-5').getPHPLoaderModule(); + case '8.4': + // @ts-ignore + return require('@php-wasm/node-8-4').getPHPLoaderModule(); + case '8.3': + // @ts-ignore + return require('@php-wasm/node-8-3').getPHPLoaderModule(); + case '8.2': + // @ts-ignore + return require('@php-wasm/node-8-2').getPHPLoaderModule(); + case '8.1': + // @ts-ignore + return require('@php-wasm/node-8-1').getPHPLoaderModule(); + case '8.0': + // @ts-ignore + return require('@php-wasm/node-8-0').getPHPLoaderModule(); + case '7.4': + // @ts-ignore + return require('@php-wasm/node-7-4').getPHPLoaderModule(); + case '7.3': + // @ts-ignore + return require('@php-wasm/node-7-3').getPHPLoaderModule(); + case '7.2': + // @ts-ignore + return require('@php-wasm/node-7-2').getPHPLoaderModule(); + default: + throw new Error(`Unsupported PHP version ${version}`); + } + } + + // Use dynamic import() for all other environments + // Works in: ESM, modern CommonJS (Node.js 12.20+), Vitest, and other modern test runners switch (version) { case '8.5': // @ts-ignore @@ -44,6 +86,7 @@ export async function getPHPLoaderModule( case '7.2': // @ts-ignore return (await import('@php-wasm/node-7-2')).getPHPLoaderModule(); + default: + throw new Error(`Unsupported PHP version ${version}`); } - throw new Error(`Unsupported PHP version ${version}`); } diff --git a/packages/playground/test-built-npm-packages/commonjs-and-jest/package.json b/packages/playground/test-built-npm-packages/commonjs-and-jest/package.json index ea98ee33fe..78c638897b 100644 --- a/packages/playground/test-built-npm-packages/commonjs-and-jest/package.json +++ b/packages/playground/test-built-npm-packages/commonjs-and-jest/package.json @@ -19,7 +19,7 @@ }, "devDependencies": { "@types/node": "^22.14.1", - "@wp-playground/cli": "^1.2.3", + "@wp-playground/cli": "^3.0.39", "@types/jest": "^29.5.14", "jest": "^29.7.0", "ts-jest": "^29.3.2", diff --git a/packages/playground/test-built-npm-packages/commonjs-and-jest/tests/php-wasm-node.spec.ts b/packages/playground/test-built-npm-packages/commonjs-and-jest/tests/php-wasm-node.spec.ts new file mode 100644 index 0000000000..ba8e7ccd9e --- /dev/null +++ b/packages/playground/test-built-npm-packages/commonjs-and-jest/tests/php-wasm-node.spec.ts @@ -0,0 +1,27 @@ +/** + * Test that @php-wasm/node works directly in Jest (CommonJS environment). + */ + +const originalNode = jest.requireActual('@php-wasm/node'); +const originalUniversal = jest.requireActual('@php-wasm/universal'); + +describe('@php-wasm/node direct usage in Jest', () => { + // Test with PHP 8.3 as a representative version + it('should load PHP runtime directly without spawning a child process', async () => { + // This call will fail with: + // - "TypeError: A dynamic import callback was invoked without --experimental-vm-modules" + // - or "SyntaxError: Cannot use 'import.meta' outside a module" + const runtimeId = await originalNode.loadNodeRuntime('8.3'); + + const php = new originalUniversal.PHP(runtimeId); + + // Basic PHP execution test + const result = await php.run({ + code: '