Skip to content

Commit

Permalink
fix: improved support for .cts and .mts extensions
Browse files Browse the repository at this point in the history
  • Loading branch information
alexander-akait committed Jun 3, 2023
1 parent 73bd2cc commit a77daf2
Show file tree
Hide file tree
Showing 23 changed files with 206 additions and 63 deletions.
128 changes: 98 additions & 30 deletions packages/webpack-cli/src/webpack-cli.ts
Expand Up @@ -322,37 +322,83 @@ class WebpackCLI implements IWebpackCLI {
process.exit(2);
}

async tryRequireThenImport<T>(module: ModuleName, handleError = true): Promise<T> {
async tryRequireThenImport<T>(
module: ModuleName,
handleError = true,
moduleType: "unknown" | "commonjs" | "esm" = "unknown",
): Promise<T> {
let result;

try {
result = require(module);
} catch (error) {
const dynamicImportLoader: null | DynamicImport<T> =
require("./utils/dynamic-import-loader")();
if (
((error as ImportLoaderError).code === "ERR_REQUIRE_ESM" ||
process.env.WEBPACK_CLI_FORCE_LOAD_ESM_CONFIG) &&
pathToFileURL &&
dynamicImportLoader
) {
const urlForConfig = pathToFileURL(module);

result = await dynamicImportLoader(urlForConfig);
result = result.default;
switch (moduleType) {
case "unknown": {
try {
result = require(module);
} catch (error) {
const dynamicImportLoader: null | DynamicImport<T> =
require("./utils/dynamic-import-loader")();
if (
((error as ImportLoaderError).code === "ERR_REQUIRE_ESM" ||
process.env.WEBPACK_CLI_FORCE_LOAD_ESM_CONFIG) &&
pathToFileURL &&
dynamicImportLoader
) {
const urlForConfig = pathToFileURL(module);

result = await dynamicImportLoader(urlForConfig);
result = result.default;

return result;
}

return result;
if (handleError) {
this.logger.error(error);
process.exit(2);
} else {
throw error;
}
}
break;
}
case "commonjs": {
try {
result = require(module);
} catch (error) {
if (handleError) {
this.logger.error(error);
process.exit(2);
} else {
throw error;
}
}
break;
}
case "esm": {
try {
const dynamicImportLoader: null | DynamicImport<T> =
require("./utils/dynamic-import-loader")();

if (handleError) {
this.logger.error(error);
process.exit(2);
} else {
throw error;
if (pathToFileURL && dynamicImportLoader) {
const urlForConfig = pathToFileURL(module);

result = await dynamicImportLoader(urlForConfig);
result = result.default;

return result;
}
} catch (error) {
if (handleError) {
this.logger.error(error);
process.exit(2);
} else {
throw error;
}
}

break;
}
}

// For babel/typescript
// For babel and other, only commonjs
if (result && typeof result === "object" && "default" in result) {
result = result.default || {};
}
Expand Down Expand Up @@ -1749,8 +1795,15 @@ class WebpackCLI implements IWebpackCLI {

const interpret = require("interpret");
const loadConfigByPath = async (configPath: string, argv: Argv = {}) => {
const ext = path.extname(configPath);
const interpreted = Object.keys(interpret.jsVariants).find((variant) => variant === ext);
const ext = path.extname(configPath).toLowerCase();
let interpreted = interpret.jsVariants[ext];

// Fallback `.cts` to `.ts`
// TODO implement good `.mts` support after https://github.com/gulpjs/rechoir/issues/43
// For ESM and `.mts` you need to use: 'NODE_OPTIONS="--loader ts-node/esm" webpack-cli --config ./webpack.config.mts'
if (!interpreted && /\.cts$/.test(ext)) {
interpreted = interpret.jsVariants[".ts"];
}

if (interpreted && !disableInterpret) {
const rechoir: Rechoir = require("rechoir");
Expand All @@ -1777,10 +1830,24 @@ class WebpackCLI implements IWebpackCLI {

type LoadConfigOption = PotentialPromise<WebpackConfiguration>;

let moduleType: "unknown" | "commonjs" | "esm" = "unknown";

switch (ext) {
case ".cjs":
case ".cts":
moduleType = "commonjs";
break;
case ".mjs":
case ".mts":
moduleType = "esm";
break;
}

try {
options = await this.tryRequireThenImport<LoadConfigOption | LoadConfigOption[]>(
configPath,
false,
moduleType,
);
// @ts-expect-error error type assertion
} catch (error: Error) {
Expand Down Expand Up @@ -1897,14 +1964,15 @@ class WebpackCLI implements IWebpackCLI {
".cjs",
".ts",
".cts",
".mts",
...Object.keys(interpret.extensions),
];
// Order defines the priority, in decreasing order
const defaultConfigFiles = [
"webpack.config",
".webpack/webpack.config",
".webpack/webpackfile",
].flatMap((filename) => extensions.map((ext) => path.resolve(filename + ext)));
const defaultConfigFiles = new Set(
["webpack.config", ".webpack/webpack.config", ".webpack/webpackfile"].flatMap((filename) =>
extensions.map((ext) => path.resolve(filename + ext)),
),
);

let foundDefaultConfigFile;

Expand Down
2 changes: 1 addition & 1 deletion test/build/config-format/coffee/webpack.config.coffee
@@ -1,7 +1,7 @@
path = require 'path'

config =
mode: 'production'
mode: 'development'
entry: './main.js'
output:
path: path.resolve(__dirname, 'dist')
Expand Down
@@ -1,7 +1,7 @@
const path = require("path");

const config = {
mode: "production",
mode: "development",
entry: "./main.js",
output: {
path: path.resolve(__dirname, "dist"),
Expand Down
2 changes: 1 addition & 1 deletion test/build/config-format/commonjs/webpack.config.cjs
@@ -1,7 +1,7 @@
const path = require("path");

const config = {
mode: "production",
mode: "development",
entry: "./main.js",
output: {
path: path.resolve(__dirname, "dist"),
Expand Down
Expand Up @@ -2,7 +2,7 @@
import * as path from "path";

const config = {
mode: "production",
mode: "development",
entry: "./main.ts",
output: {
path: path.resolve(__dirname, "dist"),
Expand Down
File renamed without changes.
11 changes: 11 additions & 0 deletions test/build/config-format/esm/mjs.test.js
@@ -0,0 +1,11 @@
const { run } = require("../../../utils/test-utils");

describe("webpack cli", () => {
it("should support mjs config format", async () => {
const { exitCode, stderr, stdout } = await run(__dirname, ["-c", "webpack.config.mjs"]);

expect(exitCode).toBe(0);
expect(stderr).toBeFalsy();
expect(stdout).toBeTruthy();
});
});
Expand Up @@ -2,7 +2,7 @@ import { fileURLToPath } from "url";
import path from "path";

export default {
mode: "production",
mode: "development",
entry: "./main.js",
output: {
path: path.resolve(path.dirname(fileURLToPath(import.meta.url)), "dist"),
Expand Down
2 changes: 1 addition & 1 deletion test/build/config-format/failure/webpack.config.iced
@@ -1,7 +1,7 @@
path = require 'path'

config =
mode: 'production'
mode: 'development'
entry: './main.js'
output:
path: path.resolve(__dirname, 'dist')
Expand Down
18 changes: 0 additions & 18 deletions test/build/config-format/mjs/mjs.test.js

This file was deleted.

1 change: 1 addition & 0 deletions test/build/config-format/typescript-commonjs/main.ts
@@ -0,0 +1 @@
console.log("Main typescript file");
5 changes: 5 additions & 0 deletions test/build/config-format/typescript-commonjs/tsconfig.json
@@ -0,0 +1,5 @@
{
"compilerOptions": {
"module": "commonjs"
}
}
14 changes: 14 additions & 0 deletions test/build/config-format/typescript-commonjs/typescript.test.js
@@ -0,0 +1,14 @@
const { run } = require("../../../utils/test-utils");
const { existsSync } = require("fs");
const { resolve } = require("path");

describe("webpack cli", () => {
it("should support typescript file", async () => {
const { exitCode, stderr, stdout } = await run(__dirname, ["-c", "./webpack.config.cts"]);

expect(stderr).toBeFalsy();
expect(stdout).toBeTruthy();
expect(exitCode).toBe(0);
expect(existsSync(resolve(__dirname, "dist/foo.bundle.js"))).toBeTruthy();
});
});
16 changes: 16 additions & 0 deletions test/build/config-format/typescript-commonjs/webpack.config.cts
@@ -0,0 +1,16 @@
/* eslint-disable node/no-unsupported-features/es-syntax */
/** eslint-disable **/
import * as path from "path";

// cspell:ignore elopment
const mode: string = "dev" + "elopment";
const config = {
mode,
entry: "./main.ts",
output: {
path: path.resolve(__dirname, "dist"),
filename: "foo.bundle.js",
},
};

export = config;
1 change: 1 addition & 0 deletions test/build/config-format/typescript-esnext-mjs/main.ts
@@ -0,0 +1 @@
console.log("Rimuru Tempest");
6 changes: 6 additions & 0 deletions test/build/config-format/typescript-esnext-mjs/package.json
@@ -0,0 +1,6 @@
{
"type": "module",
"engines": {
"node": ">=14.15.0"
}
}
6 changes: 6 additions & 0 deletions test/build/config-format/typescript-esnext-mjs/tsconfig.json
@@ -0,0 +1,6 @@
{
"compilerOptions": {
"module": "esnext",
"allowSyntheticDefaultImports": true
}
}
20 changes: 20 additions & 0 deletions test/build/config-format/typescript-esnext-mjs/typescript.test.js
@@ -0,0 +1,20 @@
// eslint-disable-next-line node/no-unpublished-require
const { run } = require("../../../utils/test-utils");
const { existsSync } = require("fs");
const { resolve } = require("path");

describe("webpack cli", () => {
it("should support typescript esnext file", async () => {
const env = { ...process.env };

const { exitCode, stderr, stdout } = await run(__dirname, ["-c", "./webpack.config.mts"], {
nodeOptions: ["--experimental-loader=ts-node/esm"],
env,
});

expect(stderr).not.toBeFalsy(); // Deprecation warning logs on stderr
expect(stdout).toBeTruthy();
expect(exitCode).toBe(0);
expect(existsSync(resolve(__dirname, "dist/foo.bundle.js"))).toBeTruthy();
});
});
15 changes: 15 additions & 0 deletions test/build/config-format/typescript-esnext-mjs/webpack.config.mts
@@ -0,0 +1,15 @@
/* eslint-disable node/no-unsupported-features/es-syntax */
import * as path from "path";

// cspell:ignore elopment
const mode: string = "dev" + "elopment";
const config = {
mode,
entry: "./main.ts",
output: {
path: path.resolve("dist"),
filename: "foo.bundle.js",
},
};

export default config;
7 changes: 0 additions & 7 deletions test/build/config-format/typescript-esnext/typescript.test.js
Expand Up @@ -6,13 +6,6 @@ const { resolve } = require("path");
describe("webpack cli", () => {
it("should support typescript esnext file", async () => {
const majorNodeVersion = process.version.slice(1, 3);

if (majorNodeVersion < 14) {
expect(true).toBe(true);

return;
}

const env = { ...process.env };

if (majorNodeVersion >= 20) {
Expand Down
4 changes: 3 additions & 1 deletion test/build/config-format/typescript-esnext/webpack.config.ts
@@ -1,8 +1,10 @@
/* eslint-disable node/no-unsupported-features/es-syntax */
import * as path from "path";

// cspell:ignore elopment
const mode: string = "dev" + "elopment";
const config = {
mode: "production",
mode,
entry: "./main.ts",
output: {
path: path.resolve("dist"),
Expand Down
1 change: 1 addition & 0 deletions test/build/config-format/typescript/typescript.test.js
Expand Up @@ -5,6 +5,7 @@ const { resolve } = require("path");
describe("webpack cli", () => {
it("should support typescript file", async () => {
const { exitCode, stderr, stdout } = await run(__dirname, ["-c", "./webpack.config.ts"], {
// For `nyc`, remove it when we drop `nyc` usage
nodeOptions: ["--require=ts-node/register"],
});

Expand Down
4 changes: 3 additions & 1 deletion test/build/config-format/typescript/webpack.config.ts
Expand Up @@ -2,8 +2,10 @@
/** eslint-disable **/
import * as path from "path";

// cspell:ignore elopment
const mode: string = "dev" + "elopment";
const config = {
mode: "production",
mode,
entry: "./main.ts",
output: {
path: path.resolve(__dirname, "dist"),
Expand Down

0 comments on commit a77daf2

Please sign in to comment.