├── .eslintignore ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── lib ├── config.js ├── extend-require.js ├── index.js ├── isomorphic-loader.js ├── logger.js ├── posixify.js ├── utils.js └── webpack-plugin.js ├── package.json ├── test ├── .eslintrc ├── bad-version-config.json ├── client │ ├── data │ │ └── foo.bin │ ├── entry.js │ ├── fonts │ │ └── font.ttf │ ├── foo.js │ └── images │ │ ├── smiley.jpg │ │ ├── smiley.png │ │ ├── smiley.svg │ │ └── smiley2.jpg ├── lib │ ├── isomorphic.dev.spec.js │ ├── isomorphic.spec.js │ └── webpack-info.js ├── mocha.opts ├── nm │ ├── demo.css │ └── smiley2.jpg ├── spec │ ├── extend-require.spec.js │ ├── isomorphic.dev.spec.js │ ├── isomorphic.spec.js │ ├── loader.spec.js │ ├── logger.spec.js │ └── utils.spec.js ├── webpack.config.js ├── webpack3 │ ├── package.json │ └── xrequire.js └── webpack4.config.js └── xclap.js /.eslintignore: -------------------------------------------------------------------------------- 1 | test/dist 2 | node_modules -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Node template 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # node-waf configuration 22 | .lock-wscript 23 | 24 | # Compiled binary addons (http://nodejs.org/api/addons.html) 25 | build/Release 26 | 27 | # Dependency directory 28 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 29 | node_modules 30 | ### JetBrains template 31 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio 32 | 33 | *.iml 34 | 35 | ## Directory-based project format: 36 | .idea/ 37 | # if you remove the above rule, at least ignore the following: 38 | 39 | # User-specific stuff: 40 | # .idea/workspace.xml 41 | # .idea/tasks.xml 42 | # .idea/dictionaries 43 | 44 | # Sensitive or high-churn files: 45 | # .idea/dataSources.ids 46 | # .idea/dataSources.xml 47 | # .idea/sqlDataSources.xml 48 | # .idea/dynamic.xml 49 | # .idea/uiDesigner.xml 50 | 51 | # Gradle: 52 | # .idea/gradle.xml 53 | # .idea/libraries 54 | 55 | # Mongo Explorer plugin: 56 | # .idea/mongoSettings.xml 57 | 58 | ## File-based project format: 59 | *.ipr 60 | *.iws 61 | 62 | ## Plugin-specific files: 63 | 64 | # IntelliJ 65 | /out/ 66 | 67 | # mpeltonen/sbt-idea plugin 68 | .idea_modules/ 69 | 70 | # JIRA plugin 71 | atlassian-ide-plugin.xml 72 | 73 | # Crashlytics plugin (for Android Studio and IntelliJ) 74 | com_crashlytics_export_strings.xml 75 | crashlytics.properties 76 | crashlytics-build.properties 77 | ### GitBook template 78 | # Node rules: 79 | ## Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 80 | .grunt 81 | 82 | ## Dependency directory 83 | ## Commenting this out is preferred by some people, see 84 | ## https://docs.npmjs.com/misc/faq#should-i-check-my-node_modules-folder-into-git 85 | node_modules 86 | 87 | # Book build output 88 | _book 89 | 90 | # eBook build output 91 | *.epub 92 | *.mobi 93 | *.pdf 94 | 95 | test/dist 96 | .isomorphic-loader* 97 | .nyc_output 98 | .vscode 99 | *-lock.* 100 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - v8 4 | - v10 5 | before_install: npm install -g fyn 6 | install: fyn --pg none install 7 | script: npm run coverage 8 | notifications: 9 | email: 10 | - joel123@gmail.com 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016-present, WalmartLabs 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![NPM version][npm-image]][npm-url] [![Build Status][travis-image]][travis-url] 2 | [![Dependency Status][daviddm-image]][daviddm-url] [![devDependency Status][daviddm-dev-image]][daviddm-dev-url] 3 | 4 | # Webpack Isomorphic Loader 5 | 6 | Webpack loader and tools to make node.js `require` understands files such as images when you are doing server side rendering (SSR). 7 | 8 | ## Purpose 9 | 10 | With [webpack] and [file-loader], you can do things like this in your React code: 11 | 12 | ```js 13 | import smiley from "./images/smiley.jpg"; 14 | 15 | 16 | render() { 17 | return
18 | } 19 | ``` 20 | 21 | That works out nicely, but if you need to do SSR, you will get `SyntaxError` from node.js `require`. That's because `require` only understands JS files. 22 | 23 | With this module, you can extend `require` so it understands these files. 24 | 25 | It contains three parts: 26 | 27 | 1. a webpack loader - to mark asset files 28 | 2. a webpack plugin - collect asset files and generate mapping data 29 | 3. a node.js library - extend `require` for SSR using the mapping data 30 | 31 | ## Install 32 | 33 | ``` 34 | $ npm install isomorphic-loader --save 35 | ``` 36 | 37 | ## Usage 38 | 39 | ### Configuring Webpack 40 | 41 | First use the webpack loader `isomorphic-loader` to mark all your asset files that you want `extendRequire` to handle. 42 | 43 | > The webpack loader `isomorphic-loader` is just a simple pass thru loader to mark your files. It will not do anything to the file content. 44 | 45 | Next install the webpack plugin `IsomorphicLoaderPlugin` to collect and save the list of the marked files. 46 | 47 | For example, in the webpack config, to mark the usual image files to be understood by `extendRequire`: 48 | 49 | ```js 50 | const { IsomorphicLoaderPlugin } = require("isomorphic-loader"); 51 | 52 | module.exports = { 53 | plugins: [new IsomorphicLoaderPlugin()], 54 | module: { 55 | loaders: [ 56 | { 57 | test: /\.(jpe?g|png|gif|svg)$/i, 58 | loader: "file!isomorphic" 59 | } 60 | ] 61 | } 62 | }; 63 | ``` 64 | 65 | You can also mark any file in your code directly: 66 | 67 | ```js 68 | import smiley from "file!isomorphic!./images/smiley.jpg"; 69 | ``` 70 | 71 | ### Extending node.js `require` 72 | 73 | With the marked asset files collected, initialize `extendRequire` with the mapping data before your server starts: 74 | 75 | ```js 76 | const { extendRequire } = require("isomorphic-loader"); 77 | 78 | const isomorphicRequire = extendRequire(); 79 | 80 | // isomorphicRequire is an instance of the ExtendRequire class exported from the module 81 | 82 | // start your server etc 83 | ``` 84 | 85 | It will try to load the isomorphic config data from `dist/isomorphic-assets.json`. You can also pass in the config data: 86 | 87 | ```js 88 | extendRequire(options, require("./dist/isomorphic-assets.json")); 89 | ``` 90 | 91 | #### Custom Config Overrides 92 | 93 | When calling `extendRequire`, you can pass in a callback in `options.processConfig` to override the `isomorphicConfig` 94 | 95 | ```js 96 | extendRequire({ 97 | processConfig: config => { 98 | // do something with config 99 | return config; 100 | } 101 | }); 102 | ``` 103 | 104 | #### Activating and Deactivating extendRequire 105 | 106 | - `deactivate` API - deactivate `extendRequire` during run time. 107 | - `activate` API - activate `extendRequire` during run time. 108 | 109 | ```js 110 | const { extendRequire } = require("isomorphic-loader"); 111 | 112 | const isomorphicRequire = extendRequire(); 113 | 114 | isomorphicRequire.deactivate(); 115 | 116 | // and reactivate it 117 | 118 | isomorphicRequire.activate(); 119 | ``` 120 | 121 | ### Usage with CDN Server 122 | 123 | If you publish your assets to a Content Delivery Network server, and if it generates a new unique path for your assets, then you likely have to set `publicPath` after webpack compiled your project. 124 | 125 | That's why [webpack]'s document has this note in the [section about `publicPath`]: 126 | 127 | > **Note:** In cases when the eventual `publicPath` of output files isn't known at compile time, it can be left blank and set dynamically at runtime in the entry point file. 128 | > If you don't know the `publicPath` while compiling you can omit it and set `__webpack_public_path__` on your entry point. 129 | 130 | In that case, you would have to save the path CDN created for you and pass it to `extendRequire` with a [custom config override](#custom-config-overrides), or you can just modify the [config file](#config-and-assets-files) directly. 131 | 132 | If your CDN server generates an unique URL for every asset file instead of a single base path, then you have to do some custom post processing to update the asset mapping files yourself. 133 | 134 | ### Webpack Dev Server 135 | 136 | If you are using webpack dev server, then you probably have two separate processes running: 137 | 138 | 1. webpack dev server (WDS) 139 | 2. your app's node.js server (APP) 140 | 141 | And `IsomorphicLoaderPlugin` would be running in **WDS** but `extendRequire` is in **APP**. 142 | 143 | - You need to setup some way to transfer the mapping data from **WDS** to **APP**. 144 | - When starting up, **APP** needs to wait for the first mapping data before actually startup, unless your SSR code is not loaded until it's actually invoked. 145 | 146 | Here is an example using chokidar to transfer the data through a file: 147 | 148 | - In your `webpack.config.js`: 149 | 150 | ```js 151 | const fs = require("fs"); 152 | const { IsomorphicLoaderPlugin } = require("isomorphic-loader"); 153 | 154 | const isoPlugin = new IsomorphicLoaderPlugin(); 155 | isoPlugin.on("update", data => { 156 | fs.writeFileSync("./tmp/isomorphic-assets.json", JSON.stringify(data.config)); 157 | }); 158 | 159 | module.exports = { 160 | plugins: [isoPlugin] 161 | }; 162 | ``` 163 | 164 | - In your app server `index.js` 165 | 166 | ```js 167 | const { extendRequire } = require("isomorphic-loader"); 168 | 169 | // figure out if running in dev mode or not 170 | if (process.env.NODE_ENV !== "production") { 171 | const chokidar = require("chokidar"); 172 | const assetFile = "./tmp/isomorphic-assets.json"; 173 | 174 | let isomorphicRequire; 175 | 176 | function updateIsomorphicAssets() { 177 | const firstTime = !isomorphicRequire; 178 | if (firstTime) { 179 | isomorphicRequire = extendRequire(); 180 | } else { 181 | // do the necessary require cache refresh so hot module reload works in SSR 182 | } 183 | 184 | isomorphicRequire.loadAssets(assetFile); 185 | if (firstTime) { 186 | startServer(); 187 | } 188 | } 189 | 190 | const watcher = chokidar.watch(assetFile, { persistent: true }); 191 | watcher.on("add", updateIsomorphicAssets); 192 | watcher.on("change", updateIsomorphicAssets); 193 | 194 | // do some timeout check 195 | setTimeout(() => { 196 | if (!isomorphicRequire) { 197 | console.error("timeout waiting for webpack dev server"); 198 | } 199 | }, 20000).unref(); 200 | } else { 201 | extendRequire().loadAssets("./dist/isomorphic-assets.json"); 202 | startServer(); 203 | } 204 | ``` 205 | 206 | ## License 207 | 208 | [Apache License, Version 2] 209 | 210 | [file-loader]: https://github.com/webpack/file-loader 211 | [apache license, version 2]: https://www.apache.org/licenses/LICENSE-2.0 212 | [webpack-dev-server]: https://webpack.github.io/docs/webpack-dev-server.html 213 | [webpack]: https://webpack.github.io/ 214 | [section about `publicpath`]: https://github.com/webpack/docs/wiki/configuration#outputpublicpath 215 | [travis-image]: https://travis-ci.org/electrode-io/isomorphic-loader.svg?branch=master 216 | [travis-url]: https://travis-ci.org/electrode-io/isomorphic-loader 217 | [npm-image]: https://badge.fury.io/js/isomorphic-loader.svg 218 | [npm-url]: https://npmjs.org/package/isomorphic-loader 219 | [daviddm-image]: https://david-dm.org/electrode-io/isomorphic-loader/status.svg 220 | [daviddm-url]: https://david-dm.org/electrode-io/isomorphic-loader 221 | [daviddm-dev-image]: https://david-dm.org/electrode-io/isomorphic-loader/dev-status.svg 222 | [daviddm-dev-url]: https://david-dm.org/electrode-io/isomorphic-loader?type=dev 223 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const configName = `isomorphic-loader-config`; 4 | 5 | module.exports = { 6 | configName, 7 | defaultAssetsFile: "dist/isomorphic-assets.json" 8 | }; 9 | -------------------------------------------------------------------------------- /lib/extend-require.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* eslint-disable max-statements, max-len, complexity, prefer-template, no-magic-numbers */ 4 | 5 | const Path = require("path"); 6 | const Fs = require("fs"); 7 | const Module = require("module"); 8 | const Config = require("./config"); 9 | const { removeLoaders, removeCwd, getParentPath, replaceAppSrcDir } = require("./utils"); 10 | const Pkg = require("../package.json"); 11 | const logger = require("./logger"); 12 | const assert = require("assert"); 13 | const posixify = require("./posixify"); 14 | const EventEmitter = require("events"); 15 | 16 | const LOG_PREFIX = "isomorphic-loader"; 17 | 18 | class ExtendRequire extends EventEmitter { 19 | constructor(options) { 20 | super(); 21 | this.assetsCount = 0; 22 | this.clearData(); 23 | this.options = { cwd: process.cwd(), ...options }; 24 | this._urlMap = this.urlMap; 25 | this._originalLoad = undefined; 26 | this.activated = false; 27 | this.interceptLoad(); 28 | } 29 | 30 | /** 31 | * Clear config and assets to contain nothing 32 | * 33 | * @returns {void} nothing 34 | */ 35 | clearData() { 36 | this.assets = { marked: {} }; 37 | this.config = {}; 38 | this._markedNames = []; 39 | } 40 | 41 | /** 42 | * Determine if isomorphic data is generated by webpack running in dev mode 43 | * 44 | * @returns {boolean} true if webpack is running in dev mode 45 | */ 46 | isWebpackDev() { 47 | return Boolean(this.config.isWebpackDev); 48 | } 49 | 50 | /** 51 | * Stop extend node.js require, restore original require, and clear config and assets data. 52 | * 53 | * @returns {*} undefined 54 | */ 55 | reset() { 56 | this.deactivate(); 57 | this.clearData(); 58 | 59 | if (this._originalLoad) { 60 | Module._load = this._originalLoad; 61 | this._originalLoad = undefined; 62 | } 63 | this.emit("reset"); 64 | } 65 | 66 | /** 67 | * set base publicPath for creating the URL of the assets 68 | * 69 | * @param {*} p - public path (undefined to unset it) 70 | * @returns {void} none 71 | */ 72 | setPublicPath(p) { 73 | this._publicPath = (p !== undefined ? p : this.config.output.publicPath) || ""; 74 | } 75 | 76 | /** 77 | * Set a callback function that take an asset URL and map it to something else 78 | * @param {function} mapper - callback 79 | * @returns {void} none 80 | */ 81 | setUrlMapper(mapper) { 82 | this._urlMap = mapper; 83 | } 84 | 85 | /** 86 | * built-in URL mapper, which just prepend publicPath to it 87 | * 88 | * @param {string} url - url to map 89 | * @returns {string} updated URL 90 | */ 91 | urlMap(url) { 92 | return this._publicPath + url; 93 | } 94 | 95 | /** 96 | * Set the assets mapping to use for modified require behavior 97 | * 98 | * @param {*} config - isomorphic config generated from the webpack plugin 99 | * @returns {*} undefined 100 | */ 101 | initialize(config) { 102 | this.config = { output: {}, ...config }; 103 | 104 | if (this.options.processConfig) { 105 | this.config = this.options.processConfig(this.config); 106 | } 107 | 108 | if (this._publicPath === undefined) { 109 | this.setPublicPath(); 110 | } 111 | 112 | this.assets = { marked: {}, ...this.config.assets }; 113 | this._markedNames = Object.keys(this.assets.marked); 114 | assert( 115 | this.config.version === Pkg.version, 116 | `${LOG_PREFIX}: this module version ${Pkg.version} is different from config version ${this.config.version}` 117 | ); 118 | this.activate(); 119 | this.interceptLoad(); 120 | } 121 | 122 | /** 123 | * stop mapping non-standard JS module for node.js require 124 | * 125 | * @returns {*} undefined 126 | */ 127 | deactivate() { 128 | this.assetsCount = 0; 129 | this.activated = false; 130 | this.emit("deactivate"); 131 | } 132 | 133 | /** 134 | * Look for and load assets from `dist/isomorphic-assets.json` or the array of files passed in 135 | * @param {*} customFilePath - optional file to look for and load 136 | * @returns {string} the name of the file that's loaded 137 | */ 138 | loadAssets(customFilePath) { 139 | return [] 140 | .concat(customFilePath, this.options.assetsFile, Config.defaultAssetsFile) 141 | .filter(x => x) 142 | .find(file => { 143 | try { 144 | const config = JSON.parse(Fs.readFileSync(file, "utf-8")); 145 | this.initialize(config); 146 | return true; 147 | } catch (err) { 148 | if (err.code !== "ENOENT") { 149 | throw new Error(`${LOG_PREFIX}: fail to load config from ${file} - ${err.message}`); 150 | } 151 | return false; 152 | } 153 | }); 154 | } 155 | 156 | /** 157 | * start mapping non-standard JS module for node.js require. 158 | * 159 | * - assets must have been set with `setAssets` 160 | * @returns {*} undefined 161 | */ 162 | activate() { 163 | this.assetsCount = this._markedNames.length; 164 | this.activated = true; 165 | this.emit("activate"); 166 | } 167 | 168 | interceptLoad() { 169 | if (this._originalLoad) { 170 | return; 171 | } 172 | 173 | const appSrcDir = this.options.appSrcDir; 174 | 175 | const isAssetNotFound = request => { 176 | const x = Path.basename(request); 177 | return this._markedNames.find(name => Path.basename(name).indexOf(x) >= 0); 178 | }; 179 | 180 | const checkAsset = (moduleInstance, request, parent) => { 181 | const config = this.config; 182 | if (config.output && this.assetsCount > 0) { 183 | let requestMarkKey; 184 | const xRequest = removeLoaders(request); 185 | 186 | if (xRequest.startsWith(".")) { 187 | const parentPath = posixify(removeCwd(getParentPath(parent), true, this.options.cwd)); 188 | requestMarkKey = Path.posix.join(parentPath, xRequest); 189 | } else { 190 | try { 191 | requestMarkKey = posixify( 192 | removeCwd(moduleInstance._resolveFilename(xRequest, parent), false, this.options.cwd) 193 | ); 194 | } catch (e) { 195 | if (isAssetNotFound(xRequest)) { 196 | logger.error(LOG_PREFIX, "check asset " + xRequest + " exception", e); 197 | } 198 | return undefined; 199 | } 200 | } 201 | 202 | let assetUrl = this.assets.marked[replaceAppSrcDir(requestMarkKey, appSrcDir)]; 203 | 204 | if (assetUrl) { 205 | if (/\.(css|less|styl|sass|scss)$/.test(requestMarkKey)) { 206 | return assetUrl; 207 | } 208 | try { 209 | assetUrl = this._urlMap(assetUrl, request, appSrcDir); 210 | } catch (err) { 211 | logger.error(LOG_PREFIX, "urlMap thrown error", err); 212 | } 213 | 214 | if (config.isWebpackDev && config.webpackDev.addUrl && config.webpackDev.url) { 215 | const sep = 216 | !config.webpackDev.url.endsWith("/") && !assetUrl.startsWith("/") ? "/" : ""; 217 | assetUrl = config.webpackDev.url + sep + assetUrl; 218 | } 219 | 220 | return assetUrl; 221 | } 222 | } 223 | 224 | if (this.options.ignoreExtensions) { 225 | const ext = Path.extname(request); 226 | if (ext && this.options.ignoreExtensions.includes(ext)) { 227 | return {}; 228 | } 229 | } 230 | 231 | return undefined; 232 | }; 233 | 234 | const originalLoad = (this._originalLoad = Module._load); 235 | 236 | Module._load = function(request, parent, isMain) { 237 | const asset = checkAsset(this, request, parent); 238 | if (asset !== undefined) { 239 | return asset; 240 | } 241 | 242 | return originalLoad.apply(this, [request, parent, isMain]); 243 | }; 244 | } 245 | } 246 | 247 | module.exports = { ExtendRequire }; 248 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { ExtendRequire } = require("./extend-require"); 4 | const { IsomorphicLoaderPlugin } = require("./webpack-plugin"); 5 | const IsomorphicLoader = require("./isomorphic-loader"); 6 | 7 | /** 8 | * Initialize node.js extend require to handle isomorphic assets in SSR 9 | * @param {*} options - options 10 | * @param {*} isomorphicConfig - isomorphic config data 11 | * 12 | * @returns {*} an instance of ExtendRequire 13 | */ 14 | function extendRequire(options, isomorphicConfig) { 15 | const extReq = new ExtendRequire(options); 16 | 17 | if (isomorphicConfig) { 18 | extReq.initialize(isomorphicConfig); 19 | } else if (!process.env.hasOwnProperty("WEBPACK_DEV")) { 20 | // 21 | // don't try to load assets in webpack dev mode, because there could be an outdated 22 | // and staled dist directory with a config file that's bad. User may have 23 | // updated dependencies and run dev but didn't do a build to update dist yet, and 24 | // that could cause loading the assets to have unexpected failures 25 | // 26 | extReq.loadAssets(); 27 | } 28 | 29 | return extReq; 30 | } 31 | 32 | IsomorphicLoader.extendRequire = extendRequire; 33 | IsomorphicLoader.ExtendRequire = ExtendRequire; 34 | IsomorphicLoader.IsomorphicLoaderPlugin = IsomorphicLoaderPlugin; 35 | 36 | const GLOBAL_INSTANCE = Symbol.for("isomorphic-loader-global-extend-require"); 37 | 38 | IsomorphicLoader.setXRequire = instance => (global[GLOBAL_INSTANCE] = instance); 39 | IsomorphicLoader.getXRequire = () => global[GLOBAL_INSTANCE]; 40 | 41 | module.exports = IsomorphicLoader; 42 | -------------------------------------------------------------------------------- /lib/isomorphic-loader.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { getCssModuleGlobalMapping } = require("./utils"); 4 | 5 | module.exports = function (content) { 6 | const userRequest = (this._module && this._module.userRequest) || ""; 7 | if (/\.(css|less|styl|sass|scss)$/.test(userRequest)) { 8 | const mapping = getCssModuleGlobalMapping(); 9 | mapping[userRequest] = content.toString("utf-8"); 10 | } 11 | this.cacheable && this.cacheable(); // eslint-disable-line 12 | return content; 13 | }; 14 | 15 | module.exports.raw = true; 16 | -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const LEVELS = { 4 | info: 10, 5 | error: 99, 6 | none: 999 7 | }; 8 | 9 | let LEVEL = LEVELS.info; 10 | 11 | function log() { 12 | if (LEVEL <= LEVELS.info) { 13 | console.log.apply(console, arguments); // eslint-disable-line 14 | } 15 | } 16 | 17 | function error() { 18 | if (LEVEL <= LEVELS.error) { 19 | console.error.apply(console, arguments); // eslint-disable-line 20 | } 21 | } 22 | 23 | module.exports = { log: log, error: error, setLevel: l => (LEVEL = LEVELS[l] || LEVELS.info) }; 24 | -------------------------------------------------------------------------------- /lib/posixify.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* istanbul ignore next */ 4 | 5 | module.exports = process.platform.startsWith("win32") ? p => p.replace(/\\/g, "/") : p => p; 6 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Path = require("path"); 4 | const posixify = require("./posixify"); 5 | 6 | const CSS_MODULE_MAPPINGS = Symbol.for("css-module-content-mapping"); 7 | 8 | /** 9 | * Get the path of a parent module that's making a require call 10 | * 11 | * @param {*} parent module 12 | * @returns {string} parent module's path 13 | */ 14 | exports.getParentPath = parent => { 15 | return (parent && (parent.path || (parent.filename && Path.dirname(parent.filename)))) || ""; 16 | }; 17 | 18 | /** 19 | * Get the verbatim string as passed to require that webpack processed 20 | * @param {*} mod module 21 | * @returns {string} require request 22 | * 23 | */ 24 | exports.getWebpackRequest = mod => { 25 | const reason = mod && mod.reasons && mod.reasons[0]; 26 | return (reason && reason.userRequest) || ""; 27 | }; 28 | 29 | /** 30 | * Remove CWD from path 31 | * @param {*} path - path 32 | * @param {*} last - if true then use lastIndexOf to search 33 | * @param {string} cwd - optional current working dir 34 | * @returns {string} new path 35 | */ 36 | exports.removeCwd = (path, last, cwd = process.cwd()) => { 37 | const x = last ? path.lastIndexOf(cwd) : path.indexOf(cwd); 38 | 39 | if (x >= 0) { 40 | return path.substr(x + cwd.length + 1); 41 | } 42 | 43 | return path; 44 | }; 45 | 46 | /** 47 | * remove webpack loader marks like "file-loader!" from a require request 48 | * 49 | * @param {string} request - require request 50 | * @returns {string} request without webpack loader marks 51 | */ 52 | exports.removeLoaders = request => { 53 | const markIx = request.lastIndexOf("!"); 54 | if (markIx >= 0) { 55 | return request.substr(markIx + 1); 56 | } 57 | return request; 58 | }; 59 | 60 | /** 61 | * Replace app src dir in request path with a consistent marker 62 | * 63 | * @param {string} request - require request 64 | * @param {string} appSrcDir - app src dir 65 | * @returns {string} updated request 66 | */ 67 | exports.replaceAppSrcDir = (request, appSrcDir) => { 68 | if (appSrcDir) { 69 | const asd = Path.posix.join(appSrcDir, "/"); 70 | return request.replace(new RegExp(`^${asd}`), "$AppSrc/"); 71 | } 72 | return request; 73 | }; 74 | 75 | /** 76 | * Returns local to global classnames mapping for css modules 77 | * 78 | * @returns {object} mapping object 79 | */ 80 | exports.getCssModuleGlobalMapping = () => { 81 | if (!global[CSS_MODULE_MAPPINGS]) { 82 | global[CSS_MODULE_MAPPINGS] = {}; 83 | } 84 | 85 | return global[CSS_MODULE_MAPPINGS]; 86 | }; 87 | 88 | /** 89 | * Load node.js module from string 90 | * 91 | * @param {string} src - file's content 92 | * @param {string} filename - optional filename 93 | * @returns {object} exported object 94 | */ 95 | exports.requireFromString = (src, filename = "") => { 96 | const Module = module.constructor; 97 | const m = new Module(); 98 | m._compile(src, filename); 99 | return m.exports; 100 | }; 101 | 102 | exports.getMyNodeModulesPath = (dir = __dirname) => { 103 | const myName = "isomorphic-loader"; 104 | const ixName = dir.lastIndexOf(myName); 105 | 106 | if (ixName >= 0) { 107 | return posixify(dir.substr(ixName)); 108 | } 109 | 110 | const parts = dir.split(Path.sep); 111 | // gather up to last 4 parts and join with posix path sep 112 | return parts.slice(Math.max(0, parts.length - 4)).join("/"); // eslint-disable-line 113 | }; 114 | -------------------------------------------------------------------------------- /lib/webpack-plugin.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* eslint-disable no-unused-expressions, prefer-template, no-magic-numbers, max-statements */ 4 | 5 | // https://webpack.github.io/docs/how-to-write-a-plugin.html 6 | 7 | const Path = require("path"); 8 | const Config = require("./config"); 9 | const { 10 | getWebpackRequest, 11 | removeLoaders, 12 | removeCwd, 13 | replaceAppSrcDir, 14 | getCssModuleGlobalMapping, 15 | getMyNodeModulesPath, 16 | requireFromString 17 | } = require("./utils"); 18 | const Pkg = require("../package.json"); 19 | const posixify = require("./posixify"); 20 | const EventEmitter = require("events"); 21 | 22 | const pluginName = "IsomorphicLoaderPlugin"; 23 | 24 | class IsomorphicLoaderPlugin extends EventEmitter { 25 | /** 26 | * 27 | * @param {*} options options 28 | * 29 | */ 30 | constructor(options) { 31 | super(); 32 | this.config = { valid: false }; 33 | this.options = { cwd: process.cwd(), assetsFile: "isomorphic-assets.json", ...options }; 34 | 35 | const directJs = Path.posix.join(getMyNodeModulesPath(), "index.js!"); 36 | this._loaderSigs = [directJs, `isomorphic-loader/lib/index.js!`, `isomorphic-loader!`]; 37 | } 38 | 39 | apply(compiler) { 40 | const createConfig = (compilerOpts, assets = { marked: {}, chunks: {} }) => { 41 | // 42 | // There is no easy way to detect that webpack-dev-server is running, but it 43 | // does change the output path to "/". Since that normally is "build" or "dist" 44 | // and it's unlikely anyone would use "/", assuming it's webpack-dev-server if 45 | // output path is "/". 46 | // 47 | const isWebpackDev = 48 | compilerOpts.output.path === "/" || 49 | require.main.filename.indexOf("webpack-dev-server") >= 0; 50 | 51 | const config = { 52 | valid: false, 53 | version: Pkg.version, 54 | timestamp: Date.now(), 55 | context: removeCwd(compilerOpts.context, false, this.options.cwd), 56 | output: { 57 | path: removeCwd(compilerOpts.output.path, false, this.options.cwd), 58 | filename: removeCwd(compilerOpts.output.filename, false, this.options.cwd), 59 | publicPath: compilerOpts.output.publicPath || "" 60 | }, 61 | assets 62 | }; 63 | 64 | if (isWebpackDev) { 65 | config.webpackDev = Object.assign( 66 | { 67 | skipSetEnv: false, 68 | url: "http://localhost:8080", 69 | 70 | // should extend require prepend webpack dev URL when returning URL for asset file 71 | addUrl: true 72 | }, 73 | this.options.webpackDev 74 | ); 75 | config.isWebpackDev = true; 76 | } 77 | 78 | return config; 79 | }; 80 | 81 | const updateConfigForDevMode = () => { 82 | if (!this.config.isWebpackDev) { 83 | return; 84 | } 85 | 86 | this.emit("update", { name: Config.configName, config: this.config }); 87 | }; 88 | 89 | const handleEmit = (compilation, webpackAssets) => { 90 | const stats = compilation.getStats().toJson(); 91 | 92 | const marked = stats.modules.reduce((acc, m) => { 93 | const ident = posixify(m.identifier); 94 | const foundSig = this._loaderSigs.find(sig => ident.includes(sig)); 95 | if (foundSig && m.assets && m.assets.length > 0) { 96 | const userRequest = removeLoaders(getWebpackRequest(m)); 97 | 98 | let requestMarkKey; 99 | // if userRequest starts with ., it's a relative path, so try to reconstruct its 100 | // full path by joining it with the issuer path 101 | // npm module name can't start with ., so it's not needed to check ./ or ../ 102 | if (userRequest.startsWith(".")) { 103 | const issuerPath = posixify( 104 | Path.dirname(removeCwd(removeLoaders(m.issuer), true, this.options.cwd)) 105 | ); 106 | // require paths always use / so use posix path 107 | requestMarkKey = Path.posix.join(issuerPath, userRequest); 108 | } else { 109 | // no need to worry about target's location relative to issuer's location, just 110 | // use full identifier path directly 111 | requestMarkKey = posixify( 112 | removeCwd(removeLoaders(m.identifier), false, this.options.cwd) 113 | ); 114 | } 115 | 116 | acc[replaceAppSrcDir(requestMarkKey, this.options.appSrcDir)] = 117 | m.assets[m.assets.length - 1]; 118 | } 119 | 120 | return acc; 121 | }, {}); 122 | 123 | const cssModuleMap = getCssModuleGlobalMapping(); 124 | Object.keys(cssModuleMap).forEach(userRequest => { 125 | const requestKey = posixify(removeCwd(userRequest, false, this.options.cwd)); 126 | const content = requireFromString(cssModuleMap[userRequest]); 127 | marked[replaceAppSrcDir(requestKey, this.options.appSrcDir)] = content; 128 | }); 129 | 130 | const assets = { 131 | marked, 132 | chunks: stats.assetsByChunkName 133 | }; 134 | 135 | const config = createConfig(compiler.options, assets); 136 | config.valid = true; 137 | 138 | const configStr = JSON.stringify(config, null, 2) + "\n"; 139 | 140 | (webpackAssets || compilation.assets)[this.options.assetsFile] = { 141 | source: () => configStr, 142 | size: () => configStr.length 143 | }; 144 | 145 | this.config = config; 146 | }; 147 | 148 | const handleMake = (compilation, callback) => { 149 | this.config = createConfig(compiler.options); 150 | 151 | // If running in webpack dev server mode, then create a config file ASAP so not to leave 152 | // extend require waiting. 153 | updateConfigForDevMode(); 154 | 155 | if (compiler.hooks && compilation.hooks.processAssets) { 156 | // handle emit for webpack 5 157 | compilation.hooks.processAssets.tap(pluginName, assets => handleEmit(compilation, assets)); 158 | } 159 | 160 | /* istanbul ignore next */ 161 | callback && callback(); 162 | }; 163 | 164 | const handleInvalid = () => { 165 | this.config.valid = false; 166 | updateConfigForDevMode(); 167 | }; 168 | 169 | /* istanbul ignore else */ 170 | if (compiler.hooks) { 171 | // use .hooks 172 | compiler.hooks.make.tap(pluginName, handleMake); 173 | compiler.hooks.emit.tap(pluginName, compilation => { 174 | // handle emit for webpack 4 175 | if (!compilation.hooks.processAssets) { 176 | handleEmit(compilation, compilation.assets); 177 | } 178 | }); 179 | compiler.hooks.invalid.tap(pluginName, handleInvalid); 180 | compiler.hooks.done.tap(pluginName, updateConfigForDevMode); 181 | } else { 182 | compiler.plugin("make", handleMake); 183 | compiler.plugin("emit", handleEmit); 184 | compiler.plugin("invalid", handleInvalid); 185 | compiler.plugin("done", updateConfigForDevMode); 186 | } 187 | } 188 | } 189 | 190 | module.exports = { IsomorphicLoaderPlugin }; 191 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "isomorphic-loader", 3 | "version": "4.4.1", 4 | "description": "Webpack isomorphic loader tools to make Node require handle files like images for Server Side Rendering (SSR)", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "wp": "webpack --config test/webpack.config.js", 8 | "wpd": "webpack-dev-server --config test/webpack.config.js", 9 | "test": "clap test", 10 | "coverage": "clap check", 11 | "lint": "clap lint", 12 | "prepublishOnly": "clap check" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/electrode-io/isomorphic-loader.git" 17 | }, 18 | "files": [ 19 | "lib" 20 | ], 21 | "keywords": [ 22 | "webpack", 23 | "isomorphic", 24 | "loader", 25 | "images", 26 | "assets", 27 | "SSR", 28 | "react", 29 | "server", 30 | "side", 31 | "rendering", 32 | "node", 33 | "nodejs", 34 | "require" 35 | ], 36 | "author": "Joel Chen", 37 | "license": "Apache-2.0", 38 | "dependencies": {}, 39 | "devDependencies": { 40 | "clone": "^1.0.2", 41 | "css-loader": "^1.0.1", 42 | "electrode-archetype-njs-module-dev": "^3.0.0", 43 | "electrode-cdn-file-loader": "^1.1.1", 44 | "fetch": "^1.0.1", 45 | "file-loader": "^1.0.0", 46 | "lodash": "^4.17.20", 47 | "mini-css-extract-plugin": "^0.9.0", 48 | "prettier": "^1.19.1", 49 | "require-at": "^1.0.4", 50 | "rimraf": "^2.5.2", 51 | "run-verify": "^1.2.4", 52 | "webpack": "^4.1.0", 53 | "webpack-cli": "^2", 54 | "webpack-dev-server": "^3", 55 | "webpack3": "./test/webpack3", 56 | "xaa": "^1.2.2", 57 | "xstdout": "^0.1.1" 58 | }, 59 | "nyc": { 60 | "all": true, 61 | "reporter": [ 62 | "lcov", 63 | "text", 64 | "text-summary" 65 | ], 66 | "exclude": [ 67 | "coverage", 68 | "*clap.js", 69 | "gulpfile.js", 70 | "dist", 71 | "test" 72 | ], 73 | "check-coverage": true, 74 | "statements": 100, 75 | "branches": 100, 76 | "functions": 100, 77 | "lines": 100, 78 | "cache": true 79 | }, 80 | "engines": { 81 | "node": ">=10" 82 | }, 83 | "prettier": { 84 | "printWidth": 100 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | --- 2 | env: 3 | "mocha": true 4 | rules: 5 | no-console: 0 6 | -------------------------------------------------------------------------------- /test/bad-version-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "valid": true, 3 | "version": "3.0.1", 4 | "timestamp": 1602539652450, 5 | "context": "src", 6 | "output": { 7 | "path": "dist/js", 8 | "filename": "[name].bundle.[contenthash].js", 9 | "publicPath": "/js/" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/client/data/foo.bin: -------------------------------------------------------------------------------- 1 | test test 2 | -------------------------------------------------------------------------------- /test/client/entry.js: -------------------------------------------------------------------------------- 1 | require("./images/smiley.jpg"); 2 | require("./images/smiley2.jpg"); 3 | require("./foo"); 4 | -------------------------------------------------------------------------------- /test/client/fonts/font.ttf: -------------------------------------------------------------------------------- 1 | testtesttest -------------------------------------------------------------------------------- /test/client/foo.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | require("./images/smiley.svg"); 3 | require("../..!./images/smiley.png"); 4 | require("file-loader!../..!./data/foo.bin"); 5 | require("./fonts/font.ttf"); 6 | require("smiley2Jpg"); 7 | require("demoCss"); 8 | 9 | console.log("foo"); 10 | -------------------------------------------------------------------------------- /test/client/images/smiley.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchip/isomorphic-loader/f4e5fa18b778a517ebf7391e07092c9262d1c00c/test/client/images/smiley.jpg -------------------------------------------------------------------------------- /test/client/images/smiley.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchip/isomorphic-loader/f4e5fa18b778a517ebf7391e07092c9262d1c00c/test/client/images/smiley.png -------------------------------------------------------------------------------- /test/client/images/smiley.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/client/images/smiley2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchip/isomorphic-loader/f4e5fa18b778a517ebf7391e07092c9262d1c00c/test/client/images/smiley2.jpg -------------------------------------------------------------------------------- /test/lib/isomorphic.dev.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* eslint-disable max-nested-callbacks */ 4 | 5 | const fs = require("fs"); 6 | const Path = require("path"); 7 | const rimraf = require("rimraf"); 8 | const chai = require("chai"); 9 | const clone = require("clone"); 10 | const _ = require("lodash"); 11 | const fetchUrl = require("fetch").fetchUrl; 12 | const xaa = require("xaa"); 13 | const { asyncVerify, runFinally } = require("run-verify"); 14 | 15 | const expect = chai.expect; 16 | 17 | const { extendRequire, IsomorphicLoaderPlugin } = require("../.."); 18 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 19 | 20 | module.exports = function isomorphicDevSpec({ 21 | title, 22 | tag, 23 | timeout, 24 | webpack, 25 | WebpackDevServer, 26 | webpackConfig 27 | }) { 28 | describe(title, function() { 29 | this.timeout(timeout); 30 | 31 | webpackConfig.output.path = "/"; 32 | 33 | const defaultFontHash = "1e2bf10d5113abdb2ca03d0d0f4f7dd1.ttf"; 34 | const changedFontHash = "1fb0e331c05a52d5eb847d6fc018320d.ttf"; 35 | 36 | function writeFont(data) { 37 | // default font file md5 1e2bf10d5113abdb2ca03d0d0f4f7dd1 38 | fs.writeFileSync( 39 | Path.resolve("test/client/fonts/font.ttf"), 40 | data || "ttfttfttf\nfontfontfont" 41 | ); 42 | } 43 | 44 | function writeCss(data) { 45 | fs.writeFileSync( 46 | Path.resolve("test/nm/demo.css"), 47 | data || ".demo1 { background-color: greenyellow; }" 48 | ); 49 | } 50 | 51 | function cleanup() { 52 | delete process.send; 53 | try { 54 | rimraf.sync(Path.resolve("test/dist")); 55 | writeFont(); 56 | writeCss(); 57 | } catch (e) { 58 | // 59 | } 60 | } 61 | 62 | let webpackDevServer; 63 | 64 | function start(config, devConfig, callback) { 65 | const compiler = webpack(config); 66 | webpackDevServer = new WebpackDevServer(compiler, _.merge({}, devConfig)); 67 | webpackDevServer.listen(8080, "localhost", function() { 68 | callback(); 69 | }); 70 | } 71 | 72 | const devConfig = { 73 | host: "localhost", 74 | port: 8080, 75 | publicPath: webpackConfig.output.publicPath, 76 | // outputPath: "/", 77 | filename: webpackConfig.output.filename, 78 | hot: false, 79 | contentBase: process.cwd(), 80 | quiet: true, 81 | stats: { 82 | cached: false, 83 | cachedAssets: false, 84 | colors: { 85 | level: 2, 86 | hasBasic: true, 87 | has256: true, 88 | has16m: false 89 | } 90 | } 91 | }; 92 | 93 | function stopWebpackDevServer(callback) { 94 | if (webpackDevServer) { 95 | webpackDevServer.close(); 96 | webpackDevServer.listeningApp.close(function() { 97 | webpackDevServer = undefined; 98 | setTimeout(callback, 200); 99 | }); 100 | } else { 101 | callback(); 102 | } 103 | } 104 | 105 | before(cleanup); 106 | 107 | beforeEach(function(done) { 108 | cleanup(); 109 | done(); 110 | }); 111 | 112 | let isomorphicRequire; 113 | let isomorphicConfig; 114 | 115 | afterEach(function(done) { 116 | // cleanup(); 117 | isomorphicRequire && isomorphicRequire.reset(); 118 | isomorphicRequire = undefined; 119 | isomorphicConfig = undefined; 120 | 121 | stopWebpackDevServer(done); 122 | }); 123 | 124 | const fontFile = "test/client/fonts/font.ttf"; 125 | const cssFile = "test/nm/demo.css"; 126 | 127 | function verifyRemoteAssets(fontHash, callback) { 128 | fetchUrl("http://localhost:8080/test/isomorphic-assets.json", function(err, meta, body) { 129 | if (err) return callback(err); 130 | expect(meta.status).to.equal(200); 131 | const isomorphicData = JSON.parse(body.toString()); 132 | expect(isomorphicData.assets.marked[fontFile]).to.equal(fontHash); 133 | expect(isomorphicData.assets.marked[cssFile]).to.deep.equal({ "demo1": "demo__demo1" }); 134 | return callback(); 135 | }); 136 | } 137 | 138 | function testWebpackDevServer(config, plugin) { 139 | isomorphicRequire = extendRequire({}); 140 | 141 | plugin.on("update", data => { 142 | isomorphicConfig = data.config; 143 | isomorphicRequire.initialize(data.config); 144 | }); 145 | 146 | function verifyAssetChanges(callback) { 147 | const oldHash = isomorphicConfig.assets.marked[fontFile]; 148 | expect(oldHash).to.be.a("string").that.is.not.empty; 149 | writeFont("testtesttest"); // font.ttf md5 1fb0e331c05a52d5eb847d6fc018320d 150 | writeCss(".demo { background-color: greenyellow; }"); 151 | 152 | const startTime = Date.now(); 153 | function check() { 154 | const newHash = isomorphicConfig.assets.marked[fontFile]; 155 | const newMap = isomorphicConfig.assets.marked[cssFile]; 156 | if (newHash && newHash !== oldHash) { 157 | expect(newHash).to.be.a("string").that.is.not.empty; 158 | expect(newHash).contains("1fb0e331c05a52d5eb847d6fc018320d"); 159 | expect(newMap).to.deep.equal({ "demo": "demo__demo" }); 160 | callback(); 161 | } else if (Date.now() - startTime > 5000) { 162 | callback(new Error("waiting for font change valid message timeout")); 163 | } else { 164 | setTimeout(check, 50); 165 | } 166 | } 167 | 168 | check(); 169 | } 170 | 171 | return asyncVerify( 172 | next => start(config, devConfig, next), 173 | () => expect(isomorphicRequire.isWebpackDev()).to.equal(true), 174 | next => verifyRemoteAssets(defaultFontHash, next), 175 | () => xaa.delay(25), 176 | next => verifyAssetChanges(next), 177 | runFinally(() => { 178 | plugin.removeAllListeners(); 179 | }) 180 | ); 181 | } 182 | 183 | function testAddUrl(publicPath) { 184 | const wpConfig = clone(webpackConfig); 185 | wpConfig.output.publicPath = publicPath; 186 | 187 | const plugin = new IsomorphicLoaderPlugin({ 188 | webpackDev: { url: "http://localhost:8080", addUrl: true } 189 | }); 190 | 191 | wpConfig.plugins = [plugin, new MiniCssExtractPlugin({ filename: "[name].style.css" })]; 192 | 193 | return asyncVerify( 194 | () => testWebpackDevServer(wpConfig, plugin), 195 | () => { 196 | const ttf = "../client/fonts/font.ttf"; 197 | const fontFullPath = require.resolve(ttf); 198 | delete require.cache[fontFullPath]; 199 | const font = require(ttf); 200 | expect(font).to.equal(`http://localhost:8080/test/${changedFontHash}`); 201 | delete require.cache[fontFullPath]; 202 | }, 203 | runFinally(() => { 204 | isomorphicRequire && isomorphicRequire.reset(); 205 | 206 | return new Promise(resolve => stopWebpackDevServer(resolve)); 207 | }) 208 | ); 209 | } 210 | 211 | it(`should start and add webpack dev server URL @${tag}`, function() { 212 | return testAddUrl("/test/", true); 213 | }); 214 | 215 | it(`should start and add webpack dev server URL and / @${tag}`, function() { 216 | return testAddUrl("test/"); 217 | }); 218 | }); 219 | }; 220 | -------------------------------------------------------------------------------- /test/lib/isomorphic.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* eslint-disable */ 4 | 5 | const Fs = require("fs"); 6 | const Path = require("path"); 7 | const clone = require("clone"); 8 | const chai = require("chai"); 9 | const rimraf = require("rimraf"); 10 | const Config = require("../../lib/config"); 11 | const _ = require("lodash"); 12 | const expect = chai.expect; 13 | const { extendRequire, setXRequire, getXRequire } = require("../.."); 14 | const { asyncVerify, expectError, runFinally } = require("run-verify"); 15 | const Pkg = require("../../package.json"); 16 | 17 | const logger = require("../../lib/logger"); 18 | 19 | module.exports = function isomorphicExtend({ tag, webpack, webpackConfig }) { 20 | const SAVE_CONFIG = clone(Config); 21 | 22 | Config.defaultStartDelay = 0; 23 | 24 | function cleanup() { 25 | try { 26 | rimraf.sync(Path.resolve("test/dist")); 27 | } catch (e) { 28 | // 29 | } 30 | } 31 | 32 | function generate(config, callback) { 33 | if (!callback) { 34 | callback = config; 35 | config = webpackConfig; 36 | } 37 | const compiler = webpack(config); 38 | compiler.run(function(err, stats) { 39 | stats.toString(); 40 | callback(err); 41 | }); 42 | } 43 | 44 | before(cleanup); 45 | 46 | after(() => { 47 | Object.assign(Config, SAVE_CONFIG); 48 | }); 49 | 50 | const origLog = Object.assign({}, logger); 51 | let logs = []; 52 | 53 | beforeEach(function() { 54 | cleanup(); 55 | logs = []; 56 | logger.log = function() { 57 | logs.push(Array.prototype.slice.apply(arguments).join(" ")); 58 | }; 59 | logger.error = logger.log; 60 | }); 61 | 62 | afterEach(function() { 63 | cleanup(); 64 | Object.assign(logger, origLog); 65 | }); 66 | 67 | it(`should generate assets file @${tag}`, function() { 68 | function verify() { 69 | const isomorphicConfig = JSON.parse( 70 | Fs.readFileSync(Path.resolve("test/dist/isomorphic-assets.json")) 71 | ); 72 | const expected = { 73 | valid: true, 74 | version: Pkg.version, 75 | timestamp: 0, 76 | context: "test/client", 77 | output: { 78 | path: "test/dist", 79 | filename: "bundle.js", 80 | publicPath: "/test/" 81 | }, 82 | assets: { 83 | marked: { 84 | "test/client/images/smiley.jpg": "2029f1bb8dd109eb06f59157de62b529.jpg", 85 | "test/client/images/smiley2.jpg": "2029f1bb8dd109eb06f59157de62b529.jpg", 86 | "test/client/images/smiley.svg": "47869791f9dd9ef1be6e258e1a766ab8.svg", 87 | "test/client/images/smiley.png": "f958aee9742689b14418e8efef2b4032.png", 88 | "test/client/data/foo.bin": "71f74d0894d9ce89e22c678f0d8778b2.bin", 89 | "test/client/fonts/font.ttf": "1fb0e331c05a52d5eb847d6fc018320d.ttf", 90 | "test/nm/smiley2.jpg": "2029f1bb8dd109eb06f59157de62b529.jpg", 91 | "test/nm/demo.css": { 92 | "demo": "demo__demo" 93 | } 94 | }, 95 | chunks: { 96 | main: [ 97 | "main.style.css", 98 | "bundle.js" 99 | ] 100 | } 101 | } 102 | }; 103 | 104 | isomorphicConfig.timestamp = 0; 105 | expect(isomorphicConfig).to.deep.equal(expected); 106 | } 107 | 108 | return asyncVerify( 109 | next => generate(next), 110 | () => verify() 111 | ); 112 | }); 113 | 114 | function verifyRequireAssets(publicPath) { 115 | publicPath = publicPath === undefined ? "/test/" : publicPath; 116 | 117 | const smiley = require("../client/images/smiley.jpg"); 118 | const smiley2 = require("../client/images/smiley2.jpg"); 119 | const smileyFull = require(Path.resolve("test/client/images/smiley.jpg")); 120 | const smileyPng = require("../client/images/smiley.png"); 121 | const smileySvg = require("../client/images/smiley.svg"); 122 | const fooBin = require("file-loader!isomorphic!../client/data/foo.bin"); 123 | const expectedUrl = publicPath + "2029f1bb8dd109eb06f59157de62b529.jpg"; 124 | 125 | expect(smiley).to.equal(expectedUrl); 126 | expect(smiley2).to.equal(expectedUrl); 127 | expect(smileyFull).to.equal(expectedUrl); 128 | expect(smileyPng).to.equal(publicPath + "f958aee9742689b14418e8efef2b4032.png"); 129 | expect(smileySvg).to.equal(publicPath + "47869791f9dd9ef1be6e258e1a766ab8.svg"); 130 | expect(fooBin).to.equal(publicPath + "71f74d0894d9ce89e22c678f0d8778b2.bin"); 131 | 132 | try { 133 | require("bad_module"); 134 | chai.assert(false, "expect exception"); 135 | } catch (e) { 136 | expect(e).to.be.ok; 137 | } 138 | 139 | try { 140 | require("../client/images/smiley"); 141 | chai.assert(false, "expect exception"); 142 | } catch (e) { 143 | expect(e).to.be.ok; 144 | } 145 | } 146 | 147 | const testIsomorphicOutputFile = Path.resolve("test/dist/isomorphic-assets.json"); 148 | 149 | it(`should load isomorphic config and extend require @${tag}`, function() { 150 | let isomorphicRequire; 151 | return asyncVerify( 152 | next => generate(next), 153 | () => { 154 | isomorphicRequire = extendRequire(); 155 | isomorphicRequire.loadAssets([testIsomorphicOutputFile]); 156 | expect(isomorphicRequire.isWebpackDev()).to.equal(false); 157 | verifyRequireAssets(); 158 | }, 159 | runFinally(() => { 160 | isomorphicRequire && isomorphicRequire.reset(); 161 | }) 162 | ); 163 | }); 164 | 165 | it(`should call processConfig @${tag}`, function() { 166 | const sampleConfig = { 167 | valid: true, 168 | version: Pkg.version, 169 | timestamp: 0, 170 | context: "test/client", 171 | output: { 172 | path: "test/dist", 173 | filename: "bundle.js", 174 | publicPath: "/test/" 175 | }, 176 | assets: {} 177 | }; 178 | let isomorphicRequire; 179 | let testConfig; 180 | return asyncVerify( 181 | () => { 182 | isomorphicRequire = extendRequire( 183 | { 184 | processConfig: config => { 185 | return (testConfig = config); 186 | } 187 | }, 188 | sampleConfig 189 | ); 190 | expect(testConfig).to.deep.equal(sampleConfig); 191 | }, 192 | 193 | runFinally(() => { 194 | isomorphicRequire && isomorphicRequire.reset(); 195 | }) 196 | ); 197 | }); 198 | 199 | it(`should default publicPath to "" @${tag}`, function() { 200 | const config = _.merge({}, webpackConfig); 201 | delete config.output.publicPath; 202 | return asyncVerify( 203 | next => { 204 | generate(config, next); 205 | }, 206 | () => { 207 | const isomorphicConfig = JSON.parse(Fs.readFileSync(testIsomorphicOutputFile)); 208 | expect(isomorphicConfig.output) 209 | .to.have.property("publicPath") 210 | .equal(""); 211 | } 212 | ); 213 | }); 214 | 215 | it(`should fail if config version and package version mismatch @${tag}`, function() { 216 | const sampleConfig = { 217 | valid: true, 218 | version: "3.0.0", 219 | timestamp: 0, 220 | context: "test/client", 221 | output: { 222 | path: "test/dist", 223 | filename: "bundle.js", 224 | publicPath: "/test/" 225 | }, 226 | assets: {} 227 | }; 228 | 229 | return asyncVerify( 230 | expectError(() => { 231 | extendRequire({}, sampleConfig); 232 | }), 233 | error => { 234 | expect(error.message).contains("is different from config version 3.0.0"); 235 | } 236 | ); 237 | }); 238 | 239 | it(`should fail if loadAssets got a config file with a mismatched version @${tag}`, () => { 240 | return asyncVerify( 241 | expectError(() => { 242 | extendRequire().loadAssets("test/bad-version-config.json"); 243 | }), 244 | error => { 245 | expect(error.message).includes("is different from config version 3.0.1"); 246 | } 247 | ); 248 | }); 249 | 250 | it(`extendRequire should avoid loading assets if WEBPACK_DEV env exist`, () => { 251 | process.env.WEBPACK_DEV = true; 252 | return asyncVerify( 253 | () => { 254 | // should not fail due to reading bad config file 255 | extendRequire({ assetsFile: "test/bad-version-config.json" }); 256 | }, 257 | runFinally(() => { 258 | delete process.env.WEBPACK_DEV; 259 | }) 260 | ); 261 | }); 262 | 263 | it(`should help maintain a global extend require intance`, () => { 264 | setXRequire("blah"); 265 | expect(getXRequire()).equal("blah"); 266 | }); 267 | }; 268 | -------------------------------------------------------------------------------- /test/lib/webpack-info.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const testInfo = { 4 | v4: { 5 | skip: false, 6 | xrequire: require, 7 | webpack: "webpack", 8 | devServer: "webpack-dev-server", 9 | config: require.resolve("../webpack4.config") 10 | }, 11 | v3: { 12 | skip: true, 13 | xrequire: require("webpack3/xrequire"), 14 | webpack: "webpack", 15 | devServer: "webpack-dev-server", 16 | config: require.resolve("../webpack.config") 17 | } 18 | }; 19 | 20 | module.exports = testInfo; 21 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require node_modules/electrode-archetype-njs-module-dev/config/test/setup.js 2 | --recursive 3 | -------------------------------------------------------------------------------- /test/nm/demo.css: -------------------------------------------------------------------------------- 1 | .demo { background-color: greenyellow; } -------------------------------------------------------------------------------- /test/nm/smiley2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchip/isomorphic-loader/f4e5fa18b778a517ebf7391e07092c9262d1c00c/test/nm/smiley2.jpg -------------------------------------------------------------------------------- /test/spec/extend-require.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { ExtendRequire } = require("../../lib/extend-require"); 4 | const Pkg = require("../../package.json"); 5 | const { asyncVerify, runFinally, expectError } = require("run-verify"); 6 | const xstdout = require("xstdout"); 7 | 8 | describe("extend-require", function() { 9 | it("should log error if require on non-path request is asset that doesn't exist", () => { 10 | const extendRequire = new ExtendRequire({}); 11 | const intercept = xstdout.intercept(true); 12 | 13 | return asyncVerify( 14 | () => { 15 | extendRequire.initialize({ 16 | version: Pkg.version, 17 | assets: { 18 | marked: { 19 | "blah-test": "test" 20 | } 21 | }, 22 | output: { 23 | publicPath: "" 24 | } 25 | }); 26 | }, 27 | expectError(() => { 28 | require("blah-test"); 29 | }), 30 | error => { 31 | expect(error.code).equal("MODULE_NOT_FOUND"); 32 | expect(intercept.stderr.join("")).contains( 33 | `isomorphic-loader check asset blah-test exception` 34 | ); 35 | }, 36 | runFinally(() => { 37 | intercept.restore(); 38 | extendRequire.reset(); 39 | }) 40 | ); 41 | }); 42 | 43 | it("should prepend publicPath from config", () => { 44 | const extendRequire = new ExtendRequire({}); 45 | 46 | return asyncVerify( 47 | () => { 48 | extendRequire.initialize({ 49 | version: Pkg.version, 50 | assets: { 51 | marked: { 52 | "test/nm/smiley2.jpg": "test.jpg" 53 | } 54 | }, 55 | output: { 56 | publicPath: "/blah/" 57 | } 58 | }); 59 | }, 60 | () => require("../nm/smiley2.jpg"), 61 | assetUrl => { 62 | expect(assetUrl).equals("/blah/test.jpg"); 63 | }, 64 | runFinally(() => { 65 | extendRequire.reset(); 66 | }) 67 | ); 68 | }); 69 | 70 | it("should prepend custom publicPath", () => { 71 | const extendRequire = new ExtendRequire({}); 72 | 73 | return asyncVerify( 74 | () => { 75 | extendRequire.initialize({ 76 | version: Pkg.version, 77 | assets: { 78 | marked: { 79 | "test/nm/smiley2.jpg": "test.jpg" 80 | } 81 | }, 82 | output: { 83 | publicPath: "/blah/" 84 | } 85 | }); 86 | extendRequire.setPublicPath("/foo/"); 87 | }, 88 | () => require("../nm/smiley2.jpg"), 89 | assetUrl => { 90 | expect(assetUrl).equals("/foo/test.jpg"); 91 | }, 92 | runFinally(() => { 93 | extendRequire.reset(); 94 | }) 95 | ); 96 | }); 97 | 98 | it("should use custom url map", () => { 99 | const extendRequire = new ExtendRequire({}); 100 | 101 | return asyncVerify( 102 | () => { 103 | extendRequire.initialize({ 104 | version: Pkg.version, 105 | assets: { 106 | marked: { 107 | "test/nm/smiley2.jpg": "test.jpg" 108 | } 109 | }, 110 | output: { 111 | publicPath: "/blah/" 112 | } 113 | }); 114 | extendRequire.setUrlMapper(url => "/woop/" + url); // eslint-disable-line 115 | }, 116 | () => require("../nm/smiley2.jpg"), 117 | assetUrl => { 118 | expect(assetUrl).equals("/woop/test.jpg"); 119 | }, 120 | runFinally(() => { 121 | extendRequire.reset(); 122 | }) 123 | ); 124 | }); 125 | 126 | it("should log error urlMap throws", () => { 127 | const extendRequire = new ExtendRequire({}); 128 | const intercept = xstdout.intercept(true); 129 | 130 | return asyncVerify( 131 | () => { 132 | extendRequire.initialize({ 133 | version: Pkg.version, 134 | assets: { 135 | marked: { 136 | "test/nm/smiley2.jpg": "test.jpg" 137 | } 138 | }, 139 | output: { 140 | publicPath: "" 141 | } 142 | }); 143 | extendRequire.setUrlMapper(() => { 144 | throw new Error("test oops"); 145 | }); 146 | }, 147 | () => require("../nm/smiley2.jpg"), 148 | assetUrl => { 149 | intercept.restore(); 150 | expect(assetUrl).equals("test.jpg"); 151 | 152 | expect(intercept.stderr.join("")).contains("urlMap thrown error Error: test oops"); 153 | }, 154 | runFinally(() => { 155 | intercept.restore(); 156 | extendRequire.reset(); 157 | }) 158 | ); 159 | }); 160 | 161 | it("should return object for css", () => { 162 | const extendRequire = new ExtendRequire({}); 163 | 164 | return asyncVerify( 165 | () => { 166 | extendRequire.initialize({ 167 | version: Pkg.version, 168 | assets: { 169 | marked: { 170 | "test/nm/demo.css": { demo: "abc" } 171 | } 172 | }, 173 | output: { 174 | publicPath: "/blah/" 175 | } 176 | }); 177 | }, 178 | () => require("../nm/demo.css"), 179 | assetUrl => { 180 | expect(assetUrl).deep.equals({ demo: "abc" }); 181 | }, 182 | runFinally(() => { 183 | extendRequire.reset(); 184 | }) 185 | ); 186 | }); 187 | 188 | it("should intercept by extensions", () => { 189 | const extendRequire = new ExtendRequire({ 190 | ignoreExtensions: ".css" 191 | }); 192 | 193 | return asyncVerify( 194 | () => require("blah.css"), 195 | asset => { 196 | expect(asset).deep.equals({}); 197 | }, 198 | expectError(() => require("blah.jpg")), 199 | error => { 200 | expect(error.message).contains("Cannot find module 'blah.jpg"); 201 | }, 202 | runFinally(() => { 203 | extendRequire.reset(); 204 | }) 205 | ); 206 | }); 207 | 208 | it("should be ok to reset before initializing", () => { 209 | const extendRequire = new ExtendRequire(); 210 | extendRequire.reset(); 211 | // no error => good 212 | }); 213 | }); 214 | -------------------------------------------------------------------------------- /test/spec/isomorphic.dev.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const clone = require("clone"); 4 | const webpackInfo = require("../lib/webpack-info"); 5 | 6 | Object.keys(webpackInfo).forEach(ver => { 7 | const info = webpackInfo[ver]; 8 | if (info.skip) return; 9 | 10 | const xrequire = info.xrequire; 11 | const webpack = xrequire(info.webpack); 12 | const WebpackDevServer = xrequire(info.devServer); 13 | const webpackConfig = clone(xrequire(info.config)); 14 | 15 | require("../lib/isomorphic.dev.spec")({ 16 | title: `isomorphic extend with webpack ${ver} & webpack-dev-server`, 17 | tag: `webpack_${ver}`, 18 | timeout: 10000, 19 | webpack, 20 | WebpackDevServer, 21 | webpackConfig 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /test/spec/isomorphic.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const clone = require("clone"); 4 | const webpackInfo = require("../lib/webpack-info"); 5 | 6 | Object.keys(webpackInfo).forEach(ver => { 7 | const info = webpackInfo[ver]; 8 | if (info.skip) return; 9 | describe(`isomorphic extend with webpack ${ver}`, function() { 10 | this.timeout(20000); 11 | const webpack = info.xrequire(info.webpack); 12 | const webpackConfig = clone(info.xrequire(info.config)); 13 | require("../lib/isomorphic.spec")({ 14 | tag: `webpack_${ver}`, 15 | webpack, 16 | webpackConfig 17 | }); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /test/spec/loader.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const isomorphicLoader = require("../../lib/isomorphic-loader"); 4 | const CSS_MODULE_MAPPINGS = Symbol.for("css-module-content-mapping"); 5 | 6 | describe("isomorphic-loader", function() { 7 | it("should return the content as such", () => { 8 | const boundFn = isomorphicLoader.bind({ _module: { userRequest: "" } }); 9 | const content = boundFn("testData"); 10 | expect(content).to.equal("testData"); 11 | }); 12 | 13 | it("should add content mapping for .(css|less|styl|sass|scss) files", () => { 14 | const boundFn = isomorphicLoader.bind({ _module: { userRequest: "test.css" } }); 15 | const content = boundFn("testData"); 16 | expect(content).to.equal("testData"); 17 | expect(global[CSS_MODULE_MAPPINGS]["test.css"]).to.equal("testData"); 18 | }); 19 | 20 | it("should not add mapping for unsupported filetypes", () => { 21 | const boundFn = isomorphicLoader.bind({ _module: { userRequest: "foo.js" } }); 22 | const content = boundFn("testData"); 23 | expect(content).to.equal("testData"); 24 | expect(global[CSS_MODULE_MAPPINGS]["foo.js"]).to.be.undefined; 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /test/spec/logger.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const logger = require("../../lib/logger"); 4 | const xstdout = require("xstdout"); 5 | 6 | describe("logger", function() { 7 | after(() => { 8 | logger.setLevel(); 9 | }); 10 | 11 | it("should handle log info", () => { 12 | const intercept = xstdout.intercept(true); 13 | 14 | logger.log("blah"); 15 | const a = intercept.stdout.join("").trim(); 16 | intercept.stdout = []; 17 | logger.setLevel("error"); 18 | logger.log("oops"); 19 | const b = intercept.stdout.join("").trim(); 20 | intercept.restore(); 21 | expect(a).equal("blah"); 22 | expect(b).equal(""); 23 | }); 24 | 25 | it("should handle log error", () => { 26 | const intercept = xstdout.intercept(true); 27 | 28 | logger.setLevel(); 29 | logger.log("blah"); 30 | logger.error("error"); 31 | const a = intercept.stdout.join("").trim() + intercept.stderr.join("").trim(); 32 | intercept.stdout.splice(0, intercept.stdout.length); 33 | intercept.stderr.splice(0, intercept.stderr.length); 34 | logger.setLevel("error"); 35 | logger.log("oops"); 36 | logger.error("error"); 37 | logger.setLevel("none"); 38 | logger.error("none"); 39 | const b = intercept.stdout.join("").trim() + intercept.stderr.join("").trim(); 40 | intercept.restore(); 41 | expect(a).equal("blaherror"); 42 | expect(b).equal("error"); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /test/spec/utils.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Path = require("path"); 4 | const utils = require("../../lib/utils"); 5 | 6 | describe("utils", function() { 7 | describe("getParentPath", function() { 8 | it("should return path if it exist", () => { 9 | expect(utils.getParentPath({ path: "/test" })).to.equal("/test"); 10 | }); 11 | 12 | it("should return dirname of filename if only that exist", () => { 13 | expect(utils.getParentPath({ filename: "/test/foo.txt" })).to.equal("/test"); 14 | }); 15 | 16 | it(`should return "" if nothing exist`, () => { 17 | expect(utils.getParentPath()).to.equal(""); 18 | expect(utils.getParentPath({})).to.equal(""); 19 | }); 20 | }); 21 | 22 | describe("getWebpackRequest", function() { 23 | it("should return userRequest", () => { 24 | expect(utils.getWebpackRequest({ reasons: [{ userRequest: "test" }] })).to.equal("test"); 25 | }); 26 | 27 | it(`should return "" if can't find userRequest`, () => { 28 | expect(utils.getWebpackRequest()).to.equal(""); 29 | expect(utils.getWebpackRequest({})).to.equal(""); 30 | expect(utils.getWebpackRequest({ reasons: [] })).to.equal(""); 31 | }); 32 | }); 33 | 34 | describe("replaceAppSrcDir", function() { 35 | it("should return request without appSrcDir", () => { 36 | expect(utils.replaceAppSrcDir("test")).to.equal("test"); 37 | }); 38 | 39 | it("should return request if appSrcDir not match", () => { 40 | expect(utils.replaceAppSrcDir("test/hello", "src")).to.equal("test/hello"); 41 | }); 42 | 43 | it("should replace request with appSrcDir if match", () => { 44 | expect(utils.replaceAppSrcDir("test/hello", "test")).to.equal("$AppSrc/hello"); 45 | expect(utils.replaceAppSrcDir("test/hello", "test/")).to.equal("$AppSrc/hello"); 46 | }); 47 | }); 48 | 49 | describe("removeCwd", function() { 50 | it("should remove process.cwd() from path", () => { 51 | expect(utils.removeCwd(Path.resolve("test"))).to.equal("test"); 52 | }); 53 | }); 54 | 55 | describe("requireFromString", function() { 56 | it("should load module from string", () => { 57 | const result = utils.requireFromString("module.exports = { test: 'abc'}"); 58 | expect(result).to.deep.equal({ test: "abc" }); 59 | }); 60 | }); 61 | 62 | describe("getMyNodeModulesPath", function() { 63 | it("should find simple path", () => { 64 | expect(utils.getMyNodeModulesPath("/test/node_modules/isomorphic-loader/lib")).equals( 65 | "isomorphic-loader/lib" 66 | ); 67 | }); 68 | 69 | it("should return up to 4 parts path if dir doesn't contain name", () => { 70 | expect(utils.getMyNodeModulesPath("/test/node_modules/123456789/lib")).equals( 71 | "test/node_modules/123456789/lib" 72 | ); 73 | expect(utils.getMyNodeModulesPath("/foo/blah/test/node_modules/123456789/lib")).equals( 74 | "test/node_modules/123456789/lib" 75 | ); 76 | expect(utils.getMyNodeModulesPath("/foo/123456789/lib")).equals("/foo/123456789/lib"); 77 | }); 78 | 79 | it("should find path with extras between name", () => { 80 | expect(utils.getMyNodeModulesPath("/test/node_modules/isomorphic-loader/123/lib")).equals( 81 | "isomorphic-loader/123/lib" 82 | ); 83 | }); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /test/webpack.config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { IsomorphicLoaderPlugin } = require(".."); 4 | const Path = require("path"); 5 | 6 | module.exports = { 7 | context: Path.resolve("test/client"), 8 | entry: "./entry.js", 9 | output: { 10 | path: Path.resolve("test/dist"), 11 | filename: "bundle.js", 12 | publicPath: "/test/" 13 | }, 14 | plugins: [new IsomorphicLoaderPlugin({ webpackDev: { addUrl: false } })], 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.(jpe?g|png|gif|svg)$/i, 19 | use: [{ loader: "electrode-cdn-file-loader", options: { limit: 10000 } }, "../.."] 20 | }, 21 | { 22 | test: /\.(ttf|eot)$/, 23 | use: [{ loader: "electrode-cdn-file-loader", options: { limit: 10000 } }, "../.."] 24 | } 25 | ] 26 | }, 27 | resolve: { 28 | alias: { 29 | smiley2Jpg: Path.join(__dirname, "./nm/smiley2.jpg") 30 | } 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /test/webpack3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack3", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": {}, 7 | "dependencies": { 8 | "webpack": "^3", 9 | "webpack-dev-server": "^2" 10 | }, 11 | "private": true 12 | } 13 | -------------------------------------------------------------------------------- /test/webpack3/xrequire.js: -------------------------------------------------------------------------------- 1 | module.exports = require; 2 | -------------------------------------------------------------------------------- /test/webpack4.config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { IsomorphicLoaderPlugin } = require(".."); 4 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 5 | const Path = require("path"); 6 | 7 | module.exports = { 8 | context: Path.resolve("test/client"), 9 | entry: "./entry.js", 10 | output: { 11 | path: Path.resolve("test/dist"), 12 | filename: "bundle.js", 13 | publicPath: "/test/" 14 | }, 15 | plugins: [ 16 | new IsomorphicLoaderPlugin({ webpackDev: { addUrl: false } }), 17 | new MiniCssExtractPlugin({ filename: "[name].style.css" }) 18 | ], 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.(jpe?g|png|gif|svg)$/i, 23 | loader: "file-loader!../.." 24 | }, 25 | { 26 | test: /\.(ttf|eot)$/, 27 | loader: "file-loader!../.." 28 | }, 29 | { 30 | test: /\.css$/, 31 | use: [ 32 | "../..", 33 | { 34 | loader: MiniCssExtractPlugin.loader, 35 | options: { 36 | publicPath: "", 37 | esModule: false, 38 | modules: true 39 | } 40 | }, 41 | { 42 | loader: "css-loader", 43 | options: { 44 | context: Path.resolve("test/client"), 45 | modules: true, 46 | localIdentName: "[name]__[local]" 47 | } 48 | } 49 | ] 50 | } 51 | ] 52 | }, 53 | resolve: { 54 | alias: { 55 | smiley2Jpg: Path.join(__dirname, "./nm/smiley2.jpg"), 56 | demoCss: Path.join(__dirname, "./nm/demo.css") 57 | } 58 | } 59 | }; 60 | -------------------------------------------------------------------------------- /xclap.js: -------------------------------------------------------------------------------- 1 | require("electrode-archetype-njs-module-dev")(); 2 | --------------------------------------------------------------------------------