diff --git a/.gitignore b/.gitignore index 9f342c46..b7965c83 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,7 @@ yarn-error.log .turbo _ci_* -.pnpm-store \ No newline at end of file +.pnpm-store + +# Test artifacts +packages/*/test/webpack5-angular-project \ No newline at end of file diff --git a/packages/plugin-print-ready-pdfs-web/CHANGELOG.md b/packages/plugin-print-ready-pdfs-web/CHANGELOG.md index a86ceb96..c8951c5d 100644 --- a/packages/plugin-print-ready-pdfs-web/CHANGELOG.md +++ b/packages/plugin-print-ready-pdfs-web/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.1.1] - 2025-12-18 + +### Fixed + +- Fixed Webpack 5 compatibility issue where Node.js module imports (`module`, `path`, `fs`, `url`) caused build failures in Angular 17+ and other Webpack 5 environments ([#11471](https://github.com/imgly/ubq/issues/11471)) + ## [1.1.0] - 2025-12-03 ### Added diff --git a/packages/plugin-print-ready-pdfs-web/esbuild/config.mjs b/packages/plugin-print-ready-pdfs-web/esbuild/config.mjs index dec17a4b..3ab9b1c3 100644 --- a/packages/plugin-print-ready-pdfs-web/esbuild/config.mjs +++ b/packages/plugin-print-ready-pdfs-web/esbuild/config.mjs @@ -1,5 +1,5 @@ import chalk from 'chalk'; -import { readFile, copyFile, mkdir } from 'fs/promises'; +import { readFile, copyFile, mkdir, writeFile } from 'fs/promises'; import { existsSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; @@ -7,6 +7,27 @@ import { fileURLToPath } from 'url'; import baseConfig from '../../../esbuild/config.mjs'; import log from '../../../esbuild/log.mjs'; +/** + * Add webpackIgnore comments to Node.js module imports in gs.js + * This prevents Webpack 5 from trying to resolve these modules in browser builds. + * See: https://github.com/imgly/ubq/issues/11471 + */ +function addWebpackIgnoreComments(content) { + // Transform: await import("module") -> await import(/* webpackIgnore: true */ "module") + // Transform: await import("path") -> await import(/* webpackIgnore: true */ "path") + // Also handle other Node.js modules that might be imported + const nodeModules = ['module', 'path', 'fs', 'url', 'os']; + let transformed = content; + + for (const mod of nodeModules) { + // Match: import("module") or import('module') with optional whitespace + const pattern = new RegExp(`import\\(\\s*["']${mod}["']\\s*\\)`, 'g'); + transformed = transformed.replace(pattern, `import(/* webpackIgnore: true */ "${mod}")`); + } + + return transformed; +} + const __dirname = dirname(fileURLToPath(import.meta.url)); // Avoid the Experimental Feature warning when using the above. @@ -31,11 +52,14 @@ const copyWasmPlugin = { await copyFile(srcWasm, distWasm); log(chalk.green('✓ Copied gs.wasm to dist/')); - // Copy gs.js file + // Copy and transform gs.js file to add webpackIgnore comments + // This fixes Webpack 5 compatibility (see https://github.com/imgly/ubq/issues/11471) const srcJs = join(__dirname, '../src/wasm/gs.js'); const distJs = join(distDir, 'gs.js'); - await copyFile(srcJs, distJs); - log(chalk.green('✓ Copied gs.js to dist/')); + const gsContent = await readFile(srcJs, 'utf-8'); + const transformedContent = addWebpackIgnoreComments(gsContent); + await writeFile(distJs, transformedContent); + log(chalk.green('✓ Copied and transformed gs.js to dist/ (added webpackIgnore comments)')); // Copy ICC profile files const iccProfiles = [ diff --git a/packages/plugin-print-ready-pdfs-web/package.json b/packages/plugin-print-ready-pdfs-web/package.json index 8dc1f0b1..3c50ff0c 100644 --- a/packages/plugin-print-ready-pdfs-web/package.json +++ b/packages/plugin-print-ready-pdfs-web/package.json @@ -1,6 +1,6 @@ { "name": "@imgly/plugin-print-ready-pdfs-web", - "version": "1.1.0", + "version": "1.1.1", "description": "Print-Ready PDFs plugin for CE.SDK editor - PDF/X conversion and export functionality. Contains AGPL-3.0 licensed Ghostscript WASM.", "keywords": [ "CE.SDK", @@ -67,7 +67,8 @@ "test:content": "pnpm run build && vitest run content-preservation", "test:silent": "pnpm run build && vitest run silent-conversion", "test:all": "pnpm run test:browser && pnpm run test:integration", - "test:visual": "pnpm run build && node test/visual-test.mjs" + "test:visual": "pnpm run build && node test/visual-test.mjs", + "test:webpack5": "bash test/webpack5-compatibility-test.sh" }, "devDependencies": { "@cesdk/cesdk-js": "~1.61.0", diff --git a/packages/plugin-print-ready-pdfs-web/src/pdfx.ts b/packages/plugin-print-ready-pdfs-web/src/pdfx.ts index 9eae3e40..f71a798b 100644 --- a/packages/plugin-print-ready-pdfs-web/src/pdfx.ts +++ b/packages/plugin-print-ready-pdfs-web/src/pdfx.ts @@ -145,9 +145,25 @@ async function convertToPDFX3Single( if (isNode) { // Node.js: Load from filesystem using readFileSync - const { readFileSync } = await import('fs'); - const { fileURLToPath } = await import('url'); - const { dirname, join } = await import('path'); + // Use indirect dynamic import to prevent Webpack 5 from statically analyzing these imports + // But use direct imports in test environments (vitest) where indirect imports bypass mocking + // See: https://github.com/imgly/ubq/issues/11471 + const isTestEnv = + process.env.VITEST === 'true' || process.env.NODE_ENV === 'test'; + + // Note: new Function() could fail in CSP-restricted environments, but CSP is a browser + // security mechanism and doesn't apply to Node.js. This code only runs in Node.js (isNode check above). + // eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func + const indirectImport = new Function('s', 'return import(s)') as ( + s: string + ) => Promise; + const dynamicImport = isTestEnv + ? (s: string) => import(s) + : indirectImport; + + const { readFileSync } = await dynamicImport('fs'); + const { fileURLToPath } = await dynamicImport('url'); + const { dirname, join } = await dynamicImport('path'); // Get the directory of the built module const moduleDir = dirname(fileURLToPath(import.meta.url)); diff --git a/packages/plugin-print-ready-pdfs-web/src/wasm/ghostscript-module.ts b/packages/plugin-print-ready-pdfs-web/src/wasm/ghostscript-module.ts index ef488dcc..f5a61693 100644 --- a/packages/plugin-print-ready-pdfs-web/src/wasm/ghostscript-module.ts +++ b/packages/plugin-print-ready-pdfs-web/src/wasm/ghostscript-module.ts @@ -27,8 +27,27 @@ export default async function createGhostscriptModule( if (isNode) { // Node.js: Use require.resolve to find gs.js relative to this module - const { createRequire } = await import('module'); - const { dirname, join } = await import('path'); + // Use indirect dynamic import to prevent Webpack 5 from statically analyzing these imports + // But use direct imports in test environments (vitest) where indirect imports bypass mocking + // See: https://github.com/imgly/ubq/issues/11471 + const isTestEnv = + typeof process !== 'undefined' && + (process.env.VITEST === 'true' || process.env.NODE_ENV === 'test'); + + // Helper for dynamic imports - uses indirect import in production to avoid Webpack static analysis + // Note: new Function() could fail in CSP-restricted environments, but CSP is a browser + // security mechanism and doesn't apply to Node.js. This code only runs in Node.js (isNode check above). + // eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func + const indirectImport = new Function('s', 'return import(s)') as ( + s: string + ) => Promise; + const dynamicImport = isTestEnv ? (s: string) => import(s) : indirectImport; + + const moduleLib = await dynamicImport('module'); + const pathLib = await dynamicImport('path'); + const createRequire = moduleLib.createRequire; + const dirname = pathLib.dirname; + const join = pathLib.join; const requireForESM = createRequire(import.meta.url); @@ -37,7 +56,7 @@ export default async function createGhostscriptModule( const moduleDir = dirname(gsPath); wasmPath = join(moduleDir, 'gs.wasm'); - GhostscriptModule = await import(gsPath); + GhostscriptModule = await dynamicImport(gsPath); } else { // Browser: Use URL-based imports const moduleUrl = new URL('./gs.js', import.meta.url).href; diff --git a/packages/plugin-print-ready-pdfs-web/test/webpack5-compatibility-test.sh b/packages/plugin-print-ready-pdfs-web/test/webpack5-compatibility-test.sh new file mode 100755 index 00000000..f64c877f --- /dev/null +++ b/packages/plugin-print-ready-pdfs-web/test/webpack5-compatibility-test.sh @@ -0,0 +1,78 @@ +#!/bin/bash +# +# Test: Verify @imgly/plugin-print-ready-pdfs-web works with Webpack 5 +# Issue: https://github.com/imgly/ubq/issues/11471 +# +# Exit 0 = PASS (plugin is Webpack 5 compatible) +# Exit 1 = FAIL (plugin has Webpack 5 compatibility issues) +# + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PACKAGE_DIR="$(dirname "$SCRIPT_DIR")" +TEST_PROJECT_DIR="${SCRIPT_DIR}/webpack5-angular-project" + +echo "Testing Webpack 5 compatibility..." + +# Build plugin if needed +if [ ! -f "${PACKAGE_DIR}/dist/index.mjs" ]; then + echo "Building plugin..." + cd "$PACKAGE_DIR" + pnpm run build +fi + +# Create test project if it doesn't exist +if [ ! -d "$TEST_PROJECT_DIR" ]; then + mkdir -p "$TEST_PROJECT_DIR" + cd "$TEST_PROJECT_DIR" + + cat > package.json << 'EOF' +{ + "name": "webpack5-test", + "scripts": { "build": "webpack --mode production" }, + "devDependencies": { + "webpack": "^5.90.0", + "webpack-cli": "^5.1.4", + "typescript": "^5.3.0", + "ts-loader": "^9.5.0" + } +} +EOF + + cat > tsconfig.json << 'EOF' +{ "compilerOptions": { "target": "ES2020", "module": "ESNext", "moduleResolution": "bundler", "strict": false, "skipLibCheck": true, "noImplicitAny": false } } +EOF + + cat > webpack.config.js << 'EOF' +const path = require('path'); +module.exports = { + entry: './src/index.ts', + output: { path: path.resolve(__dirname, 'dist'), filename: 'bundle.js' }, + resolve: { extensions: ['.ts', '.js', '.mjs'] }, + module: { rules: [{ test: /\.ts$/, use: 'ts-loader', exclude: /node_modules/ }] } +}; +EOF + + mkdir -p src + cat > src/index.ts << 'EOF' +// @ts-ignore +import { convertToPDFX3 } from '@imgly/plugin-print-ready-pdfs-web'; +console.log('Plugin loaded:', typeof convertToPDFX3); +EOF + + npm install --silent +fi + +cd "$TEST_PROJECT_DIR" + +# Link the local plugin +mkdir -p "node_modules/@imgly/plugin-print-ready-pdfs-web" +cp -r "${PACKAGE_DIR}/dist/"* "node_modules/@imgly/plugin-print-ready-pdfs-web/" +cp "${PACKAGE_DIR}/package.json" "node_modules/@imgly/plugin-print-ready-pdfs-web/" + +# Run webpack build - should succeed if plugin is Webpack 5 compatible +echo "Running Webpack 5 build..." +npm run build --silent + +echo "PASS: Plugin is Webpack 5 compatible"