diff --git a/lib/craco-fix-plugin.js b/lib/craco-fix-plugin.js index 9bbe7c6..e27c607 100644 --- a/lib/craco-fix-plugin.js +++ b/lib/craco-fix-plugin.js @@ -46,6 +46,44 @@ const isolateAppCache = (webpackConfig) => { }); }; +const NODE_MODULES_MJS = /\.mjs$/; +const ESM_CONDITION_NAMES = ['import', 'module', '...']; + +const isNodeModulesMjsAutoRule = (rule) => { + if (!rule || rule.type !== 'javascript/auto') { + return false; + } + return Boolean(rule.test) && String(rule.test) === String(NODE_MODULES_MJS); +}; + +// webpack 5.107+ 解析 dual package 时可能先命中 exports.require(CJS), +// CRA 再按 ESM named export 校验 → `X is not exported from 'motion-utils'`。 +// 优先 import/module,并把 node_modules 的 .mjs 当普通 JS(fullySpecified: false)。 +const preferEsmFromNodeModules = (webpackConfig) => { + webpackConfig.module = webpackConfig.module || {}; + webpackConfig.module.rules = webpackConfig.module.rules || []; + if (!webpackConfig.module.rules.some(isNodeModulesMjsAutoRule)) { + webpackConfig.module.rules.unshift({ + test: NODE_MODULES_MJS, + include: /node_modules/, + type: 'javascript/auto', + resolve: { + fullySpecified: false + } + }); + } + + webpackConfig.resolve = webpackConfig.resolve || {}; + const current = webpackConfig.resolve.conditionNames; + const alreadyPreferred = Array.isArray(current) + && current[0] === 'import' + && current[1] === 'module' + && current[2] === '...'; + if (!alreadyPreferred) { + webpackConfig.resolve.conditionNames = ESM_CONDITION_NAMES.slice(); + } +}; + // alias 到绝对路径时,即便落在 node_modules 下,ModuleScopePlugin 仍按 src 外拦截 const allowReadmeOutsideSrc = (webpackConfig) => { const plugins = webpackConfig.resolve && webpackConfig.resolve.plugins; @@ -97,6 +135,7 @@ module.exports = { "zlib": false, "fs": false }; + preferEsmFromNodeModules(webpackConfig); allowReadmeOutsideSrc(webpackConfig); isolateAppCache(webpackConfig); return webpackConfig; diff --git a/lib/readme-webpack-plugin.js b/lib/readme-webpack-plugin.js index b08dbe1..60674c8 100644 --- a/lib/readme-webpack-plugin.js +++ b/lib/readme-webpack-plugin.js @@ -1,6 +1,6 @@ const path = require('path'); const fs = require('fs-extra'); -const VirtualModulesPlugin = require('webpack-virtual-modules'); +const VirtualModulesPlugin = require('./webpack-virtual-modules'); const virtualModules = new VirtualModulesPlugin(); const env = require('./env'); const { getModuleList } = require('./utils'); diff --git a/lib/webpack-virtual-modules/LICENSE b/lib/webpack-virtual-modules/LICENSE new file mode 100644 index 0000000..913422a --- /dev/null +++ b/lib/webpack-virtual-modules/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 SysGears + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/lib/webpack-virtual-modules/README.md b/lib/webpack-virtual-modules/README.md new file mode 100644 index 0000000..e5498f5 --- /dev/null +++ b/lib/webpack-virtual-modules/README.md @@ -0,0 +1,7 @@ +# webpack-virtual-modules (vendored) + +Copied from [webpack-virtual-modules@0.6.2](https://github.com/sysgears/webpack-virtual-modules) (MIT). + +Used by `readme-webpack-plugin.js` to inject virtual readme/manifest modules during webpack compile. + +**Why vendor:** `@kne/modules-dev` previously depended on `webpack-virtual-modules@^0.5.0`. That release throws `Cannot read properties of null (reading 'fileWatchers')` when `writeModule` runs while `WatchFileSystem.watcher` is still `null` (common on newer Node/webpack). `0.6.2` guards this; keeping the implementation in-repo avoids relying on the external package resolution. diff --git a/lib/webpack-virtual-modules/index.d.ts b/lib/webpack-virtual-modules/index.d.ts new file mode 100644 index 0000000..2d8f611 --- /dev/null +++ b/lib/webpack-virtual-modules/index.d.ts @@ -0,0 +1,15 @@ +import type { Compiler } from 'webpack'; +declare const ALL = "all"; +declare const STATIC = "static"; +declare const DYNAMIC = "dynamic"; +declare type AvailableModules = typeof ALL | typeof STATIC | typeof DYNAMIC; +declare class VirtualModulesPlugin { + private _staticModules; + private _compiler; + private _watcher; + constructor(modules?: Record); + getModuleList(filter?: AvailableModules): {}; + writeModule(filePath: string, contents: string): void; + apply(compiler: Compiler): void; +} +export = VirtualModulesPlugin; diff --git a/lib/webpack-virtual-modules/index.js b/lib/webpack-virtual-modules/index.js new file mode 100644 index 0000000..06fa523 --- /dev/null +++ b/lib/webpack-virtual-modules/index.js @@ -0,0 +1,292 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +const path_1 = __importDefault(require("path")); +const virtual_stats_1 = require("./virtual-stats"); +let inode = 45000000; +const ALL = 'all'; +const STATIC = 'static'; +const DYNAMIC = 'dynamic'; +function checkActivation(instance) { + if (!instance._compiler) { + throw new Error('You must use this plugin only after creating webpack instance!'); + } +} +function getModulePath(filePath, compiler) { + return path_1.default.isAbsolute(filePath) ? filePath : path_1.default.join(compiler.context, filePath); +} +function createWebpackData(result) { + return (backendOrStorage) => { + if (backendOrStorage._data) { + const curLevelIdx = backendOrStorage._currentLevel; + const curLevel = backendOrStorage._levels[curLevelIdx]; + return { + result, + level: curLevel, + }; + } + return [null, result]; + }; +} +function getData(storage, key) { + if (storage._data instanceof Map) { + return storage._data.get(key); + } + else if (storage._data) { + return storage.data[key]; + } + else if (storage.data instanceof Map) { + return storage.data.get(key); + } + else { + return storage.data[key]; + } +} +function setData(backendOrStorage, key, valueFactory) { + const value = valueFactory(backendOrStorage); + if (backendOrStorage._data instanceof Map) { + backendOrStorage._data.set(key, value); + } + else if (backendOrStorage._data) { + backendOrStorage.data[key] = value; + } + else if (backendOrStorage.data instanceof Map) { + backendOrStorage.data.set(key, value); + } + else { + backendOrStorage.data[key] = value; + } +} +function getStatStorage(fileSystem) { + if (fileSystem._statStorage) { + return fileSystem._statStorage; + } + else if (fileSystem._statBackend) { + return fileSystem._statBackend; + } + else { + throw new Error("Couldn't find a stat storage"); + } +} +function getFileStorage(fileSystem) { + if (fileSystem._readFileStorage) { + return fileSystem._readFileStorage; + } + else if (fileSystem._readFileBackend) { + return fileSystem._readFileBackend; + } + else { + throw new Error("Couldn't find a readFileStorage"); + } +} +function getReadDirBackend(fileSystem) { + if (fileSystem._readdirBackend) { + return fileSystem._readdirBackend; + } + else if (fileSystem._readdirStorage) { + return fileSystem._readdirStorage; + } + else { + throw new Error("Couldn't find a readDirStorage from Webpack Internals"); + } +} +function getRealpathBackend(fileSystem) { + if (fileSystem._realpathBackend) { + return fileSystem._realpathBackend; + } +} +class VirtualModulesPlugin { + constructor(modules) { + this._compiler = null; + this._watcher = null; + this._staticModules = modules || null; + } + getModuleList(filter = ALL) { + var _a, _b; + let modules = {}; + const shouldGetStaticModules = filter === ALL || filter === STATIC; + const shouldGetDynamicModules = filter === ALL || filter === DYNAMIC; + if (shouldGetStaticModules) { + modules = Object.assign(Object.assign({}, modules), this._staticModules); + } + if (shouldGetDynamicModules) { + const finalInputFileSystem = (_a = this._compiler) === null || _a === void 0 ? void 0 : _a.inputFileSystem; + const virtualFiles = (_b = finalInputFileSystem === null || finalInputFileSystem === void 0 ? void 0 : finalInputFileSystem._virtualFiles) !== null && _b !== void 0 ? _b : {}; + const dynamicModules = {}; + Object.keys(virtualFiles).forEach((key) => { + dynamicModules[key] = virtualFiles[key].contents; + }); + modules = Object.assign(Object.assign({}, modules), dynamicModules); + } + return modules; + } + writeModule(filePath, contents) { + if (!this._compiler) { + throw new Error(`Plugin has not been initialized`); + } + checkActivation(this); + const len = contents ? contents.length : 0; + const time = Date.now(); + const date = new Date(time); + const stats = new virtual_stats_1.VirtualStats({ + dev: 8675309, + nlink: 0, + uid: 1000, + gid: 1000, + rdev: 0, + blksize: 4096, + ino: inode++, + mode: 33188, + size: len, + blocks: Math.floor(len / 4096), + atime: date, + mtime: date, + ctime: date, + birthtime: date, + }); + const modulePath = getModulePath(filePath, this._compiler); + if (process.env.WVM_DEBUG) + console.log(this._compiler.name, 'Write virtual module:', modulePath, contents); + let finalWatchFileSystem = this._watcher && this._watcher.watchFileSystem; + while (finalWatchFileSystem && finalWatchFileSystem.wfs) { + finalWatchFileSystem = finalWatchFileSystem.wfs; + } + let finalInputFileSystem = this._compiler.inputFileSystem; + while (finalInputFileSystem && finalInputFileSystem._inputFileSystem) { + finalInputFileSystem = finalInputFileSystem._inputFileSystem; + } + finalInputFileSystem._writeVirtualFile(modulePath, stats, contents); + if (finalWatchFileSystem && + finalWatchFileSystem.watcher && + (finalWatchFileSystem.watcher.fileWatchers.size || finalWatchFileSystem.watcher.fileWatchers.length)) { + const fileWatchers = finalWatchFileSystem.watcher.fileWatchers instanceof Map + ? Array.from(finalWatchFileSystem.watcher.fileWatchers.values()) + : finalWatchFileSystem.watcher.fileWatchers; + for (let fileWatcher of fileWatchers) { + if ('watcher' in fileWatcher) { + fileWatcher = fileWatcher.watcher; + } + if (fileWatcher.path === modulePath) { + if (process.env.DEBUG) + console.log(this._compiler.name, 'Emit file change:', modulePath, time); + delete fileWatcher.directoryWatcher._cachedTimeInfoEntries; + fileWatcher.emit('change', time, null); + } + } + } + } + apply(compiler) { + this._compiler = compiler; + const afterEnvironmentHook = () => { + let finalInputFileSystem = compiler.inputFileSystem; + while (finalInputFileSystem && finalInputFileSystem._inputFileSystem) { + finalInputFileSystem = finalInputFileSystem._inputFileSystem; + } + if (!finalInputFileSystem._writeVirtualFile) { + const originalPurge = finalInputFileSystem.purge; + finalInputFileSystem.purge = () => { + originalPurge.apply(finalInputFileSystem, []); + if (finalInputFileSystem._virtualFiles) { + Object.keys(finalInputFileSystem._virtualFiles).forEach((file) => { + const data = finalInputFileSystem._virtualFiles[file]; + finalInputFileSystem._writeVirtualFile(file, data.stats, data.contents); + }); + } + }; + finalInputFileSystem._writeVirtualFile = (file, stats, contents) => { + const statStorage = getStatStorage(finalInputFileSystem); + const fileStorage = getFileStorage(finalInputFileSystem); + const readDirStorage = getReadDirBackend(finalInputFileSystem); + const realPathStorage = getRealpathBackend(finalInputFileSystem); + finalInputFileSystem._virtualFiles = finalInputFileSystem._virtualFiles || {}; + finalInputFileSystem._virtualFiles[file] = { stats: stats, contents: contents }; + setData(statStorage, file, createWebpackData(stats)); + setData(fileStorage, file, createWebpackData(contents)); + const segments = file.split(/[\\/]/); + let count = segments.length - 1; + const minCount = segments[0] ? 1 : 0; + while (count > minCount) { + const dir = segments.slice(0, count).join(path_1.default.sep) || path_1.default.sep; + try { + finalInputFileSystem.readdirSync(dir); + } + catch (e) { + const time = Date.now(); + const dirStats = new virtual_stats_1.VirtualStats({ + dev: 8675309, + nlink: 0, + uid: 1000, + gid: 1000, + rdev: 0, + blksize: 4096, + ino: inode++, + mode: 16877, + size: stats.size, + blocks: Math.floor(stats.size / 4096), + atime: time, + mtime: time, + ctime: time, + birthtime: time, + }); + setData(readDirStorage, dir, createWebpackData([])); + if (realPathStorage) { + setData(realPathStorage, dir, createWebpackData(dir)); + } + setData(statStorage, dir, createWebpackData(dirStats)); + } + let dirData = getData(getReadDirBackend(finalInputFileSystem), dir); + dirData = dirData[1] || dirData.result; + const filename = segments[count]; + if (dirData.indexOf(filename) < 0) { + const files = dirData.concat([filename]).sort(); + setData(getReadDirBackend(finalInputFileSystem), dir, createWebpackData(files)); + } + else { + break; + } + count--; + } + }; + } + }; + const afterResolversHook = () => { + if (this._staticModules) { + for (const [filePath, contents] of Object.entries(this._staticModules)) { + this.writeModule(filePath, contents); + } + this._staticModules = null; + } + }; + const version = typeof compiler.webpack === 'undefined' ? 4 : 5; + const watchRunHook = (watcher, callback) => { + this._watcher = watcher.compiler || watcher; + const virtualFiles = compiler.inputFileSystem._virtualFiles; + const fts = compiler.fileTimestamps; + if (virtualFiles && fts && typeof fts.set === 'function') { + Object.keys(virtualFiles).forEach((file) => { + const mtime = +virtualFiles[file].stats.mtime; + fts.set(file, version === 4 + ? mtime + : { + safeTime: mtime, + timestamp: mtime, + }); + }); + } + callback(); + }; + if (compiler.hooks) { + compiler.hooks.afterEnvironment.tap('VirtualModulesPlugin', afterEnvironmentHook); + compiler.hooks.afterResolvers.tap('VirtualModulesPlugin', afterResolversHook); + compiler.hooks.watchRun.tapAsync('VirtualModulesPlugin', watchRunHook); + } + else { + compiler.plugin('after-environment', afterEnvironmentHook); + compiler.plugin('after-resolvers', afterResolversHook); + compiler.plugin('watch-run', watchRunHook); + } + } +} +module.exports = VirtualModulesPlugin; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/lib/webpack-virtual-modules/package.json b/lib/webpack-virtual-modules/package.json new file mode 100644 index 0000000..9f4e918 --- /dev/null +++ b/lib/webpack-virtual-modules/package.json @@ -0,0 +1,8 @@ +{ + "name": "@kne/modules-dev-webpack-virtual-modules", + "version": "0.6.2", + "private": true, + "description": "Vendored webpack-virtual-modules@0.6.2 (MIT) for @kne/modules-dev", + "main": "index.js", + "license": "MIT" +} diff --git a/lib/webpack-virtual-modules/virtual-stats.d.ts b/lib/webpack-virtual-modules/virtual-stats.d.ts new file mode 100644 index 0000000..ee7b643 --- /dev/null +++ b/lib/webpack-virtual-modules/virtual-stats.d.ts @@ -0,0 +1,11 @@ +export declare class VirtualStats { + constructor(config: any); + private _checkModeProperty; + isDirectory(): boolean; + isFile(): boolean; + isBlockDevice(): boolean; + isCharacterDevice(): boolean; + isSymbolicLink(): boolean; + isFIFO(): boolean; + isSocket(): boolean; +} diff --git a/lib/webpack-virtual-modules/virtual-stats.js b/lib/webpack-virtual-modules/virtual-stats.js new file mode 100644 index 0000000..b2ec3ee --- /dev/null +++ b/lib/webpack-virtual-modules/virtual-stats.js @@ -0,0 +1,43 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.VirtualStats = void 0; +const constants_1 = __importDefault(require("constants")); +class VirtualStats { + constructor(config) { + for (const key in config) { + if (!Object.prototype.hasOwnProperty.call(config, key)) { + continue; + } + this[key] = config[key]; + } + } + _checkModeProperty(property) { + return (this.mode & constants_1.default.S_IFMT) === property; + } + isDirectory() { + return this._checkModeProperty(constants_1.default.S_IFDIR); + } + isFile() { + return this._checkModeProperty(constants_1.default.S_IFREG); + } + isBlockDevice() { + return this._checkModeProperty(constants_1.default.S_IFBLK); + } + isCharacterDevice() { + return this._checkModeProperty(constants_1.default.S_IFCHR); + } + isSymbolicLink() { + return this._checkModeProperty(constants_1.default.S_IFLNK); + } + isFIFO() { + return this._checkModeProperty(constants_1.default.S_IFIFO); + } + isSocket() { + return this._checkModeProperty(constants_1.default.S_IFSOCK); + } +} +exports.VirtualStats = VirtualStats; +//# sourceMappingURL=virtual-stats.js.map \ No newline at end of file diff --git a/package.json b/package.json index 5add8ac..92e3a9c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kne/modules-dev", - "version": "2.4.14", + "version": "2.4.15", "description": "用于辅助在项目内启动一个规范化组件开发的环境", "publishConfig": { "access": "public", @@ -57,8 +57,7 @@ "highlight.js": "^11.7.0", "inquirer": "^8.2.4", "lodash": "^4.17.21", - "sass": "^1.75.0", - "webpack-virtual-modules": "^0.5.0" + "sass": "^1.75.0" }, "peerDependencies": { "@kne/craco": ">=7.0.0", diff --git a/test/craco-fix-plugin.test.js b/test/craco-fix-plugin.test.js index 967dbbf..710bde3 100644 --- a/test/craco-fix-plugin.test.js +++ b/test/craco-fix-plugin.test.js @@ -42,6 +42,44 @@ describe('craco-fix-plugin readme alias', () => { }); }); +describe('craco-fix-plugin ESM node_modules', () => { + it('应为 node_modules 的 .mjs 加上 javascript/auto 且 fullySpecified: false', () => { + const webpackConfig = createConfig(true); + plugin.overrideWebpackConfig({ webpackConfig, context: { env: 'production' } }); + const rule = webpackConfig.module.rules.find( + (item) => item && item.type === 'javascript/auto' && String(item.test) === String(/\.mjs$/) + ); + expect(rule).to.exist; + expect(rule.include).to.deep.equal(/node_modules/); + expect(rule.resolve).to.deep.equal({ fullySpecified: false }); + }); + + it('重复 override 不应重复添加 .mjs 规则', () => { + const webpackConfig = createConfig(true); + plugin.overrideWebpackConfig({ webpackConfig, context: { env: 'production' } }); + plugin.overrideWebpackConfig({ webpackConfig, context: { env: 'production' } }); + const rules = webpackConfig.module.rules.filter( + (item) => item && item.type === 'javascript/auto' && String(item.test) === String(/\.mjs$/) + ); + expect(rules).to.have.length(1); + }); + + it('resolve.conditionNames 应优先 import/module', () => { + const webpackConfig = createConfig(true); + webpackConfig.resolve.conditionNames = ['require', 'browser']; + plugin.overrideWebpackConfig({ webpackConfig, context: { env: 'production' } }); + expect(webpackConfig.resolve.conditionNames).to.deep.equal(['import', 'module', '...']); + }); + + it('已是 import/module/... 时不改写 conditionNames', () => { + const webpackConfig = createConfig(true); + const preferred = ['import', 'module', '...']; + webpackConfig.resolve.conditionNames = preferred; + plugin.overrideWebpackConfig({ webpackConfig, context: { env: 'production' } }); + expect(webpackConfig.resolve.conditionNames).to.equal(preferred); + }); +}); + describe('craco-fix-plugin app cache isolation', () => { it('应将 webpack filesystem cache 指到项目 .cache/webpack', () => { const webpackConfig = createConfig(true); @@ -65,7 +103,8 @@ describe('craco-fix-plugin app cache isolation', () => { ] }; plugin.overrideWebpackConfig({ webpackConfig, context: { env: 'development' } }); - expect(webpackConfig.module.rules[0].oneOf[0].options.cacheDirectory).to.equal( + const babelRule = webpackConfig.module.rules.find((item) => item && item.oneOf); + expect(babelRule.oneOf[0].options.cacheDirectory).to.equal( path.resolve(env.appDir, '.cache/babel-loader') ); });