From 05db4edbdafdb15c660f1181f1515cba0ddb65f3 Mon Sep 17 00:00:00 2001 From: Valerii Sidorenko Date: Thu, 12 Dec 2024 00:48:32 +0100 Subject: [PATCH] fix: externalize node_modules --- package-lock.json | 9 --- package.json | 2 +- src/common/webpack/config.ts | 6 +- src/common/webpack/node-externals.ts | 108 +++++++++++++++++++++++++++ 4 files changed, 112 insertions(+), 13 deletions(-) create mode 100644 src/common/webpack/node-externals.ts diff --git a/package-lock.json b/package-lock.json index d11a84d..cccf88d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -81,7 +81,6 @@ "webpack-bundle-analyzer": "^4.10.2", "webpack-dev-server": "^5.1.0", "webpack-manifest-plugin": "^5.0.0", - "webpack-node-externals": "^3.0.0", "worker-loader": "^3.0.8", "yargs": "^17.7.2" }, @@ -19324,14 +19323,6 @@ "node": ">=10.13.0" } }, - "node_modules/webpack-node-externals": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz", - "integrity": "sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==", - "engines": { - "node": ">=6" - } - }, "node_modules/webpack-sources": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", diff --git a/package.json b/package.json index a027df5..21ee28c 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "prettier": "prettier '**/*.{md,yaml,yml,json}'", "typecheck": "tsc --noEmit", "test": "jest", + "watch": "tsc -p tsconfig.production.json --watch", "build": "npm run build:clean && npm run build:compile", "build:compile": "tsc -p tsconfig.production.json", "build:clean": "rimraf dist", @@ -129,7 +130,6 @@ "webpack-bundle-analyzer": "^4.10.2", "webpack-dev-server": "^5.1.0", "webpack-manifest-plugin": "^5.0.0", - "webpack-node-externals": "^3.0.0", "worker-loader": "^3.0.8", "yargs": "^17.7.2" }, diff --git a/src/common/webpack/config.ts b/src/common/webpack/config.ts index e6a4e4d..a4108cd 100644 --- a/src/common/webpack/config.ts +++ b/src/common/webpack/config.ts @@ -14,7 +14,6 @@ import MomentTimezoneDataPlugin from 'moment-timezone-data-webpack-plugin'; import StatoscopeWebpackPlugin from '@statoscope/webpack-plugin'; import CircularDependencyPlugin from 'circular-dependency-plugin'; import type {sentryWebpackPlugin} from '@sentry/webpack-plugin'; -import nodeExternals from 'webpack-node-externals'; import type TerserWebpackPlugin from 'terser-webpack-plugin'; import type * as Lightningcss from 'lightningcss'; @@ -30,6 +29,7 @@ import {resolveTsConfigPathsToAlias} from './utils'; import {S3UploadPlugin} from '../s3-upload'; import {logConfig} from '../logger/log-config'; import {resolveTypescript} from '../typescript/utils'; +import {nodeExternals} from './node-externals'; const imagesSizeLimit = 2048; const fontSizeLimit = 8192; @@ -75,8 +75,8 @@ export async function webpackConfigFactory( config.ssr?.noExternal === true ? undefined : nodeExternals({ - allowlist: config.ssr?.noExternal, - importType: config.ssr?.moduleType === 'esm' ? 'module' : 'commonjs', + noExternal: config.ssr?.noExternal, + module: config.ssr?.moduleType === 'esm', }); } diff --git a/src/common/webpack/node-externals.ts b/src/common/webpack/node-externals.ts new file mode 100644 index 0000000..7c40d2b --- /dev/null +++ b/src/common/webpack/node-externals.ts @@ -0,0 +1,108 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import type * as webpack from 'webpack'; +import paths from '../paths'; + +type Pattern = RegExp | ((v: string) => boolean) | string; + +export interface NodeExternalsOptions { + noExternal?: Pattern | Pattern[]; + module?: boolean; +} + +const webpackInternal = /^webpack\/container\/reference\//; + +export function nodeExternals({noExternal = [], module}: NodeExternalsOptions) { + const noExternals = Array().concat(webpackInternal).concat(noExternal); + + const nodeModules = readPackagesNames(paths.appNodeModules); + + return async (data: webpack.ExternalItemFunctionData) => { + const {request} = data; + if (!request) { + return undefined; + } + + const moduleName = getModuleName(request); + if ( + !request || + !containsPattern(nodeModules, moduleName) || + containsPattern(noExternals, request) + ) { + return undefined; + } + + if (!module) { + return `commonjs ${data.request}`; + } + + if ( + data.dependencyType === 'commonjs' || + // lodash/something without extension can't be imported so always require it + (moduleName === 'lodash' && request.match(/^lodash\/[\w_]+($|\/[\w_]+$)/)) + ) { + return `node-commonjs ${data.request}`; + } + + return `module-import ${data.request}`; + }; +} + +function readPackagesNames(dirName: string) { + if (!fs.existsSync(dirName)) { + return []; + } + + try { + return fs + .readdirSync(dirName) + .map((module) => { + if ( + module.startsWith('.') || + !fs.statSync(path.join(dirName, module)).isDirectory() + ) { + return undefined; + } + if (module.startsWith('@')) { + try { + return fs.readdirSync(path.join(dirName, module)).map(function (scopedMod) { + return module + '/' + scopedMod; + }); + } catch (e) { + return [module]; + } + } + return module; + }) + .flat() + .filter((v) => v !== undefined); + } catch (e) { + return []; + } +} + +function containsPattern(patterns: Pattern[], value: string) { + return patterns.some((pattern) => { + if (pattern instanceof RegExp) { + return pattern.test(value); + } else if (typeof pattern === 'function') { + return pattern(value); + } else { + return pattern === value; + } + }); +} + +function getModuleName(request: string) { + const req = request; + const delimiter = '/'; + + // check if scoped module + if (req.startsWith('@')) { + const parts = req.split(delimiter, 2); + if (parts.length === 2) { + return parts.join(delimiter); + } + } + return req.split(delimiter, 1)[0] || ''; +}