Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,7 @@ yarn-error.log
.turbo
_ci_*

.pnpm-store
.pnpm-store

# Test artifacts
packages/*/test/webpack5-angular-project
6 changes: 6 additions & 0 deletions packages/plugin-print-ready-pdfs-web/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 28 additions & 4 deletions packages/plugin-print-ready-pdfs-web/esbuild/config.mjs
Original file line number Diff line number Diff line change
@@ -1,12 +1,33 @@
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';

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.
Expand All @@ -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 = [
Expand Down
5 changes: 3 additions & 2 deletions packages/plugin-print-ready-pdfs-web/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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",
Expand Down
22 changes: 19 additions & 3 deletions packages/plugin-print-ready-pdfs-web/src/pdfx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>;
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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>;
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);

Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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"