From 9e21948874b7b38adca64a3d39b0b311dd2e31b7 Mon Sep 17 00:00:00 2001 From: Dario Lehmhus Date: Thu, 10 Nov 2022 21:48:48 +0100 Subject: [PATCH] fix: embed webpack-virtual-modules to the project pnpm patch doesn't work when we publish the package... --- README.md | 2 +- package.json | 12 +- pnpm-lock.yaml | 12 - src/VirtualModuleStore.ts | 2 +- src/webpack-virtual-modules/index.ts | 333 +++++++++++++++++++ src/webpack-virtual-modules/virtual-stats.ts | 61 ++++ 6 files changed, 399 insertions(+), 23 deletions(-) create mode 100644 src/webpack-virtual-modules/index.ts create mode 100644 src/webpack-virtual-modules/virtual-stats.ts diff --git a/README.md b/README.md index 624884b..3ac88f6 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## What is this? -The new Next.js 13 app directory feature doesn't work with the [@linaria/webpack5-loader](https://github.com/callstack/linaria/tree/master/packages/webpack5-loader) anymore, therefore the [next-linaria](https://github.com/Mistereo/next-linaria) package sadly also doesn't work. This package tries to solve that issue with a custom linaria webpack loader and [Webpack Virtual Modules](https://github.com/sysgears/webpack-virtual-modules). +The new Next.js 13 app directory feature doesn't work with the [@linaria/webpack5-loader](https://github.com/callstack/linaria/tree/master/packages/webpack5-loader) anymore, therefore the [next-linaria](https://github.com/Mistereo/next-linaria) package sadly also doesn't work. This package solves that issue with a custom linaria webpack loader and [Webpack Virtual Modules](https://github.com/sysgears/webpack-virtual-modules). ## Disclaimer diff --git a/package.json b/package.json index 1fbe9d2..f062020 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ }, "packageManager": "pnpm@7.14.0", "devDependencies": { + "@linaria/babel-preset": "4.3.0", "@typescript-eslint/eslint-plugin": "5.42.1", "@typescript-eslint/parser": "5.42.1", "eslint": "8.27.0", @@ -27,21 +28,14 @@ "prettier": "2.7.1", "simple-git-hooks": "2.8.1", "typescript": "4.8.4", - "webpack": "5.75.0", - "@linaria/babel-preset": "4.3.0" + "webpack": "5.75.0" }, "peerDependencies": { "@linaria/babel-preset": "4.x", "webpack": "5.x" }, "dependencies": { - "file-system-cache": "2.0.1", - "webpack-virtual-modules": "0.4.6" - }, - "pnpm": { - "patchedDependencies": { - "webpack-virtual-modules@0.4.6": "patches/webpack-virtual-modules@0.4.6.patch" - } + "file-system-cache": "2.0.1" }, "files": [ "lib" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4d440b0..6640f16 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,10 +1,5 @@ lockfileVersion: 5.4 -patchedDependencies: - webpack-virtual-modules@0.4.6: - hash: aebn33dqwkfk4mi4cr2zdj7mca - path: patches/webpack-virtual-modules@0.4.6.patch - specifiers: '@linaria/babel-preset': 4.3.0 '@typescript-eslint/eslint-plugin': 5.42.1 @@ -23,11 +18,9 @@ specifiers: simple-git-hooks: 2.8.1 typescript: 4.8.4 webpack: 5.75.0 - webpack-virtual-modules: 0.4.6 dependencies: file-system-cache: 2.0.1 - webpack-virtual-modules: 0.4.6_aebn33dqwkfk4mi4cr2zdj7mca devDependencies: '@linaria/babel-preset': 4.3.0 @@ -3746,11 +3739,6 @@ packages: engines: {node: '>=10.13.0'} dev: true - /webpack-virtual-modules/0.4.6_aebn33dqwkfk4mi4cr2zdj7mca: - resolution: {integrity: sha512-5tyDlKLqPfMqjT3Q9TAqf2YqjwmnUleZwzJi1A5qXnlBCdj2AtOJ6wAWdglTIDOPgOiOrXeBeFcsQ8+aGQ6QbA==} - dev: false - patched: true - /webpack/5.75.0: resolution: {integrity: sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ==} engines: {node: '>=10.13.0'} diff --git a/src/VirtualModuleStore.ts b/src/VirtualModuleStore.ts index 1d378d9..4abb5e3 100644 --- a/src/VirtualModuleStore.ts +++ b/src/VirtualModuleStore.ts @@ -1,9 +1,9 @@ import Cache, { type FileSystemCache } from 'file-system-cache'; import path from 'path'; import type * as Webpack from 'webpack'; -import VirtualModulesPlugin from 'webpack-virtual-modules'; import { isFSCache } from './utils'; +import VirtualModulesPlugin from './webpack-virtual-modules'; type CachedFile = { path: string; diff --git a/src/webpack-virtual-modules/index.ts b/src/webpack-virtual-modules/index.ts new file mode 100644 index 0000000..ab17123 --- /dev/null +++ b/src/webpack-virtual-modules/index.ts @@ -0,0 +1,333 @@ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-nocheck + +// Thanks https://github.com/sysgears/webpack-virtual-modules/blob/ea53626016db74de66b14401b7377cbc3fc31059/src/index.ts +// This is the webpack-virtual-modules package with the slight alteration that +// we can write modules before the compiler is available. +import path from 'path'; +import type { Compiler } from 'webpack'; + +import { VirtualStats } from './virtual-stats'; + +let inode = 45000000; + +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.isAbsolute(filePath) + ? filePath + : path.join(compiler.context, filePath); +} + +function createWebpackData(result) { + return (backendOrStorage) => { + // In Webpack v5, this variable is a "Backend", and has the data stored in a field + // _data. In V4, the `_` prefix isn't present. + if (backendOrStorage._data) { + const curLevelIdx = backendOrStorage._currentLevel; + const curLevel = backendOrStorage._levels[curLevelIdx]; + return { + result, + level: curLevel, + }; + } + // Webpack 4 + return [null, result]; + }; +} + +function getData(storage, key) { + // Webpack 5 + if (storage._data instanceof Map) { + return storage._data.get(key); + } else if (storage._data) { + return storage.data[key]; + } else if (storage.data instanceof Map) { + // Webpack v4 + return storage.data.get(key); + } else { + return storage.data[key]; + } +} + +function setData(backendOrStorage, key, valueFactory) { + const value = valueFactory(backendOrStorage); + + // Webpack v5 + if (backendOrStorage._data instanceof Map) { + backendOrStorage._data.set(key, value); + } else if (backendOrStorage._data) { + backendOrStorage.data[key] = value; + } else if (backendOrStorage.data instanceof Map) { + // Webpack 4 + backendOrStorage.data.set(key, value); + } else { + backendOrStorage.data[key] = value; + } +} + +function getStatStorage(fileSystem) { + if (fileSystem._statStorage) { + // Webpack v4 + return fileSystem._statStorage; + } else if (fileSystem._statBackend) { + // webpack v5 + return fileSystem._statBackend; + } else { + // Unknown version? + throw new Error("Couldn't find a stat storage"); + } +} + +function getFileStorage(fileSystem) { + if (fileSystem._readFileStorage) { + // Webpack v4 + return fileSystem._readFileStorage; + } else if (fileSystem._readFileBackend) { + // Webpack v5 + 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"); + } +} + +class VirtualModulesPlugin { + private _staticModules: Record | null; + private _compiler: Compiler | null = null; + private _watcher: any = null; + + public constructor(modules?: Record) { + this._staticModules = modules || null; + } + + public writeModule(filePath: string, contents: string): void { + // next-with-linaria patch, if not initialized yet, add to static modules + if (!this._compiler) { + if (!this._staticModules) { + this._staticModules = {}; + } + this._staticModules[filePath] = contents; + return; + } + + checkActivation(this); + + const len = contents ? contents.length : 0; + const time = Date.now(); + const date = new Date(time); + + const stats = new 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) + // eslint-disable-next-line no-console + console.log( + this._compiler.name, + 'Write virtual module:', + modulePath, + contents, + ); + + // When using the WatchIgnorePlugin (https://github.com/webpack/webpack/blob/52184b897f40c75560b3630e43ca642fcac7e2cf/lib/WatchIgnorePlugin.js), + // the original watchFileSystem is stored in `wfs`. The following "unwraps" the ignoring + // wrappers, giving us access to the "real" watchFileSystem. + let finalWatchFileSystem = this._watcher && this._watcher.watchFileSystem; + + while (finalWatchFileSystem && finalWatchFileSystem.wfs) { + finalWatchFileSystem = finalWatchFileSystem.wfs; + } + + let finalInputFileSystem: any = this._compiler.inputFileSystem; + while (finalInputFileSystem && finalInputFileSystem._inputFileSystem) { + finalInputFileSystem = finalInputFileSystem._inputFileSystem; + } + + finalInputFileSystem._writeVirtualFile(modulePath, stats, contents); + if ( + finalWatchFileSystem && + (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) + // eslint-disable-next-line no-console + console.log( + this._compiler.name, + 'Emit file change:', + modulePath, + time, + ); + delete fileWatcher.directoryWatcher._cachedTimeInfoEntries; + fileWatcher.emit('change', time, null); + } + } + } + } + + public apply(compiler: Compiler) { + this._compiler = compiler; + + const afterEnvironmentHook = () => { + let finalInputFileSystem: any = 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); + 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.sep) || path.sep; + try { + finalInputFileSystem.readdirSync(dir); + } catch (e) { + const time = Date.now(); + const dirStats = new 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([])); + setData(statStorage, dir, createWebpackData(dirStats)); + } + let dirData = getData(getReadDirBackend(finalInputFileSystem), dir); + // Webpack v4 returns an array, webpack v5 returns an object + 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 watchRunHook = (watcher, callback) => { + this._watcher = watcher.compiler || watcher; + const virtualFiles = (compiler as any).inputFileSystem._virtualFiles; + const fts = compiler.fileTimestamps as any; + if (virtualFiles && fts && typeof fts.set === 'function') { + Object.keys(virtualFiles).forEach((file) => { + fts.set(file, +virtualFiles[file].stats.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 as any).plugin('after-environment', afterEnvironmentHook); + (compiler as any).plugin('after-resolvers', afterResolversHook); + (compiler as any).plugin('watch-run', watchRunHook); + } + } +} + +export = VirtualModulesPlugin; diff --git a/src/webpack-virtual-modules/virtual-stats.ts b/src/webpack-virtual-modules/virtual-stats.ts new file mode 100644 index 0000000..22e3030 --- /dev/null +++ b/src/webpack-virtual-modules/virtual-stats.ts @@ -0,0 +1,61 @@ +/** + * Used to cache a stats object for the virtual file. + * Extracted from the `mock-fs` package. + * + * @author Tim Schaub http://tschaub.net/ + * @author `webpack-virtual-modules` Contributors + * @link https://github.com/tschaub/mock-fs/blob/master/lib/binding.js + * @link https://github.com/tschaub/mock-fs/blob/master/license.md + */ +import constants from 'constants'; + +export class VirtualStats { + /** + * Create a new stats object. + * + * @param config Stats properties. + */ + public constructor(config) { + for (const key in config) { + if (!Object.prototype.hasOwnProperty.call(config, key)) { + continue; + } + this[key] = config[key]; + } + } + + /** + * Check if mode indicates property. + */ + private _checkModeProperty(property): boolean { + return ((this as any).mode & constants.S_IFMT) === property; + } + + public isDirectory(): boolean { + return this._checkModeProperty(constants.S_IFDIR); + } + + public isFile(): boolean { + return this._checkModeProperty(constants.S_IFREG); + } + + public isBlockDevice(): boolean { + return this._checkModeProperty(constants.S_IFBLK); + } + + public isCharacterDevice(): boolean { + return this._checkModeProperty(constants.S_IFCHR); + } + + public isSymbolicLink(): boolean { + return this._checkModeProperty(constants.S_IFLNK); + } + + public isFIFO(): boolean { + return this._checkModeProperty(constants.S_IFIFO); + } + + public isSocket(): boolean { + return this._checkModeProperty(constants.S_IFSOCK); + } +}