├── additional-configs ├── jest.d.ts ├── tsconfig.build.json └── jestSetupAfterEnv.ts ├── src ├── peers │ ├── webpack │ │ ├── lib │ │ │ ├── ModuleProfile.d.ts │ │ │ └── ModuleProfile.js │ │ ├── index.d.ts │ │ └── index.js │ ├── terser-webpack-plugin-from-webpack │ │ ├── index.d.ts │ │ └── index.js │ └── terser-webpack-plugin │ │ ├── index.js │ │ └── index.d.ts ├── index.ts ├── resolvePeer.ts ├── readJson.ts ├── types.ts ├── TranspileExternalModule.ts ├── conditionTest.ts ├── constants.ts ├── walkDependencies.ts ├── optionsSchema.json ├── commonDir.ts ├── TerserWebpackPluginController.ts ├── defaultsSetters.js ├── compilerOptions.ts ├── SourceMapDevToolPluginController.ts └── TranspileWebpackPlugin.ts ├── .env ├── .gitattributes ├── commitlint.config.js ├── .editorconfig ├── .gitignore ├── support-helpers ├── index.ts ├── textOp.ts ├── constants.ts ├── logging.ts ├── fileOp.ts ├── webpackProject.ts └── scriptRunners.ts ├── modules.d.ts ├── .github └── workflows │ ├── lint-code.yml │ ├── lint-pr-title.yml │ ├── verify-and-release.yml │ └── e2e.yml ├── .prettierrc ├── scripts ├── cross-dotenv-shell.js └── e2e-with-debuglog.js ├── jest.config.js ├── .eslintrc ├── tsconfig.json ├── .releaserc ├── LICENSE ├── e2e ├── builds-lib-then-bundles-it.spec.ts ├── works-with-webpack-configs-in-popular-tools.spec.ts ├── validates-inputs.spec.ts ├── handles-node_modules.spec.ts ├── handles-non-js.spec.ts └── handles-js-outside-node_modules.spec.ts ├── CONTRIBUTING.md ├── package.json ├── CODE_OF_CONDUCT.md └── README.md /additional-configs/jest.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/peers/webpack/lib/ModuleProfile.d.ts: -------------------------------------------------------------------------------- 1 | export = T as new () => never; 2 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | FILE_GLOB='@(src|e2e|additional-configs|support-helpers|scripts)/**/*.@([jt]s|json)' 2 | -------------------------------------------------------------------------------- /src/peers/webpack/index.d.ts: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | 3 | export = webpack; 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | }; 4 | -------------------------------------------------------------------------------- /src/peers/terser-webpack-plugin-from-webpack/index.d.ts: -------------------------------------------------------------------------------- 1 | import T from '../terser-webpack-plugin'; 2 | 3 | export = T; 4 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { TranspileWebpackPlugin } from './TranspileWebpackPlugin'; 2 | 3 | export = TranspileWebpackPlugin; 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | -------------------------------------------------------------------------------- /src/peers/webpack/index.js: -------------------------------------------------------------------------------- 1 | import { resolvePeer } from '../../resolvePeer'; 2 | 3 | module.exports = require(resolvePeer('webpack')); 4 | -------------------------------------------------------------------------------- /src/peers/terser-webpack-plugin/index.js: -------------------------------------------------------------------------------- 1 | import { resolvePeer } from '../../resolvePeer'; 2 | 3 | module.exports = require(resolvePeer('terser-webpack-plugin')); 4 | -------------------------------------------------------------------------------- /src/peers/webpack/lib/ModuleProfile.js: -------------------------------------------------------------------------------- 1 | import { resolvePeer } from '../../../resolvePeer'; 2 | 3 | module.exports = require(resolvePeer('webpack/lib/ModuleProfile')); 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | node_modules 3 | 4 | lib 5 | !src/**/lib 6 | 7 | coverage 8 | e2e/**/__projects__ 9 | 10 | *.log 11 | 12 | .vscode 13 | 14 | .DS_Store 15 | -------------------------------------------------------------------------------- /support-helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './constants'; 2 | export * from './fileOp'; 3 | export * from './logging'; 4 | export * from './scriptRunners'; 5 | export * from './textOp'; 6 | export * from './webpackProject'; 7 | -------------------------------------------------------------------------------- /src/resolvePeer.ts: -------------------------------------------------------------------------------- 1 | import { memoize } from 'lodash'; 2 | import resolve from 'resolve'; 3 | 4 | export const resolvePeer = memoize((id: string): string => { 5 | return resolve.sync(id, { basedir: process.cwd() }); 6 | }); 7 | -------------------------------------------------------------------------------- /src/readJson.ts: -------------------------------------------------------------------------------- 1 | import fs, { PathLike } from 'node:fs'; 2 | 3 | export function readJsonSync< 4 | T extends Record 5 | >(p: PathLike): T { 6 | return JSON.parse(fs.readFileSync(p, 'utf8')) as T; 7 | } 8 | -------------------------------------------------------------------------------- /src/peers/terser-webpack-plugin-from-webpack/index.js: -------------------------------------------------------------------------------- 1 | import resolve from 'resolve'; 2 | 3 | import { resolvePeer } from '../../resolvePeer'; 4 | 5 | module.exports = require(resolve.sync('terser-webpack-plugin', { 6 | basedir: resolvePeer('webpack'), 7 | })); 8 | -------------------------------------------------------------------------------- /additional-configs/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "../src", 5 | "noEmit": false 6 | }, 7 | "include": ["../modules.d.ts", "../src"], 8 | "exclude": ["../**/__tests__", "../**/*.spec.*", "../**/*.test.*"] 9 | } 10 | -------------------------------------------------------------------------------- /modules.d.ts: -------------------------------------------------------------------------------- 1 | import { Module, ModuleGraphConnection } from 'webpack'; 2 | 3 | declare module 'webpack' { 4 | class ModuleGraphModule { 5 | incomingConnections: Set; 6 | outgoingConnections: Set; 7 | } 8 | 9 | class ModuleGraph { 10 | _getModuleGraphModule(module: Module): ModuleGraphModule; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/lint-code.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | branches: 4 | - master 5 | workflow_call: 6 | 7 | jobs: 8 | main: 9 | runs-on: ubuntu-20.04 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: actions/setup-node@v3 13 | with: 14 | node-version: 18 15 | - run: npm i 16 | - run: npm run lint-all 17 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { SourceMapDevToolPlugin, WebpackOptionsNormalized } from 'webpack'; 2 | 3 | export type MaybeArray = T | T[]; 4 | 5 | export type MaybePromise = T | Promise; 6 | 7 | export type SourceMapDevToolPluginOptions = NonNullable< 8 | ConstructorParameters[0] 9 | >; 10 | 11 | export type CompilerOptions = WebpackOptionsNormalized; 12 | -------------------------------------------------------------------------------- /src/TranspileExternalModule.ts: -------------------------------------------------------------------------------- 1 | import { ExternalModule } from './peers/webpack'; 2 | 3 | export class TranspileExternalModule extends ExternalModule { 4 | originPath: string; 5 | 6 | constructor(request: string, type: string, originPath: string) { 7 | super(request, type, request); 8 | this.originPath = originPath; 9 | } 10 | 11 | identifier() { 12 | return `${super.identifier()} (${this.originPath})`; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/peers/terser-webpack-plugin/index.d.ts: -------------------------------------------------------------------------------- 1 | import * as terser from 'terser'; 2 | import _TerserWebpackPlugin from 'terser-webpack-plugin'; 3 | 4 | declare class TerserWebpackPlugin extends _TerserWebpackPlugin { 5 | options: _TerserWebpackPlugin.InternalPluginOptions; 6 | } 7 | 8 | declare namespace TerserWebpackPlugin { 9 | export = _TerserWebpackPlugin; 10 | } 11 | 12 | export = TerserWebpackPlugin; 13 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "auto", 3 | "printWidth": 100, 4 | "singleQuote": true, 5 | "tabWidth": 2, 6 | "useTabs": false, 7 | "importOrder": ["^node:", "", "^\\."], 8 | "importOrderCaseInsensitive": true, 9 | "importOrderSeparation": true, 10 | "importOrderSortSpecifiers": true, 11 | "overrides": [ 12 | { 13 | "files": ["src/**/*.json"], 14 | "options": { 15 | "parser": "json-stringify" 16 | } 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/lint-pr-title.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | types: 4 | - opened 5 | - synchronize 6 | - reopened 7 | - edited 8 | branches: 9 | - master 10 | 11 | jobs: 12 | main: 13 | runs-on: ubuntu-20.04 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: actions/setup-node@v3 17 | with: 18 | node-version: 18 19 | - run: npm i 20 | - run: echo '${{ github.event.pull_request.title }}' | npx -y @commitlint/cli@17 21 | -------------------------------------------------------------------------------- /scripts/cross-dotenv-shell.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const crossSpawn = require('cross-spawn'); 4 | const dotenv = require('dotenv'); 5 | 6 | const [, , argv0, ...args] = process.argv; 7 | 8 | dotenv.config(); 9 | 10 | const evaluatedArgs = args.map((arg) => { 11 | if (!/^[A-Z0-9_]+$/.test(arg)) { 12 | return arg; 13 | } 14 | const envVal = process.env[arg]; 15 | if (typeof envVal !== 'string') { 16 | return arg; 17 | } 18 | return envVal; 19 | }); 20 | 21 | crossSpawn.sync(argv0, evaluatedArgs, { stdio: 'inherit' }); 22 | -------------------------------------------------------------------------------- /scripts/e2e-with-debuglog.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { program } = require('commander'); 4 | const { name: packageName } = require('../package.json'); 5 | const crossSpawn = require('cross-spawn'); 6 | 7 | program.option('-s,--section '); 8 | program.parse(); 9 | 10 | process.env.NODE_DEBUG = `${packageName}:${program.section ?? '*'}`; 11 | 12 | const moreArgs = program.args.length ? ['--', ...program.args] : []; 13 | 14 | const { status } = crossSpawn.sync('npm', ['run', 'e2e', ...moreArgs], { stdio: 'inherit' }); 15 | 16 | process.exitCode = status; 17 | -------------------------------------------------------------------------------- /additional-configs/jestSetupAfterEnv.ts: -------------------------------------------------------------------------------- 1 | import waitForExpect from 'wait-for-expect'; 2 | 3 | import { 4 | cleanAllWebpackProjects, 5 | createE2eDebuglogByFilePath, 6 | killAllExecAsyncProcesses, 7 | } from '../support-helpers'; 8 | 9 | const debuglog = createE2eDebuglogByFilePath(__filename); 10 | 11 | jest.setTimeout(60000); 12 | 13 | waitForExpect.defaults.interval = 200; 14 | waitForExpect.defaults.timeout = 10000; 15 | 16 | cleanAllWebpackProjects(); 17 | 18 | afterAll(async () => { 19 | debuglog('Killing all the unkilled execAsync processes...'); 20 | await killAllExecAsyncProcesses(); 21 | }); 22 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // Visit https://jestjs.io/docs/configuration to read more about this file. 2 | 3 | const testPath = process.env.JEST_TEST_PATH; 4 | 5 | /** @type {import('jest').Config} */ 6 | const jestConfig = { 7 | testRegex: ['__test__/.*\\.[jt]s$', '(.*\\.)?(test|spec)\\.[jt]s$'].map((s) => { 8 | if (testPath) { 9 | s = `${testPath}/(.*/)*${s}`; 10 | } 11 | return s; 12 | }), 13 | preset: 'ts-jest/presets/js-with-ts', 14 | coverageProvider: 'v8', 15 | setupFilesAfterEnv: ['jest-extended/all', '/additional-configs/jestSetupAfterEnv.ts'], 16 | }; 17 | 18 | module.exports = jestConfig; 19 | -------------------------------------------------------------------------------- /src/conditionTest.ts: -------------------------------------------------------------------------------- 1 | import { flatten } from 'lodash'; 2 | 3 | import { MaybeArray } from './types'; 4 | 5 | export type Condition = MaybeArray; 6 | 7 | export type ConditionTest = (p: string) => boolean; 8 | 9 | export function createConditionTest(condition: Condition): ConditionTest { 10 | const conditionAsArray = flatten([condition]); 11 | 12 | return (p) => 13 | conditionAsArray.reduce( 14 | (b, c) => 15 | b || 16 | (typeof c === 'string' && p.startsWith(c)) || 17 | (c instanceof RegExp && c.test(p)) || 18 | (typeof c === 'function' && c(p)), 19 | false 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": [ 4 | "eslint:recommended", 5 | "plugin:@typescript-eslint/recommended", 6 | "plugin:jest/recommended", 7 | "prettier" 8 | ], 9 | "env": { 10 | "node": true, 11 | "jest": true 12 | }, 13 | "settings": { 14 | "jest": { 15 | "version": 28 16 | } 17 | }, 18 | "rules": { 19 | "no-empty": "off", 20 | "@typescript-eslint/no-inferrable-types": "off", 21 | "@typescript-eslint/no-non-null-assertion": "off" 22 | }, 23 | "overrides": [ 24 | { 25 | "files": ["scripts/**"], 26 | "rules": { 27 | "@typescript-eslint/no-var-requires": "off" 28 | } 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | // Visit https://aka.ms/tsconfig to read more about this file. 2 | { 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "allowSyntheticDefaultImports": true, 6 | "declaration": true, 7 | "declarationMap": true, 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "isolatedModules": true, 11 | "module": "commonjs", 12 | "noEmit": true, 13 | "outDir": "lib", 14 | "resolveJsonModule": true, 15 | "rootDir": ".", 16 | "skipLibCheck": true, 17 | "sourceMap": true, 18 | "strict": true, 19 | "target": "es2015" 20 | }, 21 | "include": ["."], 22 | "exclude": ["node_modules", "lib", "coverage", "e2e/**/__projects__"] 23 | } 24 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "branches": [ 3 | { 4 | "name": "master", 5 | "channel": "alpha", 6 | "prerelease": "alpha" 7 | }, 8 | { 9 | "name": "beta", 10 | "channel": "beta", 11 | "prerelease": "beta" 12 | }, 13 | { 14 | "name": "latest", 15 | "channel": "latest" 16 | } 17 | ], 18 | "plugins": [ 19 | [ 20 | "@semantic-release/commit-analyzer", 21 | { 22 | "preset": "conventionalcommits" 23 | } 24 | ], 25 | [ 26 | "@semantic-release/release-notes-generator", 27 | { 28 | "preset": "conventionalcommits" 29 | } 30 | ], 31 | "@semantic-release/npm", 32 | "@semantic-release/github" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /support-helpers/textOp.ts: -------------------------------------------------------------------------------- 1 | export function boolToText( 2 | booleanValue?: boolean, 3 | trueString: string = '', 4 | falseString: string = '', 5 | fallbackString: string = '' 6 | ): string { 7 | return booleanValue === true ? trueString : booleanValue === false ? falseString : fallbackString; 8 | } 9 | 10 | export function mapStrToText( 11 | value: TValue, 12 | stringMapper: (stringValue: string) => string, 13 | fallbackMapper: (nonStringValue: Exclude) => string 14 | ) { 15 | if (typeof value === 'string') { 16 | return stringMapper ? stringMapper(value) : value; 17 | } else { 18 | return fallbackMapper ? fallbackMapper(value as Exclude) : String(value); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /support-helpers/constants.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | 3 | export const rootPath = path.resolve(__dirname, '..'); 4 | 5 | export const encodingText = 'utf8'; 6 | 7 | export const depVerWebpack = process.env.E2E_DEP_VER_WEBPACK ?? '^5'; 8 | export const depVerWebpackCli = process.env.E2E_DEP_VER_WEBPACK_CLI ?? '^4'; 9 | 10 | export const webpackConfigDefaultFileName = 'webpack.config.js'; 11 | export const webpackProjectParentDirName = '__projects__'; 12 | export const webpackProjectMustHavePackageJson = { 13 | ['devDependencies']: { 14 | ['webpack']: depVerWebpack, 15 | ['webpack-cli']: depVerWebpackCli, 16 | }, 17 | }; 18 | export const webpackProjectMustHaveFiles = { 19 | 'package.json': JSON.stringify(webpackProjectMustHavePackageJson), 20 | }; 21 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import { startCase } from 'lodash'; 2 | 3 | import { readJsonSync } from './readJson'; 4 | 5 | export const { name: packageName } = readJsonSync<{ name: string }>( 6 | require.resolve('../package.json') 7 | ); 8 | 9 | export const pluginName = startCase(packageName); 10 | 11 | export const reNodeModules = /[\\/]node_modules[\\/]/; 12 | export const reMjsFile = /\.mjs$/; 13 | 14 | export const baseNodeModules = 'node_modules'; 15 | 16 | export const resolveByDependencyTypeCjs = 'commonjs'; 17 | export const outputLibraryTypeCjs = 'commonjs-module'; 18 | export const externalModuleTypeCjs = 'commonjs-module'; 19 | 20 | export const extJson = '.json'; 21 | 22 | export const sourceTypeAsset = 'asset'; 23 | 24 | export const hookStageVeryEarly = -1000000; 25 | -------------------------------------------------------------------------------- /.github/workflows/verify-and-release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | - beta 6 | - latest 7 | 8 | jobs: 9 | lint-code: 10 | uses: ./.github/workflows/lint-code.yml 11 | e2e: 12 | uses: ./.github/workflows/e2e.yml 13 | release: 14 | needs: 15 | - lint-code 16 | - e2e 17 | runs-on: ubuntu-20.04 18 | steps: 19 | - uses: actions/checkout@v3 20 | with: 21 | fetch-depth: 0 22 | - uses: actions/setup-node@v3 23 | with: 24 | node-version: 18 25 | - run: npm i 26 | - run: npm run build 27 | - env: 28 | NPM_TOKEN: ${{secrets.SL_NPM_TOKEN}} 29 | GITHUB_TOKEN: ${{secrets.SL_GITHUB_TOKEN}} 30 | run: npx -y semantic-release@19 31 | -------------------------------------------------------------------------------- /.github/workflows/e2e.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | branches: 4 | - master 5 | workflow_call: 6 | 7 | jobs: 8 | main: 9 | timeout-minutes: 30 10 | strategy: 11 | matrix: 12 | os: [ubuntu-20.04] 13 | node-version: [18] 14 | dep-ver-webpack: ['^5', '5.61.0'] 15 | include: 16 | - os: ubuntu-20.04 17 | node-version: 16 18 | dep-ver-webpack: '^5' 19 | - os: windows-2019 20 | node-version: 18 21 | dep-ver-webpack: '^5' 22 | runs-on: ${{ matrix.os }} 23 | env: 24 | E2E_DEP_VER_WEBPACK: ${{ matrix.dep-ver-webpack }} 25 | steps: 26 | - uses: actions/checkout@v3 27 | - uses: actions/setup-node@v3 28 | with: 29 | node-version: ${{ matrix.node-version }} 30 | - run: npm i 31 | - run: npm run build 32 | - run: node scripts/e2e-with-debuglog.js 33 | -------------------------------------------------------------------------------- /src/walkDependencies.ts: -------------------------------------------------------------------------------- 1 | import type { Dependency, Module } from 'webpack'; 2 | 3 | export function walkDependenciesSync( 4 | m: Module, 5 | fn: (dep: Dependency, depIndex: number, dependencies: Dependency[]) => void 6 | ): void { 7 | for (let i = 0, n = m.dependencies.length; i < n; i++) { 8 | fn(m.dependencies[i], i, m.dependencies); 9 | } 10 | for (const b of m.blocks) { 11 | for (let i = 0, n = b.dependencies.length; i < n; i++) { 12 | fn(b.dependencies[i], i, b.dependencies); 13 | } 14 | } 15 | } 16 | 17 | export async function walkDependencies( 18 | m: Module, 19 | fn: (dep: Dependency, depIndex: number, dependencies: Dependency[]) => Promise 20 | ): Promise { 21 | for (let i = 0, n = m.dependencies.length; i < n; i++) { 22 | await fn(m.dependencies[i], i, m.dependencies); 23 | } 24 | for (const b of m.blocks) { 25 | for (let i = 0, n = b.dependencies.length; i < n; i++) { 26 | await fn(b.dependencies[i], i, b.dependencies); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/optionsSchema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "exclude": { 5 | "$ref": "#/$defs/Condition" 6 | }, 7 | "hoistNodeModules": { 8 | "type": "boolean" 9 | }, 10 | "longestCommonDir": { 11 | "type": "string" 12 | }, 13 | "extentionMapping": { 14 | "type": "object" 15 | }, 16 | "preferResolveByDependencyAsCjs": { 17 | "type": "boolean" 18 | } 19 | }, 20 | "additionalProperties": false, 21 | "$defs": { 22 | "Condition": { 23 | "oneOf": [ 24 | { 25 | "$ref": "#/$defs/SingularCondition" 26 | }, 27 | { 28 | "type": "array", 29 | "items": { 30 | "$ref": "#/$defs/SingularCondition" 31 | } 32 | } 33 | ] 34 | }, 35 | "SingularCondition": { 36 | "oneOf": [ 37 | { 38 | "type": "string" 39 | }, 40 | { 41 | "instanceof": "RegExp" 42 | }, 43 | { 44 | "instanceof": "Function" 45 | } 46 | ] 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 React Easier 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /support-helpers/logging.ts: -------------------------------------------------------------------------------- 1 | import os from 'node:os'; 2 | import path from 'node:path'; 3 | import tty from 'node:tty'; 4 | import { debuglog, DebugLoggerFunction, inspect, InspectOptions } from 'node:util'; 5 | 6 | import { dim, magentaBright } from 'colorette'; 7 | 8 | import { packageName } from '../src/constants'; 9 | import { rootPath } from './constants'; 10 | 11 | export function createLog(ws: tty.WriteStream): (...args: unknown[]) => void { 12 | const inspectOpts: InspectOptions = { colors: ws.hasColors?.() }; 13 | return (...args) => { 14 | const prefix = evaluateLogPrefix(); 15 | if (prefix) { 16 | args.unshift(prefix); 17 | } 18 | ws.write( 19 | args.map((o) => (typeof o === 'string' ? o : inspect(o, inspectOpts))).join(' ') + os.EOL 20 | ); 21 | }; 22 | } 23 | 24 | export const logStdout = createLog(process.stdout); 25 | export const logStderr = createLog(process.stderr); 26 | 27 | export function createE2eDebuglogByFilePath(filePath: string): DebugLoggerFunction { 28 | const fn = debuglog(`${packageName}:e2e:${path.relative(rootPath, filePath)}`); 29 | return (msg, ...params) => { 30 | const prefix = evaluateLogPrefix(); 31 | if (prefix) { 32 | msg = `${prefix} ${msg}`; 33 | } 34 | fn(msg, ...params); 35 | }; 36 | } 37 | 38 | function evaluateLogPrefix(): string | false { 39 | const { currentTestName, testPath } = expect.getState(); 40 | 41 | const prefixParts = []; 42 | 43 | if (testPath) { 44 | prefixParts.push(path.relative(rootPath, testPath)); 45 | } 46 | 47 | if (currentTestName) { 48 | prefixParts.push(currentTestName); 49 | } 50 | 51 | if (!prefixParts.length) { 52 | return false; 53 | } 54 | 55 | return dim(magentaBright(`[${prefixParts.join(' - ')}]:`)); 56 | } 57 | -------------------------------------------------------------------------------- /src/commonDir.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import os from 'node:os'; 3 | import path from 'node:path'; 4 | 5 | import { pluginName } from './constants'; 6 | 7 | function longestCommonPrefix(strs: string[]): string { 8 | if (!strs.length) return ''; 9 | 10 | let k = strs[0].length; 11 | for (let i = 1, n = strs.length; i < n; i++) { 12 | k = Math.min(k, strs[i].length); 13 | for (let j = 0; j < k; j++) 14 | if (strs[i][j] !== strs[0][j]) { 15 | k = j; 16 | break; 17 | } 18 | } 19 | return strs[0].substring(0, k); 20 | } 21 | 22 | function isDir(p: string): boolean { 23 | try { 24 | if (!fs.statSync(p).isDirectory()) throw 0; 25 | return true; 26 | } catch { 27 | return false; 28 | } 29 | } 30 | 31 | function normalizePath(p: string, opts: { context?: string }): string { 32 | return opts.context ? path.resolve(opts.context, p) : path.normalize(p); 33 | } 34 | 35 | export function commonDirSync( 36 | filePaths: string[], 37 | opts: { context?: string; longestCommonDir?: string } = {} 38 | ): string { 39 | let prefix = longestCommonPrefix(filePaths.map((p) => normalizePath(p, opts))); 40 | 41 | if (!isDir(prefix)) { 42 | prefix = path.dirname(prefix); 43 | if (!isDir(prefix)) { 44 | throw new Error(`${pluginName}${os.EOL}No valid common dir is figured out`); 45 | } 46 | } 47 | 48 | if (opts.longestCommonDir) { 49 | const finalLongestCommonDir = normalizePath(opts.longestCommonDir, opts); 50 | 51 | if (!isDir(finalLongestCommonDir)) { 52 | throw new Error( 53 | `${pluginName}${os.EOL}The longestCommonDir '${opts.longestCommonDir}' doesn't exist` 54 | ); 55 | } 56 | 57 | if (prefix.startsWith(finalLongestCommonDir)) { 58 | prefix = finalLongestCommonDir; 59 | } 60 | } 61 | 62 | return prefix; 63 | } 64 | -------------------------------------------------------------------------------- /e2e/builds-lib-then-bundles-it.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | chdir, 3 | execNode, 4 | execWebpack, 5 | expectCommonDirToIncludeSameFilesAnd, 6 | rootPath, 7 | setupWebpackProject, 8 | webpackConfigDefaultFileName, 9 | } from '../support-helpers'; 10 | 11 | it('builds lib, then bundles it in app', () => { 12 | setupWebpackProject({ 13 | [`app/${webpackConfigDefaultFileName}`]: ` 14 | module.exports = { 15 | entry: { 16 | app: './src/index.js' 17 | }, 18 | mode: 'production', 19 | target: 'node', 20 | output: { 21 | path: __dirname + '/dist', 22 | }, 23 | }; 24 | `, 25 | 'app/src/index.js': ` 26 | import { print } from '../../lib/dist'; 27 | 28 | print(); 29 | `, 30 | 31 | [`lib/${webpackConfigDefaultFileName}`]: ` 32 | const Plugin = require(${JSON.stringify(rootPath)}); 33 | module.exports = { 34 | entry: './src/index.js', 35 | mode: 'production', 36 | target: 'node', 37 | output: { 38 | path: __dirname + '/dist', 39 | }, 40 | plugins: [new Plugin()], 41 | }; 42 | `, 43 | 'lib/src/index.js': ` 44 | import { greeting } from './constants'; 45 | 46 | export function print() { 47 | console.log(greeting); 48 | } 49 | `, 50 | 'lib/src/constants.js': ` 51 | export const greeting = 'Hi, there!'; 52 | `, 53 | }); 54 | chdir('lib'); 55 | expect(execWebpack().status).toBe(0); 56 | expectCommonDirToIncludeSameFilesAnd({ 57 | 'dist/index.js': (t) => expect(t).not.toInclude('Hi, there'), 58 | 'dist/constants.js': (t) => expect(t).toInclude('Hi, there'), 59 | }); 60 | chdir('../app'); 61 | expect(execWebpack().status).toBe(0); 62 | expectCommonDirToIncludeSameFilesAnd({ 63 | 'dist/app.js': (t) => expect(t).toIncludeMultiple(['print', 'Hi, there']), 64 | }); 65 | const { stdout, status } = execNode('dist/app.js'); 66 | expect(status).toBe(0); 67 | expect(stdout).toInclude('Hi, there!'); 68 | }); 69 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Transpile Webpack Plugin 2 | 3 | ## Submission Guidelines 4 | 5 | ### Submitting an Issue 6 | 7 | If you have discovered a bug or have a feature suggestion, please [submit a new issue on GitHub](https://github.com/licg9999/transpile-webpack-plugin/issues/new). 8 | 9 | Also, in case an issue for your problem might already exist, please search the issue tracker first. 10 | 11 | ### Submitting a Pull Request 12 | 13 | For changes to commit, please checkout a new branch with a format of `author-name/change-brief` from the latest `master` branch. 14 | 15 | On writing commits messages, please briefly state what changes are included in each commit. 16 | 17 | Then, on commits ready, please create a PR with a descriptive title following [Conventional Commits](https://www.conventionalcommits.org/). 18 | 19 | With one approval, the PR will become mergeable. 20 | 21 | On a PR merged into `master` branch, a new alpha version will be released. Just feel free to use it for some initial trials. 22 | 23 | ## Local Setup 24 | 25 | In this repo, `npm` is used. Please run `npm i` in the root dir of the repo to get dependencies installed. Preset scripts are available in `scripts` field of `package.json` as well as `scripts` dir. Just try them to help yourself as you see fit. 26 | 27 | ## Directory Structure 28 | 29 | ```sh 30 | . 31 | ├── additional-configs # Various non-default configs 32 | ├── e2e # End-to-end tests 33 | ├── scripts # Runnable helpers 34 | ├── src # The source code of main logics 35 | └── support-helpers # Helpers for logics outside src 36 | ``` 37 | 38 | ## Code Of Conduct 39 | 40 | Transpile Webpack Plugin has adopted a Code of Conduct that we expect project participants to adhere to. Please read [the full text](https://github.com/licg9999/transpile-webpack-plugin/blob/master/CODE_OF_CONDUCT.md) so that you can understand what actions will and will not be tolerated. 41 | -------------------------------------------------------------------------------- /src/TerserWebpackPluginController.ts: -------------------------------------------------------------------------------- 1 | import { flatten } from 'lodash'; 2 | import type { Compiler } from 'webpack'; 3 | 4 | import { A } from './defaultsSetters'; 5 | import TerserWebpackPlugin from './peers/terser-webpack-plugin'; 6 | import TerserWebpackPluginFromWebpack from './peers/terser-webpack-plugin-from-webpack'; 7 | import { CompilerOptions } from './types'; 8 | 9 | export class TerserWebpackPluginController { 10 | terserWebpackPlugin?: TerserWebpackPlugin; 11 | iniTest?: TerserWebpackPlugin.Rules; 12 | 13 | apply(compiler: Compiler): void { 14 | this.findOrInitTerserWebpackPlugin(compiler.options); 15 | this.iniTest = this.terserWebpackPlugin?.options.test; 16 | } 17 | 18 | findOrInitTerserWebpackPlugin(compilerOptions: CompilerOptions) { 19 | // Aligns to: 20 | // https://github.com/webpack/webpack/blob/4b4ca3bb53f36a5b8fc6bc1bd976ed7af161bd80/lib/config/defaults.js#L1160-L1174 21 | A(compilerOptions.optimization, 'minimizer', () => [ 22 | new TerserWebpackPluginFromWebpack({ 23 | terserOptions: { 24 | compress: { 25 | passes: 2, 26 | }, 27 | }, 28 | }), 29 | ]); 30 | 31 | this.terserWebpackPlugin = compilerOptions.optimization.minimizer?.find( 32 | (p) => 33 | p instanceof TerserWebpackPlugin || 34 | p instanceof TerserWebpackPluginFromWebpack || 35 | p.constructor.name === 'TerserPlugin' 36 | ) as TerserWebpackPlugin | undefined; 37 | } 38 | 39 | setNamesToBeMinimized(names: Iterable): void { 40 | if (!this.terserWebpackPlugin) return; 41 | 42 | const newTest: (string | RegExp)[] = this.iniTest ? flatten([this.iniTest]) : []; 43 | for (const name of names) { 44 | newTest.push( 45 | // Aligns to: 46 | // https://github.com/webpack/webpack/blob/4b4ca3bb53f36a5b8fc6bc1bd976ed7af161bd80/lib/ModuleFilenameHelpers.js#L73 47 | new RegExp(`^${name.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')}$`, 'i') 48 | ); 49 | } 50 | this.terserWebpackPlugin.options.test = newTest as TerserWebpackPlugin.Rules; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /support-helpers/fileOp.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import path from 'node:path'; 3 | 4 | import glob from 'glob'; 5 | import { clone } from 'lodash'; 6 | 7 | import { commonDirSync } from '../src/commonDir'; 8 | import { encodingText } from './constants'; 9 | 10 | export type FilesMatching = Record void>; 11 | 12 | export function expectCommonDirToIncludeAllFiles(filePaths: string[]): void { 13 | filePaths = filePaths.map((fp) => path.normalize(fp)); 14 | expect(filePathsInCommonDirOf(filePaths)).toIncludeAllMembers(filePaths); 15 | } 16 | 17 | export function expectCommonDirToIncludeAllFilesAnd(matching: FilesMatching): void { 18 | matching = normalizeFilesMatching(matching); 19 | 20 | const filePaths = Object.keys(matching); 21 | 22 | expectCommonDirToIncludeAllFiles(filePaths); 23 | 24 | for (const fp of filePaths) { 25 | matching[fp](fs.readFileSync(fp, encodingText)); 26 | } 27 | } 28 | 29 | export function expectCommonDirToIncludeSameFiles(filePaths: string[]): void { 30 | filePaths = filePaths.map((fp) => path.normalize(fp)); 31 | expect(filePathsInCommonDirOf(filePaths)).toIncludeSameMembers(filePaths); 32 | } 33 | 34 | export function expectCommonDirToIncludeSameFilesAnd(matching: FilesMatching): void { 35 | matching = normalizeFilesMatching(matching); 36 | 37 | const filePaths = Object.keys(matching); 38 | 39 | expectCommonDirToIncludeSameFiles(filePaths); 40 | 41 | for (const fp of filePaths) { 42 | matching[fp](fs.readFileSync(fp, encodingText)); 43 | } 44 | } 45 | 46 | export function normalizeFilesMatching(inputMatching: FilesMatching): FilesMatching { 47 | const outputMatching = clone(inputMatching); 48 | for (const fp of Object.keys(inputMatching)) { 49 | const fn = outputMatching[fp]; 50 | delete outputMatching[fp]; 51 | outputMatching[path.normalize(fp)] = fn; 52 | } 53 | return outputMatching; 54 | } 55 | 56 | export function filePathsInCommonDirOf(filePaths: string[]): string[] { 57 | const commonDir = commonDirSync(filePaths); 58 | return glob.sync('**', { nodir: true, cwd: commonDir }).map((fp) => path.join(commonDir, fp)); 59 | } 60 | -------------------------------------------------------------------------------- /src/defaultsSetters.js: -------------------------------------------------------------------------------- 1 | // Copied from: 2 | // https://github.com/webpack/webpack/blob/4b4ca3bb53f36a5b8fc6bc1bd976ed7af161bd80/lib/config/defaults.js#L47-L114 3 | 4 | /** 5 | * Sets a constant default value when undefined 6 | * @template T 7 | * @template {keyof T} P 8 | * @param {T} obj an object 9 | * @param {P} prop a property of this object 10 | * @param {T[P]} value a default value of the property 11 | * @returns {void} 12 | */ 13 | const D = (obj, prop, value) => { 14 | if (obj[prop] === undefined) { 15 | obj[prop] = value; 16 | } 17 | }; 18 | 19 | /** 20 | * Sets a dynamic default value when undefined, by calling the factory function 21 | * @template T 22 | * @template {keyof T} P 23 | * @param {T} obj an object 24 | * @param {P} prop a property of this object 25 | * @param {function(): T[P]} factory a default value factory for the property 26 | * @returns {void} 27 | */ 28 | const F = (obj, prop, factory) => { 29 | if (obj[prop] === undefined) { 30 | obj[prop] = factory(); 31 | } 32 | }; 33 | 34 | /** 35 | * Sets a dynamic default value when undefined, by calling the factory function. 36 | * factory must return an array or undefined 37 | * When the current value is already an array an contains "..." it's replaced with 38 | * the result of the factory function 39 | * @template T 40 | * @template {keyof T} P 41 | * @param {T} obj an object 42 | * @param {P} prop a property of this object 43 | * @param {function(): T[P]} factory a default value factory for the property 44 | * @returns {void} 45 | */ 46 | const A = (obj, prop, factory) => { 47 | const value = obj[prop]; 48 | if (value === undefined) { 49 | obj[prop] = factory(); 50 | } else if (Array.isArray(value)) { 51 | /** @type {any[]} */ 52 | let newArray = undefined; 53 | for (let i = 0; i < value.length; i++) { 54 | const item = value[i]; 55 | if (item === '...') { 56 | if (newArray === undefined) { 57 | newArray = value.slice(0, i); 58 | obj[prop] = /** @type {T[P]} */ (/** @type {unknown} */ (newArray)); 59 | } 60 | const items = /** @type {any[]} */ (/** @type {unknown} */ (factory())); 61 | if (items !== undefined) { 62 | for (const item of items) { 63 | newArray.push(item); 64 | } 65 | } 66 | } else if (newArray !== undefined) { 67 | newArray.push(item); 68 | } 69 | } 70 | } 71 | }; 72 | 73 | export { D, F, A }; 74 | -------------------------------------------------------------------------------- /src/compilerOptions.ts: -------------------------------------------------------------------------------- 1 | import os from 'node:os'; 2 | 3 | import { flatten, pick, set } from 'lodash'; 4 | 5 | import { pluginName } from './constants'; 6 | import { D } from './defaultsSetters'; 7 | import { HotModuleReplacementPlugin } from './peers/webpack'; 8 | import { CompilerOptions } from './types'; 9 | 10 | export function forceDisableSplitChunks(compilerOptions: CompilerOptions): void { 11 | set(compilerOptions, 'optimization.splitChunks', false); 12 | } 13 | 14 | export function forceSetLibraryType(compilerOptions: CompilerOptions, libraryType: string): void { 15 | set(compilerOptions, 'output.library.type', libraryType); 16 | } 17 | 18 | export function forceDisableOutputModule(compilerOptions: CompilerOptions): void { 19 | set(compilerOptions, 'experiments.outputModule', false); 20 | } 21 | 22 | export function throwErrIfOutputPathNotSpecified(compilerOptions: CompilerOptions): void { 23 | const { output } = compilerOptions; 24 | if (!output.path) 25 | throw new Error(`${pluginName}${os.EOL}The output.path in webpack config is not specified`); 26 | } 27 | 28 | export function throwErrIfHotModuleReplacementEnabled(compilerOptions: CompilerOptions): void { 29 | const { plugins } = compilerOptions; 30 | for (const p of plugins) { 31 | if ( 32 | p instanceof HotModuleReplacementPlugin || 33 | p.constructor.name === 'HotModuleReplacementPlugin' 34 | ) { 35 | throw new Error( 36 | `${pluginName}${os.EOL}Hot module replacement is not supported when using plugin '${pluginName}'` 37 | ); 38 | } 39 | } 40 | } 41 | 42 | export function enableBuiltinNodeGlobalsByDefault(compilerOptions: CompilerOptions): void { 43 | if (compilerOptions.node) { 44 | D(compilerOptions.node, '__dirname', false); 45 | D(compilerOptions.node, '__filename', false); 46 | } 47 | } 48 | 49 | export function isTargetNodeCompatible(target: CompilerOptions['target']): boolean { 50 | return flatten([target]).some((t) => typeof t === 'string' && t.includes('node')); 51 | } 52 | 53 | export function alignResolveByDependency(compilerOptions: CompilerOptions, preferredType: string) { 54 | const { byDependency } = compilerOptions.resolve; 55 | if (!byDependency) return; 56 | if (!Object.prototype.hasOwnProperty.call(byDependency, preferredType)) preferredType = 'unknown'; 57 | const preferredOpts = byDependency[preferredType]; 58 | for (const [type, opts] of Object.entries(byDependency)) { 59 | if (type !== preferredType) { 60 | Object.assign(opts, pick(preferredOpts, Object.keys(opts))); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /support-helpers/webpackProject.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import path from 'node:path'; 3 | 4 | import { blueBright, dim } from 'colorette'; 5 | import crossSpawn from 'cross-spawn'; 6 | import glob from 'glob'; 7 | import { merge } from 'lodash'; 8 | import { v4 as uuidv4 } from 'uuid'; 9 | 10 | import { 11 | encodingText, 12 | rootPath, 13 | webpackConfigDefaultFileName, 14 | webpackProjectMustHaveFiles, 15 | webpackProjectMustHavePackageJson, 16 | webpackProjectParentDirName, 17 | } from './constants'; 18 | import { logStdout } from './logging'; 19 | 20 | export function setupWebpackProject( 21 | files: Record & { [webpackConfigDefaultFileName]?: string } 22 | ): void { 23 | autoChWebpackProject(); 24 | 25 | writeFiles({ ...webpackProjectMustHaveFiles, ...files }); 26 | 27 | const { status } = crossSpawn.sync('npm', ['i'], { encoding: encodingText, stdio: 'ignore' }); 28 | expect(status).toBe(0); 29 | 30 | logStdout( 31 | `Did setup webpack project with package.json: ${dim( 32 | JSON.stringify(JSON.parse(fs.readFileSync('package.json', encodingText)), null, 0) 33 | )}` 34 | ); 35 | } 36 | 37 | export function autoChWebpackProject(): string { 38 | const { testPath } = expect.getState(); 39 | 40 | if (!testPath) { 41 | throw new Error('Required fields are not returned from expect.getState()'); 42 | } 43 | 44 | const projectPath = path.join(path.dirname(testPath), webpackProjectParentDirName, uuidv4()); 45 | 46 | fs.mkdirSync(projectPath, { recursive: true }); 47 | 48 | chdir(projectPath); 49 | 50 | return projectPath; 51 | } 52 | 53 | export function chdir(targetPath: string): void { 54 | targetPath = path.resolve(targetPath); 55 | process.chdir(targetPath); 56 | logStdout(`Did change into dir: ${blueBright(path.relative(rootPath, targetPath))}`); 57 | } 58 | 59 | export function writeFiles(files: Record): void { 60 | for (const [filePath, fileText] of Object.entries(files)) { 61 | fs.mkdirSync(path.dirname(filePath), { recursive: true }); 62 | fs.writeFileSync(filePath, fileText, encodingText); 63 | } 64 | } 65 | 66 | export function cleanAllWebpackProjects() { 67 | for (const p of glob.sync(`**/${webpackProjectParentDirName}/`, { 68 | absolute: true, 69 | cwd: rootPath, 70 | ignore: '**/node_modules/**', 71 | })) { 72 | try { 73 | fs.rmSync(p, { recursive: true }); 74 | logStdout(`Did clean webpack projects under: ${blueBright(path.relative(rootPath, p))}`); 75 | } catch {} 76 | } 77 | } 78 | 79 | export function evaluateMustHavePackageJsonText(packageJsonOverride: object): string { 80 | return JSON.stringify(merge({}, webpackProjectMustHavePackageJson, packageJsonOverride)); 81 | } 82 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "transpile-webpack-plugin", 3 | "version": "1.0.0-semantic-release", 4 | "license": "MIT", 5 | "description": "Transpiles input files into output files individually without bundling together", 6 | "keywords": [ 7 | "webpack", 8 | "plugin", 9 | "transpile", 10 | "transpile-webpack-plugin" 11 | ], 12 | "author": "Chungen Li (https://github.com/licg9999)", 13 | "bugs": "https://github.com/licg9999/transpile-webpack-plugin/issues", 14 | "homepage": "https://github.com/licg9999/transpile-webpack-plugin", 15 | "repository": "https://github.com/licg9999/transpile-webpack-plugin.git", 16 | "engines": { 17 | "node": ">=16" 18 | }, 19 | "main": "lib/index.js", 20 | "types": "lib/index.d.ts", 21 | "files": [ 22 | "lib", 23 | "modules.d.ts", 24 | "src" 25 | ], 26 | "scripts": { 27 | "build": "tsc --project additional-configs/tsconfig.build.json", 28 | "watch": "npm run build -- --watch", 29 | "unittest": "cross-env JEST_TEST_PATH=src jest", 30 | "e2e": "cross-env JEST_TEST_PATH=e2e jest --maxWorkers 1", 31 | "lint-all": "run-p lint:*", 32 | "lint:tsc": "tsc", 33 | "lint:prettier": "node scripts/cross-dotenv-shell prettier --check FILE_GLOB", 34 | "lint:eslint": "node scripts/cross-dotenv-shell eslint FILE_GLOB", 35 | "fix-all": "run-s fix:*", 36 | "fix:prettier": "node scripts/cross-dotenv-shell prettier --write FILE_GLOB", 37 | "fix:eslint": "node scripts/cross-dotenv-shell eslint --fix FILE_GLOB", 38 | "git-clean": "git clean -d -f -x -e node_modules -e package-lock.json" 39 | }, 40 | "peerDependencies": { 41 | "webpack": "^5.61.0" 42 | }, 43 | "dependencies": { 44 | "lodash": "^4.17.21", 45 | "resolve": "^1.22.1", 46 | "schema-utils": "^4.0.0" 47 | }, 48 | "devDependencies": { 49 | "@commitlint/config-conventional": "^17.1.0", 50 | "@trivago/prettier-plugin-sort-imports": "^3.4.0", 51 | "@types/cross-spawn": "^6.0.2", 52 | "@types/glob": "^8.0.0", 53 | "@types/jest": "^28.1.8", 54 | "@types/lodash": "^4.14.186", 55 | "@types/node": "^18.11.9", 56 | "@types/resolve": "^1.20.2", 57 | "@types/uuid": "^8.3.4", 58 | "@typescript-eslint/eslint-plugin": "^5.46.1", 59 | "@typescript-eslint/parser": "^5.46.1", 60 | "colorette": "^2.0.19", 61 | "commander": "^9.4.1", 62 | "cross-env": "^7.0.3", 63 | "cross-spawn": "^7.0.3", 64 | "dotenv": "^16.0.3", 65 | "eslint": "^8.30.0", 66 | "eslint-config-prettier": "^8.5.0", 67 | "eslint-plugin-jest": "^27.1.7", 68 | "glob": "^8.0.3", 69 | "jest": "^28.1.3", 70 | "jest-extended": "^3.1.0", 71 | "npm-run-all": "^4.1.5", 72 | "prettier": "^2.7.1", 73 | "tree-kill": "^1.2.2", 74 | "ts-jest": "^28.0.8", 75 | "typescript": "^4.8.4", 76 | "uuid": "^8.3.2", 77 | "wait-for-expect": "^3.0.2" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/SourceMapDevToolPluginController.ts: -------------------------------------------------------------------------------- 1 | import { hookStageVeryEarly, pluginName } from './constants'; 2 | import { Compiler, EvalSourceMapDevToolPlugin, SourceMapDevToolPlugin } from './peers/webpack'; 3 | import { CompilerOptions, SourceMapDevToolPluginOptions } from './types'; 4 | 5 | export class SourceMapDevToolPluginController { 6 | sourceMapDevToolPluginOptions?: SourceMapDevToolPluginOptions; 7 | oldDevtool: CompilerOptions['devtool']; 8 | 9 | apply(compiler: Compiler): void { 10 | compiler.hooks.environment.tap({ name: pluginName, stage: hookStageVeryEarly }, () => { 11 | if (compiler.options.devtool) { 12 | if (compiler.options.devtool.includes('source-map')) { 13 | this.initSourceMapDevToolPlugin(compiler); 14 | 15 | // Prevents devtool getting processed again inside webpack. 16 | this.disableDevtool(compiler.options); 17 | } 18 | } 19 | }); 20 | 21 | compiler.hooks.initialize.tap({ name: pluginName, stage: hookStageVeryEarly }, () => { 22 | // Restore devtool after compiler options get processed inside webpack. 23 | this.restoreDevtool(compiler.options); 24 | }); 25 | } 26 | 27 | initSourceMapDevToolPlugin(compiler: Compiler) { 28 | if (!compiler.options.devtool) return; 29 | 30 | // Aligns to: 31 | // https://github.com/webpack/webpack/blob/86a8bd9618c4677e94612ff7cbdf69affeba1268/lib/WebpackOptionsApply.js#L228-L247 32 | const hidden = compiler.options.devtool.includes('hidden'); 33 | const inline = compiler.options.devtool.includes('inline'); 34 | const evalWrapped = compiler.options.devtool.includes('eval'); 35 | const cheap = compiler.options.devtool.includes('cheap'); 36 | const moduleMaps = compiler.options.devtool.includes('module'); 37 | const noSources = compiler.options.devtool.includes('nosources'); 38 | const Plugin = evalWrapped ? EvalSourceMapDevToolPlugin : SourceMapDevToolPlugin; 39 | this.sourceMapDevToolPluginOptions = { 40 | filename: inline ? null : compiler.options.output.sourceMapFilename, 41 | moduleFilenameTemplate: compiler.options.output.devtoolModuleFilenameTemplate, 42 | fallbackModuleFilenameTemplate: compiler.options.output.devtoolFallbackModuleFilenameTemplate, 43 | append: hidden ? false : undefined, 44 | module: moduleMaps ? true : cheap ? false : true, 45 | columns: cheap ? false : true, 46 | noSources: noSources, 47 | namespace: compiler.options.output.devtoolNamespace, 48 | }; 49 | new Plugin(this.sourceMapDevToolPluginOptions).apply(compiler); 50 | } 51 | 52 | disableDevtool(compilerOptions: CompilerOptions): void { 53 | this.oldDevtool = compilerOptions.devtool; 54 | compilerOptions.devtool = false; 55 | } 56 | 57 | restoreDevtool(compilerOptions: CompilerOptions): void { 58 | compilerOptions.devtool = this.oldDevtool; 59 | } 60 | 61 | setExtensionsToHaveSourceMaps(extensions: Iterable): void { 62 | if (this.sourceMapDevToolPluginOptions) { 63 | // Aligns to: 64 | // https://github.com/webpack/webpack/blob/6fa6e30254f0eb2673a3525739da1df0a5f51791/lib/SourceMapDevToolPlugin.js#L155 65 | const reEndsWithExts = new RegExp( 66 | `(${Array.from(extensions) 67 | .map((e) => `\\${e}`) 68 | .join('|')})$`, 69 | 'i' 70 | ); 71 | 72 | this.sourceMapDevToolPluginOptions.test = reEndsWithExts; 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /e2e/works-with-webpack-configs-in-popular-tools.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | autoChWebpackProject, 3 | chdir, 4 | depVerWebpack, 5 | depVerWebpackCli, 6 | exec, 7 | execNode, 8 | execWebpack, 9 | expectCommonDirToIncludeAllFilesAnd, 10 | rootPath, 11 | writeFiles, 12 | } from '../support-helpers'; 13 | 14 | it('works with webpack config in facebook/create-react-app', () => { 15 | autoChWebpackProject(); 16 | exec('npm', 'init', '-y'); 17 | exec('npm', 'i', '-D', 'create-react-app'); 18 | exec('npx', 'create-react-app', 'the-app'); 19 | chdir('the-app'); 20 | writeFiles({ 21 | 'webpack.config.js': ` 22 | const Plugin = require(${JSON.stringify(rootPath)}); 23 | 24 | const envInQuestion = 'production'; 25 | 26 | process.env.NODE_ENV = envInQuestion; 27 | const webpackConfig = require('react-scripts/config/webpack.config')(envInQuestion); 28 | 29 | webpackConfig.entry = './src/server.js'; 30 | 31 | webpackConfig.plugins = webpackConfig.plugins.filter( 32 | (p) => p.constructor.name !== 'WebpackManifestPlugin' 33 | ); 34 | 35 | webpackConfig.plugins.push(new Plugin()); 36 | 37 | module.exports = webpackConfig; 38 | `, 39 | 'src/server.js': ` 40 | import { renderToString } from 'react-dom/server'; 41 | import App from './App'; 42 | 43 | console.log(renderToString()); 44 | `, 45 | }); 46 | exec('npm', 'i', `webpack@${depVerWebpack}`, `webpack-cli@${depVerWebpackCli}`); 47 | expect(execWebpack().status).toBe(0); 48 | expectCommonDirToIncludeAllFilesAnd({ 49 | 'build/App.js': (t) => expect(t).toInclude('Learn React'), 50 | 'build/logo.svg': (t) => expect(t).toInclude('static/media/logo'), 51 | 'build/server.js': (t) => 52 | expect(t).not.toIncludeAnyMembers(['Learn React', 'static/media/logo']), 53 | }); 54 | const { stdout, status } = execNode('build/server.js'); 55 | expect(status).toBe(0); 56 | expect(stdout).toIncludeMultiple(['Learn React', 'static/media/logo']); 57 | }); 58 | 59 | it('works with webpack config in vue/vue-cli', () => { 60 | autoChWebpackProject(); 61 | exec('npm', 'init', '-y'); 62 | exec('npm', 'i', '-D', '@vue/cli'); 63 | exec('npx', 'vue', 'create', '-d', '-m', 'npm', 'the-app'); 64 | chdir('the-app'); 65 | writeFiles({ 66 | 'webpack.config.js': ` 67 | const Plugin = require(${JSON.stringify(rootPath)}); 68 | 69 | const envInQuestion = 'production'; 70 | 71 | process.env.NODE_ENV = envInQuestion; 72 | const webpackConfig = require('@vue/cli-service/webpack.config'); 73 | 74 | webpackConfig.entry = './src/server.js'; 75 | 76 | webpackConfig.plugins.push(new Plugin()); 77 | 78 | module.exports = webpackConfig; 79 | `, 80 | 'src/server.js': ` 81 | import { createSSRApp } from 'vue'; 82 | import { renderToString } from 'vue/server-renderer'; 83 | import App from './App.vue'; 84 | const app = createSSRApp(App); 85 | renderToString(app).then(console.log); 86 | `, 87 | }); 88 | exec('npm', 'i', `webpack@${depVerWebpack}`, `webpack-cli@${depVerWebpackCli}`); 89 | expect(execWebpack().status).toBe(0); 90 | expectCommonDirToIncludeAllFilesAnd({ 91 | 'dist/App.vue': (t) => expect(t).toInclude('Vue.js App'), 92 | 'dist/assets/logo.png': (t) => expect(t).toInclude('data:image/png;base64'), 93 | 'dist/server.js': (t) => 94 | expect(t).not.toIncludeAnyMembers(['Vue.js App', 'data:image/png;base64']), 95 | }); 96 | const { stdout, status } = execNode('dist/server.js'); 97 | expect(status).toBe(0); 98 | expect(stdout).toIncludeMultiple(['Vue.js App', 'data:image/png;base64']); 99 | }); 100 | -------------------------------------------------------------------------------- /support-helpers/scriptRunners.ts: -------------------------------------------------------------------------------- 1 | import { ChildProcess, SpawnOptions, SpawnSyncReturns } from 'node:child_process'; 2 | import { promisify } from 'node:util'; 3 | 4 | import { blueBright, dim } from 'colorette'; 5 | import crossSpawn from 'cross-spawn'; 6 | import treeKill from 'tree-kill'; 7 | 8 | import { encodingText } from './constants'; 9 | import { createE2eDebuglogByFilePath, logStdout } from './logging'; 10 | 11 | const debuglog = createE2eDebuglogByFilePath(__filename); 12 | 13 | const defaultSpawnOptions: SpawnOptions = { 14 | stdio: 'pipe', 15 | env: { ...process.env, NO_COLOR: 'true' }, 16 | }; 17 | 18 | export function exec(cmd: string, ...args: string[]): SpawnSyncReturns { 19 | const ret = crossSpawn.sync(cmd, args, { 20 | ...defaultSpawnOptions, 21 | encoding: encodingText, 22 | }); 23 | logStdout( 24 | `Did run ${blueBright([cmd, ...args].join(' '))}, with exit code ${blueBright( 25 | String(ret.status) 26 | )}.` 27 | ); 28 | (['stdout', 'stderr'] as const).forEach((k) => { 29 | if (ret[k]) debuglog(`[${k}]: ${dim(`${ret[k]}`)}`); 30 | }); 31 | return ret; 32 | } 33 | 34 | const KeyOfExecAsyncPidsInExpectState = 'execAsyncPids'; 35 | 36 | export function execAsync( 37 | cmd: string, 38 | ...args: string[] 39 | ): ChildProcess & { 40 | getStdoutAsString(): string; 41 | getStderrAsString(): string; 42 | } { 43 | const enhancedProc = crossSpawn(cmd, args, { ...defaultSpawnOptions }); 44 | 45 | const expectState = expect.getState(); 46 | const pids: number[] = expectState[KeyOfExecAsyncPidsInExpectState] ?? []; 47 | pids.push(enhancedProc.pid!); 48 | expect.setState({ ...expectState, [KeyOfExecAsyncPidsInExpectState]: pids }); 49 | 50 | const cmdAsString = [cmd, ...args].join(' '); 51 | logStdout(`Running ${blueBright(cmdAsString)} ...`); 52 | 53 | const stdoutAsStrings: string[] = []; 54 | const stderrAsStrings: string[] = []; 55 | ( 56 | [ 57 | ['stdout', stdoutAsStrings], 58 | ['stderr', stderrAsStrings], 59 | ] as const 60 | ).forEach(([k, asStrings]) => { 61 | enhancedProc[k]?.on('data', (b: Buffer) => { 62 | const s = b.toString(encodingText); 63 | asStrings.push(s); 64 | debuglog(`[${k}]: ${dim(`${s}`)}`); 65 | }); 66 | }); 67 | 68 | enhancedProc.on('close', (status) => { 69 | logStdout(`Did run ${blueBright(cmdAsString)}, with exit code ${blueBright(String(status))}.`); 70 | }); 71 | 72 | return Object.assign(enhancedProc, { 73 | getStdoutAsString: () => stdoutAsStrings.join(''), 74 | getStderrAsString: () => stderrAsStrings.join(''), 75 | }); 76 | } 77 | 78 | export async function killExecAsyncProcess(pid: number): Promise { 79 | const expectState = expect.getState(); 80 | await promisify(treeKill)(pid); 81 | debuglog(`Did kill execAsync process id ${blueBright(pid)}.`); 82 | const latestPids: number[] = expectState[KeyOfExecAsyncPidsInExpectState] ?? []; 83 | const leftPids = latestPids.filter((i) => i !== pid); 84 | expect.setState({ ...expectState, [KeyOfExecAsyncPidsInExpectState]: leftPids }); 85 | } 86 | 87 | export async function killAllExecAsyncProcesses(): Promise { 88 | const expectState = expect.getState(); 89 | const pids: number[] = expectState[KeyOfExecAsyncPidsInExpectState] ?? []; 90 | for (const pid of pids) { 91 | await promisify(treeKill)(pid); 92 | debuglog(`Did kill execAsync process id ${blueBright(pid)}.`); 93 | } 94 | const latestPids: number[] = expectState[KeyOfExecAsyncPidsInExpectState] ?? []; 95 | const leftPids = latestPids.filter((i) => !pids.includes(i)); 96 | expect.setState({ ...expectState, [KeyOfExecAsyncPidsInExpectState]: leftPids }); 97 | } 98 | 99 | export function execWebpack(...args: string[]) { 100 | return exec('npx', 'webpack', ...args); 101 | } 102 | 103 | export function execWebpackAsync(...args: string[]) { 104 | return execAsync('npx', 'webpack', ...args); 105 | } 106 | 107 | export function execNode(...args: string[]) { 108 | return exec('node', ...args); 109 | } 110 | 111 | export function execNodeAsync(...args: string[]) { 112 | return execAsync('node', ...args); 113 | } 114 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | [INSERT CONTACT METHOD]. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series of 86 | actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or permanent 93 | ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within the 113 | community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.1, available at 119 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 120 | 121 | Community Impact Guidelines were inspired by 122 | [Mozilla's code of conduct enforcement ladder][mozilla coc]. 123 | 124 | For answers to common questions about this code of conduct, see the FAQ at 125 | [https://www.contributor-covenant.org/faq][faq]. Translations are available at 126 | [https://www.contributor-covenant.org/translations][translations]. 127 | 128 | [homepage]: https://www.contributor-covenant.org 129 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 130 | [mozilla coc]: https://github.com/mozilla/diversity 131 | [faq]: https://www.contributor-covenant.org/faq 132 | [translations]: https://www.contributor-covenant.org/translations 133 | -------------------------------------------------------------------------------- /e2e/validates-inputs.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | boolToText, 3 | execWebpack, 4 | mapStrToText, 5 | rootPath, 6 | setupWebpackProject, 7 | } from '../support-helpers'; 8 | 9 | describe('validates compiler options', () => { 10 | function setup(validOpts: { hmr?: boolean }) { 11 | setupWebpackProject({ 12 | 'webpack.config.js': ` 13 | const { HotModuleReplacementPlugin } = require('webpack'); 14 | const Plugin = require(${JSON.stringify(rootPath)}); 15 | module.exports = { 16 | plugins: [ 17 | ${boolToText(validOpts.hmr, '', 'new HotModuleReplacementPlugin(),')} 18 | new Plugin(), 19 | ], 20 | }; 21 | `, 22 | }); 23 | } 24 | 25 | it('throws error if hot module replacement enabled', () => { 26 | setup({ hmr: false }); 27 | const { status, stderr } = execWebpack(); 28 | expect(status).toBeGreaterThan(0); 29 | expect(stderr).toIncludeMultiple(['Error', 'Hot module replacement']); 30 | }); 31 | }); 32 | 33 | describe('validates options', () => { 34 | function setup(validOpts: { 35 | exclude?: boolean; 36 | hoistNodeModules?: boolean; 37 | longestCommonDir?: boolean | string; 38 | extentionMapping?: boolean; 39 | preferResolveByDependencyAsCjs?: boolean; 40 | }) { 41 | setupWebpackProject({ 42 | 'webpack.config.js': ` 43 | const Plugin = require(${JSON.stringify(rootPath)}); 44 | module.exports = { 45 | entry: './src/index.js', 46 | plugins: [ 47 | new Plugin({ 48 | ${boolToText(validOpts.exclude, 'exclude: /bower_components/,', 'exclude: false,')} 49 | ${boolToText(validOpts.hoistNodeModules, 'hoistNodeModules: false,', 'hoistNodeModules: 0,')} 50 | ${mapStrToText( 51 | validOpts.longestCommonDir, 52 | (s) => `longestCommonDir: '${s}',`, 53 | (b) => boolToText(b, 'longestCommonDir: __dirname,', 'longestCommonDir: 0,') 54 | )} 55 | ${boolToText(validOpts.extentionMapping, 'extentionMapping: {},', 'extentionMapping: 0,')} 56 | ${boolToText( 57 | validOpts.preferResolveByDependencyAsCjs, 58 | 'preferResolveByDependencyAsCjs: true,', 59 | 'preferResolveByDependencyAsCjs: 0,' 60 | )} 61 | }), 62 | ], 63 | }; 64 | `, 65 | 'src/index.js': '', 66 | }); 67 | } 68 | 69 | it('throws error if exclude not valid in format', () => { 70 | setup({ exclude: false }); 71 | const { status, stderr } = execWebpack(); 72 | expect(status).toBeGreaterThan(0); 73 | expect(stderr).toIncludeMultiple(['Invalid', 'exclude']); 74 | }); 75 | 76 | it('throws error if hoistNodeModules not valid in format', () => { 77 | setup({ hoistNodeModules: false }); 78 | const { status, stderr } = execWebpack(); 79 | expect(status).toBeGreaterThan(0); 80 | expect(stderr).toIncludeMultiple(['Invalid', 'hoistNodeModules']); 81 | }); 82 | 83 | it('throws error if longestCommonDir not valid in format', () => { 84 | setup({ longestCommonDir: false }); 85 | const { status, stderr } = execWebpack(); 86 | expect(status).toBeGreaterThan(0); 87 | expect(stderr).toIncludeMultiple(['Invalid', 'longestCommonDir']); 88 | }); 89 | 90 | it(`throws error if longestCommonDir doesn't exist`, () => { 91 | setup({ longestCommonDir: './src/some/where' }); 92 | const { status, stderr } = execWebpack(); 93 | expect(status).toBeGreaterThan(0); 94 | expect(stderr).toIncludeMultiple(['Error', 'longestCommonDir', './src/some/where']); 95 | }); 96 | 97 | it('throws error if extentionMapping not valid in format', () => { 98 | setup({ extentionMapping: false }); 99 | const { status, stderr } = execWebpack(); 100 | expect(status).toBeGreaterThan(0); 101 | expect(stderr).toIncludeMultiple(['Invalid', 'extentionMapping']); 102 | }); 103 | 104 | it('throws error if preferResolveByDependencyAsCjs not valid in format', () => { 105 | setup({ preferResolveByDependencyAsCjs: false }); 106 | const { status, stderr } = execWebpack(); 107 | expect(status).toBeGreaterThan(0); 108 | expect(stderr).toIncludeMultiple(['Invalid', 'preferResolveByDependencyAsCjs']); 109 | }); 110 | }); 111 | 112 | describe('validates entries', () => { 113 | it(`throws error if no entry found outside 'node_modules'`, () => { 114 | setupWebpackProject({ 115 | 'webpack.config.js': ` 116 | const Plugin = require(${JSON.stringify(rootPath)}); 117 | module.exports = { 118 | entry: require.resolve('lodash'), 119 | plugins: [new Plugin()], 120 | }; 121 | `, 122 | }); 123 | const { status, stderr } = execWebpack(); 124 | expect(status).toBeGreaterThan(0); 125 | expect(stderr).toIncludeMultiple(['Error', 'No entry', `outside 'node_modules'`]); 126 | }); 127 | 128 | it(`prints warning if any '.mjs' file found with target 'node'`, () => { 129 | setupWebpackProject({ 130 | 'webpack.config.js': ` 131 | const Plugin = require(${JSON.stringify(rootPath)}); 132 | module.exports = { 133 | mode: 'production', 134 | target: 'node', 135 | output: { 136 | path: __dirname + '/dist', 137 | }, 138 | entry: './src/index.mjs', 139 | plugins: [new Plugin()], 140 | }; 141 | `, 142 | 'src/index.mjs': '', 143 | }); 144 | const { status, stdout } = execWebpack(); 145 | expect(status).toBe(0); 146 | expect(stdout).toIncludeMultiple(['WARNING', `'.mjs' files`, './src/index.mjs']); 147 | }); 148 | 149 | it(`throws error if any '.json' file is not type of JSON`, () => { 150 | setupWebpackProject({ 151 | 'webpack.config.js': ` 152 | const Plugin = require(${JSON.stringify(rootPath)}); 153 | module.exports = { 154 | entry: './src/index.js', 155 | module: { 156 | rules: [ 157 | { 158 | test: /\\.json$/, 159 | type: 'javascript/auto', 160 | } 161 | ] 162 | }, 163 | plugins: [new Plugin()], 164 | }; 165 | `, 166 | 'src/index.js': ` 167 | import './constants.json'; 168 | `, 169 | 'src/constants.json': '{}', 170 | }); 171 | const { status, stderr } = execWebpack(); 172 | expect(status).toBeGreaterThan(0); 173 | expect(stderr).toIncludeMultiple(['Error', 'not type of JSON']); 174 | }); 175 | }); 176 | -------------------------------------------------------------------------------- /e2e/handles-node_modules.spec.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | 3 | import { noop } from 'lodash'; 4 | 5 | import { 6 | encodingText, 7 | evaluateMustHavePackageJsonText, 8 | execNode, 9 | execWebpack, 10 | expectCommonDirToIncludeAllFilesAnd, 11 | expectCommonDirToIncludeSameFilesAnd, 12 | rootPath, 13 | setupWebpackProject, 14 | } from '../support-helpers'; 15 | 16 | const webpackConfigReusable = ` 17 | mode: 'production', 18 | target: 'node', 19 | output: { 20 | path: __dirname + '/dist' 21 | }, 22 | `; 23 | 24 | function useSubEntry(subEntryName: string, topEntryFile: string = 'src/index.js'): void { 25 | fs.writeFileSync(topEntryFile, `import './${subEntryName}';`, encodingText); 26 | } 27 | 28 | describe('with deps directly imported from node_modules', () => { 29 | it( 30 | 'with hoistNodeModules as true, ' + 31 | 'outputs files outside node_modules relative to the longest common dir of them, ' + 32 | 'outputs files in node_modules to node_modules just under the output dir, ' + 33 | 'correctly resolves files in the output node_modules', 34 | () => { 35 | setupWebpackProject({ 36 | 'webpack.config.js': ` 37 | const Plugin = require(${JSON.stringify(rootPath)}); 38 | module.exports = { 39 | ${webpackConfigReusable} 40 | entry: './src/index.js', 41 | plugins: [ 42 | new Plugin({ 43 | hoistNodeModules: true, 44 | }) 45 | ], 46 | }; 47 | `, 48 | 'src/index.js': ` 49 | import { upperCase } from 'lodash'; 50 | console.log(upperCase('Hi, there!')); 51 | `, 52 | 'package.json': evaluateMustHavePackageJsonText({ 53 | ['dependencies']: { 54 | ['lodash']: '^4.17.21', 55 | }, 56 | }), 57 | }); 58 | expect(execWebpack().status).toBe(0); 59 | expectCommonDirToIncludeSameFilesAnd({ 60 | 'dist/index.js': (t) => 61 | expect(t).toIncludeMultiple(['require("./node_modules/lodash/lodash.js")', 'Hi, there!']), 62 | 'dist/node_modules/lodash/lodash.js': noop, 63 | 'dist/node_modules/lodash/lodash.js.LICENSE.txt': noop, 64 | }); 65 | const { status, stdout } = execNode('dist/index.js'); 66 | expect(status).toBe(0); 67 | expect(stdout).toInclude('HI THERE'); 68 | } 69 | ); 70 | 71 | it( 72 | 'with hoistNodeModules as false, ' + 73 | 'outputs files relative to the longest common dir of all files ' + 74 | 'including those inside node_modules', 75 | () => { 76 | setupWebpackProject({ 77 | 'webpack.config.js': ` 78 | const Plugin = require(${JSON.stringify(rootPath)}); 79 | module.exports = { 80 | ${webpackConfigReusable} 81 | entry: './src/index.js', 82 | plugins: [ 83 | new Plugin({ 84 | hoistNodeModules: false, 85 | }) 86 | ], 87 | }; 88 | `, 89 | 'src/index.js': ` 90 | import { upperCase } from 'lodash'; 91 | console.log(upperCase('Hi, there!')); 92 | `, 93 | 'package.json': evaluateMustHavePackageJsonText({ 94 | ['dependencies']: { 95 | ['lodash']: '^4.17.21', 96 | }, 97 | }), 98 | }); 99 | expect(execWebpack().status).toBe(0); 100 | expectCommonDirToIncludeSameFilesAnd({ 101 | 'dist/src/index.js': (t) => 102 | expect(t).toIncludeMultiple([ 103 | 'require("../node_modules/lodash/lodash.js")', 104 | 'Hi, there!', 105 | ]), 106 | 'dist/node_modules/lodash/lodash.js': noop, 107 | 'dist/node_modules/lodash/lodash.js.LICENSE.txt': noop, 108 | }); 109 | const { status, stdout } = execNode('dist/src/index.js'); 110 | expect(status).toBe(0); 111 | expect(stdout).toInclude('HI THERE'); 112 | } 113 | ); 114 | 115 | it( 116 | 'with preferResolveByDependencyAsCjs as true, ' + 117 | 'resolves files in node_modules by CommonJS exports ignoring type of import statement', 118 | () => { 119 | setupWebpackProject({ 120 | 'webpack.config.js': ` 121 | const Plugin = require(${JSON.stringify(rootPath)}); 122 | module.exports = { 123 | ${webpackConfigReusable} 124 | entry: './src/index.js', 125 | plugins: [ 126 | new Plugin({ 127 | preferResolveByDependencyAsCjs: true, 128 | }), 129 | ], 130 | }; 131 | `, 132 | 'src/withEsmImport.js': ` 133 | import { green } from 'colorette'; 134 | console.log(green('Hi, there!')); 135 | `, 136 | 'src/withCjsImport.js': ` 137 | const { green } = require('colorette'); 138 | console.log(green('Hi, there!')); 139 | `, 140 | 'package.json': evaluateMustHavePackageJsonText({ 141 | ['dependencies']: { 142 | ['colorette']: '^2.0.19', 143 | }, 144 | }), 145 | }); 146 | 147 | for (const subEntryName of ['withEsmImport', 'withCjsImport']) { 148 | try { 149 | fs.rmSync('dist', { recursive: true }); 150 | } catch {} 151 | useSubEntry(subEntryName); 152 | expect(execWebpack().status).toBe(0); 153 | expectCommonDirToIncludeSameFilesAnd({ 154 | 'dist/index.js': noop, 155 | [`dist/${subEntryName}.js`]: (t) => 156 | expect(t).toIncludeMultiple([ 157 | 'require("./node_modules/colorette/index.cjs")', 158 | 'Hi, there!', 159 | ]), 160 | 'dist/node_modules/colorette/index.cjs': noop, 161 | }); 162 | const { status, stdout } = execNode('dist/index.js'); 163 | expect(status).toBe(0); 164 | expect(stdout).toInclude('Hi, there!'); 165 | } 166 | } 167 | ); 168 | 169 | it( 170 | 'with preferResolveByDependencyAsCjs as false, ' + 171 | 'resolves files in node_modules according to type of import statement', 172 | () => { 173 | setupWebpackProject({ 174 | 'webpack.config.js': ` 175 | const Plugin = require(${JSON.stringify(rootPath)}); 176 | module.exports = { 177 | ${webpackConfigReusable} 178 | entry: './src/index.js', 179 | plugins: [ 180 | new Plugin({ 181 | preferResolveByDependencyAsCjs: false, 182 | }), 183 | ], 184 | }; 185 | `, 186 | 'src/withEsmImport.js': ` 187 | import { green } from 'colorette'; 188 | console.log(green('Hi, there!')); 189 | `, 190 | 'src/withCjsImport.js': ` 191 | const { green } = require('colorette'); 192 | console.log(green('Hi, there!')); 193 | `, 194 | 'package.json': evaluateMustHavePackageJsonText({ 195 | ['dependencies']: { 196 | ['colorette']: '^2.0.19', 197 | }, 198 | }), 199 | }); 200 | 201 | subCaseWithEsmImport(); 202 | subCaseWithCjsImport(); 203 | 204 | function subCaseWithEsmImport() { 205 | try { 206 | fs.rmSync('dist', { recursive: true }); 207 | } catch {} 208 | useSubEntry('withEsmImport'); 209 | expect(execWebpack().status).toBe(0); 210 | expectCommonDirToIncludeSameFilesAnd({ 211 | 'dist/index.js': noop, 212 | [`dist/withEsmImport.js`]: (t) => 213 | expect(t).toIncludeMultiple([ 214 | 'require("./node_modules/colorette/index.js")', 215 | 'Hi, there!', 216 | ]), 217 | 'dist/node_modules/colorette/index.js': noop, 218 | }); 219 | const { status, stdout } = execNode('dist/index.js'); 220 | expect(status).toBe(0); 221 | expect(stdout).toInclude('Hi, there!'); 222 | } 223 | 224 | function subCaseWithCjsImport() { 225 | try { 226 | fs.rmSync('dist', { recursive: true }); 227 | } catch {} 228 | useSubEntry('withCjsImport'); 229 | expect(execWebpack().status).toBe(0); 230 | expectCommonDirToIncludeSameFilesAnd({ 231 | 'dist/index.js': noop, 232 | [`dist/withCjsImport.js`]: (t) => 233 | expect(t).toIncludeMultiple([ 234 | 'require("./node_modules/colorette/index.cjs")', 235 | 'Hi, there!', 236 | ]), 237 | 'dist/node_modules/colorette/index.cjs': noop, 238 | }); 239 | const { status, stdout } = execNode('dist/index.js'); 240 | expect(status).toBe(0); 241 | expect(stdout).toInclude('Hi, there!'); 242 | } 243 | } 244 | ); 245 | }); 246 | 247 | describe('with loader helpers indirectly included from node_modules', () => { 248 | it('handles the included loader helpers in the same way as the imported deps', () => { 249 | setupWebpackProject({ 250 | 'webpack.config.js': ` 251 | const Plugin = require(${JSON.stringify(rootPath)}); 252 | module.exports = { 253 | ${webpackConfigReusable} 254 | entry: './src/index.js', 255 | module: { 256 | rules: [ 257 | { 258 | test: /\\.hbs$/, 259 | use: 'handlebars-loader', 260 | } 261 | ] 262 | }, 263 | plugins: [new Plugin()], 264 | }; 265 | `, 266 | 'src/index.js': ` 267 | import assert from 'node:assert'; 268 | import testHbs from './test.hbs'; 269 | 270 | console.log(testHbs({ title: 'Hi, there!' })); 271 | `, 272 | 'src/test.hbs': ` 273 |

{{title}}

274 | `, 275 | 'package.json': evaluateMustHavePackageJsonText({ 276 | ['devDependencies']: { 277 | ['handlebars-loader']: '^1.7.2', 278 | }, 279 | }), 280 | }); 281 | expect(execWebpack().status).toBe(0); 282 | expectCommonDirToIncludeAllFilesAnd({ 283 | 'dist/index.js': (t) => expect(t).toInclude('require("./test.hbs")'), 284 | 'dist/test.hbs': noop, 285 | 'dist/node_modules/handlebars/runtime.js': noop, 286 | }); 287 | const { status, stdout } = execNode('dist/index.js'); 288 | expect(status).toBe(0); 289 | expect(stdout).toInclude('

Hi, there!

'); 290 | }); 291 | }); 292 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |

Transpile Webpack Plugin

6 |
7 | 8 | [![npm][npm]][npm-url] 9 | [![node][node]][node-url] 10 | [![download][download]][npm-url] 11 | [![license][license]][license-url] 12 | [![size][size]][size-url] 13 | [![cicd][cicd]][cicd-url] 14 | 15 | # Transpile Webpack Plugin 16 | 17 | The webpack plugin that transpiles input files into output files individually without bundling together. 18 | 19 | Input files are collected from files directly or indirectly imported by the [entry](https://webpack.js.org/configuration/entry-context/#entry), then get compiled and ouputted keeping the same directory structure in the output directory. 20 | 21 | Transpiling with webpack is especially helpful when source file path based logics are involved, such as registering events in AWS Lambda, or migrations managing with Sequelize CLI. 22 | 23 | Notice that this plugin replies on features of webpack v5. The latest webpack is supposed to be used when possible. 24 | 25 | ## Getting Started 26 | 27 | To begin, you'll need to install `transpile-webpack-plugin`: 28 | 29 | ```sh 30 | npm i -D transpile-webpack-plugin 31 | ``` 32 | 33 | Or, any other package manager you prefer, like `yarn` or `pnpm`, would work, too. 34 | 35 | Then, add the plugin to your webpack config. For example: 36 | 37 | ```js 38 | const TranspilePlugin = require('transpile-webpack-plugin'); 39 | 40 | module.exports = { 41 | entry: './src/index.js', 42 | output: { 43 | path: __dirname + '/dist', 44 | }, 45 | plugins: [new TranspilePlugin(/* options */)], 46 | }; 47 | ``` 48 | 49 | Now, assuming in the dir `src`, the entry file `src/index.js` imports another file `src/constants/greeting.js`: 50 | 51 | ```sh 52 | src 53 | ├── constants 54 | │   └── greeting.js 55 | └── index.js 56 | ``` 57 | 58 | With the webpack config above, after compilation, output files will be: 59 | 60 | ```sh 61 | dist 62 | ├── constants 63 | │   └── greeting.js 64 | └── index.js 65 | ``` 66 | 67 | Files `src/index.js` and `src/constants/greeting.js` are collected as input files. Then, the common dir of input files is used as the base dir to evaluate the relative paths of output files in the output dir `dist`, which results in output files `dist/index.js` and `dist/constants/greeting.js`. 68 | 69 | Just a reminder, if to run output files with NodeJS, don't forget to set the [target](https://webpack.js.org/configuration/target/) as `node` or a node-compatible value so that no breaking code is generated by webpack unexpectedly. 70 | 71 | ## Blogs 72 | 73 | - [Server-side rendering for any React app on any FaaS provider - elaborating by a demo app of SSR with CRA on Netlify](https://github.com/licg9999/server-side-rendering-with-cra-on-netlify) 74 | - [Transpile Webpack Plugin: transpiling files with webpack without bundling](https://medium.com/@licg9999/introducing-transpile-webpack-plugin-b8c86c7b0a21) ([中文版本](https://segmentfault.com/a/1190000043177608)) 75 | 76 | ## Options 77 | 78 | - **[exclude](#exclude)** 79 | - **[hoistNodeModules](#hoistnodemodules)** 80 | - **[longestCommonDir](#longestcommondir)** 81 | - **[extentionMapping](#extentionmapping)** 82 | - **[preferResolveByDependencyAsCjs](#preferresolvebydependencyascjs)** 83 | 84 | ### `exclude` 85 | 86 | Type: `{string|RegExp|((p: string) => boolean)|string[]|RegExp[]|((p: string) => boolean)[]}` 87 | 88 | Default: `[]` 89 | 90 | Option `exclude` indicates files to be excluded. It's similar to the [externals](https://webpack.js.org/configuration/externals/) except that the import paths to the excluded are properly adjusted automatically. It's useful when you copy some third-party files and want to use them as they are. 91 | 92 | Though, excluding `node_modules` with this option is not a good idea. Some helpers of webpack loaders have to be compiled before they become runnable. If you need to exclude dependencies in `node_modules`, using [webpack-node-externals](https://github.com/liady/webpack-node-externals) might be a better choice because it doesn't exclude helpers of webpack loaders. 93 | 94 | With this option as a string (or strings), input files whose aboslute paths begin with it will be excluded. With this option as a regular expression (or regular expression), input files whose absolute paths match it will be excluded. With this option as a function (or functions), input files whose absolute paths are passed into the call of it and end up with `true` will be excluded. 95 | 96 | ### `hoistNodeModules` 97 | 98 | Type: `{boolean}` 99 | 100 | Default: `true` 101 | 102 | Option `hoistNodeModules` indicates whether to evaluate output paths for input files inside or outside `node_modules` separately, then keep input files from `node_modules` outputted into `node_modules` just under the output dir. It's usable to flatten the output directory structure a little bit. 103 | 104 | Given input files `src/index.js`, `node_modules/lodash/lodash.js` and the output dir `dist`, with this option `true`, output files will be `dist/index.js` and `dist/node_modules/lodash/lodash.js`. But with this option `false`, output files will be `dist/src/index.js` and `dist/node_modules/lodash/lodash.js`. 105 | 106 | ### `longestCommonDir` 107 | 108 | Type: `{string|undefined}` 109 | 110 | Default: `undefined` 111 | 112 | Option `longestCommonDir` indicates the limit of the common dir to evaluate relative paths of output files in the output dir. When the dir that this option represents is the parent dir of the common dir of input files, this option is used against input files to evaluate relative paths of output files in the output dir. Otherwise, the common dir of input files is used. 113 | 114 | Given input files `src/server/index.js`, `src/server/constants/greeting.js` and the output dir `dist`, with this option `undefined`, output files will be `dist/index.js` `dist/constants/greeting.js`. But with this option `'./src'`, output files will be `dist/server/index.js` `dist/server/constants/greeting.js`. 115 | 116 | Though, given input files `src/index.js`, `src/server/constants/greeting.js` and the output dir `dist`, with this option `'./src/server'`, output files will still be `dist/index.js` `dist/server/constants/greeting.js` because the dir that this options represents is not the parent dir of the common dir of input files. 117 | 118 | ### `extentionMapping` 119 | 120 | Type: `{Record}` 121 | 122 | Default: `{}` 123 | 124 | Option `extentionMapping` indicates how file extensions are mapped from the input to the output. By default, an output file will have exactly the same file extension as its input file. But you may change the behavior by this option. With this option `{ '.ts': '.js' }`, any input file with ext `.ts` will have the output file with ext `.js`. 125 | 126 | ### `preferResolveByDependencyAsCjs` 127 | 128 | Type: `boolean` 129 | 130 | Default: `true` 131 | 132 | Options `preferResolveByDependencyAsCjs` indicates whether to try to resolve dependencies by CommonJS [exports](https://nodejs.org/api/packages.html#conditional-exports) regardless of types of import statements. It's useful when the target is `node` because `.mjs` files are treated as ES modules in NodeJS and [can't be required](https://nodejs.org/api/esm.html#require) by webpack generated CommonJS files. 133 | 134 | Given `{ "exports": { "import": "index.mjs", "require": "index.cjs" } }` in `package.json` of a dependency, with this option `true`, either `import` or `require` to this dependency will end up with `index.cjs` imported. While, with this option `false`, `import` ends up with `index.mjs` imported and `require` ends up with `index.cjs` imported (, which is the default behavior of webpack). 135 | 136 | ## Known limits 137 | 138 | **01: Can't handle circular dependencies in the same way as NodeJS.** 139 | 140 | In NodeJS, top-level logics in a file run exactly at the time when it's imported, which makes circular dependencies possible to work. Take an example of files `a.js` and `b.js`: 141 | 142 | ```js 143 | // In file 'a.js' 144 | const b = require('./b'); 145 | 146 | function main() { 147 | b.goo(); 148 | } 149 | 150 | function foo() { 151 | console.log('lorem ipsum'); 152 | } 153 | 154 | module.exports = { foo }; 155 | 156 | main(); 157 | 158 | // In file 'b.js' 159 | 160 | const a = require('./a'); 161 | 162 | function goo() { 163 | a.foo(); 164 | } 165 | 166 | module.exports = { goo }; 167 | ``` 168 | 169 | When `a.js` runs, an error of `TypeError: a.foo is not a function` thrown from `b.js`. But putting the line `const b = require('./b');` just after `module.exports = { foo };` resolves the problem: 170 | 171 | ```diff 172 | // In file 'a.js' 173 | - 174 | -const b = require('./b'); 175 | 176 | function main() { 177 | b.goo(); 178 | } 179 | 180 | function foo() { 181 | console.log('lorem ipsum'); 182 | } 183 | 184 | module.exports = { foo }; 185 | + 186 | +const b = require('./b'); 187 | 188 | main(); 189 | ``` 190 | 191 | Though, for a webpack generated file, the real exporting is always done in the end of it. Webpack collects all the exports into an internal variable `__webpack_exports__`, then exports it at last, which makes circular dependencies always break. 192 | 193 | Making circular dependencies is a bad practice. But you might have to face them if using some libs that are popular but maintained since the early releases of NodeJS, like [jsdom](https://github.com/jsdom/jsdom). When this happens, please use the [externals](https://webpack.js.org/configuration/externals/) to leave the libs untouched. 194 | 195 | **02: Can't conditionally import not-yet-installed dependencies.** 196 | 197 | Webpack always detects and resolves import statements regardless of whether they run conditionally. Logics as below end up with the conditionally imported dependency `colorette` resolved: 198 | 199 | ```js 200 | function print(message, color) { 201 | if (typeof color === 'string') { 202 | message = require('colorette')[color](message); 203 | } 204 | console.log(message); 205 | } 206 | ``` 207 | 208 | As a result, conditionally importing any not-yet-installed dependency causes the compile-time error of `Module not found` in webpack. So, either, you need to make sure the conditionally imported dependency installed. Or, use the [externals](https://webpack.js.org/configuration/externals/) to leave it untouched. 209 | 210 | **03: Can't import `.node` files directly.** 211 | 212 | By default, importing `.node` files causes the compile-time error of `Module parse failed` in webpack. Using [node-loader](http://github.com/webpack-contrib/node-loader) along with the plugin option [extentionMapping](#extentionmapping) as `{ '.node': '.js' }` resolves some very basic cases. But as the node-loader itself doesn't handle paths well, the practice is not recommeneded. Instead, you may use the [externals](https://webpack.js.org/configuration/externals/) to leave the JS files that use the `.node` files untouched. 213 | 214 | ## Contributing 215 | 216 | Please take a moment to read our contributing guidelines if you haven't yet done so. 217 | [CONTRIBUTING][contributing-url] 218 | 219 | ## License 220 | 221 | [MIT][license-url] 222 | 223 | [npm]: https://img.shields.io/npm/v/transpile-webpack-plugin.svg 224 | [npm-url]: https://npmjs.com/package/transpile-webpack-plugin 225 | [node-url]: https://nodejs.org/ 226 | [node]: https://img.shields.io/node/v/transpile-webpack-plugin.svg 227 | [download]: https://img.shields.io/npm/dw/transpile-webpack-plugin 228 | [license]: https://img.shields.io/github/license/licg9999/transpile-webpack-plugin 229 | [license-url]: https://github.com/licg9999/transpile-webpack-plugin/blob/master/LICENSE 230 | [size]: https://packagephobia.com/badge?p=transpile-webpack-plugin 231 | [size-url]: https://packagephobia.com/result?p=transpile-webpack-plugin 232 | [cicd]: https://github.com/licg9999/transpile-webpack-plugin/actions/workflows/verify-and-release.yml/badge.svg 233 | [cicd-url]: https://github.com/licg9999/transpile-webpack-plugin/actions/workflows/verify-and-release.yml 234 | [contributing-url]: https://github.com/licg9999/transpile-webpack-plugin/blob/master/CONTRIBUTING.md 235 | -------------------------------------------------------------------------------- /src/TranspileWebpackPlugin.ts: -------------------------------------------------------------------------------- 1 | import os from 'node:os'; 2 | import path from 'node:path'; 3 | import { promisify } from 'node:util'; 4 | 5 | import { clone } from 'lodash'; 6 | import { validate } from 'schema-utils'; 7 | 8 | import { commonDirSync } from './commonDir'; 9 | import { 10 | alignResolveByDependency, 11 | enableBuiltinNodeGlobalsByDefault, 12 | forceDisableOutputModule, 13 | forceDisableSplitChunks, 14 | forceSetLibraryType, 15 | isTargetNodeCompatible, 16 | throwErrIfHotModuleReplacementEnabled, 17 | throwErrIfOutputPathNotSpecified, 18 | } from './compilerOptions'; 19 | import { Condition, createConditionTest } from './conditionTest'; 20 | import { 21 | baseNodeModules, 22 | externalModuleTypeCjs, 23 | extJson, 24 | hookStageVeryEarly, 25 | outputLibraryTypeCjs, 26 | pluginName, 27 | reMjsFile, 28 | reNodeModules, 29 | resolveByDependencyTypeCjs, 30 | sourceTypeAsset, 31 | } from './constants'; 32 | import optionsSchema from './optionsSchema.json'; 33 | import { 34 | Compiler, 35 | Dependency, 36 | ExternalModule, 37 | Module, 38 | NormalModule, 39 | sources, 40 | WebpackError, 41 | } from './peers/webpack'; 42 | import ModuleProfile from './peers/webpack/lib/ModuleProfile'; 43 | import { SourceMapDevToolPluginController } from './SourceMapDevToolPluginController'; 44 | import { TerserWebpackPluginController } from './TerserWebpackPluginController'; 45 | import { TranspileExternalModule } from './TranspileExternalModule'; 46 | import { walkDependencies, walkDependenciesSync } from './walkDependencies'; 47 | 48 | const { RawSource } = sources; 49 | 50 | export type TranspileWebpackPluginOptions = Partial; 51 | 52 | export interface TranspileWebpackPluginInternalOptions { 53 | exclude: Condition; 54 | hoistNodeModules: boolean; 55 | longestCommonDir?: string; 56 | extentionMapping: Record; 57 | preferResolveByDependencyAsCjs: boolean; 58 | } 59 | 60 | export class TranspileWebpackPlugin { 61 | options: TranspileWebpackPluginInternalOptions; 62 | sourceMapDevToolPluginController: SourceMapDevToolPluginController; 63 | terserWebpackPluginController: TerserWebpackPluginController; 64 | 65 | constructor(options: TranspileWebpackPluginOptions = {}) { 66 | validate(optionsSchema as object, options, { 67 | name: pluginName, 68 | baseDataPath: 'options', 69 | }); 70 | 71 | this.options = { 72 | ...options, 73 | exclude: options.exclude ?? [], 74 | hoistNodeModules: options.hoistNodeModules ?? true, 75 | extentionMapping: options.extentionMapping ?? {}, 76 | preferResolveByDependencyAsCjs: options.preferResolveByDependencyAsCjs ?? true, 77 | }; 78 | this.sourceMapDevToolPluginController = new SourceMapDevToolPluginController(); 79 | this.terserWebpackPluginController = new TerserWebpackPluginController(); 80 | } 81 | 82 | apply(compiler: Compiler) { 83 | const { 84 | exclude, 85 | hoistNodeModules, 86 | longestCommonDir, 87 | extentionMapping, 88 | preferResolveByDependencyAsCjs, 89 | } = this.options; 90 | 91 | forceDisableSplitChunks(compiler.options); 92 | forceSetLibraryType(compiler.options, outputLibraryTypeCjs); 93 | forceDisableOutputModule(compiler.options); 94 | 95 | const isPathExcluded = createConditionTest(exclude); 96 | const isPathInNodeModules = createConditionTest(reNodeModules); 97 | const isPathMjsFile = createConditionTest(reMjsFile); 98 | 99 | this.sourceMapDevToolPluginController.apply(compiler); 100 | this.terserWebpackPluginController.apply(compiler); 101 | 102 | compiler.hooks.environment.tap({ name: pluginName, stage: hookStageVeryEarly }, () => { 103 | throwErrIfOutputPathNotSpecified(compiler.options); 104 | throwErrIfHotModuleReplacementEnabled(compiler.options); 105 | 106 | if (isTargetNodeCompatible(compiler.options.target)) { 107 | enableBuiltinNodeGlobalsByDefault(compiler.options); 108 | } 109 | 110 | if (preferResolveByDependencyAsCjs) { 111 | alignResolveByDependency(compiler.options, resolveByDependencyTypeCjs); 112 | } 113 | }); 114 | 115 | compiler.hooks.finishMake.tapPromise(pluginName, async (compilation) => { 116 | // Only evaluates new entries in the main compilation. 117 | if (compilation.compiler !== compiler) return; 118 | 119 | const outputPath = compiler.options.output.path!; 120 | const outputPathOfNodeModules = path.resolve(outputPath, baseNodeModules); 121 | const context = compiler.options.context!; 122 | 123 | const entryDeps = new Map(); 124 | const touchedMods = new Set(); 125 | for (const e of compilation.entries.values()) { 126 | for (const d of e.dependencies) { 127 | collectEntryDepsRecursively(d); 128 | } 129 | } 130 | touchedMods.clear(); 131 | 132 | const entryResourcePaths = Array.from(entryDeps.keys()); 133 | const entryResourcePathsWoNodeModules = entryResourcePaths.filter( 134 | (p) => !isPathInNodeModules(p) 135 | ); 136 | 137 | if (entryResourcePathsWoNodeModules.length === 0) { 138 | throw new Error(`${pluginName}${os.EOL}No entry is found outside 'node_modules'`); 139 | } 140 | 141 | if (isTargetNodeCompatible(compiler.options.target)) { 142 | const entryResourceMjsFiles = entryResourcePaths.filter(isPathMjsFile); 143 | if (entryResourceMjsFiles.length > 0) { 144 | const warning = new WebpackError( 145 | `${pluginName}${os.EOL}Might be problematic to run '.mjs' files with target 'node'. Found '.mjs' files:${os.EOL}` + 146 | entryResourceMjsFiles 147 | .map((p) => ` .${path.sep}${path.relative(context, p)}`) 148 | .join(os.EOL) 149 | ); 150 | compilation.warnings.push(warning); 151 | } 152 | } 153 | 154 | const commonDir = commonDirSync(entryResourcePaths, { 155 | context, 156 | longestCommonDir, 157 | }); 158 | const commonDirWoNodeModules = commonDirSync(entryResourcePathsWoNodeModules, { 159 | context, 160 | longestCommonDir, 161 | }); 162 | for (const entryDep of entryDeps.values()) { 163 | await makeExtDepsRecursively(entryDep); 164 | } 165 | touchedMods.clear(); 166 | 167 | const entries = new Map() as typeof compilation.entries; 168 | const entryExtentionsToHaveSourceMaps = new Set(); 169 | makeEntriesAndCollectEntryExtentions(); 170 | entryDeps.clear(); 171 | this.sourceMapDevToolPluginController.setExtensionsToHaveSourceMaps( 172 | entryExtentionsToHaveSourceMaps 173 | ); 174 | entryExtentionsToHaveSourceMaps.clear(); 175 | this.terserWebpackPluginController.setNamesToBeMinimized(entries.keys()); 176 | compilation.entries.clear(); 177 | compilation.entries = entries; 178 | 179 | /* **** */ 180 | 181 | function collectEntryDepsRecursively(entryDep: Dependency): void { 182 | const entryMod = compilation.moduleGraph.getModule(entryDep); 183 | if (!entryMod || touchedMods.has(entryMod)) return; 184 | 185 | if (entryMod instanceof NormalModule) { 186 | const isEntryResourceLocalFile = path.isAbsolute(entryMod.resource); 187 | const entryResourcePath = entryMod.resourceResolveData?.path; 188 | if ( 189 | isEntryResourceLocalFile && 190 | typeof entryResourcePath === 'string' && 191 | !isPathExcluded(entryResourcePath) 192 | ) { 193 | // Collects the dependency closest to root as the entry dependency. 194 | if (!entryDeps.has(entryResourcePath)) { 195 | entryDeps.set(entryResourcePath, entryDep); 196 | } 197 | } 198 | } 199 | 200 | touchedMods.add(entryMod); 201 | walkDependenciesSync(entryMod, collectEntryDepsRecursively); 202 | } 203 | 204 | async function makeExtDepsRecursively(entryDep: Dependency): Promise { 205 | const entryMod = compilation.moduleGraph.getModule(entryDep); 206 | if (!entryMod || touchedMods.has(entryMod)) return; 207 | 208 | let isEntryModExcluded = false; 209 | if (entryMod instanceof NormalModule) { 210 | const entryResourcePath = entryMod.resourceResolveData?.path; 211 | isEntryModExcluded = 212 | typeof entryResourcePath !== 'string' || 213 | !entryResourcePaths.includes(entryResourcePath); 214 | } 215 | if (isEntryModExcluded) return; 216 | 217 | const allDependencies = new Set(entryMod.dependencies); 218 | for (const b of entryMod.blocks) { 219 | for (const d of b.dependencies) { 220 | allDependencies.add(d); 221 | } 222 | } 223 | 224 | await walkDependencies(entryMod, (d, i, deps) => 225 | makeExtDepToReplaceChildDepIfNotInSameResourcePath(deps, i, entryDep) 226 | ); 227 | 228 | touchedMods.add(entryMod); 229 | for (const d of allDependencies) await makeExtDepsRecursively(d); 230 | } 231 | 232 | async function makeExtDepToReplaceChildDepIfNotInSameResourcePath( 233 | childDependencies: Dependency[], 234 | childDepIndex: number, 235 | entryDep: Dependency 236 | ): Promise { 237 | const childDep = childDependencies[childDepIndex]; 238 | const childMod = compilation.moduleGraph.getModule(childDep); 239 | 240 | if (!(childMod instanceof NormalModule)) return; 241 | const childResourcePath: unknown = childMod.resourceResolveData?.path; 242 | 243 | const entryMod = compilation.moduleGraph.getModule(entryDep); 244 | const entryParentMod = compilation.moduleGraph.getParentModule(entryDep); 245 | let entryResourcePath: unknown; 246 | if (entryMod instanceof NormalModule) { 247 | entryResourcePath = entryMod.resourceResolveData?.path; 248 | } else if (entryParentMod instanceof NormalModule) { 249 | entryResourcePath = entryParentMod.resourceResolveData?.path; 250 | } else return; 251 | 252 | if (typeof childResourcePath !== 'string' || typeof entryResourcePath !== 'string') return; 253 | if (childResourcePath === entryResourcePath) return; 254 | 255 | // Makes the requireable relative path for the external mod. 256 | const entryBundlePath = evaluateBundlePath(entryResourcePath); 257 | const childBundlePath = evaluateBundlePath(childResourcePath); 258 | let request = path.relative(path.dirname(entryBundlePath), childBundlePath); 259 | if (!path.isAbsolute(request) && !request.startsWith('.')) { 260 | request = `.${path.sep}${request}`; 261 | } 262 | request = request.replace(/\\/g, path.posix.sep); 263 | 264 | const extModCandidate = new TranspileExternalModule( 265 | request, 266 | externalModuleTypeCjs, 267 | entryResourcePath 268 | ); 269 | let extMod = compilation.getModule(extModCandidate); 270 | let doesExtModNeedBuild = false; 271 | if (!(extMod instanceof ExternalModule)) { 272 | if (compilation.profile) { 273 | compilation.moduleGraph.setProfile(extModCandidate, new ModuleProfile()); 274 | } 275 | const extModReturned = await promisify(compilation.addModule).call( 276 | compilation, 277 | extModCandidate 278 | ); 279 | // Uses extModReturned prior to extModCandidate in case some cached module 280 | // is used in compilation.addModule. 281 | extMod = extModReturned ?? extModCandidate; 282 | doesExtModNeedBuild = true; 283 | } 284 | 285 | // Clones child dep to make external dep for connecting external mod so that 286 | // connections of child dep get preserved for making entries. 287 | const extDep = clone(childDep); 288 | childDependencies[childDepIndex] = extDep; 289 | 290 | const childConnection = compilation.moduleGraph.getConnection(childDep); 291 | if (childConnection) { 292 | const entryMgm = compilation.moduleGraph._getModuleGraphModule(entryMod); 293 | entryMgm.outgoingConnections.delete(childConnection); 294 | } 295 | 296 | compilation.moduleGraph.setResolvedModule(entryMod, extDep, extMod); 297 | compilation.moduleGraph.setIssuerIfUnset(extMod, entryMod); 298 | 299 | if (doesExtModNeedBuild) { 300 | await promisify(compilation.buildModule).call(compilation, extMod); 301 | } 302 | } 303 | 304 | function evaluateBundlePath(resourcePath: string): string { 305 | let bundlePath = resourcePath; 306 | if (entryResourcePaths.includes(resourcePath)) { 307 | if (hoistNodeModules) { 308 | const matchesNodeModules = resourcePath.match(reNodeModules); 309 | if (matchesNodeModules) { 310 | bundlePath = path.resolve( 311 | outputPathOfNodeModules, 312 | resourcePath.substring(matchesNodeModules.index! + matchesNodeModules[0].length) 313 | ); 314 | } else { 315 | bundlePath = path.resolve( 316 | outputPath, 317 | path.relative(commonDirWoNodeModules, resourcePath) 318 | ); 319 | } 320 | } else { 321 | bundlePath = path.resolve(outputPath, path.relative(commonDir, resourcePath)); 322 | } 323 | 324 | const bundlePathParsed = path.parse(bundlePath); 325 | const bundlePathNewExt = extentionMapping[bundlePathParsed.ext]; 326 | if (typeof bundlePathNewExt === 'string') { 327 | bundlePath = path.format({ 328 | ...bundlePathParsed, 329 | ext: bundlePathNewExt, 330 | base: bundlePathParsed.name + bundlePathNewExt, 331 | }); 332 | } 333 | } 334 | return bundlePath; 335 | } 336 | 337 | function evaluateBundleRelPath(resourcePath: string): string { 338 | return path.relative(outputPath, evaluateBundlePath(resourcePath)); 339 | } 340 | 341 | function makeEntriesAndCollectEntryExtentions(): void { 342 | for (const [entryResourcePath, entryDep] of entryDeps) { 343 | const entryBundleRelPath = evaluateBundleRelPath(entryResourcePath); 344 | const entryBundleRelPathParsed = path.parse(entryBundleRelPath); 345 | 346 | if (entryBundleRelPathParsed.ext === extJson) { 347 | emitJson(entryBundleRelPath, entryResourcePath, entryDep); 348 | } else { 349 | assignEntry(entryBundleRelPath, entryDep); 350 | 351 | const entryMod = compilation.moduleGraph.getModule(entryDep); 352 | if (entryMod) { 353 | if (!entryMod.type.startsWith(sourceTypeAsset)) { 354 | entryExtentionsToHaveSourceMaps.add(entryBundleRelPathParsed.ext); 355 | } 356 | } 357 | } 358 | } 359 | } 360 | 361 | function emitJson( 362 | entryBundleRelPath: string, 363 | entryResourcePath: string, 364 | entryDep: Dependency 365 | ): void { 366 | const entryMod = compilation.moduleGraph.getModule(entryDep); 367 | if (entryMod instanceof NormalModule) { 368 | const { jsonData } = entryMod.buildInfo; 369 | if (!jsonData) { 370 | throw new Error( 371 | `${pluginName}${os.EOL}File '${path.relative( 372 | context, 373 | entryResourcePath 374 | )}' is not type of JSON` 375 | ); 376 | } 377 | entryMod.buildInfo.assets = { 378 | [entryBundleRelPath]: new RawSource(JSON.stringify(jsonData.get())), 379 | }; 380 | } 381 | } 382 | 383 | function assignEntry(entryBundleRelPath: string, entryDep: Dependency): void { 384 | const name = entryBundleRelPath; 385 | const filename = entryBundleRelPath; 386 | entries.set(name, { 387 | dependencies: [entryDep], 388 | includeDependencies: [], 389 | options: { name, filename }, 390 | }); 391 | } 392 | }); 393 | } 394 | } 395 | -------------------------------------------------------------------------------- /e2e/handles-non-js.spec.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | 3 | import { noop } from 'lodash'; 4 | 5 | import { 6 | evaluateMustHavePackageJsonText, 7 | execNode, 8 | execWebpack, 9 | expectCommonDirToIncludeAllFilesAnd, 10 | expectCommonDirToIncludeSameFiles, 11 | expectCommonDirToIncludeSameFilesAnd, 12 | rootPath, 13 | setupWebpackProject, 14 | } from '../support-helpers'; 15 | 16 | const webpackConfigReusable = ` 17 | mode: 'production', 18 | target: 'node', 19 | output: { 20 | path: __dirname + '/dist', 21 | }, 22 | `; 23 | 24 | describe('handles json', () => { 25 | it('with default JSON parser, outputs .json file as JSON file', () => { 26 | setupWebpackProject({ 27 | 'webpack.config.js': ` 28 | const Plugin = require(${JSON.stringify(rootPath)}); 29 | module.exports = { 30 | ${webpackConfigReusable} 31 | entry: './src/index.js', 32 | plugins: [new Plugin()], 33 | }; 34 | `, 35 | 'src/index.js': ` 36 | import { greeting } from './constants.json'; 37 | console.log(greeting); 38 | `, 39 | 'src/constants.json': ` 40 | { "greeting": "Hi, there!" } 41 | `, 42 | }); 43 | expect(execWebpack().status).toBe(0); 44 | expectCommonDirToIncludeSameFilesAnd({ 45 | 'dist/index.js': (t) => expect(t).not.toInclude('Hi, there!'), 46 | 'dist/constants.json': (t) => expect(JSON.parse(t)).toEqual({ greeting: 'Hi, there!' }), 47 | }); 48 | const { stdout, status } = execNode('dist/index.js'); 49 | expect(status).toBe(0); 50 | expect(stdout).toInclude('Hi, there!'); 51 | }); 52 | 53 | it(`with default JSON parser, doesn't generate source-map file for outputted JSON file`, () => { 54 | setupWebpackProject({ 55 | 'webpack.config.js': ` 56 | const Plugin = require(${JSON.stringify(rootPath)}); 57 | module.exports = { 58 | ${webpackConfigReusable} 59 | entry: './src/index.js', 60 | devtool: 'source-map', 61 | plugins: [new Plugin()], 62 | }; 63 | `, 64 | 'src/index.js': ` 65 | import { greeting } from './constants.json'; 66 | console.log(greeting); 67 | `, 68 | 'src/constants.json': ` 69 | { "greeting": "Hi, there!" } 70 | `, 71 | }); 72 | expect(execWebpack().status).toBe(0); 73 | expectCommonDirToIncludeSameFilesAnd({ 74 | 'dist/index.js': (t) => expect(t).not.toInclude('Hi, there!'), 75 | 'dist/index.js.map': noop, 76 | 'dist/constants.json': (t) => expect(JSON.parse(t)).toEqual({ greeting: 'Hi, there!' }), 77 | }); 78 | const { stdout, status } = execNode('dist/index.js'); 79 | expect(status).toBe(0); 80 | expect(stdout).toInclude('Hi, there!'); 81 | }); 82 | 83 | it('with JSON5 parser, outputs .json file w/ comment as JSON file w/o comment', () => { 84 | setupWebpackProject({ 85 | 'webpack.config.js': ` 86 | const Plugin = require(${JSON.stringify(rootPath)}); 87 | const JSON5 = require('json5'); 88 | module.exports = { 89 | ${webpackConfigReusable} 90 | entry: './src/index.js', 91 | module: { 92 | rules: [ 93 | { 94 | test: /\\.json$/, 95 | parser: { 96 | parse: JSON5.parse, 97 | } 98 | } 99 | ] 100 | }, 101 | plugins: [new Plugin()] 102 | }; 103 | `, 104 | 'src/index.js': ` 105 | import { greeting } from './constants.json'; 106 | console.log(greeting); 107 | `, 108 | 'src/constants.json': ` 109 | { 110 | // First few words for a talk 111 | "greeting": "Hi, there!" 112 | } 113 | `, 114 | }); 115 | expect(execWebpack().status).toBe(0); 116 | expectCommonDirToIncludeSameFilesAnd({ 117 | 'dist/index.js': (t) => expect(t).not.toInclude('Hi, there!'), 118 | 'dist/constants.json': (t) => expect(JSON.parse(t)).toEqual({ greeting: 'Hi, there!' }), 119 | }); 120 | const { stdout, status } = execNode('dist/index.js'); 121 | expect(status).toBe(0); 122 | expect(stdout).toInclude('Hi, there!'); 123 | }); 124 | 125 | it('with JSON5 parser, outputs file with ext other than .json as equivalent same-ext JS file', () => { 126 | setupWebpackProject({ 127 | 'webpack.config.js': ` 128 | const Plugin = require(${JSON.stringify(rootPath)}); 129 | const JSON5 = require('json5'); 130 | module.exports = { 131 | ${webpackConfigReusable} 132 | entry: './src/index.js', 133 | module: { 134 | rules: [ 135 | { 136 | test: /\\.jsonc$/, 137 | type: 'json', 138 | parser: { 139 | parse: JSON5.parse, 140 | }, 141 | } 142 | ], 143 | }, 144 | plugins: [new Plugin()], 145 | }; 146 | `, 147 | 'src/index.js': ` 148 | import { greeting } from './constants.jsonc'; 149 | console.log(greeting); 150 | `, 151 | 'src/constants.jsonc': ` 152 | { 153 | // First few words for a talk 154 | "greeting": "Hi, there!" 155 | } 156 | `, 157 | }); 158 | expect(execWebpack().status).toBe(0); 159 | expectCommonDirToIncludeSameFilesAnd({ 160 | 'dist/index.js': (t) => expect(t).not.toInclude('Hi, there!'), 161 | 'dist/constants.jsonc': (t) => expect(t).toIncludeMultiple(['module.exports', 'Hi, there!']), 162 | }); 163 | const { stdout, status } = execNode('dist/index.js'); 164 | expect(status).toBe(0); 165 | expect(stdout).toInclude('Hi, there!'); 166 | }); 167 | }); 168 | 169 | describe('handles url', () => { 170 | it('bundles data url into output file of the importer', () => { 171 | setupWebpackProject({ 172 | 'webpack.config.js': ` 173 | const Plugin = require(${JSON.stringify(rootPath)}); 174 | module.exports = { 175 | ${webpackConfigReusable} 176 | entry: './src/index.js', 177 | plugins: [new Plugin()], 178 | }; 179 | `, 180 | 'src/index.js': ` 181 | import { greeting } from 'data:text/javascrip,export const greeting = \\'Hi, there!\\''; 182 | console.log(greeting); 183 | `, 184 | }); 185 | expect(execWebpack().status).toBe(0); 186 | expectCommonDirToIncludeSameFilesAnd({ 187 | 'dist/index.js': (t) => expect(t).toInclude('Hi, there!'), 188 | }); 189 | const { stdout, status } = execNode('dist/index.js'); 190 | expect(status).toBe(0); 191 | expect(stdout).toInclude('Hi, there!'); 192 | }); 193 | 194 | it('bundles http url into output file of the importer', () => { 195 | setupWebpackProject({ 196 | 'webpack.config.js': ` 197 | const Plugin = require(${JSON.stringify(rootPath)}); 198 | module.exports = { 199 | ${webpackConfigReusable} 200 | entry: './src/index.js', 201 | experiments: { 202 | buildHttp: { 203 | allowedUris: ['https://cdn.skypack.dev/'], 204 | frozen: false, 205 | }, 206 | }, 207 | plugins: [new Plugin()], 208 | }; 209 | `, 210 | 'src/index.js': ` 211 | import { upperCase } from 'https://cdn.skypack.dev/lodash'; 212 | console.log(upperCase('Hi, there!')); 213 | `, 214 | }); 215 | expect(execWebpack().status).toBe(0); 216 | expectCommonDirToIncludeSameFilesAnd({ 217 | 'dist/index.js': (t) => expect(t).toIncludeMultiple(['.upperCase', '.cloneDeep']), 218 | }); 219 | const { stdout, status } = execNode('dist/index.js'); 220 | expect(status).toBe(0); 221 | expect(stdout).toInclude('HI THERE'); 222 | }); 223 | }); 224 | 225 | describe('handles asset', () => { 226 | it( 227 | 'with asset/resource, ' + 228 | 'outputs asset file as same-ext JS file exporting url to the emitted asset', 229 | () => { 230 | setupWebpackProject({ 231 | 'webpack.config.js': ` 232 | const Plugin = require(${JSON.stringify(rootPath)}); 233 | module.exports = { 234 | ${webpackConfigReusable.replace( 235 | /output:\s*\{([^}]*)\}/s, 236 | `output: { 237 | $1 238 | publicPath: '/public/', 239 | }` 240 | )} 241 | entry: './src/index.js', 242 | module: { 243 | rules: [ 244 | { 245 | test: /\\.txt$/, 246 | type: 'asset/resource', 247 | generator: { 248 | emit: false, 249 | }, 250 | } 251 | ], 252 | }, 253 | plugins: [new Plugin()], 254 | }; 255 | `, 256 | 'src/index.js': ` 257 | import textsUrl from './texts.txt'; 258 | console.log(textsUrl); 259 | `, 260 | 'src/texts.txt': 'Hi, there!', 261 | }); 262 | expect(execWebpack().status).toBe(0); 263 | expectCommonDirToIncludeSameFilesAnd({ 264 | 'dist/index.js': (t) => { 265 | expect(t).not.toInclude('Hi, there!'); 266 | expect(t).toInclude('require("./texts.txt")'); 267 | }, 268 | 'dist/texts.txt': (t) => expect(t).toIncludeMultiple(['/public/', '.txt']), 269 | }); 270 | const { stdout, status } = execNode('dist/index.js'); 271 | expect(status).toBe(0); 272 | expect(stdout).toIncludeMultiple(['/public/', '.txt']); 273 | } 274 | ); 275 | 276 | it( 277 | 'with asset/inline, ' + 278 | 'outputs asset file as same-ext JS file exporting data url of the asset', 279 | () => { 280 | setupWebpackProject({ 281 | 'webpack.config.js': ` 282 | const Plugin = require(${JSON.stringify(rootPath)}); 283 | module.exports = { 284 | ${webpackConfigReusable} 285 | entry: './src/index.js', 286 | module: { 287 | rules: [ 288 | { 289 | test: /\\.txt$/, 290 | type: 'asset/inline', 291 | } 292 | ], 293 | }, 294 | plugins: [new Plugin()], 295 | }; 296 | `, 297 | 'src/index.js': ` 298 | import { Buffer } from 'node:buffer'; 299 | import textsDataUrl from './texts.txt'; 300 | console.log( 301 | Buffer.from(textsDataUrl.substring(textsDataUrl.indexOf(',') + 1), 'base64').toString('utf8') 302 | ); 303 | `, 304 | 'src/texts.txt': 'Hi, there!', 305 | }); 306 | expect(execWebpack().status).toBe(0); 307 | expectCommonDirToIncludeSameFilesAnd({ 308 | 'dist/index.js': (t) => { 309 | expect(t).not.toInclude('Hi, there!'); 310 | expect(t).toInclude('require("./texts.txt")'); 311 | }, 312 | 'dist/texts.txt': (t) => { 313 | expect(t).not.toInclude('Hi, there!'); 314 | expect(t).toInclude('data:text/plain;base64'); 315 | }, 316 | }); 317 | const { stdout, status } = execNode('dist/index.js'); 318 | expect(status).toBe(0); 319 | expect(stdout).toInclude('Hi, there!'); 320 | } 321 | ); 322 | 323 | it( 324 | 'with asset/source, ' + 325 | 'outputs asset file as same-ext JS file exporting source code of the asset', 326 | () => { 327 | setupWebpackProject({ 328 | 'webpack.config.js': ` 329 | const Plugin = require(${JSON.stringify(rootPath)}); 330 | module.exports = { 331 | ${webpackConfigReusable} 332 | entry: './src/index.js', 333 | module: { 334 | rules: [ 335 | { 336 | test: /\\.txt$/, 337 | type: 'asset/source', 338 | } 339 | ], 340 | }, 341 | plugins: [new Plugin()], 342 | }; 343 | `, 344 | 'src/index.js': ` 345 | import { Buffer } from 'node:buffer'; 346 | import textsContent from './texts.txt'; 347 | console.log(textsContent); 348 | `, 349 | 'src/texts.txt': 'Hi, there!', 350 | }); 351 | expect(execWebpack().status).toBe(0); 352 | expectCommonDirToIncludeSameFilesAnd({ 353 | 'dist/index.js': (t) => { 354 | expect(t).not.toInclude('Hi, there!'); 355 | expect(t).toInclude('require("./texts.txt")'); 356 | }, 357 | 'dist/texts.txt': (t) => expect(t).toInclude('Hi, there!'), 358 | }); 359 | const { stdout, status } = execNode('dist/index.js'); 360 | expect(status).toBe(0); 361 | expect(stdout).toInclude('Hi, there!'); 362 | } 363 | ); 364 | 365 | it(`with asset/*, doesn't generate source-map file for outputted JS file`, () => { 366 | setupWebpackProject({ 367 | 'webpack.config.js': ` 368 | const Plugin = require(${JSON.stringify(rootPath)}); 369 | module.exports = { 370 | ${webpackConfigReusable.replace( 371 | /output:\s*\{([^}]*)\}/s, 372 | `output: { 373 | $1 374 | publicPath: '/public/', 375 | }` 376 | )} 377 | entry: './src/index.js', 378 | devtool: 'source-map', 379 | module: { 380 | rules: [ 381 | { 382 | test: /\\.1\\.txt$/, 383 | type: 'asset/resource', 384 | generator: { 385 | emit: false, 386 | }, 387 | }, 388 | { 389 | test: /\\.2\\.txt$/, 390 | type: 'asset/inline', 391 | }, 392 | { 393 | test: /\\.3\\.txt$/, 394 | type: 'asset/source', 395 | }, 396 | ], 397 | }, 398 | plugins: [new Plugin()], 399 | }; 400 | `, 401 | 'src/index.js': ` 402 | import { Buffer } from 'node:buffer'; 403 | import t1 from './texts.1.txt'; 404 | import t2 from './texts.2.txt'; 405 | import t3 from './texts.3.txt'; 406 | console.log(t1, Buffer.from(t2.substring(t2.indexOf(',') + 1), 'base64').toString('utf8'), t3); 407 | `, 408 | 'src/texts.1.txt': 'Hi, t1!', 409 | 'src/texts.2.txt': 'Hi, t2!', 410 | 'src/texts.3.txt': 'Hi, t3!', 411 | }); 412 | expect(execWebpack().status).toBe(0); 413 | expectCommonDirToIncludeSameFilesAnd({ 414 | 'dist/index.js': (t) => { 415 | expect(t).not.toInclude('Hi'); 416 | expect(t).toIncludeMultiple([ 417 | 'require("./texts.1.txt")', 418 | 'require("./texts.2.txt")', 419 | 'require("./texts.3.txt")', 420 | ]); 421 | }, 422 | 'dist/index.js.map': noop, 423 | 'dist/texts.1.txt': (t) => expect(t).toIncludeMultiple(['/public/', '.txt']), 424 | 'dist/texts.2.txt': (t) => { 425 | expect(t).not.toInclude('Hi, t2!'); 426 | expect(t).toInclude('data:text/plain;base64'); 427 | }, 428 | 'dist/texts.3.txt': (t) => expect(t).toInclude('Hi, t3!'), 429 | }); 430 | const { stdout, status } = execNode('dist/index.js'); 431 | expect(status).toBe(0); 432 | expect(stdout).toIncludeMultiple(['/public/', '.txt', 'Hi, t2!', 'Hi, t3!']); 433 | }); 434 | }); 435 | 436 | describe('handles vue SFC', () => { 437 | it( 438 | 'with vue-loader and its plugin, ' + 439 | 'outputs .vue file as same-ext JS file containing logics of all blocks', 440 | () => { 441 | setupWebpackProject({ 442 | 'webpack.config.js': ` 443 | const Plugin = require(${JSON.stringify(rootPath)}); 444 | const { VueLoaderPlugin } = require('vue-loader'); 445 | module.exports = { 446 | ${webpackConfigReusable} 447 | entry: './src/index.js', 448 | module: { 449 | rules: [ 450 | { 451 | test: /\\.js/, 452 | use: 'babel-loader', 453 | }, 454 | { 455 | test: /\\.css/, 456 | use: [ 457 | 'vue-style-loader', 458 | 'css-loader' 459 | ] 460 | }, 461 | { 462 | test: /\\.vue$/, 463 | use: 'vue-loader', 464 | } 465 | ], 466 | }, 467 | plugins: [new Plugin(), new VueLoaderPlugin()], 468 | }; 469 | `, 470 | 'src/index.js': ` 471 | import { createSSRApp } from 'vue'; 472 | import { renderToString } from 'vue/server-renderer'; 473 | import App from './App.vue'; 474 | const app = createSSRApp(App); 475 | renderToString(app).then(console.log); 476 | `, 477 | 'src/App.vue': ` 478 | 487 | 488 | 491 | 492 | 497 | `, 498 | 'package.json': evaluateMustHavePackageJsonText({ 499 | ['dependencies']: { 500 | ['vue']: '^3.2.45', 501 | }, 502 | ['devDependencies']: { 503 | ['@babel/core']: '^7.20.5', 504 | ['babel-loader']: '^9.1.0', 505 | ['css-loader']: '^6.7.2', 506 | ['vue-loader']: '^17.0.1', 507 | ['vue-style-loader']: '^4.1.3', 508 | ['vue-template-compiler']: '^2.7.14', 509 | }, 510 | }), 511 | }); 512 | expect(execWebpack().status).toBe(0); 513 | expectCommonDirToIncludeAllFilesAnd({ 514 | 'dist/index.js': (t) => { 515 | expect(t).not.toInclude('Hi, there!'); 516 | expect(t).toInclude('require("./App.vue")'); 517 | }, 518 | 'dist/App.vue': (t) => 519 | expect(t).toIncludeMultiple([ 520 | 'greeting:', 521 | 'Hi, there!', 522 | '', 524 | 'font-size:', 525 | '64px', 526 | ]), 527 | }); 528 | const { stdout, status } = execNode('dist/index.js'); 529 | expect(status).toBe(0); 530 | expect(stdout).toInclude('

Hi, there!

'); 531 | } 532 | ); 533 | }); 534 | 535 | describe('with option.extentionMapping', () => { 536 | it('maps but correctly resolves the mapped in the output', () => { 537 | setupWebpackProject({ 538 | 'webpack.config.js': ` 539 | const Plugin = require(${JSON.stringify(rootPath)}); 540 | module.exports = { 541 | ${webpackConfigReusable} 542 | entry: './src/index.es', 543 | module: { 544 | rules: [ 545 | { 546 | test: /\\.(es|ts)$/, 547 | type: 'javascript/auto', 548 | } 549 | ] 550 | }, 551 | plugins: [ 552 | new Plugin({ extentionMapping: { '.ts': '.js' } }) 553 | ], 554 | }; 555 | `, 556 | 'src/index.es': ` 557 | import { greeting } from './constants.ts'; 558 | console.log(greeting); 559 | `, 560 | 'src/constants.ts': ` 561 | export const greeting = 'Hi, there!'; 562 | `, 563 | }); 564 | expect(execWebpack().status).toBe(0); 565 | expectCommonDirToIncludeSameFilesAnd({ 566 | 'dist/index.es': (t) => { 567 | expect(t).not.toInclude('Hi, there!'); 568 | expect(t).toInclude('require("./constants.js")'); 569 | }, 570 | 'dist/constants.js': (t) => expect(t).toInclude('Hi, there!'), 571 | }); 572 | const { stdout, status } = execNode('dist/index.es'); 573 | expect(status).toBe(0); 574 | expect(stdout).toInclude('Hi, there!'); 575 | }); 576 | }); 577 | 578 | describe('edge cases on outputting JS file with ext other than .js', () => { 579 | for (const devtool of ['source-map', 'inline-source-map']) { 580 | it(`works with ${devtool}`, () => { 581 | setupWebpackProject({ 582 | 'webpack.config.js': ` 583 | const Plugin = require(${JSON.stringify(rootPath)}); 584 | module.exports = { 585 | ${webpackConfigReusable} 586 | entry: './src/index.coffee', 587 | devtool: '${devtool}', 588 | module: { 589 | rules: [ 590 | { 591 | test: /\\.coffee$/, 592 | use: 'coffee-loader', 593 | } 594 | ] 595 | }, 596 | plugins: [new Plugin()], 597 | }; 598 | `, 599 | 'src/index.coffee': ` 600 | import { throwErrUnconditional } from './throw.coffee' 601 | throwErrUnconditional() 602 | `, 603 | 'src/throw.coffee': ` 604 | export throwErrUnconditional = () -> 605 | console.log('Something before err...') 606 | throw new Error('Some error happened') 607 | `, 608 | 'package.json': evaluateMustHavePackageJsonText({ 609 | ['devDependencies']: { 610 | ['coffee-loader']: '^4.0.0', 611 | ['coffeescript']: '^2.7.0', 612 | ['source-map-support']: '^0.5.21', 613 | }, 614 | }), 615 | }); 616 | expect(execWebpack().status).toBe(0); 617 | expectCommonDirToIncludeSameFiles([ 618 | 'dist/index.coffee', 619 | 'dist/throw.coffee', 620 | ...(devtool === 'source-map' ? ['dist/index.coffee.map', 'dist/throw.coffee.map'] : []), 621 | ]); 622 | const { stdout, stderr, status } = execNode( 623 | '-r', 624 | 'source-map-support/register', 625 | 'dist/index.coffee' 626 | ); 627 | expect(status).toBeGreaterThan(0); 628 | expect(stdout).toInclude('Something before err'); 629 | expect(stderr).toIncludeMultiple([ 630 | 'Error', 631 | 'Some error happened', 632 | `${path.normalize('src/throw.coffee')}:4`, 633 | `${path.normalize('src/index.coffee')}:3`, 634 | ]); 635 | }); 636 | } 637 | 638 | it('works with minimize', () => { 639 | setupWebpackProject({ 640 | 'webpack.config.js': ` 641 | const Plugin = require(${JSON.stringify(rootPath)}); 642 | module.exports = { 643 | ${webpackConfigReusable} 644 | entry: './src/index.coffee', 645 | module: { 646 | rules: [ 647 | { 648 | test: /\\.coffee$/, 649 | use: 'coffee-loader', 650 | } 651 | ] 652 | }, 653 | plugins: [new Plugin()], 654 | }; 655 | `, 656 | 'src/index.coffee': ` 657 | console.log('Hi, there!'); 658 | `, 659 | 'package.json': evaluateMustHavePackageJsonText({ 660 | ['devDependencies']: { 661 | ['coffee-loader']: '^4.0.0', 662 | ['coffeescript']: '^2.7.0', 663 | }, 664 | }), 665 | }); 666 | expect(execWebpack().status).toBe(0); 667 | expectCommonDirToIncludeSameFilesAnd({ 668 | 'dist/index.coffee': (t) => expect(t).not.toIncludeAnyMembers(['/*', '*/']), 669 | }); 670 | const { stdout, status } = execNode('dist/index.coffee'); 671 | expect(status).toBe(0); 672 | expect(stdout).toInclude('Hi, there!'); 673 | }); 674 | 675 | it('works with resolve.extensions', () => { 676 | setupWebpackProject({ 677 | 'webpack.config.js': ` 678 | const Plugin = require(${JSON.stringify(rootPath)}); 679 | module.exports = { 680 | ${webpackConfigReusable} 681 | entry: './src/index.coffee', 682 | module: { 683 | rules: [ 684 | { 685 | test: /\\.coffee$/, 686 | use: 'coffee-loader', 687 | } 688 | ] 689 | }, 690 | resolve: { 691 | extensions: ['.coffee'] 692 | }, 693 | plugins: [new Plugin()], 694 | }; 695 | `, 696 | 'src/index.coffee': ` 697 | import { greeting } from './constants' 698 | console.log(greeting) 699 | `, 700 | 'src/constants.coffee': ` 701 | export greeting = 'Hi, there!' 702 | `, 703 | 'package.json': evaluateMustHavePackageJsonText({ 704 | ['devDependencies']: { 705 | ['coffee-loader']: '^4.0.0', 706 | ['coffeescript']: '^2.7.0', 707 | }, 708 | }), 709 | }); 710 | expect(execWebpack().status).toBe(0); 711 | expectCommonDirToIncludeSameFilesAnd({ 712 | 'dist/index.coffee': (t) => { 713 | expect(t).not.toInclude('Hi, there!'); 714 | expect(t).toInclude('require("./constants.coffee")'); 715 | }, 716 | 'dist/constants.coffee': (t) => expect(t).toInclude('Hi, there!'), 717 | }); 718 | const { stdout, status } = execNode('dist/index.coffee'); 719 | expect(status).toBe(0); 720 | expect(stdout).toInclude('Hi, there!'); 721 | }); 722 | }); 723 | -------------------------------------------------------------------------------- /e2e/handles-js-outside-node_modules.spec.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import os from 'node:os'; 3 | import path from 'node:path'; 4 | 5 | import { noop } from 'lodash'; 6 | import waitForExpect from 'wait-for-expect'; 7 | 8 | import { 9 | encodingText, 10 | evaluateMustHavePackageJsonText, 11 | execNode, 12 | execWebpack, 13 | execWebpackAsync, 14 | expectCommonDirToIncludeAllFiles, 15 | expectCommonDirToIncludeAllFilesAnd, 16 | expectCommonDirToIncludeSameFiles, 17 | expectCommonDirToIncludeSameFilesAnd, 18 | killExecAsyncProcess, 19 | rootPath, 20 | setupWebpackProject, 21 | } from '../support-helpers'; 22 | 23 | const webpackConfigReusable = ` 24 | mode: 'production', 25 | target: 'node', 26 | output: { 27 | path: __dirname + '/dist', 28 | }, 29 | `; 30 | 31 | function useSubEntry(subEntryName: string, topEntryFile: string = 'src/index.js'): void { 32 | fs.writeFileSync(topEntryFile, `import './${subEntryName}';`, encodingText); 33 | } 34 | 35 | describe('with default options', () => { 36 | it( 37 | 'evaluates new entries from single user specified entry and ' + 38 | 'outputs them by relative paths to their longest common dir', 39 | () => { 40 | setupWebpackProject({ 41 | 'webpack.config.js': ` 42 | const Plugin = require(${JSON.stringify(rootPath)}); 43 | module.exports = { 44 | ${webpackConfigReusable} 45 | entry: './src/index.js', 46 | plugins: [new Plugin()], 47 | }; 48 | `, 49 | 'src/index.js': ` 50 | import { greeting } from './constants'; 51 | console.log(greeting); 52 | `, 53 | 'src/constants.js': ` 54 | export const greeting = 'Hi, there!'; 55 | `, 56 | }); 57 | expect(execWebpack().status).toBe(0); 58 | expectCommonDirToIncludeSameFilesAnd({ 59 | 'dist/index.js': (t) => expect(t).not.toInclude('Hi, there!'), 60 | 'dist/constants.js': (t) => expect(t).toInclude('Hi, there!'), 61 | }); 62 | const { stdout, status } = execNode('dist/index.js'); 63 | expect(status).toBe(0); 64 | expect(stdout).toInclude('Hi, there!'); 65 | } 66 | ); 67 | 68 | it('works with multiple user specified entries', () => { 69 | setupWebpackProject({ 70 | 'webpack.config.js': ` 71 | const Plugin = require(${JSON.stringify(rootPath)}); 72 | module.exports = { 73 | ${webpackConfigReusable} 74 | entry: { 75 | e1: './src/index.js', 76 | e2: './src/constants.js', 77 | }, 78 | plugins: [new Plugin()], 79 | }; 80 | `, 81 | 'src/index.js': ` 82 | import { greeting } from './constants'; 83 | console.log(greeting); 84 | `, 85 | 'src/constants.js': ` 86 | export const greeting = 'Hi, there!'; 87 | `, 88 | }); 89 | expect(execWebpack().status).toBe(0); 90 | expectCommonDirToIncludeSameFilesAnd({ 91 | 'dist/index.js': (t) => expect(t).not.toInclude('Hi, there!'), 92 | 'dist/constants.js': (t) => expect(t).toInclude('Hi, there!'), 93 | }); 94 | const { stdout, status } = execNode('dist/index.js'); 95 | expect(status).toBe(0); 96 | expect(stdout).toInclude('Hi, there!'); 97 | }); 98 | 99 | it('works with multi-file user specified entry', () => { 100 | setupWebpackProject({ 101 | 'webpack.config.js': ` 102 | const Plugin = require(${JSON.stringify(rootPath)}); 103 | module.exports = { 104 | ${webpackConfigReusable} 105 | entry: { 106 | whatever: ['./src/index.js', './src/constants.js'] 107 | }, 108 | plugins: [new Plugin()], 109 | }; 110 | `, 111 | 'src/index.js': ` 112 | import { greeting } from './constants'; 113 | console.log(greeting); 114 | `, 115 | 'src/constants.js': ` 116 | export const greeting = 'Hi, there!'; 117 | `, 118 | }); 119 | expect(execWebpack().status).toBe(0); 120 | expectCommonDirToIncludeSameFilesAnd({ 121 | 'dist/index.js': (t) => expect(t).not.toInclude('Hi, there!'), 122 | 'dist/constants.js': (t) => expect(t).toInclude('Hi, there!'), 123 | }); 124 | const { stdout, status } = execNode('dist/index.js'); 125 | expect(status).toBe(0); 126 | expect(stdout).toInclude('Hi, there!'); 127 | }); 128 | 129 | it('works with entry descriptor', () => { 130 | setupWebpackProject({ 131 | 'webpack.config.js': ` 132 | const Plugin = require(${JSON.stringify(rootPath)}); 133 | module.exports = { 134 | ${webpackConfigReusable} 135 | entry: { 136 | e1: { 137 | import: './src/index.js', 138 | dependOn: 'e2', 139 | runtime: 'runtime', 140 | }, 141 | e2: { 142 | import: './src/constants.js', 143 | filename: '[name].[ext]', 144 | chunkLoading: false, 145 | }, 146 | }, 147 | plugins: [new Plugin()], 148 | }; 149 | `, 150 | 'src/index.js': ` 151 | import { greeting } from './constants'; 152 | console.log(greeting); 153 | `, 154 | 'src/constants.js': ` 155 | export const greeting = 'Hi, there!'; 156 | `, 157 | }); 158 | expect(execWebpack().status).toBe(0); 159 | expectCommonDirToIncludeSameFilesAnd({ 160 | 'dist/index.js': (t) => expect(t).not.toInclude('Hi, there!'), 161 | 'dist/constants.js': (t) => expect(t).toInclude('Hi, there!'), 162 | }); 163 | const { stdout, status } = execNode('dist/index.js'); 164 | expect(status).toBe(0); 165 | expect(stdout).toInclude('Hi, there!'); 166 | }); 167 | 168 | it('works with externals', () => { 169 | setupWebpackProject({ 170 | 'webpack.config.js': ` 171 | const Plugin = require(${JSON.stringify(rootPath)}); 172 | module.exports = { 173 | ${webpackConfigReusable} 174 | entry: './src/index.js', 175 | externals: { 176 | greeting: ['commonjs ../third_party/constants', 'greeting'] 177 | }, 178 | plugins: [new Plugin()], 179 | }; 180 | `, 181 | 'src/index.js': ` 182 | import greeting from 'greeting'; 183 | console.log(greeting); 184 | `, 185 | 'third_party/constants.js': ` 186 | exports.greeting = 'Hi, there!'; 187 | `, 188 | }); 189 | expect(execWebpack().status).toBe(0); 190 | expectCommonDirToIncludeSameFilesAnd({ 191 | 'dist/index.js': (t) => { 192 | expect(t).toInclude('require("../third_party/constants"'); 193 | expect(t).not.toInclude('Hi, there!'); 194 | }, 195 | }); 196 | const { stdout, status } = execNode('dist/index.js'); 197 | expect(status).toBe(0); 198 | expect(stdout).toInclude('Hi, there!'); 199 | }); 200 | 201 | it('works with resolve.alias', () => { 202 | setupWebpackProject({ 203 | 'webpack.config.js': ` 204 | const Plugin = require(${JSON.stringify(rootPath)}); 205 | module.exports = { 206 | ${webpackConfigReusable} 207 | entry: './server/index.js', 208 | resolve: { 209 | alias: { 210 | '@client': __dirname + '/client', 211 | }, 212 | }, 213 | plugins: [new Plugin()], 214 | }; 215 | `, 216 | 'server/index.js': ` 217 | import { greeting } from '@client/constants'; 218 | console.log(greeting); 219 | `, 220 | 'client/constants.js': ` 221 | export const greeting = 'Hi, there!'; 222 | `, 223 | }); 224 | expect(execWebpack().status).toBe(0); 225 | expectCommonDirToIncludeSameFilesAnd({ 226 | 'dist/server/index.js': (t) => { 227 | expect(t).not.toInclude('Hi, there!'); 228 | expect(t).toInclude('require("../client/constants.js")'); 229 | }, 230 | 'dist/client/constants.js': (t) => expect(t).toInclude('Hi, there!'), 231 | }); 232 | const { stdout, status } = execNode('dist/server/index.js'); 233 | expect(status).toBe(0); 234 | expect(stdout).toInclude('Hi, there!'); 235 | }); 236 | 237 | it('works with resource query', () => { 238 | setupWebpackProject({ 239 | 'webpack.config.js': ` 240 | const Plugin = require(${JSON.stringify(rootPath)}); 241 | module.exports = { 242 | ${webpackConfigReusable} 243 | entry: './src/index.js?t=1', 244 | plugins: [new Plugin()], 245 | }; 246 | `, 247 | 'src/index.js': ` 248 | import assert from 'node:assert'; 249 | import { greeting } from './constants?u=0'; 250 | 251 | console.log(greeting); 252 | assert.strictEqual(__resourceQuery, '?t=1'); 253 | `, 254 | 'src/constants.js': ` 255 | import assert from 'node:assert'; 256 | 257 | export const greeting = 'Hi, there!'; 258 | assert.strictEqual(__resourceQuery, '?u=0'); 259 | `, 260 | }); 261 | expect(execWebpack().status).toBe(0); 262 | expectCommonDirToIncludeSameFilesAnd({ 263 | 'dist/index.js': (t) => expect(t).not.toInclude('Hi, there!'), 264 | 'dist/constants.js': (t) => expect(t).toInclude('Hi, there!'), 265 | }); 266 | const { stdout, status } = execNode('dist/index.js'); 267 | expect(status).toBe(0); 268 | expect(stdout).toInclude('Hi, there!'); 269 | }); 270 | 271 | for (const devtool of ['source-map', 'inline-source-map']) { 272 | it(`works with ${devtool}`, () => { 273 | setupWebpackProject({ 274 | 'webpack.config.js': ` 275 | const Plugin = require(${JSON.stringify(rootPath)}); 276 | module.exports = { 277 | ${webpackConfigReusable} 278 | entry: './src/index.js', 279 | devtool: '${devtool}', 280 | plugins: [new Plugin()], 281 | }; 282 | `, 283 | 'src/index.js': ` 284 | import { throwErrUnconditional } from './throw'; 285 | throwErrUnconditional(); 286 | `, 287 | 'src/throw.js': ` 288 | export function throwErrUnconditional() { 289 | console.log('Something before err...'); 290 | throw new Error('Some error happened'); 291 | } 292 | `, 293 | 'package.json': evaluateMustHavePackageJsonText({ 294 | ['devDependencies']: { 295 | ['source-map-support']: '^0.5.21', 296 | }, 297 | }), 298 | }); 299 | expect(execWebpack().status).toBe(0); 300 | expectCommonDirToIncludeSameFiles([ 301 | 'dist/index.js', 302 | 'dist/throw.js', 303 | ...(devtool === 'source-map' ? ['dist/index.js.map', 'dist/throw.js.map'] : []), 304 | ]); 305 | const { stdout, stderr, status } = execNode( 306 | '-r', 307 | 'source-map-support/register', 308 | 'dist/index.js' 309 | ); 310 | expect(status).toBeGreaterThan(0); 311 | expect(stdout).toInclude('Something before err'); 312 | expect(stderr).toIncludeMultiple([ 313 | 'Error', 314 | 'Some error happened', 315 | `${path.normalize('src/throw.js')}:4`, 316 | `${path.normalize('src/index.js')}:3`, 317 | ]); 318 | }); 319 | } 320 | 321 | it('works with watch mode', async () => { 322 | setupWebpackProject({ 323 | 'webpack.config.js': ` 324 | const Plugin = require(${JSON.stringify(rootPath)}); 325 | module.exports = { 326 | ${webpackConfigReusable} 327 | entry: './src/index.js', 328 | plugins: [new Plugin()], 329 | }; 330 | `, 331 | 'src/index.js': ` 332 | import { greeting } from './constants'; 333 | console.log(greeting); 334 | `, 335 | 'src/constants.js': ` 336 | export const greeting = 'Hi, there!'; 337 | `, 338 | }); 339 | const webpackProc = execWebpackAsync('--watch'); 340 | await waitForExpect(() => { 341 | expect(webpackProc.getStdoutAsString()).toIncludeRepeated('compiled successfully', 1); 342 | }); 343 | expectCommonDirToIncludeSameFiles(['dist/index.js', 'dist/constants.js']); 344 | 345 | const newTextToLog = 'c0b6f254daf5f4cee4b3e01170b55d453c511589'; 346 | fs.writeFileSync( 347 | 'src/index.js', 348 | ` 349 | import { greeting } from './constants'; 350 | console.log(greeting, '(${newTextToLog})'); 351 | ` 352 | ); 353 | await waitForExpect(() => { 354 | expect(webpackProc.getStdoutAsString()).toIncludeRepeated('compiled successfully', 2); 355 | }); 356 | expect(fs.readFileSync('dist/index.js', encodingText)).toIncludeMultiple([ 357 | 'greeting', 358 | newTextToLog, 359 | ]); 360 | 361 | await killExecAsyncProcess(webpackProc.pid!); 362 | 363 | const { stdout, status } = execNode('dist/index.js'); 364 | expect(status).toBe(0); 365 | expect(stdout).toIncludeMultiple(['Hi, there!', newTextToLog]); 366 | }); 367 | 368 | it('works with plain import()', () => { 369 | setupWebpackProject({ 370 | 'webpack.config.js': ` 371 | const Plugin = require(${JSON.stringify(rootPath)}); 372 | module.exports = { 373 | ${webpackConfigReusable} 374 | entry: './src/index.js', 375 | plugins: [new Plugin()], 376 | }; 377 | `, 378 | 'src/index.js': ` 379 | import('./constants').then(({ greeting }) => { 380 | console.log(greeting); 381 | }); 382 | `, 383 | 'src/constants.js': ` 384 | export const greeting = 'Hi, there!'; 385 | `, 386 | }); 387 | expect(execWebpack().status).toBe(0); 388 | expectCommonDirToIncludeSameFilesAnd({ 389 | 'dist/index.js': (t) => expect(t).not.toInclude('Hi, there!'), 390 | 'dist/constants.js': (t) => expect(t).toInclude('Hi, there!'), 391 | }); 392 | const { stdout, status } = execNode('dist/index.js'); 393 | expect(status).toBe(0); 394 | expect(stdout).toInclude('Hi, there!'); 395 | }); 396 | 397 | it('works with dynamic import()', () => { 398 | setupWebpackProject({ 399 | 'webpack.config.js': ` 400 | const Plugin = require(${JSON.stringify(rootPath)}); 401 | module.exports = { 402 | ${webpackConfigReusable} 403 | entry: './src/index.js', 404 | plugins: [new Plugin()], 405 | }; 406 | `, 407 | 'src/index.js': ` 408 | const constFileName = 'greeting'; 409 | import( 410 | \`./constants/\${constFileName}\` 411 | ).then(({ greeting }) => { 412 | console.log(greeting); 413 | }); 414 | `, 415 | 'src/constants/greeting.js': ` 416 | export const greeting = 'Hi, there!'; 417 | `, 418 | 'src/constants/farewell.js': ` 419 | export const farewell = 'Peace out.'; 420 | `, 421 | }); 422 | expect(execWebpack().status).toBe(0); 423 | expectCommonDirToIncludeSameFilesAnd({ 424 | 'dist/index.js': (t) => { 425 | expect(t).toIncludeMultiple([ 426 | 'require("./constants/greeting.js")', 427 | 'require("./constants/farewell.js")', 428 | ]); 429 | expect(t).not.toInclude('Hi, there!'); 430 | expect(t).not.toInclude('Peace out.'); 431 | }, 432 | 'dist/constants/greeting.js': (t) => expect(t).toInclude('Hi, there!'), 433 | 'dist/constants/farewell.js': (t) => expect(t).toInclude('Peace out.'), 434 | }); 435 | const { status, stdout } = execNode('dist/index.js'); 436 | expect(status).toBe(0); 437 | expect(stdout).toInclude('Hi, there!'); 438 | }); 439 | 440 | it('works with magic comment inlined import()', () => { 441 | setupWebpackProject({ 442 | 'webpack.config.js': ` 443 | const Plugin = require(${JSON.stringify(rootPath)}); 444 | module.exports = { 445 | ${webpackConfigReusable} 446 | entry: './src/index.js', 447 | plugins: [new Plugin()], 448 | }; 449 | `, 450 | 'src/withIgnore.js': ` 451 | import( 452 | /* webpackIgnore: true */ 453 | './constants/greeting' 454 | ).then(({ greeting }) => { 455 | console.log(greeting); 456 | }); 457 | `, 458 | 'src/withChunkName.js': ` 459 | import( 460 | /* webpackChunkName: 'some-chunk-name' */ 461 | './constants/greeting' 462 | ).then(({ greeting }) => { 463 | console.log(greeting); 464 | }); 465 | `, 466 | 'src/withModeEager.js': ` 467 | import( 468 | /* webpackMode: 'eager' */ 469 | './constants/greeting' 470 | ).then(({ greeting }) => { 471 | console.log(greeting); 472 | }); 473 | `, 474 | 'src/withPrefetch.js': ` 475 | import( 476 | /* webpackPrefetch: true */ 477 | './constants/greeting' 478 | ).then(({ greeting }) => { 479 | console.log(greeting); 480 | }); 481 | `, 482 | 'src/withPreload.js': ` 483 | import( 484 | /* webpackPreload: true */ 485 | './constants/greeting' 486 | ).then(({ greeting }) => { 487 | console.log(greeting); 488 | }); 489 | `, 490 | 'src/withInclude.js': ` 491 | const constFileName = 'greeting'; 492 | import( 493 | /* webpackInclude: /(greeting|farewell)\\.js$/ */ 494 | \`./constants/\${constFileName}\` 495 | ).then(({ greeting }) => { 496 | console.log(greeting); 497 | }); 498 | `, 499 | 'src/withExclude.js': ` 500 | const constFileName = 'greeting'; 501 | import( 502 | /* webpackExclude: /excuseme\\.js$/ */ 503 | \`./constants/\${constFileName}\` 504 | ).then(({ greeting }) => { 505 | console.log(greeting); 506 | }); 507 | `, 508 | 'src/withExports.js': ` 509 | import( 510 | /* webpackExports: ['default'] */ 511 | './constants/greeting' 512 | ).then(({ greeting }) => { 513 | console.log(greeting); 514 | }); 515 | `, 516 | 'src/constants/greeting.js': ` 517 | export const greeting = 'Hi, there!'; 518 | `, 519 | 'src/constants/farewell.js': ` 520 | export const farewell = 'Peace out.'; 521 | `, 522 | 'src/constants/excuseme.js': ` 523 | export const excuseme = 'Excuse me.'; 524 | `, 525 | }); 526 | 527 | subCaseWithIgnore(); 528 | subCasesWithIncludeExclude(); 529 | subCasesMakingNoDifference(); 530 | 531 | function subCaseWithIgnore() { 532 | try { 533 | fs.rmSync('dist', { recursive: true }); 534 | } catch {} 535 | useSubEntry('withIgnore'); 536 | expect(execWebpack().status).toBe(0); 537 | expectCommonDirToIncludeSameFilesAnd({ 538 | 'dist/index.js': noop, 539 | 'dist/withIgnore.js': (t) => { 540 | expect(t).not.toInclude('require("./constants/greeting.js")'); 541 | expect(t).not.toInclude('Hi, there!'); 542 | }, 543 | }); 544 | const { status, stderr } = execNode('dist/index.js'); 545 | expect(status).toBeGreaterThan(0); 546 | expect(stderr).toInclude('MODULE_NOT_FOUND'); 547 | } 548 | 549 | function subCasesWithIncludeExclude() { 550 | for (const subEntryName of ['withInclude', 'withExclude']) { 551 | try { 552 | fs.rmSync('dist', { recursive: true }); 553 | } catch {} 554 | useSubEntry(subEntryName); 555 | expect(execWebpack().status).toBe(0); 556 | expectCommonDirToIncludeSameFilesAnd({ 557 | 'dist/index.js': noop, 558 | [`dist/${subEntryName}.js`]: (t) => { 559 | expect(t).toIncludeMultiple([ 560 | 'require("./constants/greeting.js")', 561 | 'require("./constants/farewell.js")', 562 | ]); 563 | expect(t).not.toInclude('Hi, there!'); 564 | expect(t).not.toInclude('Peace out.'); 565 | expect(t).not.toInclude('require("./constants/excuseme.js")'); 566 | }, 567 | 'dist/constants/greeting.js': (t) => expect(t).toInclude('Hi, there!'), 568 | 'dist/constants/farewell.js': (t) => expect(t).toInclude('Peace out.'), 569 | }); 570 | const { status, stdout } = execNode('dist/index.js'); 571 | expect(status).toBe(0); 572 | expect(stdout).toInclude('Hi, there!'); 573 | } 574 | } 575 | 576 | function subCasesMakingNoDifference() { 577 | for (const subEntryName of [ 578 | 'withChunkName', 579 | 'withModeEager', 580 | 'withPrefetch', 581 | 'withPreload', 582 | 'withExports', 583 | ]) { 584 | try { 585 | fs.rmSync('dist', { recursive: true }); 586 | } catch {} 587 | useSubEntry(subEntryName); 588 | expect(execWebpack().status).toBe(0); 589 | expectCommonDirToIncludeSameFilesAnd({ 590 | 'dist/index.js': noop, 591 | [`dist/${subEntryName}.js`]: (t) => expect(t).not.toInclude('Hi, there!'), 592 | 'dist/constants/greeting.js': (t) => expect(t).toInclude('Hi, there!'), 593 | }); 594 | const { status, stdout } = execNode('dist/index.js'); 595 | expect(status).toBe(0); 596 | expect(stdout).toInclude('Hi, there!'); 597 | } 598 | } 599 | }); 600 | 601 | it('works with require.ensure()', () => { 602 | setupWebpackProject({ 603 | 'webpack.config.js': ` 604 | const Plugin = require(${JSON.stringify(rootPath)}); 605 | module.exports = { 606 | ${webpackConfigReusable} 607 | entry: './src/index.js', 608 | plugins: [new Plugin()], 609 | }; 610 | `, 611 | 'src/index.js': ` 612 | require.ensure(['./constants'], () => { 613 | const { greeting } = require('./constants'); 614 | console.log(greeting); 615 | }); 616 | `, 617 | 'src/constants.js': ` 618 | export const greeting = 'Hi, there!'; 619 | `, 620 | }); 621 | expect(execWebpack().status).toBe(0); 622 | expectCommonDirToIncludeSameFilesAnd({ 623 | 'dist/index.js': (t) => expect(t).not.toInclude('Hi, there!'), 624 | 'dist/constants.js': (t) => expect(t).toInclude('Hi, there!'), 625 | }); 626 | const { stdout, status } = execNode('dist/index.js'); 627 | expect(status).toBe(0); 628 | expect(stdout).toInclude('Hi, there!'); 629 | }); 630 | 631 | it('works with AMD require/define', () => { 632 | setupWebpackProject({ 633 | 'webpack.config.js': ` 634 | const Plugin = require(${JSON.stringify(rootPath)}); 635 | module.exports = { 636 | ${webpackConfigReusable} 637 | entry: './src/index.js', 638 | plugins: [new Plugin()], 639 | }; 640 | `, 641 | 'src/index.js': ` 642 | require(['./constants'], ({ greeting }) => { 643 | console.log(greeting); 644 | }); 645 | `, 646 | 'src/constants.js': ` 647 | define([], () => ({ 648 | greeting: 'Hi, there!' 649 | })); 650 | `, 651 | }); 652 | expect(execWebpack().status).toBe(0); 653 | expectCommonDirToIncludeSameFilesAnd({ 654 | 'dist/index.js': (t) => expect(t).not.toInclude('Hi, there!'), 655 | 'dist/constants.js': (t) => expect(t).toInclude('Hi, there!'), 656 | }); 657 | const { stdout, status } = execNode('dist/index.js'); 658 | expect(status).toBe(0); 659 | expect(stdout).toInclude('Hi, there!'); 660 | }); 661 | 662 | it('works with require/exports', () => { 663 | setupWebpackProject({ 664 | 'webpack.config.js': ` 665 | const Plugin = require(${JSON.stringify(rootPath)}); 666 | module.exports = { 667 | ${webpackConfigReusable} 668 | entry: './src/index.js', 669 | plugins: [new Plugin()], 670 | }; 671 | `, 672 | 'src/index.js': ` 673 | const { greeting } = require('./constants'); 674 | console.log(greeting); 675 | `, 676 | 'src/constants.js': ` 677 | exports.greeting = 'Hi, there!'; 678 | `, 679 | }); 680 | expect(execWebpack().status).toBe(0); 681 | expectCommonDirToIncludeSameFilesAnd({ 682 | 'dist/index.js': (t) => expect(t).not.toInclude('Hi, there!'), 683 | 'dist/constants.js': (t) => expect(t).toInclude('Hi, there!'), 684 | }); 685 | const { stdout, status } = execNode('dist/index.js'); 686 | expect(status).toBe(0); 687 | expect(stdout).toInclude('Hi, there!'); 688 | }); 689 | 690 | it('works with require.resolve/__webpack_modules__', () => { 691 | setupWebpackProject({ 692 | 'webpack.config.js': ` 693 | const Plugin = require(${JSON.stringify(rootPath)}); 694 | module.exports = { 695 | ${webpackConfigReusable} 696 | entry: './src/index.js', 697 | plugins: [new Plugin()], 698 | }; 699 | `, 700 | 'src/index.js': ` 701 | import assert from 'node:assert'; 702 | 703 | assert.strictEqual(Boolean(__webpack_modules__[require.resolve('./constants')]), true); 704 | `, 705 | 'src/constants.js': ` 706 | export const greeting = 'Hi, there!'; 707 | `, 708 | }); 709 | 710 | expect(execWebpack().status).toBe(0); 711 | expectCommonDirToIncludeSameFilesAnd({ 712 | 'dist/index.js': (t) => { 713 | expect(t).toInclude('require("./constants.js")'); 714 | expect(t).not.toInclude('Hi, there!'); 715 | }, 716 | 'dist/constants.js': (t) => expect(t).toInclude('Hi, there!'), 717 | }); 718 | expect(execNode('dist/index.js').status).toBe(0); 719 | }); 720 | 721 | it('works with require.resolveWeak/__webpack_modules__', () => { 722 | setupWebpackProject({ 723 | 'webpack.config.js': ` 724 | const Plugin = require(${JSON.stringify(rootPath)}); 725 | module.exports = { 726 | ${webpackConfigReusable} 727 | entry: './src/index.js', 728 | plugins: [new Plugin()], 729 | }; 730 | `, 731 | 'src/index.js': ` 732 | import assert from 'node:assert'; 733 | 734 | assert.strictEqual(Boolean(__webpack_modules__[require.resolveWeak('./constants')]), false); 735 | `, 736 | 'src/constants.js': ` 737 | export const greeting = 'Hi, there!'; 738 | `, 739 | }); 740 | 741 | expect(execWebpack().status).toBe(0); 742 | expectCommonDirToIncludeSameFilesAnd({ 743 | 'dist/index.js': (t) => { 744 | expect(t).not.toInclude('require("./constants.js")'); 745 | expect(t).not.toInclude('Hi, there!'); 746 | }, 747 | 'dist/constants.js': (t) => expect(t).toInclude('Hi, there!'), 748 | }); 749 | expect(execNode('dist/index.js').status).toBe(0); 750 | }); 751 | 752 | it('works with require.include/require.resolveWeak/__webpack_modules__', () => { 753 | setupWebpackProject({ 754 | 'webpack.config.js': ` 755 | const Plugin = require(${JSON.stringify(rootPath)}); 756 | module.exports = { 757 | ${webpackConfigReusable} 758 | entry: './src/index.js', 759 | plugins: [new Plugin()], 760 | }; 761 | `, 762 | 'src/index.js': ` 763 | import assert from 'node:assert'; 764 | 765 | require.include('./constants'); 766 | 767 | assert.strictEqual(Boolean(__webpack_modules__[require.resolveWeak('./constants')]), true); 768 | `, 769 | 770 | 'src/constants.js': ` 771 | export const greeting = 'Hi, there!'; 772 | `, 773 | }); 774 | expect(execWebpack().status).toBe(0); 775 | expectCommonDirToIncludeSameFilesAnd({ 776 | 'dist/index.js': (t) => { 777 | expect(t).toInclude('require("./constants.js")'); 778 | expect(t).not.toInclude('Hi, there!'); 779 | }, 780 | 'dist/constants.js': (t) => expect(t).toInclude('Hi, there!'), 781 | }); 782 | expect(execNode('dist/index.js').status).toBe(0); 783 | }); 784 | 785 | it('works with require.context', () => { 786 | setupWebpackProject({ 787 | 'webpack.config.js': ` 788 | const Plugin = require(${JSON.stringify(rootPath)}); 789 | module.exports = { 790 | ${webpackConfigReusable} 791 | entry: './src/index.js', 792 | plugins: [new Plugin()], 793 | }; 794 | `, 795 | 'src/index.js': ` 796 | const requireConstant = require.context('./constants'); 797 | console.log(requireConstant('./a').a); 798 | console.log(requireConstant('./b').b); 799 | `, 800 | 'src/constants/a.js': ` 801 | export const a = 'Hi, A!'; 802 | `, 803 | 'src/constants/b.js': ` 804 | export const b = 'Hi, B!'; 805 | `, 806 | }); 807 | expect(execWebpack().status).toBe(0); 808 | expectCommonDirToIncludeSameFilesAnd({ 809 | 'dist/index.js': (t) => expect(t).not.toIncludeAnyMembers(['Hi, A!', 'Hi, B!']), 810 | 'dist/constants/a.js': (t) => expect(t).toInclude('Hi, A!'), 811 | 'dist/constants/b.js': (t) => expect(t).toInclude('Hi, B!'), 812 | }); 813 | const { status, stdout } = execNode('dist/index.js'); 814 | expect(status).toBe(0); 815 | expect(stdout).toIncludeMultiple(['Hi, A!', 'Hi, B!']); 816 | }); 817 | 818 | it('works with node globals', () => { 819 | setupWebpackProject({ 820 | 'webpack.config.js': ` 821 | const Plugin = require(${JSON.stringify(rootPath)}); 822 | module.exports = { 823 | ${webpackConfigReusable} 824 | entry: './src/index.js', 825 | plugins: [new Plugin()], 826 | }; 827 | `, 828 | 'src/index.js': ` 829 | import assert from 'node:assert'; 830 | import path from 'node:path'; 831 | 832 | assert.strictEqual(__dirname, path.resolve('dist')); 833 | assert.strictEqual(__filename, path.resolve('dist/index.js')); 834 | `, 835 | }); 836 | expect(execWebpack().status).toBe(0); 837 | expect(execNode('dist/index.js').status).toBe(0); 838 | }); 839 | 840 | it('works with __webpack_...__ variables', () => { 841 | setupWebpackProject({ 842 | 'webpack.config.js': ` 843 | const Plugin = require(${JSON.stringify(rootPath)}); 844 | module.exports = { 845 | ${webpackConfigReusable.replace( 846 | /output:\s*\{([^}]*)\}/s, 847 | `output: { 848 | $1 849 | publicPath: '/public/', 850 | }` 851 | )} 852 | entry: ['./src/index.js', './src/texts.js'], 853 | plugins: [new Plugin()] 854 | }; 855 | `, 856 | 'src/index.js': ` 857 | import assert from 'node:assert'; 858 | 859 | assert.strictEqual(__webpack_public_path__, '/public/'); 860 | 861 | assert.strictEqual(__webpack_is_included__('./constants'), true); 862 | assert.strictEqual(__webpack_require__(require.resolve('./constants')).greeting, 'Hi, there!'); 863 | 864 | assert.strictEqual(__webpack_is_included__('./texts'), false); 865 | assert.strictEqual(__non_webpack_require__('./texts').farewell, 'Peace out.'); 866 | 867 | for(const v of [ 868 | __webpack_base_uri__, 869 | __webpack_chunk_load__, 870 | __webpack_exports_info__, 871 | __webpack_get_script_filename__, 872 | __webpack_hash__, 873 | __webpack_modules__, 874 | __webpack_runtime_id__ 875 | ]) { 876 | assert.strictEqual(Boolean(v), true) 877 | } 878 | `, 879 | 'src/constants.js': ` 880 | export const greeting = 'Hi, there!'; 881 | `, 882 | 'src/texts.js': ` 883 | export const farewell = 'Peace out.'; 884 | `, 885 | }); 886 | expect(execWebpack().status).toBe(0); 887 | expect(execNode('dist/index.js').status).toBe(0); 888 | }); 889 | 890 | it('works with differently cased files', () => { 891 | setupWebpackProject({ 892 | 'webpack.config.js': ` 893 | const Plugin = require(${JSON.stringify(rootPath)}); 894 | module.exports = { 895 | ${webpackConfigReusable} 896 | entry: './src/index.js', 897 | plugins: [new Plugin()], 898 | }; 899 | `, 900 | 'src/withFilesInSameDir.js': ` 901 | import { greeting1, greeting2 } from './c1'; 902 | console.log(greeting1, greeting2); 903 | `, 904 | 'src/c1/index.js': ` 905 | export * from './greeting'; 906 | export * from './GREETING'; 907 | `, 908 | 'src/c1/greeting.js': ` 909 | export const greeting1 = 'Hi, there!'; 910 | export const greeting2 = 'Hey, dude!'; 911 | `, 912 | 'src/c1/GREETING.js': ` 913 | export const greeting1 = 'Hi, there!'; 914 | export const greeting2 = 'Hey, dude!'; 915 | `, 916 | 'src/withFilesInDifferentDirs.js': ` 917 | import { greeting1 } from './c2-1'; 918 | import { greeting2 } from './c2-2'; 919 | console.log(greeting1, greeting2); 920 | `, 921 | 'src/c2-1/index.js': ` 922 | export * from './greeting'; 923 | `, 924 | 'src/c2-1/greeting.js': ` 925 | export const greeting1 = 'Hi, there!'; 926 | `, 927 | 'src/c2-2/index.js': ` 928 | export * from './GREETING'; 929 | `, 930 | 'src/c2-2/GREETING.js': ` 931 | export const greeting2 = 'Hey, dude!'; 932 | `, 933 | }); 934 | 935 | subCaseWithFilesInSameDir(); 936 | subCaseWithFilesInDifferentDirs(); 937 | 938 | function subCaseWithFilesInSameDir() { 939 | try { 940 | fs.rmSync('dist', { recursive: true }); 941 | } catch {} 942 | useSubEntry('withFilesInSameDir'); 943 | const { stdout: webpackStdout, status: webpackStatus } = execWebpack(); 944 | expect(webpackStatus).toBe(0); 945 | expectCommonDirToIncludeAllFiles([ 946 | 'dist/index.js', 947 | 'dist/withFilesInSameDir.js', 948 | 'dist/c1/index.js', 949 | ]); 950 | expect(webpackStdout).toIncludeMultiple(['WARNING in external', 'differ in casing']); 951 | } 952 | 953 | function subCaseWithFilesInDifferentDirs() { 954 | try { 955 | fs.rmSync('dist', { recursive: true }); 956 | } catch {} 957 | useSubEntry('withFilesInDifferentDirs'); 958 | const { stdout: webpackStdout, status: webpackStatus } = execWebpack(); 959 | expect(webpackStatus).toBe(0); 960 | expectCommonDirToIncludeSameFiles([ 961 | 'dist/index.js', 962 | 'dist/withFilesInDifferentDirs.js', 963 | 'dist/c2-1/index.js', 964 | 'dist/c2-1/greeting.js', 965 | 'dist/c2-2/index.js', 966 | 'dist/c2-2/GREETING.js', 967 | ]); 968 | expect(webpackStdout).not.toIncludeMultiple(['WARNING in external', 'differ in casing']); 969 | const { stdout: nodeStdout, status: nodeStatus } = execNode('dist/index.js'); 970 | expect(nodeStatus).toBe(0); 971 | expect(nodeStdout).toIncludeMultiple(['Hi, there!', 'Hey, dude!']); 972 | } 973 | }); 974 | 975 | if (os.platform() === 'win32') { 976 | it('generates require paths in posix format even on win32', () => { 977 | setupWebpackProject({ 978 | 'webpack.config.js': ` 979 | const Plugin = require(${JSON.stringify(rootPath)}); 980 | module.exports = { 981 | ${webpackConfigReusable} 982 | entry: './src/index.js', 983 | plugins: [new Plugin()], 984 | }; 985 | `, 986 | 'src/index.js': ` 987 | import { greeting } from './constants'; 988 | console.log(greeting); 989 | `, 990 | 'src/constants.js': ` 991 | export const greeting = 'Hi, there!'; 992 | `, 993 | }); 994 | expect(execWebpack().status).toBe(0); 995 | expectCommonDirToIncludeAllFilesAnd({ 996 | 'dist/index.js': (t) => expect(t).toInclude('require("./constants.js")'), 997 | }); 998 | }); 999 | } 1000 | }); 1001 | 1002 | describe('with options.exclude', () => { 1003 | it('excludes but correctly resolves the excluded in the output', () => { 1004 | setupWebpackProject({ 1005 | 'webpack.config.js': ` 1006 | const Plugin = require(${JSON.stringify(rootPath)}); 1007 | module.exports = { 1008 | ${webpackConfigReusable} 1009 | entry: './server/index.js', 1010 | plugins: [ 1011 | new Plugin({ exclude: /third_party/ }), 1012 | ], 1013 | }; 1014 | `, 1015 | 'server/index.js': ` 1016 | import { greeting } from '../client/constants'; 1017 | console.log(greeting); 1018 | `, 1019 | 'client/constants.js': ` 1020 | export * from '../third_party/constants'; 1021 | `, 1022 | 'third_party/constants.js': ` 1023 | exports.greeting = 'Hi, there!'; 1024 | `, 1025 | }); 1026 | expect(execWebpack().status).toBe(0); 1027 | expectCommonDirToIncludeSameFilesAnd({ 1028 | 'dist/server/index.js': (t) => { 1029 | expect(t).toInclude('require("../client/constants.js")'); 1030 | expect(t).not.toInclude('Hi, there!'); 1031 | }, 1032 | 'dist/client/constants.js': (t) => { 1033 | expect(t).toInclude('require("../../third_party/constants.js")'); 1034 | expect(t).not.toInclude('Hi, there!'); 1035 | }, 1036 | }); 1037 | const { stdout, status } = execNode('dist/server/index.js'); 1038 | expect(status).toBe(0); 1039 | expect(stdout).toInclude('Hi, there!'); 1040 | }); 1041 | }); 1042 | 1043 | describe('with options.longestCommonDir', () => { 1044 | it( 1045 | 'uses the specified longest common dir to resolve output paths ' + 1046 | 'if the evaluated common dir is longer', 1047 | () => { 1048 | setupWebpackProject({ 1049 | 'webpack.config.js': ` 1050 | const Plugin = require(${JSON.stringify(rootPath)}); 1051 | module.exports = { 1052 | ${webpackConfigReusable} 1053 | entry: './src/some/where/index.js', 1054 | plugins: [ 1055 | new Plugin({ longestCommonDir: './src' }), 1056 | ], 1057 | }; 1058 | `, 1059 | 'src/some/where/index.js': '', 1060 | }); 1061 | expect(execWebpack().status).toBe(0); 1062 | expectCommonDirToIncludeSameFiles(['dist/some/where/index.js']); 1063 | } 1064 | ); 1065 | 1066 | it( 1067 | 'uses the evaluated common dir to resolve output paths ' + 1068 | 'if the specified longest common dir is longer', 1069 | () => { 1070 | setupWebpackProject({ 1071 | 'webpack.config.js': ` 1072 | const Plugin = require(${JSON.stringify(rootPath)}); 1073 | module.exports = { 1074 | ${webpackConfigReusable} 1075 | entry: './src/index.js', 1076 | plugins: [ 1077 | new Plugin({ longestCommonDir: './src/some/where' }), 1078 | ], 1079 | }; 1080 | `, 1081 | 'src/index.js': '', 1082 | 'src/some/where/.gitkeep': '', 1083 | }); 1084 | expect(execWebpack().status).toBe(0); 1085 | expectCommonDirToIncludeSameFiles(['dist/index.js']); 1086 | } 1087 | ); 1088 | }); 1089 | --------------------------------------------------------------------------------