Skip to content

Commit

Permalink
feat: eliminate limit of .mjs (#29)
Browse files Browse the repository at this point in the history
* Only warning on outputting .mjs

* Add option preferResolveByDependencyAsCjs
  • Loading branch information
licg9999 committed Dec 26, 2022
1 parent 4af8f71 commit 849b2ff
Show file tree
Hide file tree
Showing 8 changed files with 194 additions and 63 deletions.
87 changes: 84 additions & 3 deletions e2e/handles-node_modules.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,16 +113,20 @@ console.log(upperCase('Hi, there!'));
);

it(
'always resolves files in node_modules by CommonJS exports ' +
'regardless of type of import statement',
'with preferResolveByDependencyAsCjs as true, ' +
'resolves files in node_modules by CommonJS exports ignoring type of import statement',
() => {
setupWebpackProject({
'webpack.config.js': `
const Plugin = require('${rootPath}');
module.exports = {
${webpackConfigReusable}
entry: './src/index.js',
plugins: [new Plugin()],
plugins: [
new Plugin({
preferResolveByDependencyAsCjs: true,
}),
],
};
`,
'src/withEsmImport.js': `
Expand Down Expand Up @@ -161,6 +165,83 @@ console.log(green('Hi, there!'));
}
}
);

it(
'with preferResolveByDependencyAsCjs as false, ' +
'resolves files in node_modules according to type of import statement',
() => {
setupWebpackProject({
'webpack.config.js': `
const Plugin = require('${rootPath}');
module.exports = {
${webpackConfigReusable}
entry: './src/index.js',
plugins: [
new Plugin({
preferResolveByDependencyAsCjs: false,
}),
],
};
`,
'src/withEsmImport.js': `
import { green } from 'colorette';
console.log(green('Hi, there!'));
`,
'src/withCjsImport.js': `
const { green } = require('colorette');
console.log(green('Hi, there!'));
`,
'package.json': evaluateMustHavePackageJsonText({
['dependencies']: {
['colorette']: '^2.0.19',
},
}),
});

subCaseWithEsmImport();
subCaseWithCjsImport();

function subCaseWithEsmImport() {
try {
fs.rmSync('dist', { recursive: true });
} catch {}
useSubEntry('withEsmImport');
expect(execWebpack().status).toBe(0);
expectCommonDirToIncludeSameFilesAnd({
'dist/index.js': noop,
[`dist/withEsmImport.js`]: (t) =>
expect(t).toIncludeMultiple([
'require("./node_modules/colorette/index.js")',
'Hi, there!',
]),
'dist/node_modules/colorette/index.js': noop,
});
const { status, stdout } = execNode('dist/index.js');
expect(status).toBe(0);
expect(stdout).toInclude('Hi, there!');
}

function subCaseWithCjsImport() {
try {
fs.rmSync('dist', { recursive: true });
} catch {}
useSubEntry('withCjsImport');
expect(execWebpack().status).toBe(0);
expectCommonDirToIncludeSameFilesAnd({
'dist/index.js': noop,
[`dist/withCjsImport.js`]: (t) =>
expect(t).toIncludeMultiple([
'require("./node_modules/colorette/index.cjs")',
'Hi, there!',
]),
'dist/node_modules/colorette/index.cjs': noop,
});
const { status, stdout } = execNode('dist/index.js');
expect(status).toBe(0);
expect(stdout).toInclude('Hi, there!');
}
}
);
});

describe('with loader helpers indirectly included from node_modules', () => {
Expand Down
28 changes: 23 additions & 5 deletions e2e/validates-inputs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ describe('validates options', () => {
hoistNodeModules?: boolean;
longestCommonDir?: boolean | string;
extentionMapping?: boolean;
preferResolveByDependencyAsCjs?: boolean;
}) {
setupWebpackProject({
'webpack.config.js': `
Expand All @@ -52,6 +53,11 @@ module.exports = {
(b) => boolToText(b, 'longestCommonDir: __dirname,', 'longestCommonDir: 0,')
)}
${boolToText(validOpts.extentionMapping, 'extentionMapping: {},', 'extentionMapping: 0,')}
${
(boolToText(validOpts.preferResolveByDependencyAsCjs),
'preferResolveByDependencyAsCjs: true,',
'preferResolveByDependencyAsCjs: 0,')
}
}),
],
};
Expand Down Expand Up @@ -88,12 +94,19 @@ module.exports = {
expect(stderr).toIncludeMultiple(['Error', 'longestCommonDir', './src/some/where']);
});

it(`throws error if extentionMapping not valid in format`, () => {
it('throws error if extentionMapping not valid in format', () => {
setup({ extentionMapping: false });
const { status, stderr } = execWebpack();
expect(status).toBeGreaterThan(0);
expect(stderr).toIncludeMultiple(['Invalid', 'extentionMapping']);
});

it('throws error if preferResolveByDependencyAsCjs not valid in format', () => {
setup({ preferResolveByDependencyAsCjs: false });
const { status, stderr } = execWebpack();
expect(status).toBeGreaterThan(0);
expect(stderr).toIncludeMultiple(['Invalid', 'preferResolveByDependencyAsCjs']);
});
});

describe('validates entries', () => {
Expand All @@ -112,20 +125,25 @@ module.exports = {
expect(stderr).toIncludeMultiple(['Error', 'No entry', `outside 'node_modules'`]);
});

it(`throws error if any '.mjs' file found`, () => {
it(`prints warning if any '.mjs' file found with target 'node'`, () => {
setupWebpackProject({
'webpack.config.js': `
const Plugin = require('${rootPath}');
module.exports = {
mode: 'production',
target: 'node',
output: {
path: __dirname + '/dist',
},
entry: './src/index.mjs',
plugins: [new Plugin()],
};
`,
'src/index.mjs': '',
});
const { status, stderr } = execWebpack();
expect(status).toBeGreaterThan(0);
expect(stderr).toIncludeMultiple(['Error', 'Outputting ES modules', `'.mjs' files`]);
const { status, stdout } = execWebpack();
expect(status).toBe(0);
expect(stdout).toIncludeMultiple(['WARNING', `'.mjs' files`, './src/index.mjs']);
});

it(`throws error if any '.json' file is not type of JSON`, () => {
Expand Down
73 changes: 48 additions & 25 deletions src/Plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,27 @@ import { validate } from 'schema-utils';

import { commonDirSync } from './commonDir';
import {
enableBuiltinNodeGlobalsByDefaultIfTargetNodeCompatible,
forceDisableOutputtingEsm,
alignResolveByDependency,
enableBuiltinNodeGlobalsByDefault,
forceDisableOutputModule,
forceDisableSplitChunks,
forceSetLibraryType,
isTargetNodeCompatible,
throwErrIfHotModuleReplacementEnabled,
throwErrIfOutputPathNotSpecified,
unifyDependencyResolving,
} from './compilerOptions';
import { Condition, createConditionTest } from './conditionTest';
import {
baseNodeModules,
externalModuleTypeCjs,
extJson,
moduleType,
hookStageVeryEarly,
outputLibraryTypeCjs,
pluginName,
reEsmFile,
reMjsFile,
reNodeModules,
resolveByDependencyTypeCjs,
sourceTypeAsset,
stageVeryEarly,
} from './constants';
import optionsSchema from './optionsSchema.json';
import {
Expand All @@ -34,6 +37,7 @@ import {
Module,
NormalModule,
sources,
WebpackError,
} from './peers/webpack';
import ModuleProfile from './peers/webpack/lib/ModuleProfile';
import { SourceMapDevToolPluginController } from './SourceMapDevToolPluginController';
Expand All @@ -49,6 +53,7 @@ export interface TranspileWebpackPluginInternalOptions {
hoistNodeModules: boolean;
longestCommonDir?: string;
extentionMapping: Record<string, string>;
preferResolveByDependencyAsCjs: boolean;
}

export class TranspileWebpackPlugin {
Expand All @@ -67,30 +72,43 @@ export class TranspileWebpackPlugin {
exclude: options.exclude ?? [],
hoistNodeModules: options.hoistNodeModules ?? true,
extentionMapping: options.extentionMapping ?? {},
preferResolveByDependencyAsCjs: options.preferResolveByDependencyAsCjs ?? true,
};
this.sourceMapDevToolPluginController = new SourceMapDevToolPluginController();
this.terserWebpackPluginController = new TerserWebpackPluginController();
}

apply(compiler: Compiler) {
const { exclude, hoistNodeModules, longestCommonDir, extentionMapping } = this.options;
const {
exclude,
hoistNodeModules,
longestCommonDir,
extentionMapping,
preferResolveByDependencyAsCjs,
} = this.options;

forceDisableSplitChunks(compiler.options);
forceSetLibraryType(compiler.options, moduleType);
forceDisableOutputtingEsm(compiler.options);
forceSetLibraryType(compiler.options, outputLibraryTypeCjs);
forceDisableOutputModule(compiler.options);

const isPathExcluded = createConditionTest(exclude);
const isPathInNodeModules = createConditionTest(reNodeModules);
const isPathEsmFile = createConditionTest(reEsmFile);
const isPathMjsFile = createConditionTest(reMjsFile);

this.sourceMapDevToolPluginController.apply(compiler);
this.terserWebpackPluginController.apply(compiler);

compiler.hooks.environment.tap({ name: pluginName, stage: stageVeryEarly }, () => {
compiler.hooks.environment.tap({ name: pluginName, stage: hookStageVeryEarly }, () => {
throwErrIfOutputPathNotSpecified(compiler.options);
throwErrIfHotModuleReplacementEnabled(compiler.options);
enableBuiltinNodeGlobalsByDefaultIfTargetNodeCompatible(compiler.options);
unifyDependencyResolving(compiler.options, moduleType.split('-')[0]);

if (isTargetNodeCompatible(compiler.options.target)) {
enableBuiltinNodeGlobalsByDefault(compiler.options);
}

if (preferResolveByDependencyAsCjs) {
alignResolveByDependency(compiler.options, resolveByDependencyTypeCjs);
}
});

compiler.hooks.finishMake.tapPromise(pluginName, async (compilation) => {
Expand All @@ -116,18 +134,20 @@ export class TranspileWebpackPlugin {
);

if (entryResourcePathsWoNodeModules.length === 0) {
throw new Error(`No entry is found outside 'node_modules'`);
throw new Error(`${pluginName}${os.EOL}No entry is found outside 'node_modules'`);
}

const entryResourcePathsOutputtingEsm = entryResourcePaths.filter(isPathEsmFile);
if (entryResourcePathsOutputtingEsm.length > 0) {
throw new Error(
`Outputting ES modules is not supported yet. Found '.mjs' files:${os.EOL}` +
entryResourcePathsOutputtingEsm
.map((p) => ' ' + path.relative(context, p))
.join(os.EOL) +
`${os.EOL}----`
);
if (isTargetNodeCompatible(compiler.options.target)) {
const entryResourceMjsFiles = entryResourcePaths.filter(isPathMjsFile);
if (entryResourceMjsFiles.length > 0) {
const warning = new WebpackError(
`${pluginName}${os.EOL}Might be problematic to run '.mjs' files with target 'node'. Found '.mjs' files:${os.EOL}` +
entryResourceMjsFiles
.map((p) => ` .${path.sep}${path.relative(context, p)}`)
.join(os.EOL)
);
compilation.warnings.push(warning);
}
}

const commonDir = commonDirSync(entryResourcePaths, {
Expand Down Expand Up @@ -239,7 +259,7 @@ export class TranspileWebpackPlugin {
request = `.${path.sep}${request}`;
}

const extModCandidate = new ExternalModule(request, moduleType, request);
const extModCandidate = new ExternalModule(request, externalModuleTypeCjs, request);
let extMod = compilation.getModule(extModCandidate);
let doesExtModNeedBuild = false;
if (!(extMod instanceof ExternalModule)) {
Expand Down Expand Up @@ -342,7 +362,10 @@ export class TranspileWebpackPlugin {
const { jsonData } = entryMod.buildInfo;
if (!jsonData) {
throw new Error(
`File '${path.relative(context, entryResourcePath)}' is not type of JSON`
`${pluginName}${os.EOL}File '${path.relative(
context,
entryResourcePath
)}' is not type of JSON`
);
}
entryMod.buildInfo.assets = {
Expand Down
6 changes: 3 additions & 3 deletions src/SourceMapDevToolPluginController.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { pluginName, stageVeryEarly } from './constants';
import { pluginName, hookStageVeryEarly } from './constants';
import { Compiler, EvalSourceMapDevToolPlugin, SourceMapDevToolPlugin } from './peers/webpack';
import { CompilerOptions, SourceMapDevToolPluginOptions } from './types';

Expand All @@ -7,7 +7,7 @@ export class SourceMapDevToolPluginController {
oldDevtool: CompilerOptions['devtool'];

apply(compiler: Compiler): void {
compiler.hooks.environment.tap({ name: pluginName, stage: stageVeryEarly }, () => {
compiler.hooks.environment.tap({ name: pluginName, stage: hookStageVeryEarly }, () => {
if (compiler.options.devtool) {
if (compiler.options.devtool.includes('source-map')) {
this.initSourceMapDevToolPlugin(compiler);
Expand All @@ -18,7 +18,7 @@ export class SourceMapDevToolPluginController {
}
});

compiler.hooks.initialize.tap({ name: pluginName, stage: stageVeryEarly }, () => {
compiler.hooks.initialize.tap({ name: pluginName, stage: hookStageVeryEarly }, () => {
// Restore devtool after compiler options get processed inside webpack.
this.restoreDevtool(compiler.options);
});
Expand Down
9 changes: 7 additions & 2 deletions src/commonDir.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';

import { pluginName } from './constants';

function longestCommonPrefix(strs: string[]): string {
if (!strs.length) return '';

Expand Down Expand Up @@ -38,15 +41,17 @@ export function commonDirSync(
if (!isDir(prefix)) {
prefix = path.dirname(prefix);
if (!isDir(prefix)) {
throw new Error('No valid common dir is figured out');
throw new Error(`${pluginName}${os.EOL}No valid common dir is figured out`);
}
}

if (opts.longestCommonDir) {
const finalLongestCommonDir = normalizePath(opts.longestCommonDir, opts);

if (!isDir(finalLongestCommonDir)) {
throw new Error(`The longestCommonDir '${opts.longestCommonDir}' doesn't exist`);
throw new Error(
`${pluginName}${os.EOL}The longestCommonDir '${opts.longestCommonDir}' doesn't exist`
);
}

if (prefix.startsWith(finalLongestCommonDir)) {
Expand Down
Loading

0 comments on commit 849b2ff

Please sign in to comment.