├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── appveyor.yml ├── package-lock.json ├── package.json ├── src ├── BabelOptions.d.ts ├── Cache.ts ├── ResolvedConfig.ts ├── SimulatedBabelCache.ts ├── Verifier.ts ├── cloneOptions.ts ├── codegen.ts ├── collector.ts ├── currentEnv.ts ├── errors.ts ├── getPluginOrPresetName.ts ├── hashDependencies.ts ├── hashSources.ts ├── helpers.ts ├── isFilePath.ts ├── loadConfigModule.ts ├── loadPluginOrPreset.ts ├── main.ts ├── mergePluginsOrPresets.ts ├── normalizeOptions.ts ├── readSafe.ts ├── reduceChains.ts ├── resolvePluginsAndPresets.ts └── standardizeName.ts ├── test ├── SimulatedBabelCache.js ├── Verifier.js ├── codegen.js ├── collector-errors.js ├── collector.js ├── compare.js ├── fixtures │ ├── babel-config-chain-js │ │ ├── .babelrc.js │ │ ├── LICENSE │ │ ├── README.md │ │ ├── dir2 │ │ │ ├── .babelrc.js │ │ │ └── src.js │ │ ├── env │ │ │ ├── .babelrc.js │ │ │ └── src.js │ │ ├── extended.babelrc.js │ │ └── pkg │ │ │ ├── package.json │ │ │ └── src.js │ ├── babel-config-chain │ │ ├── .babelrc │ │ ├── LICENSE │ │ ├── README.md │ │ ├── dir2 │ │ │ ├── .babelrc │ │ │ └── src.js │ │ ├── env │ │ │ ├── .babelrc │ │ │ └── src.js │ │ ├── extended.babelrc.json │ │ └── pkg │ │ │ ├── package.json │ │ │ └── src.js │ ├── babelrc │ │ └── .babelrc │ ├── bad-js │ │ ├── factory-throws │ │ │ └── .babelrc.js │ │ ├── no-cache-configuration │ │ │ └── .babelrc.js │ │ ├── promise-export │ │ │ └── .babelrc.js │ │ ├── promise-from-factory │ │ │ └── .babelrc.js │ │ └── syntax-error │ │ │ └── .babelrc.js │ ├── bad-pkg │ │ ├── array-babel │ │ │ └── package.json │ │ ├── bool-babel │ │ │ └── package.json │ │ ├── falsy │ │ │ └── package.json │ │ ├── invalid-json │ │ │ └── package.json │ │ ├── null-babel │ │ │ └── package.json │ │ ├── null │ │ │ └── package.json │ │ └── without-babel │ │ │ └── package.json │ ├── bad-rc │ │ ├── array │ │ │ └── .babelrc │ │ ├── ast-option │ │ │ └── .babelrc │ │ ├── babelrc-option │ │ │ └── .babelrc │ │ ├── bool │ │ │ └── .babelrc │ │ ├── code-option │ │ │ └── .babelrc │ │ ├── conflicting-sourcemaps-option │ │ │ └── .babelrc │ │ ├── cwd-option │ │ │ └── .babelrc │ │ ├── envName-option │ │ │ └── .babelrc │ │ ├── falsy │ │ │ └── .babelrc │ │ ├── filename-option │ │ │ └── .babelrc │ │ ├── filenameRelative-option │ │ │ └── .babelrc │ │ ├── invalid-json5 │ │ │ └── .babelrc │ │ ├── nested-env-option │ │ │ └── .babelrc │ │ ├── nested-overrides-option │ │ │ └── .babelrc │ │ ├── null │ │ │ └── .babelrc │ │ ├── overrides-contains-falsy-option │ │ │ └── .babelrc │ │ ├── overrides-in-env-option │ │ │ └── .babelrc │ │ └── overrides-not-array-option │ │ │ └── .babelrc │ ├── codegen │ │ ├── empty.js │ │ ├── env-specific.js │ │ ├── node_modules │ │ │ └── noop │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ └── plugin-resolution.js │ ├── compare │ │ ├── .babelrc │ │ ├── dir │ │ │ ├── extended-by-virtual-foo.json5 │ │ │ └── subdir │ │ │ │ └── extended-by-babelrc.js │ │ ├── extended-by-virtual.json5 │ │ ├── foo.js │ │ ├── js-env │ │ │ ├── .babelrc.js │ │ │ └── foo.js │ │ ├── js-normalization-edge-cases │ │ │ ├── .babelrc.js │ │ │ ├── foo.js │ │ │ └── no-plugins-or-presets.js │ │ ├── node_modules │ │ │ ├── env-plugin │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ │ ├── noop │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ │ ├── noop2 │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ │ ├── noop3 │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ │ ├── noop4 │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ │ ├── plugin-copy │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ │ ├── plugin-default-opts │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ │ ├── plugin │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ │ └── preset │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ ├── overrides │ │ │ ├── .babelrc │ │ │ ├── extends.json5 │ │ │ ├── foo.js │ │ │ └── js │ │ │ │ ├── .babelrc.js │ │ │ │ ├── extends.js │ │ │ │ └── foo.js │ │ ├── presets │ │ │ ├── .babelrc.js │ │ │ ├── edge-cases.js │ │ │ ├── foo.js │ │ │ ├── preset.js │ │ │ └── subdir │ │ │ │ └── base.json5 │ │ └── virtual.json │ ├── complex-env │ │ ├── .babelrc │ │ ├── extended-further.json5 │ │ ├── extended.js │ │ └── foo.json5 │ ├── cycles │ │ ├── deep │ │ │ ├── .babelrc │ │ │ ├── extended-furthest.json5 │ │ │ └── extended.json5 │ │ └── simple │ │ │ ├── .babelrc │ │ │ └── extended.json5 │ ├── dirs-not-files │ │ ├── babelrc │ │ │ └── .babelrc │ │ │ │ └── .gitkeep │ │ └── pkg │ │ │ └── package.json │ │ │ └── .gitkeep │ ├── empty │ │ └── .gitkeep │ ├── frozen-config-module.js │ ├── js │ │ ├── .babelrc.js │ │ ├── cache-usage │ │ │ ├── env │ │ │ │ └── .babelrc.js │ │ │ ├── forever │ │ │ │ └── .babelrc.js │ │ │ ├── never │ │ │ │ └── .babelrc.js │ │ │ └── static │ │ │ │ └── .babelrc.js │ │ ├── cjs-factory │ │ │ └── .babelrc.js │ │ ├── cjs-object │ │ │ └── .babelrc.js │ │ ├── esm-factory │ │ │ └── .babelrc.js │ │ └── esm-object │ │ │ └── .babelrc.js │ ├── multiple-sources │ │ ├── babelrc-and-js │ │ │ ├── .babelrc │ │ │ └── .babelrc.js │ │ ├── babelrc-and-pkg │ │ │ ├── .babelrc │ │ │ └── package.json │ │ └── js-and-pkg │ │ │ ├── .babelrc.js │ │ │ └── package.json │ ├── pkg │ │ └── package.json │ ├── repeats │ │ ├── .babelrc │ │ └── virtual.json │ ├── verifier │ │ ├── babelrc │ │ │ ├── .babelrc │ │ │ ├── extends.json5 │ │ │ ├── foo.js │ │ │ ├── foo.json5 │ │ │ └── plugin.js │ │ ├── babelrcjs │ │ │ ├── .babelrc.js │ │ │ ├── extends.json5 │ │ │ └── plugin.js │ │ └── pkg │ │ │ ├── extends.json5 │ │ │ ├── package.json │ │ │ └── plugin.js │ └── virtual │ │ └── package.json ├── getPluginOrPresetName.js ├── hashDependencies.js ├── hashSources.js ├── helpers │ ├── fixture.js │ ├── pkgDirMock.js │ └── runGeneratedCode.js ├── loadConfigModule.js ├── loadPluginOrPreset.js ├── main.js ├── readSafe.js ├── reduceChains.js ├── resolvePluginsAndPresets.js └── snapshots │ ├── codegen.js.md │ └── codegen.js.snap ├── tsconfig.json └── typings ├── json5.d.ts ├── lodash.isequal.d.ts ├── lodash.merge.d.ts ├── md5-hex.d.ts ├── package-hash.d.ts ├── pirates.d.ts ├── pkg-dir.d.ts └── resolve-from.d.ts /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | .nyc_output 3 | build 4 | coverage 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '10' 4 | - '9' 5 | - '8' 6 | - '6' 7 | env: 8 | - FRESH_DEPS=false 9 | - FRESH_DEPS=true 10 | matrix: 11 | exclude: 12 | - node_js: '9' 13 | env: FRESH_DEPS=true 14 | - node_js: '8' 15 | env: FRESH_DEPS=true 16 | - node_js: '6' 17 | env: FRESH_DEPS=true 18 | cache: 19 | directories: 20 | - $HOME/.npm 21 | before_install: 22 | - npm install --global npm@6.0.0 23 | - npm --version 24 | install: | 25 | if [[ ${FRESH_DEPS} == "true" ]]; then 26 | npm install --no-shrinkwrap --prefer-online; 27 | else 28 | npm ci; 29 | checksum=$(md5sum package-lock.json); 30 | npm install --package-lock-only; 31 | if ! echo ${checksum} | md5sum --quiet -c -; then 32 | echo "package-lock.json was modified unexpectedly. Please rebuild it using npm@$(npm -v) and commit the changes."; 33 | exit 1; 34 | fi 35 | fi 36 | script: npm run coverage 37 | after_success: npx codecov --file=coverage/lcov.info 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Mark Wubben (novemberborn.net) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hullabaloo-config-manager 2 | 3 | Manages complex [Babel] config chains, avoiding duplicated work and enabling 4 | effective caching. 5 | 6 | > Hullabaloo: informal of "babel" (noun) 7 | > 8 | > A confused noise, typically that made by a number 9 | of voices: *the babel of voices on the road.* 10 | 11 | Use this package to resolve [Babel] config chains. The resulting options result 12 | in equivalent compilation behavior as if `@babel/core` had resolved the config. 13 | 14 | A Node.js-compatible JavaScript module can be generated which exports a function 15 | that provides the options object, applicable for the current environment. This 16 | module can be written to disk and reused. 17 | 18 | Config sources and plugin and preset dependencies can be hashed and used as 19 | cache keys. The cache keys and generated module can be verified to avoid having 20 | to repeatedly resolve the config chains, and to be sure a previously 21 | transformation result can be reused. 22 | 23 | This module is used by [AVA]. 24 | 25 | ## Installation 26 | 27 | ```console 28 | $ npm install --save hullabaloo-config-manager 29 | ``` 30 | 31 | ## Usage 32 | 33 | ```js 34 | const configManager = require('hullabaloo-config-manager') 35 | ``` 36 | 37 | ## API 38 | 39 | ### `currentEnv(): string` 40 | 41 | Returns the current environment value, just like `@babel/core` would determine 42 | it. 43 | 44 | ### `fromDirectory(dir: string, options?: {cache?: Cache, expectedEnvNames?: string[]}): Promise` 45 | 46 | Asynchronously resolves config chains from the `dir` directory. If no config can 47 | be found the promise is resolved with `null`. Otherwise it is resolved with the 48 | [resulting config object](#resolvedconfig). The promise is rejected if 49 | [errors](#errors) occur. 50 | 51 | A `cache` object may be provided. 52 | 53 | Provide `expectedEnvNames` when the config chain may contain JavaScript sources 54 | (such as `.babelrc.js` files). You must specify all environment names you want 55 | to use with the resolved config. Defaults to `[currentEnv()]`. 56 | 57 | ### `createConfig(options: {options: BabelOptions, source: string, dir?: string, hash?: string, fileType?: 'JSON' | 'JSON5'}): Config` 58 | 59 | Creates and returns an in-memory [config object](#config). The first argument 60 | must be provided, and it must have a valid [`options` object](#babeloptions) and 61 | `source` value. 62 | 63 | If the `dir` value is not provided it's derived from the `source` value. 64 | Dependencies are resolved relative to this `dir`. 65 | 66 | If the config source does not exist on disk the `hash` value should be provided, 67 | otherwise hashes cannot be created for the config. 68 | 69 | The `fileType` property can be set to `JSON` if the `options` object can be 70 | serialized using `JSON.stringify()`. It defaults to `JSON5`. Use `JS` if the 71 | `options` object contains functions, maps, et cetera. 72 | 73 | Note that the `options` object is cloned before use. Options are not validated 74 | to the same extend as when configuration files are loaded using `fromDirectory` 75 | or when the `extends` option is resolved. 76 | 77 | ### `fromConfig(baseConfig: Config, options?: {cache: Cache}): Promise` 78 | 79 | Asynchronously resolves config chains, starting with the `baseConfig`. The 80 | `baseConfig` must be created using the `createConfig()` method. The promise is 81 | resolved with the [resulting config object](#resolvedconfig). The promise is 82 | rejected if [errors](#errors) occur. 83 | 84 | A `cache` object may be provided. 85 | 86 | Provide `expectedEnvNames` when the config chain may contain JavaScript sources 87 | (such as `.babelrc.js` files). You must specify all environment names you want 88 | to use with the resolved config. Defaults to `[currentEnv()]`. 89 | 90 | ### `restoreVerifier(buffer: Buffer): Verifier` 91 | 92 | Deserializes a [`Verifier`](#verifier). The `buffer` should be created using 93 | `Verifier#toBuffer()`. 94 | 95 | ### `prepareCache(): Cache` 96 | 97 | Creates a cache object that can be passed to the above functions. This may 98 | improve performance by avoiding repeatedly reading files from disk or computing 99 | hashes. 100 | 101 | --- 102 | 103 | ### `Config` 104 | 105 | Use `createConfig()` to create this object. 106 | 107 | #### `Config#extend(config: Config)` 108 | 109 | Extend the config with another config. Throws a `TypeError` if the config was 110 | created with an `extends` clause in its `options`. It throws an `Error` if it 111 | has already been extended. 112 | 113 | --- 114 | 115 | ### `BabelOptions` 116 | 117 | See . **Note that the `envName` 118 | option must not be provided**. 119 | 120 | --- 121 | 122 | ### `ResolvedConfig` 123 | 124 | Returned by `fromConfig()` and `fromDirectory()`. 125 | 126 | #### `ResolvedConfig#generateModule(): string` 127 | 128 | Generates a Node.js-compatible JavaScript module which exports a `getOptions()` 129 | function. This function returns a unique options object, applicable for the 130 | current environment, that can be passed to `@babel/core` methods. 131 | 132 | This module needs to evaluated before the `getOptions()` method can be accessed. 133 | 134 | An environment name can be provided when calling `getOptions()`, e.g. 135 | `getOptions('production')`. If no name is provided, or the name is not a string, 136 | the environment is determined by checking `process.env.BABEL_ENV`, 137 | `process.env.NODE_ENV`, and finally defaulting to `'development'`. 138 | 139 | A second `cache` argument must be provided if the resolved configuration 140 | contains JavaScript sources, e.g. `getOptions('production', cache)`. This should 141 | be the same object as passed to `fromConfig()` and `fromDirectory()`. 142 | 143 | #### `ResolvedConfig#createVerifier(): Promise` 144 | 145 | Asynchronously hashes plugin and preset dependencies of the resolved config, as 146 | well as config sources, and resolves the promise with a [`Verifier`](#verifier) 147 | object. 148 | 149 | --- 150 | 151 | ### `Verifier` 152 | 153 | Use `restoreVerifier()` or `ResolvedConfig#createVerifier()` to create this 154 | object. 155 | 156 | #### `Verifier#cacheKeysForCurrentEnv(): {dependencies: string, sources: string}` 157 | 158 | Synchronously returns cache keys for the plugin and preset dependencies, and 159 | config sources, that are applicable to the current environment. Use these values 160 | to cache the result of `@babel/core` transforms. 161 | 162 | #### `Verifier#verifyCurrentEnv(fixedHashes?: {sources: {[source: string]: string}}, cache?: Cache): Promise<{badDependency: true} | {missingSource: true} | {sourcesChanged: true} | {cacheKeys: {dependencies: string, sources: string}, dependenciesChanged: boolean, sourcesChanged: false, verifier: Verifier}>` 163 | 164 | Asynchronously verifies whether the config is still valid for the current 165 | environment. 166 | 167 | Provide `fixedHashes` if the verifier was derived from a created config with a 168 | fixed `hash` value. A `cache` object may also be provided. 169 | 170 | The promise is resolved with an object describing the verification result: 171 | 172 | * If the object has a `badDependency` property then a plugin or preset 173 | dependency could not be hashed, presumably because it no longer exists. 174 | 175 | * If it has a `missingSource` property then a config source no longer exists. 176 | 177 | * If its `sourcesChanged` property is `true` then config sources have changed 178 | and the config is no longer valid. 179 | 180 | * If its `dependenciesChanged` property is `true` then plugin or preset 181 | dependencies have changed, but the config itself is still valid. The `verifier` 182 | property holds a new `Verifier` object which takes the new dependencies into 183 | account. The `cacheKeys` property contains the same result as calling 184 | `Verifier#cacheKeysForCurrentEnv()` on the returned `verifier`. 185 | 186 | * If its `sourcesChanged` and `dependenciesChanged` properties are both `false` 187 | then the config is valid and cache keys won't have changed. The `verifier` 188 | property holds the same `Verifier` object. The `cacheKeys` properties contains 189 | the same result as calling `Verifier#cacheKeysForCurrentEnv()`. 190 | 191 | #### `Verifier#toBuffer()` 192 | 193 | Serializes the verifier state into a `Buffer` object. Use `restoreVerifier()` 194 | to deserialize. 195 | 196 | --- 197 | 198 | ### Errors 199 | 200 | Error constructors are not publicly available, but errors can be identified by 201 | their `name` property. 202 | 203 | #### `BadDependencyError` 204 | 205 | Used when a plugin or preset dependency couldn't be resolved. The corresponding 206 | package or file name is available through the `source` property. There may be 207 | another error with more details, available through the `parent` property. 208 | 209 | #### `ExtendsError` 210 | 211 | Used when an `extends` clause points at a non-existent file. The config file 212 | that contains the clause is available through the `source` property. The clause 213 | itself is available through the `clause` property. Has a `parent` property that 214 | contains a `NoSourceFile` error. 215 | 216 | #### `InvalidFileError` 217 | 218 | Used when a config file is invalid. The file path is available through the 219 | `source` property. 220 | 221 | #### `MultipleSourcesError` 222 | 223 | Used when multiple configuration files are found. File paths are available 224 | through the `source` and `otherSource` properties. 225 | 226 | #### `NoSourceFileError` 227 | 228 | Used when a file does not exist. The file path is available through the `source` 229 | property. 230 | 231 | #### `ParseError` 232 | 233 | Used when a config file cannot be parsed (this is different from it being 234 | invalid). The file path is available through the `source` property. The parsing 235 | error is available through the `parent` property. 236 | 237 | [AVA]: https://ava.li/ 238 | [Babel]: https://babeljs.io/ 239 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | build: off 2 | cache: 3 | - '%APPDATA%\npm-cache' 4 | clone_depth: 1 5 | skip_branch_with_pr: true 6 | skip_commits: 7 | files: 8 | - '**/*.md' 9 | configuration: 10 | - FreshDeps 11 | - LockedDeps 12 | environment: 13 | matrix: 14 | - nodejs_version: '10' 15 | - nodejs_version: '9' 16 | - nodejs_version: '8' 17 | - nodejs_version: '6' 18 | matrix: 19 | fast_finish: true 20 | exclude: 21 | - configuration: FreshDeps 22 | nodejs_version: '9' 23 | - configuration: FreshDeps 24 | nodejs_version: '8' 25 | - configuration: FreshDeps 26 | nodejs_version: '6' 27 | install: 28 | - ps: Install-Product node $env:nodejs_version 29 | - npm install --global npm@6.0.0 30 | - npm --version 31 | - if %configuration% == FreshDeps (npm install --no-shrinkwrap --prefer-online) 32 | - if %configuration% == LockedDeps (npm ci) 33 | test_script: 34 | - npm test 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hullabaloo-config-manager", 3 | "version": "2.0.0-beta.4", 4 | "description": "Manages complex Babel config chains, avoiding duplicated work and enabling effective caching", 5 | "main": "build/main.js", 6 | "files": [ 7 | "build", 8 | "src", 9 | "typings", 10 | "tsconfig.json" 11 | ], 12 | "typings": "./build/main.d.ts", 13 | "engines": { 14 | "node": ">=6.12.3 <7 || >=8.9.4 <9 || >=9.11.1 <10 || >=10.0.0" 15 | }, 16 | "scripts": { 17 | "build": "tsc", 18 | "build:watch": "tsc --watch", 19 | "prebuild": "rimraf ./build", 20 | "prepare": "npm run build", 21 | "lint": "as-i-preach", 22 | "test": "ava", 23 | "posttest": "as-i-preach", 24 | "coverage": "nyc npm test" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/novemberborn/hullabaloo-config-manager.git" 29 | }, 30 | "keywords": [ 31 | "babel" 32 | ], 33 | "author": "Mark Wubben (https://novemberborn.net/)", 34 | "license": "MIT", 35 | "bugs": { 36 | "url": "https://github.com/novemberborn/hullabaloo-config-manager/issues" 37 | }, 38 | "homepage": "https://github.com/novemberborn/hullabaloo-config-manager#readme", 39 | "dependencies": { 40 | "dot-prop": "^4.2.0", 41 | "graceful-fs": "^4.1.11", 42 | "indent-string": "^3.2.0", 43 | "json5": "^1.0.1", 44 | "lodash.isequal": "^4.5.0", 45 | "lodash.merge": "^4.6.1", 46 | "md5-hex": "^2.0.0", 47 | "package-hash": "^2.0.0", 48 | "pirates": "^3.0.2", 49 | "pkg-dir": "^2.0.0", 50 | "resolve-from": "^4.0.0" 51 | }, 52 | "devDependencies": { 53 | "@babel/cli": "7.0.0-beta.44", 54 | "@babel/core": "7.0.0-beta.44", 55 | "@novemberborn/as-i-preach": "^11.0.0", 56 | "@types/dot-prop": "^4.2.0", 57 | "@types/graceful-fs": "^4.1.2", 58 | "@types/indent-string": "^3.0.0", 59 | "@types/node": "^10.0.4", 60 | "ava": "1.0.0-beta.4", 61 | "codecov": "^3.0.1", 62 | "fs-extra": "^6.0.0", 63 | "mock-require": "^3.0.2", 64 | "nyc": "^11.7.1", 65 | "proxyquire": "^2.0.1", 66 | "replace-string": "^1.1.0", 67 | "testdouble": "^3.8.1", 68 | "typescript": "^2.8.3", 69 | "unique-temp-dir": "^1.0.0" 70 | }, 71 | "ava": { 72 | "sources": [ 73 | "build/**/*.js" 74 | ] 75 | }, 76 | "nyc": { 77 | "reporter": [ 78 | "html", 79 | "lcov", 80 | "text" 81 | ] 82 | }, 83 | "standard-engine": "@novemberborn/as-i-preach" 84 | } 85 | -------------------------------------------------------------------------------- /src/BabelOptions.d.ts: -------------------------------------------------------------------------------- 1 | declare type PluginOrPresetOptions = object | void | false 2 | declare type PluginOrPresetTarget = string | object | Function 3 | declare type PluginOrPresetItem = PluginOrPresetTarget 4 | | [PluginOrPresetTarget] 5 | | [PluginOrPresetTarget, PluginOrPresetOptions] 6 | | [PluginOrPresetTarget, PluginOrPresetOptions, string] 7 | declare type PluginOrPresetList = Array 8 | export {PluginOrPresetOptions, PluginOrPresetTarget, PluginOrPresetItem, PluginOrPresetList} 9 | 10 | declare interface LimitedOptions { 11 | babelrc?: boolean 12 | env?: {[name: string]: LimitedOptions} 13 | extends?: string 14 | plugins?: PluginOrPresetList 15 | presets?: PluginOrPresetList 16 | } 17 | export {LimitedOptions} 18 | 19 | // Based on 20 | /* eslint-disable typescript/member-ordering */ 21 | declare interface BabelOptions extends LimitedOptions { 22 | cwd?: string 23 | filename?: string 24 | filenameRelative?: string 25 | code?: boolean 26 | ast?: boolean 27 | inputSourceMap?: object | boolean 28 | envName?: string 29 | 30 | test?: string | Function | RegExp 31 | include?: string | Function | RegExp 32 | exclude?: string | Function | RegExp 33 | ignore?: Array 34 | only?: Array 35 | overrides?: Array 36 | 37 | passPerPreset?: boolean 38 | 39 | // Options for @babel/generator 40 | retainLines?: boolean 41 | comments?: boolean 42 | shouldPrintComment?(comment: string): boolean 43 | compact?: boolean | 'auto' 44 | minified?: boolean 45 | auxiliaryCommentBefore?: string 46 | auxiliaryCommentAfter?: string 47 | 48 | // Parser 49 | sourceType?: 'module' | 'script' | 'unambiguous' 50 | 51 | wrapPluginVisitorMethod?(pluginAlias: string, visitorType: 'enter' | 'exit', callback: (path: object, state: any) => void): (path: object, state: any) => void // eslint-disable-line max-len 52 | highlightCode?: boolean 53 | 54 | // Sourcemap generation options. 55 | sourceMaps?: boolean | 'inline' | 'both' 56 | sourceMap?: boolean | 'inline' | 'both' 57 | sourceFileName?: string 58 | sourceRoot?: string 59 | 60 | // AMD/UMD/SystemJS module naming options. 61 | getModuleId?(moduleName: string): string 62 | moduleRoot?: string 63 | moduleIds?: boolean 64 | moduleId?: string 65 | 66 | // Deprecate top level parserOpts 67 | parserOpts?: object 68 | // Deprecate top level generatorOpts 69 | generatorOpts?: object 70 | } 71 | export default BabelOptions 72 | -------------------------------------------------------------------------------- /src/Cache.ts: -------------------------------------------------------------------------------- 1 | import BabelOptions, {PluginOrPresetTarget} from './BabelOptions' 2 | 3 | export interface ModuleSource { 4 | options: BabelOptions 5 | runtimeDependencies: Map 6 | runtimeHash: string | null 7 | unrestricted: boolean 8 | } 9 | export interface UnrestrictedModuleSource extends ModuleSource { 10 | unrestricted: true 11 | } 12 | export interface EnvModuleSource { 13 | byEnv: Map 14 | factory (envName: string): ModuleSource 15 | } 16 | export type ModuleSourcesMap = Map 17 | 18 | export type NameMap = Map 19 | 20 | export type PluginsAndPresetsMapValue = Map 21 | export type PluginsAndPresetsMap = Map 22 | 23 | export default interface Cache { 24 | dependencyHashes: Map> 25 | fileExistence: Map> 26 | files: Map> 27 | moduleSources: ModuleSourcesMap 28 | nameMap: NameMap 29 | pluginsAndPresets: PluginsAndPresetsMap 30 | sourceHashes: Map> 31 | } 32 | 33 | export function prepare (): Cache { 34 | return { 35 | dependencyHashes: new Map(), 36 | fileExistence: new Map(), 37 | files: new Map(), 38 | moduleSources: new Map(), 39 | nameMap: new Map(), 40 | pluginsAndPresets: new Map(), 41 | sourceHashes: new Map() 42 | } 43 | } 44 | 45 | export function isUnrestrictedModuleSource ( 46 | value: UnrestrictedModuleSource | EnvModuleSource 47 | ): value is UnrestrictedModuleSource { 48 | return 'unrestricted' in value 49 | } 50 | -------------------------------------------------------------------------------- /src/ResolvedConfig.ts: -------------------------------------------------------------------------------- 1 | import Cache from './Cache' 2 | import codegen from './codegen' 3 | import {Chains} from './collector' 4 | import reduceChains, {ConfigList, Dependency, Source} from './reduceChains' 5 | import Verifier from './Verifier' 6 | 7 | export default class ResolvedConfig { 8 | public readonly cache?: Cache 9 | public readonly babelrcDir?: string 10 | public readonly dependencies: Dependency[] 11 | public readonly envNames: Set 12 | public readonly fixedSourceHashes: Map 13 | public readonly sources: Source[] 14 | public readonly unflattenedDefaultOptions: ConfigList 15 | public readonly unflattenedEnvOptions: Map 16 | 17 | public constructor (chains: Chains, cache?: Cache) { 18 | this.cache = cache 19 | this.babelrcDir = chains.babelrcDir 20 | 21 | const reduced = reduceChains(chains, cache) 22 | this.dependencies = reduced.dependencies 23 | this.envNames = reduced.envNames 24 | this.fixedSourceHashes = reduced.fixedSourceHashes 25 | this.sources = reduced.sources 26 | this.unflattenedDefaultOptions = reduced.unflattenedDefaultOptions 27 | this.unflattenedEnvOptions = reduced.unflattenedEnvOptions 28 | } 29 | 30 | public createVerifier () { 31 | return Verifier.hashAndCreate( 32 | this.babelrcDir, 33 | this.envNames, 34 | this.dependencies, 35 | this.sources, 36 | this.fixedSourceHashes, 37 | this.cache) 38 | } 39 | 40 | public generateModule () { 41 | return codegen(this) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/SimulatedBabelCache.ts: -------------------------------------------------------------------------------- 1 | import md5Hex = require('md5-hex') 2 | 3 | // Babel compares cache keys using strict equality. Hullabaloo needs to compare 4 | // keys across process restarts, which require the keys to be included in the 5 | // source hash. This function returns string values for all keys. Complex 6 | // values, NaN and symbols are represented by a timestamp and an offset. This 7 | // makes it unlikely they'll compare when verifying the resulting source hash 8 | // in the future, which helps avoid falsely positive hash matches. 9 | 10 | let offset = 0 11 | 12 | // TODO: Consider fingerprinting well-known and registered symbols. 13 | function fingerprint (key: any): string { 14 | if (key === null) return 'null' 15 | if (typeof key === 'boolean') return String(key) 16 | if (typeof key === 'undefined') return 'undefined' 17 | if (typeof key === 'string') return JSON.stringify(key) 18 | if (typeof key === 'number') { 19 | if (key === Infinity) return 'Infinity' 20 | if (key === -Infinity) return '-Infinity' 21 | if (!isNaN(key)) return JSON.stringify(key) 22 | } 23 | 24 | offset = (offset + 1) % Number.MAX_SAFE_INTEGER 25 | return `${Date.now()}.${offset}` 26 | } 27 | 28 | export interface HandlerFn { 29 | (data: Data): T 30 | } 31 | 32 | export interface Api { 33 | (val: boolean): void 34 | (val: HandlerFn): T 35 | 36 | forever: () => void 37 | never: () => void 38 | using (handler: HandlerFn): T 39 | invalidate (handler: HandlerFn): T 40 | } 41 | 42 | enum Configuration {Forever, Never, Using} 43 | 44 | export default class SimulatedBabelCache { 45 | public api: Api 46 | 47 | private computedHash?: {value: string | null} 48 | private configuration: Configuration | null 49 | private keys: any[] 50 | private sealed: boolean 51 | 52 | public constructor (data: Data) { 53 | this.configuration = null 54 | this.keys = [] 55 | this.sealed = false 56 | 57 | const assertNotSealed = () => { 58 | if (this.sealed) throw new Error('Cannot change caching after evaluation has completed') 59 | } 60 | const assertNotForever = () => { 61 | if (this.configuration === Configuration.Forever) throw new Error('Caching has already been configured with .forever()') 62 | } 63 | const assertNotNever = () => { 64 | if (this.configuration === Configuration.Never) throw new Error('Caching has already been configured with .never()') 65 | } 66 | 67 | const forever = () => { 68 | assertNotSealed() 69 | assertNotNever() 70 | this.configuration = Configuration.Forever 71 | } 72 | const never = () => { 73 | assertNotSealed() 74 | assertNotForever() 75 | this.configuration = Configuration.Never 76 | } 77 | const using = (handler: HandlerFn): T => { 78 | assertNotSealed() 79 | assertNotForever() 80 | assertNotNever() 81 | this.configuration = Configuration.Using 82 | const key = handler(data) 83 | this.keys.push(key) 84 | return key 85 | } 86 | 87 | this.api = Object.assign((val: boolean | HandlerFn): void | any => { 88 | assertNotSealed() 89 | if (val === true) return forever() 90 | if (val === false) return never() 91 | return using(val) 92 | }, { 93 | forever, 94 | invalidate: using, 95 | never, 96 | using 97 | }) 98 | } 99 | 100 | public get wasConfigured () { 101 | return this.configuration !== null 102 | } 103 | 104 | public get never () { 105 | return this.configuration === Configuration.Never 106 | } 107 | 108 | public hash (): string | null { 109 | if (!this.sealed) throw new Error('seal() must be called before invoking hash()') 110 | if (this.computedHash) return this.computedHash.value 111 | 112 | let value: string | null = null 113 | // Return a unique hash to ensure the resulting source hash won't match when 114 | // verified. 115 | if (this.never) { 116 | value = md5Hex(fingerprint({})) 117 | } else if (this.keys.length > 0) { 118 | value = md5Hex(this.keys.map(key => fingerprint(key))) 119 | } 120 | // The value remains null if there were no keys, which indicates that the 121 | // cache was never used, or forever() was called. 122 | 123 | this.computedHash = {value} 124 | return value 125 | } 126 | 127 | public seal () { 128 | this.sealed = true 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Verifier.ts: -------------------------------------------------------------------------------- 1 | import fs = require('fs') 2 | import path = require('path') 3 | 4 | import isEqual = require('lodash.isequal') 5 | import md5Hex = require('md5-hex') 6 | 7 | import Cache from './Cache' 8 | import currentEnv from './currentEnv' 9 | import hashDependencies from './hashDependencies' 10 | import hashSources from './hashSources' 11 | import {Dependency, Source} from './reduceChains' 12 | 13 | function ensureMissingBabelrcFile (file: string, cache?: Cache): Promise { 14 | if (cache && cache.fileExistence.has(file)) { 15 | return cache.fileExistence.get(file)! 16 | } 17 | 18 | const promise = new Promise((resolve, reject) => { 19 | fs.access(file, err => { 20 | if (err) { 21 | if (err.code !== 'ENOENT') { 22 | reject(err) 23 | } else { 24 | resolve(true) 25 | } 26 | } else { 27 | resolve(false) 28 | } 29 | }) 30 | }) 31 | 32 | if (cache) { 33 | cache.fileExistence.set(file, promise) 34 | } 35 | return promise 36 | } 37 | 38 | async function checkConfigFiles (babelrcDir: undefined | string, sourcesToHash: Source[], cache?: Cache): Promise { 39 | if (typeof babelrcDir === 'undefined') return true 40 | 41 | const checks: Promise[] = [] 42 | 43 | const babelrcFile = path.join(babelrcDir, '.babelrc') 44 | if (!sourcesToHash.some(item => item.source === babelrcFile)) { 45 | checks.push(ensureMissingBabelrcFile(babelrcFile, cache)) 46 | } 47 | const babelrcJsFile = path.join(babelrcDir, '.babelrc.js') 48 | if (!sourcesToHash.some(item => item.source === babelrcJsFile)) { 49 | checks.push(ensureMissingBabelrcFile(babelrcJsFile, cache)) 50 | } 51 | 52 | const results = await Promise.all(checks) 53 | return results.every(missing => missing) 54 | } 55 | 56 | export type VerificationResult = {sourcesChanged: true} | {missingSource: true} | {badDependency: true} | { 57 | sourcesChanged: false 58 | dependenciesChanged: boolean 59 | cacheKeys: { 60 | dependencies: string 61 | sources: string 62 | } 63 | verifier: Verifier // eslint-disable-line typescript/no-use-before-define 64 | } 65 | 66 | export default class Verifier { 67 | public readonly babelrcDir?: string 68 | public readonly dependencies: Dependency[] 69 | public readonly envNames: Set 70 | public readonly sources: Source[] 71 | 72 | private constructor (babelrcDir: string | undefined, envNames: Set, dependencies: Dependency[], sources: Source[]) { 73 | this.babelrcDir = babelrcDir 74 | this.envNames = envNames 75 | this.dependencies = dependencies 76 | this.sources = sources 77 | } 78 | 79 | public static fromBuffer (buffer: Buffer): Verifier { 80 | const json = JSON.parse(buffer.toString('utf8'), (key, value) => { 81 | return key === 'envNames' || key === 'envs' 82 | ? new Set(value) 83 | : value 84 | }) 85 | return new Verifier(json.babelrcDir, json.envNames, json.dependencies, json.sources) 86 | } 87 | 88 | public static async hashAndCreate ( 89 | babelrcDir: string | undefined, 90 | envNames: Set, 91 | dependencies: Dependency[], 92 | sources: Source[], 93 | fixedSourceHashes: Map, 94 | cache?: Cache 95 | ): Promise { 96 | const results = await Promise.all([ 97 | hashDependencies(dependencies, cache), 98 | hashSources(sources, fixedSourceHashes, cache) 99 | ]) 100 | 101 | const dependencyHashes = results[0] 102 | dependencies.forEach((item, index) => { 103 | item.hash = dependencyHashes[index] 104 | }) 105 | 106 | const sourceHashes = results[1] 107 | sources.forEach((item, index) => { 108 | item.hash = sourceHashes[index] 109 | }) 110 | 111 | return new Verifier(babelrcDir, envNames, dependencies, sources) 112 | } 113 | 114 | public cacheKeysForEnv (envName?: string): {dependencies: string; sources: string} { // eslint-disable-line typescript/member-delimiter-style 115 | if (typeof envName !== 'string') envName = currentEnv() 116 | 117 | const dependencyHashes = this.selectByEnv(this.dependencies, envName).map(item => item.hash!) 118 | const sourceHashes = this.selectByEnv(this.sources, envName).map(item => item.hash!) 119 | 120 | return { 121 | dependencies: md5Hex(dependencyHashes), 122 | sources: md5Hex(sourceHashes) 123 | } 124 | } 125 | 126 | public async verifyEnv ( 127 | envName?: string | null, 128 | fixedHashes?: {sources?: Map}, 129 | cache?: Cache 130 | ): Promise { 131 | if (typeof envName !== 'string') envName = currentEnv() 132 | 133 | const sourcesToHash = this.selectByEnv(this.sources, envName) 134 | const expectedSourceHashes = sourcesToHash.map(item => item.hash) 135 | const pendingSourceHashes = hashSources(sourcesToHash, fixedHashes && fixedHashes.sources, cache) 136 | 137 | const checkedConfigFiles = checkConfigFiles(this.babelrcDir, sourcesToHash, cache) 138 | 139 | const dependenciesToHash = this.selectByEnv(this.dependencies, envName) 140 | const expectedDependencyHashes = dependenciesToHash.map(item => item.hash) 141 | const pendingDependencyHashes = hashDependencies(dependenciesToHash, cache) 142 | 143 | try { 144 | const results = await Promise.all([ 145 | pendingSourceHashes, 146 | checkedConfigFiles 147 | ]) 148 | const sourceHashes = results[0] 149 | const configFilesAreSame = results[1] 150 | 151 | if (!configFilesAreSame || !isEqual(sourceHashes, expectedSourceHashes)) { 152 | return {sourcesChanged: true} 153 | } 154 | 155 | const dependencyHashes = await pendingDependencyHashes 156 | const dependenciesChanged = !isEqual(dependencyHashes, expectedDependencyHashes) 157 | 158 | let verifier: Verifier = this 159 | if (dependenciesChanged) { 160 | const dependencies = this.dependencies.map(item => { 161 | const rehashedIndex = dependenciesToHash.indexOf(item) 162 | if (rehashedIndex === -1) return {...item} 163 | 164 | const hash = dependencyHashes[rehashedIndex] 165 | return {...item, hash} 166 | }) 167 | 168 | verifier = new Verifier(this.babelrcDir, this.envNames, dependencies, this.sources) 169 | } 170 | 171 | return { 172 | sourcesChanged: false, 173 | dependenciesChanged, 174 | cacheKeys: { 175 | dependencies: md5Hex(dependencyHashes), 176 | sources: md5Hex(sourceHashes) 177 | }, 178 | verifier 179 | } 180 | } catch (err) { 181 | if (err.name === 'NoSourceFileError') { 182 | return {missingSource: true} 183 | } 184 | 185 | if (err.name === 'BadDependencyError') { 186 | return {badDependency: true} 187 | } 188 | 189 | throw err 190 | } 191 | } 192 | 193 | public toBuffer (): Buffer { 194 | return Buffer.from(JSON.stringify({ 195 | babelrcDir: this.babelrcDir, 196 | envNames: this.envNames, 197 | dependencies: this.dependencies, 198 | sources: this.sources 199 | }, (key, value) => { 200 | return key === 'envNames' || key === 'envs' 201 | ? Array.from(value) 202 | : value 203 | }, 2)) 204 | } 205 | 206 | private selectByEnv (arr: Item[], envName: string): Item[] { 207 | const selectDefault = !this.envNames.has(envName) 208 | return arr.filter(item => selectDefault ? item.default : item.envs.has(envName)) 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/cloneOptions.ts: -------------------------------------------------------------------------------- 1 | import BabelOptions from './BabelOptions' 2 | 3 | export default function cloneOptions (options: BabelOptions): BabelOptions { 4 | const shallow = {...options} 5 | 6 | if (options.env && typeof options.env === 'object') { 7 | shallow.env = {} 8 | for (const envName of Object.keys(options.env)) { 9 | shallow.env[envName] = cloneOptions(options.env[envName]) 10 | } 11 | } 12 | 13 | if (shallow.overrides && Array.isArray(shallow.overrides)) { 14 | shallow.overrides = shallow.overrides.map(override => cloneOptions(override)) 15 | } 16 | 17 | if (shallow.inputSourceMap && typeof shallow.inputSourceMap === 'object') { 18 | shallow.inputSourceMap = {...shallow.inputSourceMap} 19 | } 20 | if (shallow.generatorOpts && typeof shallow.generatorOpts === 'object') { 21 | shallow.generatorOpts = {...shallow.generatorOpts} 22 | } 23 | if (shallow.parserOpts && typeof shallow.parserOpts === 'object') { 24 | shallow.parserOpts = {...shallow.parserOpts} 25 | } 26 | 27 | if (Array.isArray(shallow.ignore)) { 28 | shallow.ignore = shallow.ignore.slice() 29 | } 30 | if (Array.isArray(shallow.only)) { 31 | shallow.only = shallow.only.slice() 32 | } 33 | 34 | if (Array.isArray(shallow.plugins)) { 35 | shallow.plugins = shallow.plugins.map(plugin => Array.isArray(plugin) ? plugin.slice() : plugin) 36 | } 37 | if (Array.isArray(shallow.presets)) { 38 | shallow.presets = shallow.presets.map(preset => Array.isArray(preset) ? preset.slice() : preset) 39 | } 40 | 41 | return shallow 42 | } 43 | -------------------------------------------------------------------------------- /src/codegen.ts: -------------------------------------------------------------------------------- 1 | import indentString = require('indent-string') 2 | import json5 = require('json5') 3 | 4 | import {FileType} from './collector' 5 | import {ConfigList, isModuleConfig, MergedConfig, ModuleConfig} from './reduceChains' 6 | import ResolvedConfig from './ResolvedConfig' 7 | 8 | function stringify (asJson5: boolean, value: object): string { 9 | return asJson5 10 | ? json5.stringify(value, undefined, 2) 11 | : JSON.stringify(value, undefined, 2) 12 | } 13 | 14 | function printConfig (config: MergedConfig | ModuleConfig) { 15 | if (!isModuleConfig(config)) return stringify(config.fileType === FileType.JSON5, config.options) 16 | 17 | return `helpers.loadCachedModule(cache, ${JSON.stringify(config.dir)}, ${JSON.stringify(config.source)}, envName, \ 18 | ${JSON.stringify(config.envName !== null)}, ${JSON.stringify(config.overrideIndex) || 'undefined'})` 19 | } 20 | 21 | function generateFactory (unflattened: ConfigList): string { 22 | return `(envName, cache) => { 23 | const wrapperFns = new Map() 24 | return Object.assign(helpers.mergeOptions([ 25 | ${unflattened.map(item => indentString(printConfig(item), 4)).join(',\n')} 26 | ], wrapperFns), { 27 | babelrc: false, 28 | envName, 29 | overrides: [ 30 | ${unflattened.overrides.map(items => ` helpers.mergeOptions([ 31 | ${items.map(item => indentString(printConfig(item), 8)).join(',\n')} 32 | ], wrapperFns)`).join(',\n')} 33 | ] 34 | }) 35 | }` 36 | } 37 | 38 | export default function codegen (resolvedConfig: ResolvedConfig): string { 39 | let code = `"use strict" 40 | 41 | const process = require("process") 42 | const helpers = require(${JSON.stringify(require.resolve('./helpers'))}) 43 | 44 | const defaultOptions = ${generateFactory(resolvedConfig.unflattenedDefaultOptions)} 45 | 46 | const envOptions = Object.create(null)\n` 47 | for (const envName of resolvedConfig.envNames) { 48 | const unflattened = resolvedConfig.unflattenedEnvOptions.get(envName)! 49 | code += `\nenvOptions[${JSON.stringify(envName)}] = ${generateFactory(unflattened)}\n` 50 | } 51 | 52 | return `${code} 53 | exports.getOptions = (envName, cache) => { 54 | if (typeof envName !== "string") { 55 | envName = process.env.BABEL_ENV || process.env.NODE_ENV || "development" 56 | } 57 | return envName in envOptions 58 | ? envOptions[envName](envName, cache) 59 | : defaultOptions(envName, cache) 60 | }\n` 61 | } 62 | -------------------------------------------------------------------------------- /src/currentEnv.ts: -------------------------------------------------------------------------------- 1 | import process = require('process') 2 | 3 | export const DEFAULT_ENV = 'development' 4 | 5 | const env = process.env 6 | export default function currentEnv () { 7 | return env.BABEL_ENV || env.NODE_ENV || DEFAULT_ENV 8 | } 9 | -------------------------------------------------------------------------------- /src/errors.ts: -------------------------------------------------------------------------------- 1 | import {Kind} from './resolvePluginsAndPresets' 2 | 3 | export class SourceError extends Error { 4 | public readonly source: string 5 | public readonly parent: Error | null 6 | 7 | public constructor (message: string, source: string, parent?: Error) { 8 | super(`${source}: ${message}`) 9 | this.name = 'SourceError' 10 | this.source = source 11 | this.parent = parent || null 12 | } 13 | } 14 | 15 | export class NoSourceFileError extends SourceError { 16 | public constructor (source: string) { 17 | super('No such file', source) 18 | this.name = 'NoSourceFileError' 19 | } 20 | } 21 | 22 | export class ParseError extends SourceError { 23 | public constructor (source: string, parent: Error) { 24 | super(`Error while parsing — ${parent.message}`, source, parent) 25 | this.name = 'ParseError' 26 | } 27 | } 28 | 29 | export class InvalidFileError extends SourceError { 30 | public constructor (source: string, message: string) { 31 | super(message, source) 32 | this.name = 'InvalidFileError' 33 | } 34 | } 35 | 36 | export class ExtendsError extends SourceError { 37 | public readonly clause: string 38 | 39 | public constructor (source: string, clause: string, parent: Error) { 40 | super(`Couldn't resolve extends clause: ${clause}`, source, parent) 41 | this.name = 'ExtendsError' 42 | this.clause = clause 43 | } 44 | } 45 | 46 | export class BadDependencyError extends SourceError { 47 | public constructor (source: string, parent?: Error) { 48 | super("Couldn't resolve dependency", source, parent) 49 | this.name = 'BadDependencyError' 50 | } 51 | } 52 | 53 | export class MultipleSourcesError extends SourceError { 54 | public readonly otherSource: string 55 | 56 | public constructor (source: string, otherSource: string) { 57 | super('Multiple configuration files found', source) 58 | this.name = 'MultipleSourcesError' 59 | this.otherSource = otherSource 60 | } 61 | } 62 | 63 | export class ResolveError extends SourceError { 64 | public readonly source: string 65 | public readonly ref: string 66 | public readonly isPlugin: boolean 67 | public readonly isPreset: boolean 68 | 69 | public constructor (source: string, kind: Kind, ref: string, message?: string) { 70 | super(message || `Couldn't find ${kind} ${JSON.stringify(ref)} relative to directory`, source) 71 | this.name = 'ResolveError' 72 | this.ref = ref 73 | this.isPlugin = kind === 'plugin' 74 | this.isPreset = kind === 'preset' 75 | } 76 | } 77 | 78 | export class ResolveFromCacheError extends ResolveError { 79 | public constructor (source: string, kind: Kind, ref: string) { 80 | super(source, kind, ref, `Couldn't find ${kind} ${JSON.stringify(ref)} in cache`) 81 | this.name = 'ResolveFromCacheError' 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/getPluginOrPresetName.ts: -------------------------------------------------------------------------------- 1 | import {PluginOrPresetTarget} from './BabelOptions' 2 | import {NameMap} from './Cache' 3 | import loadPluginOrPreset from './loadPluginOrPreset' 4 | 5 | export default function getPluginOrPresetName (nameMap: NameMap, target: PluginOrPresetTarget): string { 6 | if (nameMap.has(target)) return nameMap.get(target)! 7 | 8 | if (typeof target === 'string') { 9 | const resolved = loadPluginOrPreset(target) 10 | let name 11 | if (nameMap.has(resolved)) { 12 | name = nameMap.get(resolved)! 13 | } else { 14 | name = `🤡🎪🎟.${nameMap.size}` 15 | nameMap.set(resolved, name) 16 | } 17 | nameMap.set(target, name) 18 | return name 19 | } 20 | 21 | const name = `🤡🎪🎟.${nameMap.size}` 22 | nameMap.set(target, name) 23 | return name 24 | } 25 | -------------------------------------------------------------------------------- /src/hashDependencies.ts: -------------------------------------------------------------------------------- 1 | import packageHash = require('package-hash') 2 | import md5Hex = require('md5-hex') 3 | 4 | import Cache from './Cache' 5 | import {BadDependencyError} from './errors' 6 | import readSafe from './readSafe' 7 | import {Dependency} from './reduceChains' 8 | 9 | async function hashFile (filename: string, cache?: Cache): Promise { 10 | const contents = await readSafe(filename, cache) 11 | if (!contents) throw new BadDependencyError(filename) 12 | 13 | return md5Hex(contents) 14 | } 15 | 16 | async function hashPackage (filename: string, fromPackage: string): Promise { 17 | try { 18 | return await packageHash(`${fromPackage}/package.json`) 19 | } catch (err) { 20 | throw new BadDependencyError(filename, err) 21 | } 22 | } 23 | 24 | function hashDependency (filename: string, fromPackage: string | null, cache?: Cache): Promise { 25 | if (cache && cache.dependencyHashes.has(filename)) { 26 | return cache.dependencyHashes.get(filename)! 27 | } 28 | 29 | const promise = fromPackage 30 | ? hashPackage(filename, fromPackage) 31 | : hashFile(filename, cache) 32 | 33 | if (cache) { 34 | cache.dependencyHashes.set(filename, promise) 35 | } 36 | return promise 37 | } 38 | 39 | export default function hashDependencies (dependencies: Dependency[], cache?: Cache): Promise { 40 | const promises = dependencies.map(item => hashDependency(item.filename, item.fromPackage, cache)) 41 | return Promise.all(promises) 42 | } 43 | -------------------------------------------------------------------------------- /src/hashSources.ts: -------------------------------------------------------------------------------- 1 | import path = require('path') 2 | 3 | import dotProp = require('dot-prop') 4 | import md5Hex = require('md5-hex') 5 | 6 | import Cache from './Cache' 7 | import {NoSourceFileError} from './errors' 8 | import readSafe from './readSafe' 9 | import {Source} from './reduceChains' 10 | 11 | function hashSource (source: string, runtimeHash: string | null, cache?: Cache): Promise { 12 | if (cache && cache.sourceHashes.has(source)) { 13 | return cache.sourceHashes.get(source)! 14 | } 15 | 16 | const basename = path.basename(source) 17 | const parts = basename.split('#') 18 | const filename = parts[0] 19 | const filepath = path.join(path.dirname(source), filename) 20 | 21 | const pkgAccessor = filename === 'package.json' 22 | ? parts[1] || 'babel' 23 | : null 24 | 25 | const promise = readSafe(filepath, cache) 26 | .then(contents => { 27 | if (!contents) throw new NoSourceFileError(source) 28 | 29 | const inputs: Array = runtimeHash === null ? [] : [runtimeHash] 30 | if (!pkgAccessor) { 31 | inputs.push(contents) 32 | } else { 33 | const json = JSON.parse(contents.toString('utf8')) 34 | const value = dotProp.get(json, pkgAccessor) || {} 35 | inputs.push(JSON.stringify(value)) 36 | } 37 | 38 | return md5Hex(inputs) 39 | }) 40 | 41 | if (cache) { 42 | cache.sourceHashes.set(source, promise) 43 | } 44 | return promise 45 | } 46 | 47 | export default function hashSources (sources: Source[], fixedHashes?: Map, cache?: Cache): Promise { 48 | const promises = sources.map(item => { 49 | return fixedHashes && fixedHashes.has(item.source) 50 | ? fixedHashes.get(item.source)! 51 | : hashSource(item.source, item.runtimeHash, cache) 52 | }) 53 | return Promise.all(promises) 54 | } 55 | -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | import merge = require('lodash.merge') 2 | import resolveFrom = require('resolve-from') 3 | 4 | import Cache, {isUnrestrictedModuleSource, NameMap, PluginsAndPresetsMapValue} from './Cache' 5 | import BabelOptions, {PluginOrPresetItem, PluginOrPresetList, PluginOrPresetOptions, PluginOrPresetTarget} from './BabelOptions' 6 | import cloneOptions from './cloneOptions' 7 | import {ResolveFromCacheError} from './errors' 8 | import { 9 | isFileDescriptor, 10 | PluginOrPresetFileDescriptor, 11 | PluginOrPresetDescriptor, 12 | PluginOrPresetDescriptorList, 13 | ReducedBabelOptions 14 | } from './reduceChains' 15 | import getPluginOrPresetName from './getPluginOrPresetName' 16 | import loadPluginOrPreset from './loadPluginOrPreset' 17 | import mergePluginsOrPresets from './mergePluginsOrPresets' 18 | import normalizeSomeOptions from './normalizeOptions' 19 | import standardizeName from './standardizeName' 20 | import {Kind, PresetObject} from './resolvePluginsAndPresets' 21 | 22 | // Called for plugins and presets found when loading cached configuration modules. 23 | function resolvePluginOrPreset (source: string, resolutionCache: PluginsAndPresetsMapValue, kind: Kind, ref: string): string { 24 | const name = standardizeName(kind, ref).name 25 | if (!resolutionCache.has(name)) throw new ResolveFromCacheError(source, kind, ref) 26 | return resolutionCache.get(name)! 27 | } 28 | 29 | // Called for plugins and presets found at runtime in preset objects. 30 | function resolveDynamicPluginOrPreset (kind: Kind, dirname: string, ref: string) { 31 | const name = standardizeName(kind, ref).name 32 | const filename = resolveFrom(dirname, name) 33 | return loadPluginOrPreset(filename) 34 | } 35 | 36 | interface DescribedPresetObject extends PresetObject { 37 | plugins?: PluginOrPresetDescriptorList 38 | presets?: PluginOrPresetDescriptorList 39 | } 40 | 41 | function describePluginOrPreset ( 42 | dirname: string, 43 | source: string, 44 | resolutionCache: PluginsAndPresetsMapValue, 45 | nameMap: NameMap, 46 | item: PluginOrPresetItem, 47 | kind: Kind 48 | ): PluginOrPresetFileDescriptor | PluginOrPresetDescriptor { 49 | if (Array.isArray(item)) { 50 | const target = item[0] 51 | if (typeof target !== 'string') { 52 | const name = getPluginOrPresetName(nameMap, target) 53 | switch (item.length) { 54 | case 1: return {dirname, target, name} 55 | case 2: return {dirname, target, options: item[1] as PluginOrPresetOptions, name} 56 | default: return {dirname, target, options: item[1] as PluginOrPresetOptions, name: `${name}.${item[2]}`} 57 | } 58 | } 59 | 60 | const filename = resolvePluginOrPreset(source, resolutionCache, kind, target) 61 | const name = getPluginOrPresetName(nameMap, filename) 62 | switch (item.length) { 63 | case 1: return {dirname, filename, name} 64 | case 2: return {dirname, filename, options: item[1] as PluginOrPresetOptions, name} 65 | default: return {dirname, filename, options: item[1] as PluginOrPresetOptions, name: `${name}.${item[2]}`} 66 | } 67 | } 68 | 69 | if (typeof item === 'string') { 70 | const filename = resolvePluginOrPreset(source, resolutionCache, kind, item) 71 | return {dirname, filename, name: getPluginOrPresetName(nameMap, filename)} 72 | } 73 | 74 | return {dirname, target: item, name: getPluginOrPresetName(nameMap, item)} 75 | } 76 | 77 | // Rewrite plugins from cached configuration modules to match those from regular 78 | // configuration files. 79 | function describePlugin ( 80 | dirname: string, 81 | source: string, 82 | resolutionCache: PluginsAndPresetsMapValue, 83 | nameMap: NameMap, 84 | item: PluginOrPresetItem 85 | ): PluginOrPresetFileDescriptor | PluginOrPresetDescriptor { 86 | return describePluginOrPreset(dirname, source, resolutionCache, nameMap, item, Kind.PLUGIN) 87 | } 88 | 89 | // Rewrite presets from cached configuration modules to match those from regular 90 | // configuration files. 91 | function describePreset ( 92 | dirname: string, 93 | source: string, 94 | resolutionCache: PluginsAndPresetsMapValue, 95 | nameMap: NameMap, 96 | item: PluginOrPresetItem 97 | ): PluginOrPresetFileDescriptor | PluginOrPresetDescriptor { 98 | const descriptor = describePluginOrPreset(dirname, source, resolutionCache, nameMap, item, Kind.PRESET) 99 | if (!isFileDescriptor(descriptor) && typeof descriptor.target === 'object') { 100 | const target: PresetObject = {...descriptor.target} 101 | if (Array.isArray(target.plugins)) { 102 | target.plugins = target.plugins.map(plugin => describePlugin(dirname, source, resolutionCache, nameMap, plugin)) 103 | } 104 | if (Array.isArray(target.presets)) { 105 | target.presets = target.presets.map(preset => describePreset(dirname, source, resolutionCache, nameMap, preset)) 106 | } 107 | descriptor.target = target 108 | } 109 | return descriptor 110 | } 111 | 112 | export type WrapperFn = Function & {wrapped: Function} 113 | export type WrapperFnMap = Map 114 | export type WrapperFnDirMap = Map 115 | 116 | function arrifyPluginsOrPresets ( 117 | list: PluginOrPresetDescriptorList, 118 | wrapperFnsByDir: WrapperFnDirMap, 119 | rewrite?: (dirname: string, pluginOrPreset: object) => object 120 | ): PluginOrPresetList { 121 | return list.map(item => { 122 | let wrapperFns: WrapperFnMap 123 | if (wrapperFnsByDir.has(item.dirname)) { 124 | wrapperFns = wrapperFnsByDir.get(item.dirname)! 125 | } else { 126 | wrapperFns = new Map() 127 | wrapperFnsByDir.set(item.dirname, wrapperFns) 128 | } 129 | 130 | let target: object | Function 131 | if (!isFileDescriptor(item) && typeof item.target === 'object') { 132 | target = item.target 133 | } else if (wrapperFns.has(item.name)) { 134 | target = wrapperFns.get(item.name)! 135 | } else { 136 | const wrapped = isFileDescriptor(item) ? loadPluginOrPreset(item.filename) : item.target as Function 137 | const targetFn: WrapperFn = Object.assign( 138 | rewrite 139 | ? (api: any, options: any) => rewrite(item.dirname, wrapped(api, options, item.dirname)) 140 | : (api: any, options: any) => wrapped(api, options, item.dirname), 141 | {wrapped}) 142 | wrapperFns.set(item.name, targetFn) 143 | target = targetFn 144 | } 145 | return [target, item.options, item.name] 146 | }) 147 | } 148 | 149 | // Turn plugin objects back into arrays for use in Babel. 150 | function arrifyPlugins (list: PluginOrPresetDescriptorList, wrapperFnsByDir: WrapperFnDirMap): PluginOrPresetList { 151 | return arrifyPluginsOrPresets(list, wrapperFnsByDir) 152 | } 153 | 154 | // Rewrite a preset object so its plugins and presets are resolved relative 155 | // to the correct dirname and factories are called with the correct dirname. 156 | function rewritePreset (dirname: string, obj: PresetObject): PresetObject { 157 | // istanbul ignore next 158 | if (!obj || typeof obj !== 'object') return obj 159 | 160 | obj = {...obj} 161 | if (Array.isArray(obj.plugins)) { 162 | const pluginWrapperFns: WrapperFnMap = new Map() 163 | obj.plugins = obj.plugins.map(plugin => { 164 | const target = Array.isArray(plugin) ? (plugin as [PluginOrPresetTarget])[0] : plugin 165 | if (typeof target === 'object') return plugin 166 | 167 | let targetFn: WrapperFn 168 | const wrapped = typeof target === 'function' ? target : resolveDynamicPluginOrPreset(Kind.PLUGIN, dirname, target) 169 | if (pluginWrapperFns.has(wrapped)) { 170 | targetFn = pluginWrapperFns.get(wrapped)! 171 | } else { 172 | targetFn = Object.assign((api: any, options: any) => wrapped(api, options, dirname), {wrapped}) 173 | pluginWrapperFns.set(wrapped, targetFn) 174 | } 175 | 176 | if (Array.isArray(plugin)) { 177 | switch (plugin.length) { 178 | case 1: return [targetFn] 179 | case 2: return [targetFn, plugin[1]] 180 | default: return [targetFn, plugin[1], plugin[2]] 181 | } 182 | } else { 183 | return targetFn 184 | } 185 | }) 186 | } 187 | 188 | if (Array.isArray(obj.presets)) { 189 | const presetWrapperFns: WrapperFnMap = new Map() 190 | obj.presets = obj.presets.map(preset => { 191 | const target = Array.isArray(preset) ? (preset as [PluginOrPresetTarget])[0] : preset 192 | if (typeof target === 'object') { 193 | const rewritten = rewritePreset(dirname, target) 194 | if (Array.isArray(preset)) { 195 | switch (preset.length) { 196 | case 1: return [rewritten] 197 | case 2: return [rewritten, preset[1]] 198 | default: return [rewritten, preset[1], preset[2]] 199 | } 200 | } else { 201 | return rewritten 202 | } 203 | } 204 | 205 | let targetFn: WrapperFn 206 | const wrapped = typeof target === 'function' ? target : resolveDynamicPluginOrPreset(Kind.PRESET, dirname, target) 207 | if (presetWrapperFns.has(wrapped)) { 208 | targetFn = presetWrapperFns.get(wrapped)! 209 | } else { 210 | targetFn = Object.assign((api: any, options: any) => rewritePreset(dirname, wrapped(api, options, dirname)), {wrapped}) 211 | presetWrapperFns.set(wrapped, targetFn) 212 | } 213 | 214 | if (Array.isArray(preset)) { 215 | switch (preset.length) { 216 | case 1: return [targetFn] 217 | case 2: return [targetFn, preset[1]] 218 | default: return [targetFn, preset[1], preset[2]] 219 | } 220 | } else { 221 | return targetFn 222 | } 223 | }) 224 | } 225 | 226 | return obj 227 | } 228 | 229 | // Turn preset objects back into arrays for use in Babel. 230 | function arrifyPresets (list: PluginOrPresetDescriptorList, wrapperFnsByDir: WrapperFnDirMap): PluginOrPresetList { 231 | return arrifyPluginsOrPresets(list, wrapperFnsByDir, rewritePreset).map(preset => { 232 | const target = (preset as [DescribedPresetObject])[0] 233 | if (typeof target === 'object') { 234 | const obj: PresetObject = {...target} 235 | if (Array.isArray(target.plugins)) { 236 | obj.plugins = arrifyPlugins(target.plugins, wrapperFnsByDir) 237 | } 238 | if (Array.isArray(target.presets)) { 239 | obj.presets = arrifyPresets(target.presets, wrapperFnsByDir) 240 | } 241 | (preset as [PluginOrPresetTarget])[0] = obj 242 | } 243 | return preset 244 | }) 245 | } 246 | 247 | export function loadCachedModule ( 248 | cache: Cache, 249 | dir: string, 250 | source: string, 251 | envName: string, 252 | selectEnv: boolean, 253 | selectOverride?: number 254 | ): ReducedBabelOptions { 255 | if (!cache) throw new Error(`A cache is required to load the configuration module at '${source}'`) 256 | 257 | const cached = cache.moduleSources.get(source) 258 | if (!cached) throw new Error(`Could not find the configuration module for '${source}' in the cache`) 259 | if (!isUnrestrictedModuleSource(cached) && !cached.byEnv.has(envName)) { 260 | throw new Error(`Could not find the configuration module, specific to the '${envName}' environment, for '${source}', in the cache`) // eslint-disable-line max-len 261 | } 262 | 263 | let options = cloneOptions(isUnrestrictedModuleSource(cached) ? cached.options : cached.byEnv.get(envName)!.options) 264 | if (selectOverride !== undefined) options = options.overrides![selectOverride] 265 | if (selectEnv) options = options.env![envName] 266 | delete options.env 267 | delete options.extends 268 | delete options.overrides 269 | normalizeSomeOptions(options) 270 | 271 | const resolutionCache = cache.pluginsAndPresets.get(dir)! 272 | if (Array.isArray(options.plugins)) { 273 | options.plugins = options.plugins.map(item => describePlugin(dir, source, resolutionCache, cache.nameMap, item)) 274 | } else { 275 | options.plugins = [] 276 | } 277 | if (Array.isArray(options.presets)) { 278 | options.presets = options.presets.map(item => describePreset(dir, source, resolutionCache, cache.nameMap, item)) 279 | } else { 280 | options.presets = [] 281 | } 282 | // Don't check for duplicate plugin or preset names. Presumably these have 283 | // been validated when configs were collected. 284 | 285 | return options as ReducedBabelOptions 286 | } 287 | 288 | export function mergeOptions (configs: ReducedBabelOptions[], wrapperFnsByDir: WrapperFnDirMap): BabelOptions { 289 | const merged = configs.reduce((target, options) => { 290 | mergePluginsOrPresets(target.plugins, options.plugins) 291 | delete options.plugins 292 | 293 | mergePluginsOrPresets(target.presets, options.presets) 294 | delete options.presets 295 | 296 | return merge(target, options) 297 | }, {plugins: [], presets: []} as ReducedBabelOptions) 298 | 299 | const retval = merged as BabelOptions 300 | if (merged.plugins.length > 0) { 301 | retval.plugins = arrifyPlugins(merged.plugins, wrapperFnsByDir) 302 | } else { 303 | delete retval.plugins 304 | } 305 | if (merged.presets.length > 0) { 306 | retval.presets = arrifyPresets(merged.presets, wrapperFnsByDir) 307 | } else { 308 | delete retval.presets 309 | } 310 | 311 | return retval 312 | } 313 | -------------------------------------------------------------------------------- /src/isFilePath.ts: -------------------------------------------------------------------------------- 1 | import path = require('path') 2 | 3 | export default function isFilePath (ref: string): boolean { 4 | return path.isAbsolute(ref) || ref.startsWith('./') || ref.startsWith('../') 5 | } 6 | -------------------------------------------------------------------------------- /src/loadConfigModule.ts: -------------------------------------------------------------------------------- 1 | import pirates = require('pirates') 2 | 3 | const accessSymbol = Symbol.for('__hullabaloo_dependencies__') 4 | 5 | export type LoadedConfigModule = { 6 | dependencies: Map 7 | options: any 8 | } 9 | 10 | export default function loadConfigModule (configFile: string): LoadedConfigModule { 11 | const revert = pirates.addHook((code, filename) => { 12 | return `var __hullabaloo_dependencies__ = new Map();(function(exports, require, module, __filename, __dirname) {${code} 13 | })(exports, Object.assign(request => { 14 | const id = require.resolve(request) 15 | __hullabaloo_dependencies__.set(id, request) 16 | return require(id) 17 | }, require), module, __filename, __dirname) 18 | try { 19 | Object.defineProperty(module.exports, Symbol.for('__hullabaloo_dependencies__'), { 20 | configurable: false, 21 | enumerable: false, 22 | writable: false, 23 | value: __hullabaloo_dependencies__ 24 | }) 25 | } catch (_) {}` 26 | }, { 27 | exts: ['.js'], 28 | matcher: filename => filename === configFile 29 | }) 30 | 31 | try { 32 | const configExports = require(configFile) // eslint-disable-line import/no-dynamic-require 33 | const dependencies: Map = configExports[accessSymbol] || new Map() 34 | const options = configExports.__esModule ? configExports.default : configExports 35 | return {dependencies, options} 36 | } finally { 37 | revert() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/loadPluginOrPreset.ts: -------------------------------------------------------------------------------- 1 | export default function loadPluginOrPreset (filename: string): Function { 2 | const m = require(filename) // eslint-disable-line import/no-dynamic-require 3 | const fn = m && m.__esModule ? m.default : m 4 | if (typeof fn !== 'function') { 5 | throw new TypeError(`Plugin or preset file '${filename}' did not export a function`) 6 | } 7 | return fn 8 | } 9 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import path = require('path') 2 | 3 | import BabelOptions from './BabelOptions' 4 | import Cache, {prepare as prepareCache} from './Cache' 5 | import cloneOptions from './cloneOptions' 6 | import * as collector from './collector' 7 | import currentEnv from './currentEnv' 8 | import ResolvedConfig from './ResolvedConfig' 9 | import Verifier from './Verifier' 10 | 11 | export {currentEnv, prepareCache} 12 | 13 | export interface CreateOptions { 14 | options: BabelOptions 15 | source: string 16 | dir?: string 17 | hash?: string 18 | fileType?: collector.FileType.JSON | collector.FileType.JSON5 19 | } 20 | 21 | export interface FromOptions { 22 | cache?: Cache 23 | expectedEnvNames?: string[] 24 | } 25 | 26 | export function createConfig (options: CreateOptions): collector.VirtualConfig { 27 | if (!options || !options.options || !options.source) { 28 | throw new TypeError("Expected 'options' and 'source' options") 29 | } 30 | if (typeof options.options !== 'object' || Array.isArray(options.options)) { 31 | throw new TypeError("'options' must be an actual object") 32 | } 33 | 34 | const source = options.source 35 | const dir = options.dir || path.dirname(source) 36 | const hash = typeof options.hash === 'string' ? options.hash : null 37 | const fileType = typeof options.fileType === 'string' ? options.fileType : collector.FileType.JSON5 38 | const babelOptions = cloneOptions(options.options) 39 | 40 | if (Object.prototype.hasOwnProperty.call(babelOptions, 'envName')) { 41 | throw new TypeError("'options' must not have an 'envName' property") 42 | } 43 | 44 | return new collector.VirtualConfig(dir, null, hash, babelOptions, source, fileType, null, null) 45 | } 46 | 47 | export async function fromConfig (baseConfig: collector.Config, options?: FromOptions): Promise { 48 | const cache = options && options.cache 49 | const expectedEnvNames = options && options.expectedEnvNames 50 | const chains = await collector.fromConfig(baseConfig, expectedEnvNames, cache) 51 | return new ResolvedConfig(chains, cache) 52 | } 53 | 54 | export async function fromDirectory (dir: string, options?: FromOptions): Promise { 55 | const cache = options && options.cache 56 | const expectedEnvNames = options && options.expectedEnvNames 57 | const chains = await collector.fromDirectory(dir, expectedEnvNames, cache) 58 | return chains && new ResolvedConfig(chains, cache) 59 | } 60 | 61 | export function restoreVerifier (buffer: Buffer): Verifier { 62 | return Verifier.fromBuffer(buffer) 63 | } 64 | -------------------------------------------------------------------------------- /src/mergePluginsOrPresets.ts: -------------------------------------------------------------------------------- 1 | import {PluginOrPresetDescriptorList} from './reduceChains' 2 | 3 | export default function mergePluginsOrPresets (target: PluginOrPresetDescriptorList, source: PluginOrPresetDescriptorList) { 4 | const reverseLookup = new Map(target.map<[string, number]>((item, index) => { 5 | return [item.name, index] 6 | })) 7 | for (const item of source) { 8 | if (reverseLookup.has(item.name)) { 9 | target[reverseLookup.get(item.name)!] = item 10 | } else { 11 | target.push(item) 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/normalizeOptions.ts: -------------------------------------------------------------------------------- 1 | import BabelOptions from './BabelOptions' 2 | 3 | export default function normalizeOptions (options: BabelOptions): BabelOptions { 4 | // Always delete `babelrc`. Babel itself no longer needs to resolve this file. 5 | delete options.babelrc 6 | 7 | // Based on . 8 | // `extends` and `env` have already been removed, and removing `plugins` and 9 | // `presets` is superfluous here. 10 | delete options.passPerPreset 11 | delete options.ignore 12 | delete options.only 13 | 14 | if (options.sourceMap) { 15 | options.sourceMaps = options.sourceMap 16 | delete options.sourceMap 17 | } 18 | 19 | return options 20 | } 21 | -------------------------------------------------------------------------------- /src/readSafe.ts: -------------------------------------------------------------------------------- 1 | import gfs = require('graceful-fs') 2 | 3 | import Cache from './Cache' 4 | 5 | export default function readSafe (source: string, cache?: Cache): Promise { 6 | if (cache && cache.files.has(source)) { 7 | return cache.files.get(source)! 8 | } 9 | 10 | const isFile = new Promise(resolve => { 11 | gfs.stat(source, (err, stat) => { 12 | resolve(!err && stat.isFile()) 13 | }) 14 | }) 15 | const promise = new Promise((resolve, reject) => { 16 | gfs.readFile(source, async (err, contents) => { 17 | if (!(await isFile)) { 18 | resolve(null) 19 | } else if (err) { 20 | reject(err) 21 | } else { 22 | resolve(contents) 23 | } 24 | }) 25 | }) 26 | 27 | if (cache) { 28 | cache.files.set(source, promise) 29 | } 30 | return promise 31 | } 32 | -------------------------------------------------------------------------------- /src/reduceChains.ts: -------------------------------------------------------------------------------- 1 | import merge = require('lodash.merge') 2 | import pkgDir = require('pkg-dir') 3 | 4 | // FIXME: Remove ESLint exception. BabelOptions *is* used but this isn't being detected. 5 | // eslint-disable-next-line no-unused-vars 6 | import BabelOptions, {PluginOrPresetItem, PluginOrPresetList, PluginOrPresetOptions} from './BabelOptions' 7 | import Cache, {NameMap} from './Cache' 8 | import cloneOptions from './cloneOptions' 9 | import {Chain, Chains, Config, FileType, OverrideConfig} from './collector' 10 | import {InvalidFileError} from './errors' 11 | import getPluginOrPresetName from './getPluginOrPresetName' 12 | import isFilePath from './isFilePath' 13 | import mergePluginsOrPresets from './mergePluginsOrPresets' 14 | import normalizeOptions from './normalizeOptions' 15 | // FIXME: Remove ESLint exception. Entry *is* used but this isn't being detected. 16 | // eslint-disable-next-line no-unused-vars 17 | import resolvePluginsAndPresets, {Entry, PresetObject, Resolutions, ResolutionMap} from './resolvePluginsAndPresets' 18 | 19 | export interface Dependency { 20 | default: boolean 21 | envs: Set 22 | filename: string 23 | fromPackage: string | null 24 | hash?: string 25 | } 26 | type DependencyMap = Map 27 | 28 | export interface Source { 29 | default: boolean 30 | envs: Set 31 | hash?: string 32 | runtimeHash: string | null 33 | source: string 34 | } 35 | type SourceMap = Map 36 | 37 | function trackDependency ( 38 | dependencyMap: DependencyMap, 39 | filename: string, 40 | fromPackage: string | null, 41 | envName: string | null 42 | ): void { 43 | if (dependencyMap.has(filename)) { 44 | const existing = dependencyMap.get(filename)! 45 | if (envName) { 46 | existing.envs.add(envName) 47 | } else { 48 | existing.default = true 49 | } 50 | return 51 | } 52 | 53 | dependencyMap.set(filename, { 54 | default: !envName, 55 | envs: envName ? new Set([envName]) : new Set(), 56 | filename, 57 | fromPackage 58 | }) 59 | } 60 | 61 | function trackSource (sourceMap: SourceMap, source: string, runtimeHash: string | null, envName: string | null): void { 62 | if (sourceMap.has(source)) { 63 | const existing = sourceMap.get(source)! 64 | if (envName) { 65 | existing.envs.add(envName) 66 | } else { 67 | existing.default = true 68 | } 69 | return 70 | } 71 | 72 | sourceMap.set(source, { 73 | default: !envName, 74 | envs: new Set(envName ? [envName] : []), 75 | runtimeHash, 76 | source 77 | }) 78 | } 79 | 80 | function mapPluginOrPresetTarget ( 81 | envName: string | null, 82 | dependencyMap: DependencyMap, 83 | getEntry: (ref: string) => Entry, 84 | target: string 85 | ): string { 86 | const entry = getEntry(target) 87 | trackDependency(dependencyMap, entry.filename, entry.fromPackage, envName) 88 | return entry.filename 89 | } 90 | 91 | export type PluginOrPresetFileDescriptor = { 92 | dirname: string 93 | filename: string 94 | name: string 95 | options?: PluginOrPresetOptions 96 | } 97 | export type PluginOrPresetDescriptor = { 98 | dirname: string 99 | name: string 100 | target: object | Function 101 | options?: PluginOrPresetOptions 102 | } 103 | export type PluginOrPresetDescriptorList = Array 104 | 105 | export function isFileDescriptor ( 106 | descriptor: PluginOrPresetFileDescriptor | PluginOrPresetDescriptor 107 | ): descriptor is PluginOrPresetFileDescriptor { 108 | return 'filename' in descriptor 109 | } 110 | 111 | function describePluginOrPreset ( 112 | dirname: string, 113 | envName: string | null, 114 | dependencyMap: DependencyMap, 115 | nameMap: NameMap, 116 | getEntry: (ref: string) => Entry, 117 | item: PluginOrPresetItem 118 | ): PluginOrPresetFileDescriptor | PluginOrPresetDescriptor { 119 | if (Array.isArray(item)) { 120 | const target = item[0] 121 | if (typeof target !== 'string') { 122 | const name = getPluginOrPresetName(nameMap, target) 123 | switch (item.length) { 124 | case 1: return {dirname, target, name} 125 | case 2: return {dirname, target, options: item[1] as PluginOrPresetOptions, name} 126 | default: return {dirname, target, options: item[1] as PluginOrPresetOptions, name: `${name}.${item[2]}`} 127 | } 128 | } 129 | 130 | const filename = mapPluginOrPresetTarget(envName, dependencyMap, getEntry, target) 131 | const name = getPluginOrPresetName(nameMap, filename) 132 | switch (item.length) { 133 | case 1: return {dirname, filename, name} 134 | case 2: return {dirname, filename, options: item[1] as PluginOrPresetOptions, name} 135 | default: return {dirname, filename, options: item[1] as PluginOrPresetOptions, name: `${name}.${item[2]}`} 136 | } 137 | } 138 | 139 | if (typeof item === 'string') { 140 | const filename = mapPluginOrPresetTarget(envName, dependencyMap, getEntry, item) 141 | return {dirname, filename, name: getPluginOrPresetName(nameMap, filename)} 142 | } 143 | 144 | return {dirname, target: item, name: getPluginOrPresetName(nameMap, item)} 145 | } 146 | 147 | function describePlugin ( 148 | dirname: string, 149 | envName: string | null, 150 | dependencyMap: DependencyMap, 151 | nameMap: NameMap, 152 | resolutions: Resolutions, 153 | item: PluginOrPresetItem 154 | ): PluginOrPresetFileDescriptor | PluginOrPresetDescriptor { 155 | return describePluginOrPreset(dirname, envName, dependencyMap, nameMap, (ref: string) => resolutions.plugins.get(ref)!, item) 156 | } 157 | 158 | function describePreset ( 159 | dirname: string, 160 | envName: string | null, 161 | dependencyMap: DependencyMap, 162 | nameMap: NameMap, 163 | resolutions: Resolutions, 164 | item: PluginOrPresetItem 165 | ): PluginOrPresetFileDescriptor | PluginOrPresetDescriptor { 166 | const descriptor = describePluginOrPreset( 167 | dirname, envName, dependencyMap, nameMap, (ref: string) => resolutions.presets.get(ref)!, item 168 | ) 169 | if (!isFileDescriptor(descriptor) && typeof descriptor.target === 'object') { 170 | const target: PresetObject = {...descriptor.target} 171 | if (Array.isArray(target.plugins)) { 172 | target.plugins = target.plugins.map(plugin => describePlugin(dirname, envName, dependencyMap, nameMap, resolutions, plugin)) 173 | } 174 | if (Array.isArray(target.presets)) { 175 | target.presets = target.presets.map(preset => describePreset(dirname, envName, dependencyMap, nameMap, resolutions, preset)) 176 | } 177 | descriptor.target = target 178 | } 179 | return descriptor 180 | } 181 | 182 | export interface ReducedBabelOptions extends BabelOptions { 183 | plugins: PluginOrPresetDescriptorList 184 | presets: PluginOrPresetDescriptorList 185 | } 186 | 187 | export interface MergedConfig { 188 | fileType: FileType 189 | options: ReducedBabelOptions 190 | overrideIndex?: number 191 | } 192 | 193 | export interface ModuleConfig { 194 | dir: string 195 | envName: string | null 196 | fileType: FileType.JS 197 | source: string 198 | overrideIndex?: number 199 | } 200 | 201 | export type ConfigList = Array & { 202 | overrides: Array[] 203 | } 204 | 205 | export function isModuleConfig (object: MergedConfig | ModuleConfig): object is ModuleConfig { 206 | return object.fileType === FileType.JS 207 | } 208 | 209 | interface QueueItem { 210 | config: Config 211 | options: ReducedBabelOptions 212 | plugins: PluginOrPresetList 213 | presets: PluginOrPresetList 214 | overrideIndex?: number 215 | } 216 | 217 | function mergeChain ( 218 | chain: Chain, 219 | envName: string | null, 220 | pluginsAndPresets: ResolutionMap, 221 | dependencyMap: DependencyMap, 222 | nameMap: NameMap, 223 | sourceMap: SourceMap, 224 | fixedSourceHashes: Map 225 | ): ConfigList { 226 | const list: ConfigList = Object.assign([], {overrides: []}) 227 | let tail: MergedConfig | null = null 228 | 229 | const queue = Array.from(chain, (config, index): QueueItem => { 230 | const options = cloneOptions(config.options) 231 | const overrideIndex = config instanceof OverrideConfig 232 | ? config.index 233 | : undefined 234 | const plugins = options.plugins 235 | const presets = options.presets 236 | delete options.plugins 237 | delete options.presets 238 | return { 239 | config, 240 | // The first config's options are not normalized. 241 | options: (index === 0 ? options : normalizeOptions(options)) as ReducedBabelOptions, 242 | plugins: Array.isArray(plugins) ? plugins : [], 243 | presets: Array.isArray(presets) ? presets : [], 244 | overrideIndex 245 | } 246 | }) 247 | for (const item of queue) { 248 | const config = item.config 249 | trackSource(sourceMap, config.source, config.runtimeHash, envName) 250 | if (config.runtimeDependencies) { 251 | for (const dependency of config.runtimeDependencies) { 252 | const filename = dependency[0] 253 | const fromPackage = isFilePath(dependency[1]) ? null : pkgDir.sync(filename) 254 | trackDependency(dependencyMap, filename, fromPackage, envName) 255 | } 256 | } 257 | if (config.hash !== null) { 258 | fixedSourceHashes.set(config.source, config.hash) 259 | } 260 | 261 | // When used properly, pluginsAndPresets *will* contain a lookup for 262 | // `config`. Don't handle situations where this is not the case. This is an 263 | // internal module after all. 264 | const resolutionMap = pluginsAndPresets.get(config)! 265 | const plugins = item.plugins.map(plugin => { 266 | return describePlugin(config.dir, envName, dependencyMap, nameMap, resolutionMap, plugin) 267 | }) 268 | const presets = item.presets.map(preset => { 269 | return describePreset(config.dir, envName, dependencyMap, nameMap, resolutionMap, preset) 270 | }) 271 | 272 | if (plugins.length !== new Set(plugins.map(plugin => plugin.name)).size) { 273 | throw new InvalidFileError(config.source, 'Duplicate plugins detected') 274 | } 275 | if (presets.length !== new Set(presets.map(preset => preset.name)).size) { 276 | throw new InvalidFileError(config.source, 'Duplicate presets detected') 277 | } 278 | 279 | if (config.fileType === FileType.JS) { 280 | // Note that preparing `plugins` and `presets` has added them to 281 | // `dependencyMap`. This will still be used when determining the 282 | // configuration hash, even if the values are discarded. 283 | const moduleConfig: ModuleConfig = { 284 | dir: config.dir, 285 | envName: config.envName, 286 | fileType: config.fileType, 287 | source: config.source 288 | } 289 | if (item.overrideIndex !== undefined) { 290 | moduleConfig.overrideIndex = item.overrideIndex 291 | } 292 | list.push(moduleConfig) 293 | tail = null 294 | } else if (tail && item.overrideIndex === tail.overrideIndex) { 295 | mergePluginsOrPresets(tail.options.plugins!, plugins) 296 | mergePluginsOrPresets(tail.options.presets!, presets) 297 | merge(tail.options, item.options) 298 | 299 | if (tail.fileType === FileType.JSON && config.fileType === FileType.JSON5) { 300 | tail.fileType = config.fileType 301 | } 302 | } else { 303 | item.options.plugins = plugins 304 | item.options.presets = presets 305 | tail = { 306 | fileType: config.fileType, 307 | options: item.options 308 | } 309 | if (item.overrideIndex !== undefined) { 310 | tail.overrideIndex = item.overrideIndex 311 | } 312 | list.push(tail) 313 | } 314 | } 315 | 316 | for (const overrideChain of chain.overrides) { 317 | const merged = mergeChain( 318 | overrideChain, 319 | envName, 320 | pluginsAndPresets, 321 | dependencyMap, 322 | nameMap, 323 | sourceMap, 324 | fixedSourceHashes 325 | ) 326 | 327 | for (const recursive of merged.overrides) { 328 | list.overrides.push(recursive) 329 | } 330 | list.overrides.push(merged.slice()) 331 | } 332 | 333 | return list 334 | } 335 | 336 | function sortKeys (a: [string, any], b: [string, any]): -1 | 1 { 337 | return a[0] < b[0] ? -1 : 1 338 | } 339 | 340 | export interface ReducedChains { 341 | dependencies: Dependency[] 342 | envNames: Set 343 | fixedSourceHashes: Map 344 | sources: Source[] 345 | unflattenedDefaultOptions: ConfigList 346 | unflattenedEnvOptions: Map 347 | } 348 | 349 | export default function reduceChains (chains: Chains, cache?: Cache): ReducedChains { 350 | const pluginsAndPresets = resolvePluginsAndPresets(chains, cache) 351 | 352 | const dependencyMap: DependencyMap = new Map() 353 | const nameMap: NameMap = cache ? cache.nameMap : new Map() 354 | const envNames = new Set() 355 | const fixedSourceHashes = new Map() 356 | const sourceMap: SourceMap = new Map() 357 | 358 | const unflattenedDefaultOptions = mergeChain( 359 | chains.defaultChain, null, pluginsAndPresets, dependencyMap, nameMap, sourceMap, fixedSourceHashes 360 | ) 361 | 362 | const unflattenedEnvOptions = new Map() 363 | for (const pair of chains.envChains) { 364 | const envName = pair[0] 365 | const chain = pair[1] 366 | 367 | envNames.add(envName) 368 | unflattenedEnvOptions.set( 369 | envName, 370 | mergeChain(chain, envName, pluginsAndPresets, dependencyMap, nameMap, sourceMap, fixedSourceHashes) 371 | ) 372 | } 373 | 374 | const dependencies = Array.from(dependencyMap).sort(sortKeys).map(entry => entry[1]) 375 | const sources = Array.from(sourceMap).sort(sortKeys).map(entry => entry[1]) 376 | 377 | return { 378 | dependencies, 379 | envNames, 380 | fixedSourceHashes, 381 | sources, 382 | unflattenedDefaultOptions, 383 | unflattenedEnvOptions 384 | } 385 | } 386 | -------------------------------------------------------------------------------- /src/resolvePluginsAndPresets.ts: -------------------------------------------------------------------------------- 1 | import path = require('path') 2 | 3 | import pkgDir = require('pkg-dir') 4 | import resolveFrom = require('resolve-from') 5 | 6 | import {PluginOrPresetList, PluginOrPresetTarget} from './BabelOptions' 7 | import Cache, {PluginsAndPresetsMap, PluginsAndPresetsMapValue} from './Cache' 8 | import {Chain, Chains, Config} from './collector' 9 | import {ResolveError} from './errors' 10 | import standardizeName from './standardizeName' 11 | 12 | export interface PresetObject { 13 | plugins?: PluginOrPresetList 14 | presets?: PluginOrPresetList 15 | } 16 | 17 | function isPresetObject (target: PluginOrPresetTarget): target is PresetObject { 18 | return typeof target === 'object' 19 | } 20 | 21 | export const enum Kind { 22 | PLUGIN = 'plugin', 23 | PRESET = 'preset' 24 | } 25 | 26 | function normalize (arr: PluginOrPresetList | void): PluginOrPresetTarget[] { 27 | if (!Array.isArray(arr)) return [] 28 | 29 | return arr.map(item => Array.isArray(item) ? item[0] : item) 30 | } 31 | 32 | function resolveName (name: string, fromDir: string, cache: PluginsAndPresetsMapValue): string | null { 33 | if (cache.has(name)) return cache.get(name)! 34 | 35 | const filename = resolveFrom.silent(fromDir, name) 36 | cache.set(name, filename) 37 | return filename 38 | } 39 | 40 | function resolvePackage (filename: string, fromFile: boolean): string | null { 41 | if (fromFile) return null 42 | 43 | return pkgDir.sync(filename) 44 | } 45 | 46 | export interface Entry { 47 | filename: string 48 | fromPackage: string | null 49 | } 50 | 51 | export type Resolutions = { 52 | plugins: Map 53 | presets: Map 54 | } 55 | export type ResolutionMap = Map 56 | 57 | export default function resolvePluginsAndPresets (chains: Chains, sharedCache?: Cache): ResolutionMap { 58 | const dirCaches: PluginsAndPresetsMap = sharedCache 59 | ? sharedCache.pluginsAndPresets 60 | : new Map() 61 | const getCache = (dir: string): PluginsAndPresetsMapValue => { 62 | if (dirCaches.has(dir)) return dirCaches.get(dir)! 63 | 64 | const cache: PluginsAndPresetsMapValue = new Map() 65 | dirCaches.set(dir, cache) 66 | return cache 67 | } 68 | 69 | const byConfig: ResolutionMap = new Map() 70 | const resolveConfig = (config: Config) => { 71 | if (byConfig.has(config)) return 72 | 73 | const plugins = new Map() 74 | const presets = new Map() 75 | byConfig.set(config, {plugins, presets}) 76 | 77 | const fromDir = config.dir 78 | const cache = getCache(fromDir) 79 | const resolve = (kind: Kind, ref: string) => { 80 | const possibility = standardizeName(kind, ref) 81 | const filename = resolveName(possibility.name, fromDir, cache) 82 | if (!filename) throw new ResolveError(config.source, kind, ref) 83 | 84 | const fromPackage = resolvePackage(filename, possibility.fromFile) 85 | const entry = {filename, fromPackage} 86 | if (kind === Kind.PLUGIN) { 87 | plugins.set(ref, entry) 88 | } else { 89 | presets.set(ref, entry) 90 | } 91 | } 92 | const resolvePlugins = (targets: PluginOrPresetTarget[]) => { 93 | for (const target of targets) { 94 | if (typeof target === 'string') resolve(Kind.PLUGIN, target) 95 | } 96 | } 97 | const resolvePresets = (targets: PluginOrPresetTarget[]) => { 98 | for (const target of targets) { 99 | if (typeof target === 'string') { 100 | resolve(Kind.PRESET, target) 101 | } else if (isPresetObject(target)) { 102 | resolvePlugins(normalize(target.plugins)) 103 | resolvePresets(normalize(target.presets)) 104 | } 105 | } 106 | } 107 | 108 | resolvePlugins(normalize(config.options.plugins)) 109 | resolvePresets(normalize(config.options.presets)) 110 | } 111 | 112 | const resolveChains = (iterable: Iterable) => { 113 | for (const chain of iterable) { 114 | for (const config of chain) { 115 | resolveConfig(config) 116 | } 117 | resolveChains(chain.overrides) 118 | } 119 | } 120 | resolveChains(chains) 121 | 122 | return byConfig 123 | } 124 | -------------------------------------------------------------------------------- /src/standardizeName.ts: -------------------------------------------------------------------------------- 1 | import isFilePath from './isFilePath' 2 | import {Kind} from './resolvePluginsAndPresets' 3 | 4 | // Based on https://github.com/babel/babel/blob/master/packages/babel-core/src/config/loading/files/plugins.js#L60:L86 5 | // but with fewer regular expressions 😉 6 | export default function standardizeName (kind: Kind, ref: string): {fromFile: boolean, name: string} { // eslint-disable-line typescript/member-delimiter-style 7 | if (isFilePath(ref)) return {fromFile: true, name: ref} 8 | if (ref.startsWith('module:')) return {fromFile: false, name: ref.slice(7)} 9 | 10 | if (kind === Kind.PLUGIN) { 11 | if (ref.startsWith('babel-plugin-') || ref.startsWith('@babel/plugin-')) return {fromFile: false, name: ref} 12 | if (ref.startsWith('@babel/')) return {fromFile: false, name: `@babel/plugin-${ref.slice(7)}`} 13 | if (!ref.startsWith('@')) return {fromFile: false, name: `babel-plugin-${ref}`} 14 | } else { 15 | if (ref.startsWith('babel-preset-') || ref.startsWith('@babel/preset-')) return {fromFile: false, name: ref} 16 | if (ref.startsWith('@babel/')) return {fromFile: false, name: `@babel/preset-${ref.slice(7)}`} 17 | if (!ref.startsWith('@')) return {fromFile: false, name: `babel-preset-${ref}`} 18 | } 19 | 20 | // At this point `ref` is guaranteed to be scoped. 21 | const matches = /^(@.+?)\/([^/]+)(.*)/.exec(ref)! 22 | const scope = matches[1] 23 | const partialName = matches[2] 24 | const remainder = matches[3] 25 | return {fromFile: false, name: `${scope}/babel-${kind === Kind.PLUGIN ? 'plugin' : 'preset'}-${partialName}${remainder}`} 26 | } 27 | -------------------------------------------------------------------------------- /test/SimulatedBabelCache.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import md5Hex from 'md5-hex' 3 | 4 | import SimulatedBabelCache from '../build/SimulatedBabelCache' 5 | 6 | test.beforeEach(t => { 7 | const data = {} 8 | const cache = new SimulatedBabelCache(data) 9 | t.context = {cache, data} 10 | }) 11 | 12 | test('provides data to api.using() and api.invalidate() handlers', t => { 13 | const key = Symbol('key') 14 | const value = Symbol('value') 15 | t.context.data[key] = value 16 | 17 | t.plan(2) 18 | t.context.cache.api.using(data => t.deepEqual(data, t.context.data)) 19 | t.context.cache.api.invalidate(data => t.deepEqual(data, t.context.data)) 20 | }) 21 | 22 | test('api.forever() configures the cache', t => { 23 | t.false(t.context.cache.wasConfigured) 24 | t.context.cache.api.forever() 25 | t.true(t.context.cache.wasConfigured) 26 | }) 27 | 28 | test('api.never() configures the cache', t => { 29 | t.false(t.context.cache.wasConfigured) 30 | t.context.cache.api.never() 31 | t.true(t.context.cache.wasConfigured) 32 | }) 33 | 34 | test('api.using() configures the cache', t => { 35 | t.false(t.context.cache.wasConfigured) 36 | t.context.cache.api.using(() => {}) 37 | t.true(t.context.cache.wasConfigured) 38 | }) 39 | 40 | test('api.invalidate() configures the cache', t => { 41 | t.false(t.context.cache.wasConfigured) 42 | t.context.cache.api.invalidate(() => {}) 43 | t.true(t.context.cache.wasConfigured) 44 | }) 45 | 46 | test('api.never() fails after api.forever()', t => { 47 | t.context.cache.api.forever() 48 | const err = t.throws(() => t.context.cache.api.never()) 49 | t.is(err.name, 'Error') 50 | t.is(err.message, 'Caching has already been configured with .forever()') 51 | }) 52 | 53 | test('api.using() fails after api.forever()', t => { 54 | t.context.cache.api.forever() 55 | const err = t.throws(() => t.context.cache.api.using(() => {})) 56 | t.is(err.name, 'Error') 57 | t.is(err.message, 'Caching has already been configured with .forever()') 58 | }) 59 | 60 | test('api.invalidate() fails after api.forever()', t => { 61 | t.context.cache.api.forever() 62 | const err = t.throws(() => t.context.cache.api.invalidate(() => {})) 63 | t.is(err.name, 'Error') 64 | t.is(err.message, 'Caching has already been configured with .forever()') 65 | }) 66 | 67 | test('api.forever() fails after api.never()', t => { 68 | t.context.cache.api.never() 69 | const err = t.throws(() => t.context.cache.api.forever()) 70 | t.is(err.name, 'Error') 71 | t.is(err.message, 'Caching has already been configured with .never()') 72 | }) 73 | 74 | test('api.using() fails after api.never()', t => { 75 | t.context.cache.api.never() 76 | const err = t.throws(() => t.context.cache.api.using(() => {})) 77 | t.is(err.name, 'Error') 78 | t.is(err.message, 'Caching has already been configured with .never()') 79 | }) 80 | 81 | test('api.invalidate() fails after api.never()', t => { 82 | t.context.cache.api.never() 83 | const err = t.throws(() => t.context.cache.api.invalidate(() => {})) 84 | t.is(err.name, 'Error') 85 | t.is(err.message, 'Caching has already been configured with .never()') 86 | }) 87 | 88 | test('.never is true after api.never()', t => { 89 | t.false(t.context.cache.never) 90 | t.context.cache.api.never() 91 | t.true(t.context.cache.never) 92 | }) 93 | 94 | test('.never is false after api.forever()', t => { 95 | t.false(t.context.cache.never) 96 | t.context.cache.api.forever() 97 | t.false(t.context.cache.never) 98 | }) 99 | 100 | test('.never is false after api.using()', t => { 101 | t.false(t.context.cache.never) 102 | t.context.cache.api.using(() => {}) 103 | t.false(t.context.cache.never) 104 | }) 105 | 106 | test('.never is false after api.invalidate()', t => { 107 | t.false(t.context.cache.never) 108 | t.context.cache.api.invalidate(() => {}) 109 | t.false(t.context.cache.never) 110 | }) 111 | 112 | test('api.forever() fails after seal()', t => { 113 | t.context.cache.seal() 114 | const err = t.throws(() => t.context.cache.api.forever()) 115 | t.is(err.name, 'Error') 116 | t.is(err.message, 'Cannot change caching after evaluation has completed') 117 | }) 118 | 119 | test('api.never() fails after seal()', t => { 120 | t.context.cache.seal() 121 | const err = t.throws(() => t.context.cache.api.never()) 122 | t.is(err.name, 'Error') 123 | t.is(err.message, 'Cannot change caching after evaluation has completed') 124 | }) 125 | 126 | test('api.using() fails after seal()', t => { 127 | t.context.cache.seal() 128 | const err = t.throws(() => t.context.cache.api.using(() => {})) 129 | t.is(err.name, 'Error') 130 | t.is(err.message, 'Cannot change caching after evaluation has completed') 131 | }) 132 | 133 | test('api.invalidate() fails after seal()', t => { 134 | t.context.cache.seal() 135 | const err = t.throws(() => t.context.cache.api.invalidate(() => {})) 136 | t.is(err.name, 'Error') 137 | t.is(err.message, 'Cannot change caching after evaluation has completed') 138 | }) 139 | 140 | test('api(false) behaves like api.never()', t => { 141 | t.false(t.context.cache.never) 142 | t.context.cache.api(false) 143 | t.true(t.context.cache.never) 144 | const err = t.throws(() => t.context.cache.api.forever()) 145 | t.is(err.name, 'Error') 146 | t.is(err.message, 'Caching has already been configured with .never()') 147 | }) 148 | 149 | test('api(true) behaves like api.forever()', t => { 150 | t.false(t.context.cache.never) 151 | t.context.cache.api(true) 152 | t.false(t.context.cache.never) 153 | const err = t.throws(() => t.context.cache.api.never()) 154 | t.is(err.name, 'Error') 155 | t.is(err.message, 'Caching has already been configured with .forever()') 156 | }) 157 | 158 | test('api(handler) behaves like api.using(handler)', t => { 159 | t.plan(4) 160 | t.false(t.context.cache.never) 161 | 162 | const key = Symbol('key') 163 | const value = Symbol('value') 164 | t.context.data[key] = value 165 | 166 | t.context.cache.api(data => t.deepEqual(data, t.context.data)) 167 | t.false(t.context.cache.never) 168 | t.true(t.context.cache.wasConfigured) 169 | }) 170 | 171 | test('hash() fails if called before seal()', t => { 172 | const err = t.throws(() => t.context.cache.hash()) 173 | t.is(err.name, 'Error') 174 | t.is(err.message, 'seal() must be called before invoking hash()') 175 | }) 176 | 177 | test('hash() returns null after api.forever()', t => { 178 | t.context.cache.api.forever() 179 | t.context.cache.seal() 180 | t.is(t.context.cache.hash(), null) 181 | }) 182 | 183 | test('hash() returns a unique value after api.never()', t => { 184 | t.context.cache.api.never() 185 | t.context.cache.seal() 186 | const value = t.context.cache.hash() 187 | t.not(value, null) 188 | 189 | const other = new SimulatedBabelCache({}) 190 | other.api.never() 191 | other.seal() 192 | t.not(value, other.hash()) 193 | }) 194 | 195 | test('hash() returns the same value if called repeatedly', t => { 196 | t.context.cache.api.never() 197 | t.context.cache.seal() 198 | t.is(t.context.cache.hash(), t.context.cache.hash()) 199 | }) 200 | 201 | { 202 | const primitive = (t, value, expected) => { 203 | t.context.cache.api.using(() => value) 204 | t.context.cache.seal() 205 | t.is(t.context.cache.hash(), expected) 206 | } 207 | primitive.title = kind => `hash() fingerprints ${kind} values` 208 | 209 | test('null', primitive, null, md5Hex('null')) 210 | test('true', primitive, true, md5Hex('true')) 211 | test('false', primitive, false, md5Hex('false')) 212 | test('undefined', primitive, undefined, md5Hex('undefined')) 213 | test('string', primitive, 'foo', md5Hex('"foo"')) 214 | test('Infinity', primitive, Infinity, md5Hex('Infinity')) 215 | test('-Infinity', primitive, -Infinity, md5Hex('-Infinity')) 216 | test('0', primitive, 0, md5Hex('0')) 217 | test('-0', primitive, -0, md5Hex('0')) 218 | test('number (not NaN)', primitive, 3.14, md5Hex('3.14')) 219 | } 220 | 221 | { 222 | const complex = (t, value) => { 223 | t.context.cache.api.using(() => value) 224 | t.context.cache.seal() 225 | const other = new SimulatedBabelCache({}) 226 | other.api.using(() => value) 227 | other.seal() 228 | t.not(t.context.cache.hash(), other.hash()) 229 | } 230 | complex.title = kind => `hash() uniquely fingerprints ${kind} values` 231 | 232 | test('NaN', complex, NaN) 233 | test('object', complex, {}) 234 | test('array', complex, []) 235 | test('Map', complex, new Map()) 236 | } 237 | -------------------------------------------------------------------------------- /test/Verifier.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | 4 | import test from 'ava' 5 | import fse from 'fs-extra' 6 | import md5Hex from 'md5-hex' 7 | import packageHash from 'package-hash' 8 | import proxyquire from 'proxyquire' 9 | import resolveFrom from 'resolve-from' 10 | import td from 'testdouble' 11 | import uniqueTempDir from 'unique-temp-dir' 12 | 13 | import {createConfig, fromConfig, fromDirectory, prepareCache} from '..' 14 | import Verifier from '../build/Verifier' 15 | 16 | import fixture from './helpers/fixture' 17 | 18 | const hashes = Object.create(null) 19 | test.before(t => { 20 | const promises = [] 21 | 22 | for (const pkg of [ 23 | fixture('compare', 'node_modules', 'env-plugin', 'package.json'), 24 | fixture('compare', 'node_modules', 'plugin', 'package.json'), 25 | fixture('compare', 'node_modules', 'plugin-copy', 'package.json'), 26 | fixture('compare', 'node_modules', 'plugin-default-opts', 'package.json'), 27 | fixture('compare', 'node_modules', 'preset', 'package.json') 28 | ]) { 29 | promises.push( 30 | packageHash(pkg) 31 | .then(hash => (hashes[pkg] = hash))) 32 | } 33 | 34 | for (const fp of [ 35 | fixture('compare', '.babelrc'), 36 | fixture('compare', 'dir', 'subdir', 'extended-by-babelrc.js'), 37 | fixture('compare', 'extended-by-virtual.json5'), 38 | fixture('compare', 'dir', 'extended-by-virtual-foo.json5'), 39 | fixture('compare', 'virtual.json') 40 | ]) { 41 | promises.push( 42 | new Promise((resolve, reject) => { 43 | fs.readFile(fp, (err, contents) => err ? reject(err) : resolve(contents)) 44 | }) 45 | .then(md5Hex) 46 | .then(hash => (hashes[fp] = hash))) 47 | } 48 | 49 | return Promise.all(promises) 50 | }) 51 | 52 | function requireWithCurrentEnv (env = {}) { 53 | const currentEnv = proxyquire('../build/currentEnv', { 54 | process: { 55 | env 56 | } 57 | }) 58 | 59 | const Verifier_ = proxyquire('../build/Verifier', { 60 | './currentEnv': currentEnv 61 | }) 62 | 63 | return proxyquire('..', { 64 | './currentEnv': currentEnv, 65 | './Verifier': Verifier_, 66 | './ResolvedConfig': proxyquire('../build/ResolvedConfig', { 67 | './Verifier': Verifier_ 68 | }) 69 | }) 70 | } 71 | 72 | const tmpDir = uniqueTempDir() 73 | test.after.always(() => { 74 | fse.removeSync(tmpDir) 75 | }) 76 | 77 | test('cache is used when creating verifier', async t => { 78 | const source = fixture('compare', 'virtual.json') 79 | 80 | const env = {} 81 | const main = requireWithCurrentEnv(env) 82 | const cache = main.prepareCache() 83 | const result = await main.fromConfig(main.createConfig({ 84 | options: require(source), // eslint-disable-line import/no-dynamic-require 85 | source 86 | }), {cache}) 87 | 88 | await result.createVerifier() 89 | for (const dependency of [ 90 | fixture('compare', 'node_modules', 'plugin', 'index.js'), 91 | fixture('compare', 'node_modules', 'plugin-default-opts', 'index.js'), 92 | fixture('compare', 'node_modules', 'preset', 'index.js') 93 | ]) { 94 | t.true(cache.dependencyHashes.has(dependency)) 95 | } 96 | for (const file of [ 97 | fixture('compare', '.babelrc'), 98 | fixture('compare', 'dir', 'subdir', 'extended-by-babelrc.js'), 99 | fixture('compare', 'extended-by-virtual.json5'), 100 | fixture('compare', 'dir', 'extended-by-virtual-foo.json5'), 101 | fixture('compare', 'virtual.json') 102 | ]) { 103 | t.true(cache.sourceHashes.has(file)) 104 | } 105 | }) 106 | 107 | test('cacheKeysForEnv()', async t => { 108 | const source = fixture('compare', 'virtual.json') 109 | 110 | const env = {} 111 | const main = requireWithCurrentEnv(env) 112 | const cache = main.prepareCache() 113 | const result = await main.fromConfig(main.createConfig({ 114 | options: require(source), // eslint-disable-line import/no-dynamic-require 115 | source 116 | }), {cache}) 117 | const verifier = await result.createVerifier() 118 | 119 | t.deepEqual(verifier.cacheKeysForEnv(), { 120 | dependencies: md5Hex([ 121 | hashes[fixture('compare', 'node_modules', 'plugin-copy', 'package.json')], 122 | hashes[fixture('compare', 'node_modules', 'plugin', 'package.json')], 123 | hashes[fixture('compare', 'node_modules', 'preset', 'package.json')] 124 | ]), 125 | sources: md5Hex([ 126 | hashes[fixture('compare', '.babelrc')], 127 | hashes[fixture('compare', 'dir', 'subdir', 'extended-by-babelrc.js')], 128 | hashes[fixture('compare', 'extended-by-virtual.json5')], 129 | hashes[fixture('compare', 'virtual.json')] 130 | ]) 131 | }) 132 | 133 | t.deepEqual(verifier.cacheKeysForEnv('foo'), { 134 | dependencies: md5Hex([ 135 | hashes[fixture('compare', 'node_modules', 'env-plugin', 'package.json')], 136 | hashes[fixture('compare', 'node_modules', 'plugin-copy', 'package.json')], 137 | hashes[fixture('compare', 'node_modules', 'plugin-default-opts', 'package.json')], 138 | hashes[fixture('compare', 'node_modules', 'plugin', 'package.json')], 139 | hashes[fixture('compare', 'node_modules', 'preset', 'package.json')] 140 | ]), 141 | sources: md5Hex([ 142 | hashes[fixture('compare', '.babelrc')], 143 | hashes[fixture('compare', 'dir', 'extended-by-virtual-foo.json5')], 144 | hashes[fixture('compare', 'dir', 'subdir', 'extended-by-babelrc.js')], 145 | hashes[fixture('compare', 'extended-by-virtual.json5')], 146 | hashes[fixture('compare', 'virtual.json')] 147 | ]) 148 | }) 149 | }) 150 | 151 | test('can be serialized and deserialized', t => { 152 | const babelrcDir = fixture('compare') 153 | const envNames = new Set(['foo', 'bar']) 154 | const dependencies = [ 155 | { 156 | default: true, 157 | envs: new Set(['foo']), 158 | filename: fixture('compare', 'node_modules', 'plugin', 'index.js'), 159 | fromPackage: fixture('compare', 'node_modules', 'plugin'), 160 | hash: hashes[fixture('compare', 'node_modules', 'plugin', 'package.json')] 161 | } 162 | ] 163 | const sources = [ 164 | { 165 | default: true, 166 | envs: new Set(['foo']), 167 | source: fixture('compare', '.babelrc'), 168 | hash: hashes[fixture('compare', '.babelrc')] 169 | } 170 | ] 171 | const verifier = new Verifier(babelrcDir, envNames, dependencies, sources) 172 | 173 | const buffer = verifier.toBuffer() 174 | const deserialized = Verifier.fromBuffer(buffer) 175 | t.is(deserialized.babelrcDir, babelrcDir) 176 | t.deepEqual(deserialized.envNames, envNames) 177 | t.deepEqual(deserialized.dependencies, dependencies) 178 | t.deepEqual(deserialized.sources, sources) 179 | }) 180 | 181 | test('verifyEnv() behavior with .babelrc file', async t => { 182 | const dir = path.join(tmpDir, 'babelrc') 183 | fse.copySync(fixture('verifier', 'babelrc'), dir) 184 | 185 | const verifier = await (await fromDirectory(dir)).createVerifier() 186 | const cacheKeys = verifier.cacheKeysForEnv() 187 | 188 | t.deepEqual(await verifier.verifyEnv(), { 189 | sourcesChanged: false, 190 | dependenciesChanged: false, 191 | cacheKeys, 192 | verifier 193 | }) 194 | 195 | { 196 | fs.writeFileSync(path.join(dir, 'plugin.js'), 'foo') 197 | const expectedCacheKeys = { 198 | dependencies: md5Hex([md5Hex('foo')]), 199 | sources: cacheKeys.sources 200 | } 201 | 202 | const result = await verifier.verifyEnv() 203 | const {verifier: newVerifier} = result 204 | delete result.verifier 205 | 206 | t.deepEqual(result, { 207 | sourcesChanged: false, 208 | dependenciesChanged: true, 209 | cacheKeys: expectedCacheKeys 210 | }) 211 | t.true(newVerifier !== verifier) 212 | t.deepEqual(newVerifier.cacheKeysForEnv(), expectedCacheKeys) 213 | } 214 | 215 | fs.writeFileSync(path.join(dir, 'extends.json5'), '{foo:true}') 216 | t.deepEqual(await verifier.verifyEnv(), { 217 | sourcesChanged: true 218 | }) 219 | 220 | fse.copySync(fixture('verifier', 'babelrc', 'extends.json5'), path.join(dir, 'extends.json5')) 221 | t.false((await verifier.verifyEnv()).sourcesChanged) 222 | 223 | fs.writeFileSync(path.join(dir, '.babelrc'), '{}') 224 | t.true((await verifier.verifyEnv()).sourcesChanged) 225 | }) 226 | 227 | test('verifyEnv() behavior with .babelrc.js file', async t => { 228 | const dir = path.join(tmpDir, 'babelrcjs') 229 | fse.copySync(fixture('verifier', 'babelrcjs'), dir) 230 | 231 | const verifier = await (await fromDirectory(dir)).createVerifier() 232 | const cacheKeys = verifier.cacheKeysForEnv() 233 | 234 | t.deepEqual(await verifier.verifyEnv(), { 235 | sourcesChanged: false, 236 | dependenciesChanged: false, 237 | cacheKeys, 238 | verifier 239 | }) 240 | 241 | { 242 | fs.writeFileSync(path.join(dir, 'plugin.js'), 'foo') 243 | const expectedCacheKeys = { 244 | dependencies: md5Hex([md5Hex('foo')]), 245 | sources: cacheKeys.sources 246 | } 247 | 248 | const result = await verifier.verifyEnv() 249 | const {verifier: newVerifier} = result 250 | delete result.verifier 251 | 252 | t.deepEqual(result, { 253 | sourcesChanged: false, 254 | dependenciesChanged: true, 255 | cacheKeys: expectedCacheKeys 256 | }) 257 | t.true(newVerifier !== verifier) 258 | t.deepEqual(newVerifier.cacheKeysForEnv(), expectedCacheKeys) 259 | } 260 | 261 | fs.writeFileSync(path.join(dir, 'extends.json5'), '{foo:true}') 262 | t.deepEqual(await verifier.verifyEnv(), { 263 | sourcesChanged: true 264 | }) 265 | 266 | fse.copySync(fixture('verifier', 'babelrcjs', 'extends.json5'), path.join(dir, 'extends.json5')) 267 | t.false((await verifier.verifyEnv()).sourcesChanged) 268 | 269 | fs.writeFileSync(path.join(dir, '.babelrc.js'), 'module.exports = {}') 270 | t.true((await verifier.verifyEnv()).sourcesChanged) 271 | }) 272 | 273 | test('verifyEnv() behavior with envName argument', async t => { 274 | const dir = path.join(tmpDir, 'env') 275 | fse.copySync(fixture('verifier', 'babelrc'), dir) 276 | 277 | const verifier = await (await fromDirectory(dir)).createVerifier() 278 | const cacheKeys = verifier.cacheKeysForEnv('foo') 279 | 280 | t.deepEqual(await verifier.verifyEnv('foo'), { 281 | sourcesChanged: false, 282 | dependenciesChanged: false, 283 | cacheKeys, 284 | verifier 285 | }) 286 | 287 | { 288 | fs.writeFileSync(path.join(dir, 'foo.js'), 'foo') 289 | const expectedCacheKeys = { 290 | dependencies: md5Hex([md5Hex('foo'), md5Hex('module.exports = () => {}\n')]), 291 | sources: cacheKeys.sources 292 | } 293 | 294 | const result = await verifier.verifyEnv('foo') 295 | const {verifier: newVerifier} = result 296 | delete result.verifier 297 | 298 | t.deepEqual(result, { 299 | sourcesChanged: false, 300 | dependenciesChanged: true, 301 | cacheKeys: expectedCacheKeys 302 | }) 303 | t.true(newVerifier !== verifier) 304 | t.deepEqual(newVerifier.cacheKeysForEnv('foo'), expectedCacheKeys) 305 | } 306 | 307 | fs.writeFileSync(path.join(dir, 'extends.json5'), '{foo:true}') 308 | t.deepEqual(await verifier.verifyEnv(), { 309 | sourcesChanged: true 310 | }) 311 | 312 | fse.copySync(fixture('verifier', 'babelrc', 'extends.json5'), path.join(dir, 'extends.json5')) 313 | t.false((await verifier.verifyEnv()).sourcesChanged) 314 | 315 | fs.writeFileSync(path.join(dir, '.babelrc'), '{}') 316 | t.true((await verifier.verifyEnv()).sourcesChanged) 317 | }) 318 | 319 | test('verifyEnv() behavior without .babelrc or .babelrcjs file', async t => { 320 | const dir = path.join(tmpDir, 'pkg') 321 | fse.copySync(fixture('verifier', 'pkg'), dir) 322 | 323 | const verifier = await (await fromDirectory(dir)).createVerifier() 324 | const cacheKeys = verifier.cacheKeysForEnv() 325 | 326 | t.deepEqual(await verifier.verifyEnv(), { 327 | sourcesChanged: false, 328 | dependenciesChanged: false, 329 | cacheKeys, 330 | verifier 331 | }) 332 | 333 | fs.writeFileSync(path.join(dir, 'extends.json5'), '{foo:true}') 334 | t.deepEqual(await verifier.verifyEnv(), { 335 | sourcesChanged: true 336 | }) 337 | 338 | fse.copySync(fixture('verifier', 'pkg', 'extends.json5'), path.join(dir, 'extends.json5')) 339 | t.false((await verifier.verifyEnv()).sourcesChanged) 340 | 341 | fs.writeFileSync(path.join(dir, '.babelrc'), '{}') 342 | t.true((await verifier.verifyEnv()).sourcesChanged) 343 | 344 | fs.unlinkSync(path.join(dir, '.babelrc')) 345 | fs.writeFileSync(path.join(dir, '.babelrc.js'), '{}') 346 | t.true((await verifier.verifyEnv()).sourcesChanged) 347 | }) 348 | 349 | test('verifyEnv() behavior if .babelrc or .babelrc.js sources do not need to be consulted at all', async t => { 350 | const dir = path.join(tmpDir, 'no-babelrc') 351 | fse.copySync(fixture('verifier', 'babelrc'), dir) 352 | 353 | const config = createConfig({ 354 | options: { 355 | babelrc: false 356 | }, 357 | source: 'source', 358 | hash: 'hash of source' 359 | }) 360 | const fixedHashes = { 361 | sources: new Map([['source', 'hash of source']]) 362 | } 363 | const verifier = await (await fromConfig(config)).createVerifier() 364 | const cacheKeys = verifier.cacheKeysForEnv() 365 | 366 | t.deepEqual(await verifier.verifyEnv(null, fixedHashes), { 367 | sourcesChanged: false, 368 | dependenciesChanged: false, 369 | cacheKeys, 370 | verifier 371 | }) 372 | 373 | fs.writeFileSync(path.join(dir, '.babelrc'), '{}') 374 | t.false((await verifier.verifyEnv(null, fixedHashes)).sourcesChanged) 375 | 376 | fs.writeFileSync(path.join(dir, '.babelrc.js'), '{}') 377 | t.false((await verifier.verifyEnv(null, fixedHashes)).sourcesChanged) 378 | }) 379 | 380 | test('verifyEnv() can take fixed source hashes', async t => { 381 | const dir = path.join(tmpDir, 'fixed-source-hashes') 382 | fse.copySync(fixture('verifier', 'pkg'), dir) 383 | 384 | const result = await fromConfig(createConfig({ 385 | options: {babelrc: true}, 386 | source: 'foo', 387 | hash: 'hash of foo' 388 | }), {cache: prepareCache()}) 389 | const verifier = await result.createVerifier() 390 | 391 | const cacheKeys = verifier.cacheKeysForEnv() 392 | 393 | const fixedHashes = {sources: new Map([['foo', 'hash of foo']])} 394 | t.deepEqual(await verifier.verifyEnv(null, fixedHashes), { 395 | sourcesChanged: false, 396 | dependenciesChanged: false, 397 | cacheKeys, 398 | verifier 399 | }) 400 | }) 401 | 402 | test('verifyEnv() can use cache', async t => { 403 | const dir = path.join(tmpDir, 'use-cache') 404 | fse.copySync(fixture('verifier', 'pkg'), dir) 405 | const plugin = resolveFrom(dir, './plugin.js') 406 | 407 | const cache = prepareCache() 408 | const verifier = await (await fromDirectory(dir)).createVerifier() 409 | await verifier.verifyEnv(null, null, cache) 410 | 411 | t.deepEqual(Array.from(cache.dependencyHashes.keys()), [ 412 | plugin 413 | ]) 414 | t.deepEqual(Array.from(cache.fileExistence.keys()), [ 415 | path.join(dir, '.babelrc'), 416 | path.join(dir, '.babelrc.js') 417 | ]) 418 | t.deepEqual(Array.from(cache.files.keys()), [ 419 | path.join(dir, 'extends.json5'), 420 | path.join(dir, 'package.json'), 421 | plugin 422 | ]) 423 | t.deepEqual(Array.from(cache.sourceHashes.keys()), [ 424 | path.join(dir, 'extends.json5'), 425 | path.join(dir, 'package.json') 426 | ]) 427 | 428 | const access = td.func() 429 | const buffer = verifier.toBuffer() 430 | const stubbedVerifier = proxyquire('../build/Verifier', { 431 | fs: {access} 432 | }).default.fromBuffer(buffer) 433 | 434 | stubbedVerifier.verifyEnv(null, null, cache) 435 | t.true(td.explain(access).callCount === 0) 436 | }) 437 | 438 | test('verifyEnv() behavior when dependency goes missing', async t => { 439 | const dir = path.join(tmpDir, 'missing-dependency') 440 | fse.copySync(fixture('verifier', 'pkg'), dir) 441 | 442 | const verifier = await (await fromDirectory(dir)).createVerifier() 443 | fse.removeSync(path.join(dir, 'plugin.js')) 444 | 445 | t.deepEqual(await verifier.verifyEnv(), { 446 | badDependency: true 447 | }) 448 | }) 449 | 450 | test('verifyEnv() behavior when source goes missing', async t => { 451 | const dir = path.join(tmpDir, 'missing-source') 452 | fse.copySync(fixture('verifier', 'pkg'), dir) 453 | 454 | const verifier = await (await fromDirectory(dir)).createVerifier() 455 | fse.removeSync(path.join(dir, 'extends.json5')) 456 | 457 | t.deepEqual(await verifier.verifyEnv(), { 458 | missingSource: true 459 | }) 460 | }) 461 | 462 | test('verifyEnv() behavior with unexpected errors', async t => { 463 | const dir = path.join(tmpDir, 'unexpected-errors') 464 | fse.copySync(fixture('verifier', 'pkg'), dir) 465 | 466 | const expected = new Error() 467 | const access = td.func() 468 | td.when(access(path.join(dir, '.babelrc'))).thenCallback(expected) 469 | 470 | const buffer = (await (await fromDirectory(dir)).createVerifier()).toBuffer() 471 | const verifier = proxyquire('../build/Verifier', { 472 | fs: {access} 473 | }).default.fromBuffer(buffer) 474 | 475 | const actual = await t.throws(verifier.verifyEnv()) 476 | t.is(actual, expected) 477 | }) 478 | -------------------------------------------------------------------------------- /test/codegen.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | import test from 'ava' 4 | import replaceString from 'replace-string' 5 | 6 | import {createConfig, prepareCache} from '..' 7 | import codegen from '../build/codegen' 8 | import * as collector from '../build/collector' 9 | import reduceChains from '../build/reduceChains' 10 | import fixture from './helpers/fixture' 11 | import runGeneratedCode from './helpers/runGeneratedCode' 12 | 13 | const source = path.join(__dirname, 'fixtures', 'empty', 'source.js') 14 | 15 | const compile = async ({options = {}, expectedEnvNames, cache}) => { 16 | const chains = await collector.fromConfig(createConfig({ 17 | fileType: 'JSON', 18 | options, 19 | dir: __dirname, 20 | source 21 | }), expectedEnvNames, cache) 22 | const code = codegen(reduceChains(chains, cache)) 23 | return runGeneratedCode(code) 24 | } 25 | 26 | test('stringifies using JSON5 unless chain is marked otherwise', async t => { 27 | const chains = await collector.fromConfig(createConfig({ 28 | fileType: 'JSON', 29 | options: {sourceType: 'module'}, 30 | source 31 | })) 32 | const code = codegen(reduceChains(chains)) 33 | 34 | t.true(code.includes('"sourceType": "module"')) 35 | }) 36 | 37 | test('by default stringifies using JSON5', async t => { 38 | const chains = await collector.fromConfig(createConfig({ 39 | options: {sourceType: 'module'}, 40 | source 41 | })) 42 | const code = codegen(reduceChains(chains)) 43 | 44 | t.true(code.includes("sourceType: 'module'")) 45 | }) 46 | 47 | test('generates a nicely indented module', async t => { 48 | const chains = await collector.fromConfig(createConfig({ 49 | options: { 50 | extends: path.join(__dirname, 'fixtures', 'compare', '.babelrc') 51 | }, 52 | source 53 | })) 54 | 55 | let code = codegen(reduceChains(chains)) 56 | let cwd = process.cwd() 57 | if (path.sep === path.win32.sep) { 58 | // File paths are JSON encoded, which means the separator is doubled. 59 | // Replace by the POSIX separator so the snapshot can be matched. 60 | code = replaceString(code, path.win32.sep + path.win32.sep, path.posix.sep) 61 | // Similarly normalize the CWD. Ensure any trailing slashes are removed, 62 | // e.g. when the CWD is `Z:/`. 63 | cwd = replaceString(cwd, path.win32.sep, path.posix.sep).replace(/\/$/, '') 64 | } 65 | 66 | t.snapshot(replaceString(code, cwd, '~')) 67 | }) 68 | 69 | test('resulting options have no plugins if resolved config has an empty plugins array', async t => { 70 | const options = (await compile({options: {plugins: []}})).getOptions() 71 | t.false(options.hasOwnProperty('plugins')) 72 | }) 73 | 74 | test('resulting options have no presets if resolved config has an empty presets array', async t => { 75 | const options = (await compile({options: {presets: []}})).getOptions() 76 | t.false(options.hasOwnProperty('presets')) 77 | }) 78 | 79 | test('JS configs require a cache to be passed', async t => { 80 | const {getOptions} = await compile({ 81 | options: { 82 | extends: fixture('codegen/empty.js') 83 | } 84 | }) 85 | const err = t.throws(() => getOptions()) 86 | t.is(err.name, 'Error') 87 | t.is(err.message, `A cache is required to load the configuration module at '${fixture('codegen/empty.js')}'`) 88 | }) 89 | 90 | test('JS configs require a primed cache to be passed', async t => { 91 | const {getOptions} = await compile({ 92 | options: { 93 | extends: fixture('codegen/empty.js') 94 | } 95 | }) 96 | const err = t.throws(() => getOptions(null, prepareCache())) 97 | t.is(err.name, 'Error') 98 | t.is(err.message, `Could not find the configuration module for '${fixture('codegen/empty.js')}' in the cache`) 99 | }) 100 | 101 | test('env-specific JS configs require an env-primed cache to be passed', async t => { 102 | const cache = prepareCache() 103 | const {getOptions} = await compile({ 104 | options: { 105 | extends: fixture('codegen/env-specific.js') 106 | }, 107 | cache 108 | }) 109 | const err = t.throws(() => getOptions('foo', cache)) 110 | t.is(err.name, 'Error') 111 | t.is(err.message, `Could not find the configuration module, specific to the 'foo' environment, for '${fixture('codegen/env-specific.js')}', in the cache`) // eslint-disable-line max-len 112 | }) 113 | 114 | test('string-based plugin or preset identifiers in JS configs must be resolved via the cache', async t => { 115 | const cache = prepareCache() 116 | const {getOptions} = await compile({ 117 | options: { 118 | extends: fixture('codegen/plugin-resolution.js') 119 | }, 120 | cache 121 | }) 122 | cache.pluginsAndPresets.get(fixture('codegen')).clear() 123 | const err = t.throws(() => getOptions(null, cache)) 124 | t.is(err.name, 'ResolveFromCacheError') 125 | t.is(err.message, `${fixture('codegen/plugin-resolution.js')}: Couldn't find plugin "module:noop" in cache`) 126 | t.is(err.source, fixture('codegen/plugin-resolution.js')) 127 | t.is(err.ref, 'module:noop') 128 | t.true(err.isPlugin) 129 | t.false(err.isPreset) 130 | }) 131 | -------------------------------------------------------------------------------- /test/collector-errors.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | 3 | import {createConfig} from '..' 4 | import * as collector from '../build/collector' 5 | import fixture from './helpers/fixture' 6 | 7 | test('fails when parsing invalid JSON5', async t => { 8 | const err = await t.throws(collector.fromDirectory(fixture('bad-rc', 'invalid-json5'))) 9 | t.is(err.name, 'ParseError') 10 | t.is(err.source, fixture('bad-rc', 'invalid-json5', '.babelrc')) 11 | }) 12 | 13 | { 14 | const fails = async (t, kind) => { 15 | const err = await t.throws(collector.fromDirectory(fixture('bad-rc', kind))) 16 | t.is(err.name, 'InvalidFileError') 17 | t.is(err.source, fixture('bad-rc', kind, '.babelrc')) 18 | } 19 | fails.title = (title, kind) => `chain contains empty options when .babelrc is ${title || kind}` 20 | 21 | test(fails, 'array') 22 | test(fails, 'falsy') 23 | test(fails, 'null') 24 | test('not an object', fails, 'bool') 25 | } 26 | 27 | test('fails when both sourceMap and sourceMaps options are present', async t => { 28 | const err = await t.throws(collector.fromDirectory(fixture('bad-rc', 'conflicting-sourcemaps-option'))) 29 | t.is(err.name, 'InvalidFileError') 30 | t.is(err.source, fixture('bad-rc', 'conflicting-sourcemaps-option', '.babelrc')) 31 | }) 32 | 33 | { 34 | const invalid = async (t, option) => { 35 | const err = await t.throws(collector.fromDirectory(fixture('bad-rc', `${option}-option`))) 36 | t.is(err.name, 'InvalidFileError') 37 | t.is(err.source, fixture('bad-rc', `${option}-option`, '.babelrc')) 38 | } 39 | invalid.title = (desc, option) => `fails when ${desc || `${option} option`} is present` 40 | 41 | test(invalid, 'cwd') 42 | test(invalid, 'filename') 43 | test(invalid, 'filenameRelative') 44 | test(invalid, 'babelrc') 45 | test(invalid, 'code') 46 | test(invalid, 'ast') 47 | test(invalid, 'envName') 48 | test('nested env option', invalid, 'nested-env') 49 | test('non-array overrides option', invalid, 'overrides-not-array') 50 | test('overrides option containing falsy values', invalid, 'overrides-contains-falsy') 51 | test('nested overrides option', invalid, 'nested-overrides') 52 | test('overrides inside env options', invalid, 'overrides-in-env') 53 | } 54 | 55 | test('fails when parsing invalid JSON', async t => { 56 | const err = await t.throws(collector.fromDirectory(fixture('bad-pkg', 'invalid-json'))) 57 | t.is(err.name, 'ParseError') 58 | t.is(err.source, fixture('bad-pkg', 'invalid-json', 'package.json')) 59 | }) 60 | 61 | { 62 | const empty = async (t, kind) => { 63 | t.is(await collector.fromDirectory(fixture('bad-pkg', kind)), null) 64 | } 65 | empty.title = (title, kind) => `no chain when package.json is ${title || kind}` 66 | 67 | test(empty, 'falsy') 68 | test(empty, 'null') 69 | test('without a "babel" key', empty, 'without-babel') 70 | test('with a "babel" value that is null', empty, 'null-babel') 71 | } 72 | 73 | { 74 | const fail = async (t, kind) => { 75 | const err = await t.throws(collector.fromDirectory(fixture('bad-pkg', kind))) 76 | t.is(err.name, 'InvalidFileError') 77 | t.is(err.source, fixture('bad-pkg', kind, 'package.json')) 78 | } 79 | fail.title = (title, kind) => `fails when "babel" value in package.json is ${title}` 80 | 81 | test('an array', fail, 'array-babel') 82 | test('truthy but not an object', fail, 'bool-babel') 83 | } 84 | 85 | test('fails when module throws when loaded', async t => { 86 | const err = await t.throws(collector.fromDirectory(fixture('bad-js', 'syntax-error'))) 87 | t.is(err.name, 'ParseError') 88 | t.is(err.source, fixture('bad-js', 'syntax-error', '.babelrc.js')) 89 | t.is(err.parent.name, 'SyntaxError') 90 | }) 91 | 92 | test('fails when module exports a promise', async t => { 93 | const err = await t.throws(collector.fromDirectory(fixture('bad-js', 'promise-from-factory'))) 94 | t.is(err.name, 'InvalidFileError') 95 | t.is(err.source, fixture('bad-js', 'promise-from-factory', '.babelrc.js')) 96 | }) 97 | 98 | test('fails when module exports a promise-returning-factory', async t => { 99 | const err = await t.throws(collector.fromDirectory(fixture('bad-js', 'promise-export'))) 100 | t.is(err.name, 'InvalidFileError') 101 | t.is(err.source, fixture('bad-js', 'promise-export', '.babelrc.js')) 102 | }) 103 | 104 | test('fails when module exports a factory that does not configure the cache', async t => { 105 | const err = await t.throws(collector.fromDirectory(fixture('bad-js', 'no-cache-configuration'))) 106 | t.is(err.name, 'InvalidFileError') 107 | t.is(err.source, fixture('bad-js', 'no-cache-configuration', '.babelrc.js')) 108 | }) 109 | 110 | test('fails when module exports a factory that throws', async t => { 111 | const err = await t.throws(collector.fromDirectory(fixture('bad-js', 'factory-throws'))) 112 | t.is(err.name, 'ParseError') 113 | t.is(err.source, fixture('bad-js', 'factory-throws', '.babelrc.js')) 114 | t.is(err.parent.message, 'Oops') 115 | }) 116 | 117 | test('fails when a directory contains .babelrc and package.json#babel', async t => { 118 | const err = await t.throws(collector.fromDirectory(fixture('multiple-sources', 'babelrc-and-pkg'))) 119 | t.is(err.name, 'MultipleSourcesError') 120 | t.is(err.source, fixture('multiple-sources', 'babelrc-and-pkg', '.babelrc')) 121 | t.is(err.otherSource, fixture('multiple-sources', 'babelrc-and-pkg', 'package.json')) 122 | }) 123 | 124 | test('fails when a directory contains .babelrc and .babelrc.js', async t => { 125 | const err = await t.throws(collector.fromDirectory(fixture('multiple-sources', 'babelrc-and-js'))) 126 | t.is(err.name, 'MultipleSourcesError') 127 | t.is(err.source, fixture('multiple-sources', 'babelrc-and-js', '.babelrc')) 128 | t.is(err.otherSource, fixture('multiple-sources', 'babelrc-and-js', '.babelrc.js')) 129 | }) 130 | 131 | test('fails when a directory contains .babelrc.js and package.json#babel', async t => { 132 | const err = await t.throws(collector.fromDirectory(fixture('multiple-sources', 'js-and-pkg'))) 133 | t.is(err.name, 'MultipleSourcesError') 134 | t.is(err.source, fixture('multiple-sources', 'js-and-pkg', '.babelrc.js')) 135 | t.is(err.otherSource, fixture('multiple-sources', 'js-and-pkg', 'package.json')) 136 | }) 137 | 138 | test('fails when extending from a non-existent .babelrc file', async t => { 139 | const err = await t.throws(collector.fromConfig(createConfig({ 140 | options: {extends: 'non-existent'}, 141 | source: fixture('source.js') 142 | }))) 143 | t.is(err.name, 'ExtendsError') 144 | t.is(err.clause, 'non-existent') 145 | t.is(err.source, fixture('source.js')) 146 | t.is(err.parent.name, 'NoSourceFileError') 147 | t.is(err.parent.source, fixture('non-existent')) 148 | }) 149 | 150 | test('fails when extending from a non-existent .babelrc.js file', async t => { 151 | const err = await t.throws(collector.fromConfig(createConfig({ 152 | options: {extends: 'non-existent.js'}, 153 | source: fixture('source.js') 154 | }))) 155 | t.is(err.name, 'ExtendsError') 156 | t.is(err.clause, 'non-existent.js') 157 | t.is(err.source, fixture('source.js')) 158 | t.is(err.parent.name, 'NoSourceFileError') 159 | t.is(err.parent.source, fixture('non-existent.js')) 160 | }) 161 | 162 | test('fails when extending from an invalid file', async t => { 163 | const err = await t.throws(collector.fromConfig(createConfig({ 164 | options: {extends: 'invalid-json5/.babelrc'}, 165 | source: fixture('bad-rc', 'source.js') 166 | }))) 167 | t.is(err.name, 'ParseError') 168 | t.is(err.source, fixture('bad-rc', 'invalid-json5', '.babelrc')) 169 | t.is(err.parent.name, 'SyntaxError') 170 | }) 171 | 172 | { 173 | const fail = async (t, kind) => { 174 | const err = await t.throws(collector.fromConfig(createConfig({ 175 | options: {extends: `${kind}/.babelrc`}, 176 | source: fixture('bad-rc', 'source.js') 177 | }))) 178 | t.is(err.name, 'InvalidFileError') 179 | t.is(err.source, fixture('bad-rc', kind, '.babelrc')) 180 | t.is(err.parent, null) 181 | } 182 | fail.title = title => `fails when extending from a ${title}` 183 | 184 | test('falsy file', fail, 'falsy') 185 | test('null file', fail, 'null') 186 | test('non-object file', fail, 'bool') 187 | } 188 | -------------------------------------------------------------------------------- /test/compare.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import {runInNewContext} from 'vm' 3 | 4 | import test from 'ava' 5 | import {transform} from '@babel/core' 6 | 7 | import {createConfig, fromConfig, fromDirectory, prepareCache} from '..' 8 | import fixture from './helpers/fixture' 9 | import runGeneratedCode from './helpers/runGeneratedCode' 10 | 11 | const virtualJson = require.resolve('./fixtures/compare/virtual.json') 12 | const virtualOptions = require('./fixtures/compare/virtual.json') 13 | 14 | function setBabelEnv (envName) { 15 | if (envName) { 16 | process.env.BABEL_ENV = envName 17 | } else { 18 | delete process.env.BABEL_ENV 19 | } 20 | } 21 | 22 | function setNodeEnv (envName) { 23 | if (envName) { 24 | process.env.NODE_ENV = envName 25 | } else { 26 | delete process.env.NODE_ENV 27 | } 28 | } 29 | 30 | function transformChain (computedOptions, filename) { 31 | const {code, map} = transform('[]', Object.assign(computedOptions, { 32 | cwd: path.dirname(filename), 33 | filename 34 | })) 35 | return [runInNewContext(code), map] 36 | } 37 | 38 | function transformBabel (options) { 39 | const {code, map} = transform('[]', options) 40 | return [runInNewContext(code), map] 41 | } 42 | 43 | test.serial('resolved config matches @babel/core', async t => { 44 | const cache = prepareCache() 45 | const config = await fromConfig(createConfig({options: virtualOptions, source: virtualJson}), {cache}) 46 | const configModule = runGeneratedCode(config.generateModule()) 47 | 48 | const babelOptions = { 49 | babelrc: true, 50 | extends: virtualJson, 51 | filename: virtualJson 52 | } 53 | 54 | setBabelEnv() 55 | t.deepEqual(transformChain(configModule.getOptions(null, cache), virtualJson), transformBabel(babelOptions), 'no BABEL_ENV') 56 | 57 | setBabelEnv('foo') 58 | t.deepEqual(transformChain(configModule.getOptions(null, cache), virtualJson), transformBabel(babelOptions), 'BABEL_ENV=foo') 59 | 60 | setBabelEnv() 61 | setNodeEnv('foo') 62 | t.deepEqual( 63 | transformChain(configModule.getOptions(null, cache), virtualJson), 64 | transformBabel(babelOptions), 65 | 'no BABEL_ENV, NODE_ENV=foo') 66 | 67 | setBabelEnv() 68 | setNodeEnv() 69 | t.deepEqual( 70 | transformChain(configModule.getOptions('foo', cache), virtualJson), 71 | transformBabel(Object.assign({envName: 'foo'}, babelOptions)), 72 | 'explicit envName') 73 | }) 74 | 75 | test.serial('resolved js-env config matches @babel/core', async t => { 76 | setBabelEnv() 77 | setNodeEnv() 78 | 79 | const cache = prepareCache() 80 | const config = await fromDirectory(fixture('compare/js-env'), {expectedEnvNames: ['foo'], cache}) 81 | const configModule = runGeneratedCode(config.generateModule()) 82 | const filename = fixture('compare/js-env/foo.js') 83 | 84 | t.deepEqual( 85 | transformChain(configModule.getOptions('foo', cache), 'foo.js'), 86 | transformBabel({filename, envName: 'foo'}), 87 | '.babelrc: envName = foo') 88 | }) 89 | 90 | test.serial('resolved overrides config matches @babel/core', async t => { 91 | setBabelEnv() 92 | setNodeEnv() 93 | 94 | { 95 | const cache = prepareCache() 96 | const config = await fromDirectory(fixture('compare/overrides'), {cache}) 97 | const configModule = runGeneratedCode(config.generateModule()) 98 | const filename = fixture('compare/overrides/foo.js') 99 | 100 | t.deepEqual( 101 | transformChain(configModule.getOptions(null, cache), filename), 102 | transformBabel({filename}), 103 | '.babelrc: no NODE_ENV or BABEL_ENV') 104 | 105 | t.deepEqual( 106 | transformChain(configModule.getOptions('foo', cache), 'foo.js'), 107 | transformBabel({filename, envName: 'foo'}), 108 | '.babelrc: envName = foo') 109 | } 110 | 111 | { 112 | const cache = prepareCache() 113 | const config = await fromDirectory(fixture('compare/overrides', 'js'), {cache}) 114 | const configModule = runGeneratedCode(config.generateModule()) 115 | const filename = fixture('compare/overrides/js/foo.js') 116 | 117 | t.deepEqual( 118 | transformChain(configModule.getOptions(null, cache), filename), 119 | transformBabel({filename}), 120 | '.babelrc.js: no NODE_ENV or BABEL_ENV') 121 | 122 | t.deepEqual( 123 | transformChain(configModule.getOptions('foo', cache), 'foo.js'), 124 | transformBabel({filename, envName: 'foo'}), 125 | '.babelrc.js: envName = foo') 126 | } 127 | }) 128 | 129 | test.serial('resolved presets config matches @babel/core', async t => { 130 | setBabelEnv() 131 | setNodeEnv() 132 | 133 | const cache = prepareCache() 134 | const config = await fromDirectory(fixture('compare/presets'), {cache}) 135 | const configModule = runGeneratedCode(config.generateModule()) 136 | const filename = fixture('compare/presets/foo.js') 137 | 138 | t.deepEqual( 139 | transformChain(configModule.getOptions(null, cache), 'foo.js'), 140 | transformBabel({filename, envName: 'foo'}), 141 | '.babelrc: envName = foo') 142 | }) 143 | 144 | // To reach 100% code coverage 145 | test.serial('resolved js-normalization-edge-cases config matches @babel/core', async t => { 146 | setBabelEnv() 147 | setNodeEnv() 148 | 149 | const cache = prepareCache() 150 | const config = await fromDirectory(fixture('compare/js-normalization-edge-cases'), {cache}) 151 | const configModule = runGeneratedCode(config.generateModule()) 152 | const filename = fixture('compare/js-normalization-edge-cases/foo.js') 153 | 154 | t.deepEqual( 155 | transformChain(configModule.getOptions(null, cache), 'foo.js'), 156 | transformBabel({filename, envName: 'foo'}), 157 | '.babelrc: envName = foo') 158 | }) 159 | -------------------------------------------------------------------------------- /test/fixtures/babel-config-chain-js/.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "plugins": ["root"], 3 | "extends": "./extended.babelrc.js" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/babel-config-chain-js/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2014-2017 Sebastian McKenzie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /test/fixtures/babel-config-chain-js/README.md: -------------------------------------------------------------------------------- 1 | These fixtures were taken from 2 | . 3 | 4 | Note that this module does not currently walk the file system in search for 5 | `.babelrc` file, so the `dir1` fixture has not been included. 6 | 7 | Similarly this module does not currently respect `.babelignore` files, and 8 | consequently `.babelignore` files have not been included. 9 | -------------------------------------------------------------------------------- /test/fixtures/babel-config-chain-js/dir2/.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "plugins": [ 3 | "dir2" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/babel-config-chain-js/dir2/src.js: -------------------------------------------------------------------------------- 1 | // empty 2 | -------------------------------------------------------------------------------- /test/fixtures/babel-config-chain-js/env/.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = context => { 2 | const env = context.env() 3 | const plugins = ['env-base'] 4 | if (env === 'foo') { 5 | return { 6 | plugins, 7 | env: { 8 | foo: { 9 | plugins: ['env-foo'] 10 | } 11 | } 12 | } 13 | } 14 | 15 | if (env === 'bar') { 16 | plugins.push(`env-${env}`) 17 | } 18 | return {plugins} 19 | } 20 | -------------------------------------------------------------------------------- /test/fixtures/babel-config-chain-js/env/src.js: -------------------------------------------------------------------------------- 1 | // empty 2 | -------------------------------------------------------------------------------- /test/fixtures/babel-config-chain-js/extended.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'plugins': [ 3 | 'extended' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/babel-config-chain-js/pkg/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "application-name", 3 | "version": "0.0.1", 4 | "babel": { 5 | "plugins": ["pkg-plugin"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/babel-config-chain-js/pkg/src.js: -------------------------------------------------------------------------------- 1 | // empty 2 | -------------------------------------------------------------------------------- /test/fixtures/babel-config-chain/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["root"], 3 | "extends": "./extended.babelrc.json" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/babel-config-chain/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2014-2017 Sebastian McKenzie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /test/fixtures/babel-config-chain/README.md: -------------------------------------------------------------------------------- 1 | These fixtures were taken from 2 | . 3 | 4 | Note that this module does not currently walk the file system in search for 5 | `.babelrc` file, so the `dir1` fixture has not been included. 6 | 7 | Similarly this module does not currently respect `.babelignore` files, and 8 | consequently `.babelignore` files have not been included. 9 | -------------------------------------------------------------------------------- /test/fixtures/babel-config-chain/dir2/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "dir2" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/babel-config-chain/dir2/src.js: -------------------------------------------------------------------------------- 1 | // empty 2 | -------------------------------------------------------------------------------- /test/fixtures/babel-config-chain/env/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["env-base"], 3 | "env": { 4 | "foo": { 5 | "plugins": ["env-foo"] 6 | }, 7 | "bar": { 8 | "plugins": ["env-bar"] 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures/babel-config-chain/env/src.js: -------------------------------------------------------------------------------- 1 | // empty 2 | -------------------------------------------------------------------------------- /test/fixtures/babel-config-chain/extended.babelrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "extended" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/babel-config-chain/pkg/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "application-name", 3 | "version": "0.0.1", 4 | "babel": { 5 | "plugins": ["pkg-plugin"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/babel-config-chain/pkg/src.js: -------------------------------------------------------------------------------- 1 | // empty 2 | -------------------------------------------------------------------------------- /test/fixtures/babelrc/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | plugins: ['babelrc'] 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/bad-js/factory-throws/.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = () => { 2 | throw new Error('Oops') 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/bad-js/no-cache-configuration/.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = () => {{}} 2 | -------------------------------------------------------------------------------- /test/fixtures/bad-js/promise-export/.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = Promise.resolve() 2 | -------------------------------------------------------------------------------- /test/fixtures/bad-js/promise-from-factory/.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = api => { 2 | api.cache.forever() 3 | return Promise.resolve() 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/bad-js/syntax-error/.babelrc.js: -------------------------------------------------------------------------------- 1 | if (true) { 2 | -------------------------------------------------------------------------------- /test/fixtures/bad-pkg/array-babel/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "babel": [] 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/bad-pkg/bool-babel/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "babel": true 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/bad-pkg/falsy/package.json: -------------------------------------------------------------------------------- 1 | "" 2 | -------------------------------------------------------------------------------- /test/fixtures/bad-pkg/invalid-json/package.json: -------------------------------------------------------------------------------- 1 | { 2 | foo: "bar" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/bad-pkg/null-babel/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "babel": null 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/bad-pkg/null/package.json: -------------------------------------------------------------------------------- 1 | null 2 | -------------------------------------------------------------------------------- /test/fixtures/bad-pkg/without-babel/package.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /test/fixtures/bad-rc/array/.babelrc: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /test/fixtures/bad-rc/ast-option/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | ast: {} 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/bad-rc/babelrc-option/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | babelrc: true 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/bad-rc/bool/.babelrc: -------------------------------------------------------------------------------- 1 | true 2 | -------------------------------------------------------------------------------- /test/fixtures/bad-rc/code-option/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | code: '{}' 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/bad-rc/conflicting-sourcemaps-option/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | sourceMap: true, 3 | sourceMaps: true 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/bad-rc/cwd-option/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | cwd: '/' 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/bad-rc/envName-option/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | envName: 'test' 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/bad-rc/falsy/.babelrc: -------------------------------------------------------------------------------- 1 | "" 2 | -------------------------------------------------------------------------------- /test/fixtures/bad-rc/filename-option/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | filename: '.babelrc' 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/bad-rc/filenameRelative-option/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | filenameRelative: '.babelrc' 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/bad-rc/invalid-json5/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | foo () {} 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/bad-rc/nested-env-option/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | env: { 3 | test: { 4 | env: {} 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/bad-rc/nested-overrides-option/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | overrides: [{overrides: []}] 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/bad-rc/null/.babelrc: -------------------------------------------------------------------------------- 1 | null 2 | -------------------------------------------------------------------------------- /test/fixtures/bad-rc/overrides-contains-falsy-option/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | overrides: [null] 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/bad-rc/overrides-in-env-option/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | env: { 3 | foo: { 4 | overrides: [] 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/bad-rc/overrides-not-array-option/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | overrides: true 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/codegen/empty.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/novemberborn/hullabaloo-config-manager/7c6a877ee4659b19a56652ad003e3856c64290e7/test/fixtures/codegen/empty.js -------------------------------------------------------------------------------- /test/fixtures/codegen/env-specific.js: -------------------------------------------------------------------------------- 1 | module.exports = api => { 2 | api.env() 3 | return {} 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/codegen/node_modules/noop/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = babel => { 4 | return { 5 | visitor: {} 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/codegen/node_modules/noop/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noop", 3 | "version": "1.0.0", 4 | "private": true, 5 | "standard-engine": "@novemberborn/as-i-preach" 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/codegen/plugin-resolution.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ['module:noop'] 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/compare/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | extends: './dir/subdir/extended-by-babelrc.js', 3 | plugins: [ 4 | ['module:plugin', {label: 'plugin@babelrc.1'}], 5 | ['module:plugin', {label: 'plugin@babelrc.2'}, 'plugin@babelrc.2'], 6 | ['module:plugin', {label: 'plugin@babelrc.3'}, 'plugin@babelrc.3'], 7 | ['module:plugin', {label: 'plugin-not-copied'}, 'copy-or-not'] 8 | ], 9 | presets: [ 10 | ['module:preset', {label: 'preset@babelrc'}, 'preset@babelrc'] 11 | ], 12 | env: { 13 | foo: { 14 | plugins: [ 15 | ['module:plugin', {label: 'plugin@babelrc.1.foo'}], 16 | ['module:plugin', {label: 'plugin@babelrc.2.foo'}, 'plugin@babelrc.2'], 17 | ['module:env-plugin', {label: 'env-plugin@babelrc.foo'}, 'plugin@babelrc.foo'], 18 | ['module:plugin-default-opts'] 19 | ], 20 | presets: [ 21 | ['module:preset', {label: 'preset@babelrc.foo'}, 'preset@babelrc.foo'] 22 | ] 23 | } 24 | }, 25 | sourceMaps: false 26 | } 27 | -------------------------------------------------------------------------------- /test/fixtures/compare/dir/extended-by-virtual-foo.json5: -------------------------------------------------------------------------------- 1 | { 2 | plugins: [ 3 | ['module:plugin', {label: 'plugin@extended-by-virtual-foo'}, 'plugin@extended-by-virtual-foo'] 4 | ], 5 | presets: [ 6 | ['module:preset', {label: 'preset@extended-by-virtual-foo'}, 'preset@extended-by-virtual-foo'] 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/compare/dir/subdir/extended-by-babelrc.js: -------------------------------------------------------------------------------- 1 | const plugin = require('plugin') // eslint-disable-line import/no-extraneous-dependencies 2 | 3 | module.exports = options => { 4 | options.cache.forever() 5 | return { 6 | plugins: [ 7 | ['module:plugin', {label: 'plugin@extended-by-babelrc.1'}], 8 | [plugin, {label: 'plugin@extended-by-babelrc.2'}, 'plugin@extended-by-babelrc.2'], 9 | ['module:plugin-copy', {label: 'plugin-copy'}, 'copy-or-not'] 10 | ], 11 | presets: [ 12 | [ 13 | require('preset').default, // eslint-disable-line import/no-extraneous-dependencies 14 | {label: 'preset@extended-by-babelrc'}, 15 | 'preset@extended-by-babelrc' 16 | ] 17 | ], 18 | env: { 19 | foo: { 20 | plugins: [ 21 | ['module:plugin', {label: 'plugin@extended-by-babelrc.1.foo'}], 22 | ['module:plugin', {label: 'plugin@extended-by-babelrc.2.foo'}, 'plugin@extended-by-babelrc.2'] 23 | ], 24 | presets: [ 25 | ['module:preset', {label: 'preset@extended-by-babelrc.foo'}, 'preset@extended-by-babelrc.foo'] 26 | ] 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/fixtures/compare/extended-by-virtual.json5: -------------------------------------------------------------------------------- 1 | { 2 | plugins: [ 3 | ['module:plugin', {label: 'plugin@extended-by-virtual'}, 'plugin@extended-by-virtual'], 4 | ['module:plugin-copy', {label: 'plugin-copy'}, 'copy-or-not'] 5 | ], 6 | presets: [ 7 | ['module:preset', {label: 'preset@extended-by-virtual'}, 'preset@extended-by-virtual'] 8 | ], 9 | env: { 10 | foo: { 11 | plugins: [ 12 | ['module:plugin', {label: 'plugin@extended-by-virtual.foo'}, 'plugin@extended-by-virtual.foo'] 13 | ], 14 | presets: [ 15 | ['module:preset', {label: 'preset@extended-by-virtual.foo'}, 'preset@extended-by-virtual.foo'] 16 | ] 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/fixtures/compare/foo.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | [] 3 | -------------------------------------------------------------------------------- /test/fixtures/compare/js-env/.babelrc.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const plugin = require('../node_modules/plugin') 3 | 4 | module.exports = api => { 5 | const envName = api.env() 6 | return { 7 | plugins: [ 8 | [plugin, {label: envName}], 9 | [plugin, {label: envName}, envName] 10 | ], 11 | env: { 12 | [envName]: { 13 | plugins: [ 14 | [plugin, {label: `${envName}.env`}, envName] 15 | ] 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/fixtures/compare/js-env/foo.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | [] 3 | -------------------------------------------------------------------------------- /test/fixtures/compare/js-normalization-edge-cases/.babelrc.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const noop = require('../node_modules/noop') 3 | const noop3 = require('../node_modules/noop3') 4 | 5 | module.exports = api => { 6 | api.cache.forever() 7 | return { 8 | extends: './no-plugins-or-presets.js', 9 | plugins: [ 10 | [{}], 11 | [noop], 12 | ['module:noop2'], 13 | noop3, 14 | 'module:noop4' 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/fixtures/compare/js-normalization-edge-cases/foo.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | [] 3 | -------------------------------------------------------------------------------- /test/fixtures/compare/js-normalization-edge-cases/no-plugins-or-presets.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/novemberborn/hullabaloo-config-manager/7c6a877ee4659b19a56652ad003e3856c64290e7/test/fixtures/compare/js-normalization-edge-cases/no-plugins-or-presets.js -------------------------------------------------------------------------------- /test/fixtures/compare/node_modules/env-plugin/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = require('plugin') // eslint-disable-line import/no-extraneous-dependencies 4 | -------------------------------------------------------------------------------- /test/fixtures/compare/node_modules/env-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plugin", 3 | "version": "1.0.0", 4 | "private": true, 5 | "standard-engine": "@novemberborn/as-i-preach" 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/compare/node_modules/noop/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = babel => { 4 | return { 5 | visitor: {} 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/compare/node_modules/noop/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noop", 3 | "version": "1.0.0", 4 | "private": true, 5 | "standard-engine": "@novemberborn/as-i-preach" 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/compare/node_modules/noop2/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = babel => { 4 | return { 5 | visitor: {} 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/compare/node_modules/noop2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noop2", 3 | "version": "1.0.0", 4 | "private": true, 5 | "standard-engine": "@novemberborn/as-i-preach" 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/compare/node_modules/noop3/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = babel => { 4 | return { 5 | visitor: {} 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/compare/node_modules/noop3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noop3", 3 | "version": "1.0.0", 4 | "private": true, 5 | "standard-engine": "@novemberborn/as-i-preach" 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/compare/node_modules/noop4/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = babel => { 4 | return { 5 | visitor: {} 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/compare/node_modules/noop4/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noop4", 3 | "version": "1.0.0", 4 | "private": true, 5 | "standard-engine": "@novemberborn/as-i-preach" 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/compare/node_modules/plugin-copy/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = (babel, options, dirname) => { 4 | const value = babel.types.objectExpression([ 5 | babel.types.objectProperty(babel.types.identifier('label'), babel.types.stringLiteral(options.label)), 6 | babel.types.objectProperty(babel.types.identifier('dirname'), babel.types.stringLiteral(dirname)) 7 | ]) 8 | 9 | return { 10 | visitor: { 11 | ArrayExpression (path) { 12 | path.node.elements.push(value) 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/fixtures/compare/node_modules/plugin-copy/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plugin-copy", 3 | "version": "1.0.0", 4 | "private": true, 5 | "standard-engine": "@novemberborn/as-i-preach" 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/compare/node_modules/plugin-default-opts/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = (babel, options, dirname) => { 4 | const value = babel.types.objectExpression([ 5 | babel.types.objectProperty(babel.types.identifier('pluginDefaultOpts'), babel.types.stringLiteral(JSON.stringify(options))), 6 | babel.types.objectProperty(babel.types.identifier('dirname'), babel.types.stringLiteral(dirname)) 7 | ]) 8 | 9 | return { 10 | visitor: { 11 | ArrayExpression (path) { 12 | path.node.elements.push(value) 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/fixtures/compare/node_modules/plugin-default-opts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plugin-default-opts", 3 | "version": "1.0.0", 4 | "private": true, 5 | "standard-engine": "@novemberborn/as-i-preach" 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/compare/node_modules/plugin/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = (babel, options, dirname) => { 4 | const value = babel.types.objectExpression([ 5 | babel.types.objectProperty(babel.types.identifier('label'), babel.types.stringLiteral(options.label)), 6 | babel.types.objectProperty(babel.types.identifier('dirname'), babel.types.stringLiteral(dirname)) 7 | ]) 8 | 9 | return { 10 | visitor: { 11 | ArrayExpression (path) { 12 | path.node.elements.push(value) 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/fixtures/compare/node_modules/plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plugin", 3 | "version": "1.0.0", 4 | "private": true, 5 | "standard-engine": "@novemberborn/as-i-preach" 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/compare/node_modules/preset/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | __esModule: true, 5 | default: (babel, options, dirname) => { 6 | const value = babel.types.objectExpression([ 7 | babel.types.objectProperty(babel.types.identifier('label'), babel.types.stringLiteral(options.label)), 8 | babel.types.objectProperty(babel.types.identifier('dirname'), babel.types.stringLiteral(dirname)) 9 | ]) 10 | 11 | return { 12 | plugins: [ 13 | { 14 | visitor: { 15 | ArrayExpression (path) { 16 | path.node.elements.push(value) 17 | } 18 | } 19 | } 20 | ] 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/fixtures/compare/node_modules/preset/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "preset", 3 | "version": "1.0.0", 4 | "private": true, 5 | "standard-engine": "@novemberborn/as-i-preach" 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/compare/overrides/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | plugins: [ 3 | ['../node_modules/plugin', {label: 'babelrc'}], 4 | ['../node_modules/plugin', {label: 'babelrc.named'}, 'named'] 5 | ], 6 | env: { 7 | foo: { 8 | plugins: [ 9 | ['../node_modules/plugin', {label: 'babelrc.foo'}] 10 | ] 11 | } 12 | }, 13 | overrides: [ 14 | { 15 | test: 'foo.js', 16 | extends: './extends.json5', 17 | plugins: [ 18 | ['../node_modules/plugin', {label: 'babelrc.named.override'}, 'named'] 19 | ], 20 | env: { 21 | foo: { 22 | plugins: [ 23 | ['../node_modules/plugin', {label: 'babelrc.override.foo'}] 24 | ] 25 | } 26 | } 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /test/fixtures/compare/overrides/extends.json5: -------------------------------------------------------------------------------- 1 | { 2 | plugins: [ 3 | ['../node_modules/plugin', {label: 'extends'}], 4 | ['../node_modules/plugin', {label: 'extends.named'}, 'named'], 5 | ['../node_modules/plugin', {label: 'extends.new'}, 'new'] 6 | ], 7 | overrides: [ 8 | { 9 | test: 'foo.js', 10 | plugins: [ 11 | ['../node_modules/plugin', {label: 'extends.named.override'}, 'named'] 12 | ], 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /test/fixtures/compare/overrides/foo.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | [] 3 | -------------------------------------------------------------------------------- /test/fixtures/compare/overrides/js/.babelrc.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const plugin = require('../../node_modules/plugin') 3 | 4 | module.exports = api => { 5 | api.cache.forever() 6 | return { 7 | plugins: [ 8 | [plugin, {label: 'babelrc'}], 9 | [plugin, {label: 'babelrc.named'}, 'named'] 10 | ], 11 | env: { 12 | foo: { 13 | plugins: [ 14 | [plugin, {label: 'babelrc.foo'}] 15 | ] 16 | } 17 | }, 18 | overrides: [ 19 | { 20 | test: 'bar.js', 21 | plugins: [ 22 | [plugin, {label: 'bar'}, 'named'] 23 | ] 24 | }, 25 | { 26 | test: 'foo.js', 27 | extends: './extends.js', 28 | plugins: [ 29 | [plugin, {label: 'babelrc.named.override'}, 'named'] 30 | ], 31 | env: { 32 | foo: { 33 | plugins: [ 34 | [plugin, {label: 'babelrc.override.foo'}] 35 | ] 36 | } 37 | } 38 | } 39 | ] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/fixtures/compare/overrides/js/extends.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const plugin = require('../../node_modules/plugin') 3 | 4 | module.exports = { 5 | plugins: [ 6 | [plugin, {label: 'extends'}], 7 | [plugin, {label: 'extends.named'}, 'named'], 8 | [plugin, {label: 'extends.new'}, 'new'] 9 | ], 10 | overrides: [ 11 | { 12 | test: 'foo.js', 13 | plugins: [ 14 | [plugin, {label: 'extends.named.override'}, 'named'] 15 | ] 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /test/fixtures/compare/overrides/js/foo.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | [] 3 | -------------------------------------------------------------------------------- /test/fixtures/compare/presets/.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: './subdir/base.json5', 3 | plugins: [ 4 | ['module:plugin', {label: 'plugin'}] 5 | ], 6 | presets: [ 7 | { 8 | plugins: [ 9 | ['module:plugin', {label: 'preset->plugin'}] 10 | ], 11 | presets: [ 12 | './preset.js', 13 | () => ({ 14 | plugins: [ 15 | ['module:plugin', {label: 'preset->preset->plugin'}] 16 | ], 17 | presets: [ 18 | { 19 | plugins: [ 20 | ['module:plugin', {label: 'preset->preset->preset->plugin'}] 21 | ] 22 | } 23 | ] 24 | }) 25 | ] 26 | }, 27 | {}, 28 | './edge-cases.js' 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /test/fixtures/compare/presets/edge-cases.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const plugin = require('../node_modules/plugin') 3 | 4 | module.exports = () => ({ 5 | presets: [ 6 | {}, 7 | [{}], 8 | [{}, {}], 9 | [{}, {}, 'name'], 10 | () => ({}), 11 | [() => ({})], 12 | ['./preset.js', {}], 13 | ['./preset.js', {}, 'repeat'] 14 | ], 15 | plugins: [ 16 | () => ({}), 17 | [() => ({})], 18 | [plugin, {label: 'edge-cases>plugin.1'}], 19 | [plugin, {label: 'edge-cases>plugin.2'}, '2'] 20 | ] 21 | }) 22 | -------------------------------------------------------------------------------- /test/fixtures/compare/presets/foo.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | [] 3 | -------------------------------------------------------------------------------- /test/fixtures/compare/presets/preset.js: -------------------------------------------------------------------------------- 1 | module.exports = () => ({ 2 | plugins: [ 3 | ['module:plugin', {label: 'preset.js->preset->plugin'}] 4 | ], 5 | presets: [ 6 | { 7 | plugins: [ 8 | ['module:plugin', {label: 'preset.js->preset->preset->plugin'}] 9 | ] 10 | } 11 | ] 12 | }) 13 | -------------------------------------------------------------------------------- /test/fixtures/compare/presets/subdir/base.json5: -------------------------------------------------------------------------------- 1 | { 2 | presets: [ 3 | { 4 | plugins: [ 5 | ['module:plugin', {label: 'base->preset->plugin'}] 6 | ], 7 | presets: [ 8 | '../preset.js', 9 | { 10 | plugins: [ 11 | ['module:plugin', {label: 'base->preset->preset->plugin'}] 12 | ] 13 | } 14 | ] 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /test/fixtures/compare/virtual.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./extended-by-virtual.json5", 3 | "plugins": [ 4 | ["module:plugin", {"label": "plugin@virtual"}, "plugin@virtual"], 5 | ["module:plugin", {"label": "plugin-not-copied"}, "copy-or-not"] 6 | ], 7 | "presets": [ 8 | ["module:preset", {"label": "preset@virtual"}, "preset@virtual"] 9 | ], 10 | "sourceMaps": true, 11 | "env": { 12 | "foo": { 13 | "extends": "./dir/extended-by-virtual-foo.json5", 14 | "plugins": [ 15 | ["module:plugin", {"label": "plugin@virtual.foo"}, "plugin@virtual.foo"] 16 | ], 17 | "presets": [ 18 | ["module:preset", {"label": "preset@virtual.foo"}, "preset@virtual.foo"] 19 | ] 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/fixtures/complex-env/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | extends: 'extended.js', 3 | plugins: ['babelrc'], 4 | env: { 5 | foo: { 6 | extends: 'foo.json5', 7 | plugins: ['babelrc/env/foo'] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/fixtures/complex-env/extended-further.json5: -------------------------------------------------------------------------------- 1 | { 2 | plugins: ['extended-further'], 3 | env: { 4 | foo: { 5 | extends: 'extended.js', 6 | plugins: ['extended-further/env/foo'] 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/complex-env/extended.js: -------------------------------------------------------------------------------- 1 | module.exports = context => { 2 | context.env() 3 | return { 4 | extends: 'extended-further.json5', 5 | plugins: ['extended'] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/complex-env/foo.json5: -------------------------------------------------------------------------------- 1 | { 2 | plugins: ['foo'], 3 | env: { 4 | foo: { 5 | plugins: ['foo/env/foo'] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/cycles/deep/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | extends: 'extended.json5', 3 | plugins: ['babelrc'], 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/cycles/deep/extended-furthest.json5: -------------------------------------------------------------------------------- 1 | { 2 | extends: '.babelrc', 3 | plugins: ['extended-furthest'], 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/cycles/deep/extended.json5: -------------------------------------------------------------------------------- 1 | { 2 | extends: 'extended-furthest.json5', 3 | plugins: ['extended'], 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/cycles/simple/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | extends: 'extended.json5', 3 | plugins: ['babelrc'], 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/cycles/simple/extended.json5: -------------------------------------------------------------------------------- 1 | { 2 | extends: '.babelrc', 3 | plugins: ['extended'], 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/dirs-not-files/babelrc/.babelrc/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/novemberborn/hullabaloo-config-manager/7c6a877ee4659b19a56652ad003e3856c64290e7/test/fixtures/dirs-not-files/babelrc/.babelrc/.gitkeep -------------------------------------------------------------------------------- /test/fixtures/dirs-not-files/pkg/package.json/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/novemberborn/hullabaloo-config-manager/7c6a877ee4659b19a56652ad003e3856c64290e7/test/fixtures/dirs-not-files/pkg/package.json/.gitkeep -------------------------------------------------------------------------------- /test/fixtures/empty/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/novemberborn/hullabaloo-config-manager/7c6a877ee4659b19a56652ad003e3856c64290e7/test/fixtures/empty/.gitkeep -------------------------------------------------------------------------------- /test/fixtures/frozen-config-module.js: -------------------------------------------------------------------------------- 1 | Object.freeze(exports) 2 | -------------------------------------------------------------------------------- /test/fixtures/js/.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ['js'] 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/js/cache-usage/env/.babelrc.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | let count = 0 3 | module.exports = { 4 | __esModule: true, 5 | default (context) { 6 | context.env() 7 | count++ 8 | return {} 9 | }, 10 | getCount () { 11 | return count 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/fixtures/js/cache-usage/forever/.babelrc.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | let count = 0 3 | module.exports = { 4 | __esModule: true, 5 | default (context) { 6 | context.cache.forever() 7 | count++ 8 | return {} 9 | }, 10 | getCount () { 11 | return count 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/fixtures/js/cache-usage/never/.babelrc.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | let count = 0 3 | module.exports = { 4 | __esModule: true, 5 | default (context) { 6 | context.cache.never() 7 | count++ 8 | return {} 9 | }, 10 | getCount () { 11 | return count 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/fixtures/js/cache-usage/static/.babelrc.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | let count = 0 3 | module.exports = { 4 | __esModule: true, 5 | get default () { 6 | count++ 7 | return {} 8 | }, 9 | getCount () { 10 | return count 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/fixtures/js/cjs-factory/.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = context => { 2 | context.cache.forever() 3 | return {plugins: ['cjs-factory']} 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/js/cjs-object/.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ['cjs-object'] 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/js/esm-factory/.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | __esModule: true, 3 | default: context => { 4 | context.cache.forever() 5 | return {plugins: ['esm-factory']} 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/js/esm-object/.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | __esModule: true, 3 | default: { 4 | plugins: ['esm-object'] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/multiple-sources/babelrc-and-js/.babelrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /test/fixtures/multiple-sources/babelrc-and-js/.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = {} 2 | -------------------------------------------------------------------------------- /test/fixtures/multiple-sources/babelrc-and-pkg/.babelrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /test/fixtures/multiple-sources/babelrc-and-pkg/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "babel": {} 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/multiple-sources/js-and-pkg/.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = {} 2 | -------------------------------------------------------------------------------- /test/fixtures/multiple-sources/js-and-pkg/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "babel": {} 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/pkg/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "babel": { 3 | "plugins": ["pkg"] 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/repeats/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | plugins: ['babelrc'] 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/repeats/virtual.json: -------------------------------------------------------------------------------- 1 | { 2 | "babelrc": true, 3 | "extends": ".babelrc", 4 | "plugins": ["virtual"] 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/verifier/babelrc/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | plugins: ['./plugin.js'], 3 | extends: './extends.json5', 4 | env: { 5 | foo: { 6 | extends: './foo.json5' 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/verifier/babelrc/extends.json5: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /test/fixtures/verifier/babelrc/foo.js: -------------------------------------------------------------------------------- 1 | module.exports = () => {} 2 | -------------------------------------------------------------------------------- /test/fixtures/verifier/babelrc/foo.json5: -------------------------------------------------------------------------------- 1 | { 2 | plugins: ['./foo.js'] 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/verifier/babelrc/plugin.js: -------------------------------------------------------------------------------- 1 | module.exports = () => {} 2 | -------------------------------------------------------------------------------- /test/fixtures/verifier/babelrcjs/.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ['./plugin.js'], 3 | extends: './extends.json5' 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/verifier/babelrcjs/extends.json5: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /test/fixtures/verifier/babelrcjs/plugin.js: -------------------------------------------------------------------------------- 1 | module.exports = () => {} 2 | -------------------------------------------------------------------------------- /test/fixtures/verifier/pkg/extends.json5: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /test/fixtures/verifier/pkg/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "babel": { 3 | "plugins": ["./plugin.js"], 4 | "extends": "./extends.json5" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/verifier/pkg/plugin.js: -------------------------------------------------------------------------------- 1 | module.exports = () => {} 2 | -------------------------------------------------------------------------------- /test/fixtures/virtual/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "deeply": { 3 | "nested": { 4 | "virtual": true 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/getPluginOrPresetName.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import getPluginOrPresetName from '../build/getPluginOrPresetName' 3 | import plugin from './fixtures/compare/node_modules/plugin' 4 | 5 | test('uses resolved value if already in map', t => { 6 | const map = new Map([[plugin, '🦄']]) 7 | t.is(getPluginOrPresetName(map, require.resolve('./fixtures/compare/node_modules/plugin')), '🦄') 8 | }) 9 | -------------------------------------------------------------------------------- /test/hashDependencies.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | 4 | import test from 'ava' 5 | import md5Hex from 'md5-hex' 6 | import packageHash from 'package-hash' 7 | 8 | import {prepareCache} from '..' 9 | import hashDependencies from '../build/hashDependencies' 10 | import fixture from './helpers/fixture' 11 | 12 | test('hashes packages', async t => { 13 | const fromPackage = fixture('compare', 'node_modules', 'plugin') 14 | const filename = path.join(fromPackage, 'index.js') 15 | const hashed = await hashDependencies([ 16 | {filename, fromPackage} 17 | ]) 18 | t.deepEqual(hashed, [await packageHash(path.join(fromPackage, 'package.json'))]) 19 | }) 20 | 21 | test('hashes files', async t => { 22 | const filename = fixture('compare', 'node_modules', 'plugin', 'index.js') 23 | const hashed = await hashDependencies([ 24 | {filename, fromPackage: null} 25 | ]) 26 | t.deepEqual(hashed, [md5Hex(fs.readFileSync(filename))]) 27 | }) 28 | 29 | test('fails when dependency package does not exist', async t => { 30 | const fromPackage = fixture('node_modules', 'non-existent') 31 | const filename = path.join(fromPackage, 'index.js') 32 | 33 | const err = await t.throws(hashDependencies([ 34 | {filename, fromPackage} 35 | ])) 36 | t.is(err.name, 'BadDependencyError') 37 | t.is(err.source, filename) 38 | t.is(err.parent.code, 'ENOENT') 39 | }) 40 | 41 | test('fails when dependency file does not exist', async t => { 42 | const filename = path.join('non-existent', 'index.js') 43 | 44 | const err = await t.throws(hashDependencies([ 45 | {filename, fromPackage: null} 46 | ])) 47 | t.is(err.name, 'BadDependencyError') 48 | t.is(err.source, filename) 49 | t.true(err.parent === null) 50 | }) 51 | 52 | test('can use a cache of computed hashes', async t => { 53 | const filename = fixture('precomputed.js') 54 | const cache = prepareCache() 55 | cache.dependencyHashes.set(filename, Promise.resolve('hash')) 56 | 57 | const hashed = await hashDependencies([ 58 | {filename, fromPackage: null} 59 | ], cache) 60 | t.deepEqual(hashed, ['hash']) 61 | }) 62 | 63 | test('caches new hashes', async t => { 64 | const fromPackage = fixture('compare', 'node_modules', 'plugin') 65 | const filename = path.join(fromPackage, 'index.js') 66 | const cache = prepareCache() 67 | 68 | const hashed = await hashDependencies([ 69 | {filename, fromPackage} 70 | ], cache) 71 | const fromCache = await cache.dependencyHashes.get(filename) 72 | t.true(hashed[0] === fromCache) 73 | }) 74 | 75 | test('can use a cache for file access', async t => { 76 | const filename = fixture('cached-access') 77 | const contents = Buffer.from('cached') 78 | const cache = prepareCache() 79 | cache.files.set(filename, Promise.resolve(contents)) 80 | 81 | const hashed = await hashDependencies([ 82 | {filename, fromPackage: null} 83 | ], cache) 84 | t.deepEqual(hashed, [md5Hex(contents)]) 85 | }) 86 | -------------------------------------------------------------------------------- /test/hashSources.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | 3 | import test from 'ava' 4 | import md5Hex from 'md5-hex' 5 | 6 | import {prepareCache} from '..' 7 | import hashSources from '../build/hashSources' 8 | import fixture from './helpers/fixture' 9 | 10 | test('hashes file contents', async t => { 11 | const source = fixture('babelrc', '.babelrc') 12 | const hashed = await hashSources([{source, runtimeHash: null}]) 13 | t.deepEqual(hashed, [md5Hex(fs.readFileSync(source))]) 14 | }) 15 | 16 | test('includes the runtimeHash', async t => { 17 | const source = fixture('babelrc', '.babelrc') 18 | const hashed = await hashSources([{source, runtimeHash: 'foo'}]) 19 | t.deepEqual(hashed, [md5Hex(['foo', fs.readFileSync(source)])]) 20 | }) 21 | 22 | test('hashes the "babel" value in package.json sources', async t => { 23 | const sources = [ 24 | fixture('pkg', 'package.json'), 25 | fixture('bad-pkg', 'array-babel', 'package.json'), 26 | fixture('bad-pkg', 'bool-babel', 'package.json'), 27 | fixture('bad-pkg', 'falsy', 'package.json'), 28 | fixture('bad-pkg', 'null', 'package.json'), 29 | fixture('bad-pkg', 'null-babel', 'package.json'), 30 | fixture('bad-pkg', 'without-babel', 'package.json') 31 | ].map(source => ({source, runtimeHash: null})) 32 | 33 | const hashed = await hashSources(sources) 34 | t.deepEqual(hashed, [ 35 | md5Hex('{"plugins":["pkg"]}'), 36 | md5Hex('[]'), 37 | md5Hex('true'), 38 | md5Hex('{}'), 39 | md5Hex('{}'), 40 | md5Hex('{}'), 41 | md5Hex('{}') 42 | ]) 43 | }) 44 | 45 | test('source may contain a dot-prop path', async t => { 46 | const source = fixture('virtual', 'package.json#deeply.nested') 47 | const hashed = await hashSources([{source, runtimeHash: null}]) 48 | t.deepEqual(hashed, [md5Hex('{"virtual":true}')]) 49 | }) 50 | 51 | test('can use a map of fixed hashes', async t => { 52 | const hashed = await hashSources([ 53 | {source: fixture('pkg', 'package.json'), runtimeHash: null}, 54 | {source: 'foo', runtimeHash: null}, 55 | {source: 'bar', runtimeHash: null} 56 | ], new Map([ 57 | ['foo', 'hash of foo'], 58 | ['bar', 'hash of bar'] 59 | ])) 60 | t.deepEqual(hashed, [ 61 | md5Hex('{"plugins":["pkg"]}'), 62 | 'hash of foo', 63 | 'hash of bar' 64 | ]) 65 | }) 66 | 67 | test('can use a cache of computed hashes', async t => { 68 | const source = fixture('precomputed') 69 | const cache = prepareCache() 70 | cache.sourceHashes.set(source, Promise.resolve('hash')) 71 | 72 | const hashed = await hashSources([{source, runtimeHash: null}], null, cache) 73 | t.deepEqual(hashed, ['hash']) 74 | }) 75 | 76 | test('caches new hashes', async t => { 77 | const source = fixture('babelrc', '.babelrc') 78 | const cache = prepareCache() 79 | 80 | const hashed = await hashSources([{source, runtimeHash: null}], null, cache) 81 | const fromCache = await cache.sourceHashes.get(source) 82 | t.true(hashed[0] === fromCache) 83 | }) 84 | 85 | test('can use a cache for file access', async t => { 86 | const source = fixture('cached-access') 87 | const contents = Buffer.from('cached') 88 | const cache = prepareCache() 89 | cache.files.set(source, Promise.resolve(contents)) 90 | 91 | const hashed = await hashSources([{source, runtimeHash: null}], null, cache) 92 | t.deepEqual(hashed, [md5Hex(contents)]) 93 | }) 94 | 95 | test('fails when source file does not exist', async t => { 96 | const source = fixture('non-existent') 97 | const err = await t.throws(hashSources([{source, runtimeHash: null}])) 98 | t.is(err.name, 'NoSourceFileError') 99 | t.is(err.source, source) 100 | }) 101 | -------------------------------------------------------------------------------- /test/helpers/fixture.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | export default function fixture (...args) { 4 | return path.join(__dirname, '..', 'fixtures', ...args) 5 | } 6 | -------------------------------------------------------------------------------- /test/helpers/pkgDirMock.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | export default { 4 | sync (filename) { 5 | return path.dirname(filename) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/helpers/runGeneratedCode.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import {runInNewContext} from 'vm' 3 | 4 | import * as helpers from '../../build/helpers' 5 | 6 | export default function runGeneratedCode (code, env = process.env) { 7 | const configModule = {} 8 | runInNewContext(code, { 9 | console, 10 | exports: configModule, 11 | require (mid) { 12 | if (mid === 'process') return {env} 13 | if (mid === require.resolve('../../build/helpers')) return helpers 14 | assert.fail(`Unexpected mid: ${mid}`) 15 | } 16 | }) 17 | 18 | return configModule 19 | } 20 | -------------------------------------------------------------------------------- /test/loadConfigModule.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import test from 'ava' 3 | import loadConfigModule from '../build/loadConfigModule' 4 | 5 | test('defaults dependencies to an empty map if they could not be added to the exports', t => { 6 | t.deepEqual(loadConfigModule(path.join(__dirname, './fixtures/frozen-config-module.js')).dependencies, new Map()) 7 | }) 8 | -------------------------------------------------------------------------------- /test/loadPluginOrPreset.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import loadPluginOrPreset from '../build/loadPluginOrPreset' 3 | 4 | test('throws when module does not export a function', t => { 5 | const err = t.throws(() => loadPluginOrPreset(__filename)) 6 | t.is(err.name, 'TypeError') 7 | t.is(err.message, `Plugin or preset file '${__filename}' did not export a function`) 8 | }) 9 | -------------------------------------------------------------------------------- /test/readSafe.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import td from 'testdouble' 3 | 4 | const gfs = td.replace('graceful-fs') 5 | const {default: readSafe} = require('../build/readSafe') 6 | 7 | td.when(gfs.stat(__filename)).thenCallback(null, {isFile () { return true }}) 8 | 9 | test('rejects when reading an actual file fails with an error', async t => { 10 | const expected = new Error() 11 | td.when(gfs.readFile(__filename)).thenCallback(expected) 12 | const actual = await t.throws(readSafe(__filename)) 13 | t.is(actual, expected) 14 | }) 15 | -------------------------------------------------------------------------------- /test/reduceChains.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import test from 'ava' 3 | import mockRequire from 'mock-require' 4 | import proxyquire from 'proxyquire' 5 | 6 | import pkgDirMock from './helpers/pkgDirMock' 7 | 8 | const {default: reduceChains} = proxyquire('../build/reduceChains', { 9 | './resolvePluginsAndPresets': proxyquire('../build/resolvePluginsAndPresets', { 10 | 'pkg-dir': pkgDirMock, 11 | 'resolve-from': { 12 | silent (dir, ref) { 13 | if (ref.startsWith('./')) return path.posix.join('~', ref + '.js') 14 | if (ref.startsWith('/')) return ref + '.js' 15 | return path.posix.join('~', ref, 'index.js') 16 | } 17 | } 18 | }) 19 | }) 20 | 21 | mockRequire('~/babel-plugin-foo/index.js', () => ({})) 22 | mockRequire('~/babel-plugin-quux/index.js', () => ({})) 23 | mockRequire('~/babel-preset-qux/index.js', () => ({})) 24 | mockRequire('~/bar.js', () => ({})) 25 | mockRequire('~/baz.js', () => ({})) 26 | mockRequire('/thud.js', () => ({})) 27 | 28 | function makeChainsObj (defaultChain, envChains) { 29 | return { 30 | defaultChain, 31 | envChains, 32 | * [Symbol.iterator] () { 33 | yield defaultChain 34 | for (const chain of envChains.values()) { 35 | yield chain 36 | } 37 | } 38 | } 39 | } 40 | 41 | const reduces = (t, defaultChain, envChains, expected) => { 42 | const chains = makeChainsObj(defaultChain, envChains) 43 | const { 44 | dependencies, 45 | envNames, 46 | fixedSourceHashes, 47 | sources, 48 | unflattenedDefaultOptions, 49 | unflattenedEnvOptions 50 | } = reduceChains(chains) 51 | 52 | if ('dependencies' in expected) t.deepEqual(dependencies, expected.dependencies) 53 | if ('fixedSourceHashes' in expected) t.deepEqual(fixedSourceHashes, expected.fixedSourceHashes) 54 | if ('envNames' in expected) t.deepEqual(envNames, expected.envNames) 55 | if ('sources' in expected) t.deepEqual(sources, expected.sources) 56 | if ('unflattenedDefaultOptions' in expected) t.deepEqual(unflattenedDefaultOptions, expected.unflattenedDefaultOptions) 57 | if ('unflattenedEnvOptions' in expected) t.deepEqual(unflattenedEnvOptions, expected.unflattenedEnvOptions) 58 | } 59 | 60 | { 61 | const zero = { 62 | fileType: 'JSON', 63 | options: {}, 64 | dir: '~', 65 | source: '0', 66 | runtimeHash: null 67 | } 68 | const one = { 69 | fileType: 'JSON', 70 | options: { 71 | plugins: ['foo'], 72 | presets: [['./bar', {hello: 'world'}]], 73 | sourceMaps: true 74 | }, 75 | dir: '~', 76 | source: '1', 77 | runtimeHash: null 78 | } 79 | const two = { 80 | fileType: 'JSON', 81 | options: { 82 | parserOpts: {foo: 1}, 83 | plugins: [['./baz', {}], 'foo'], 84 | presets: [['qux']], 85 | sourceMaps: false 86 | }, 87 | dir: '~', 88 | source: '2', 89 | runtimeHash: null 90 | } 91 | const three = { 92 | fileType: 'JSON', 93 | options: { 94 | parserOpts: {foo: 2}, 95 | presets: [['./bar', {goodbye: true}]] 96 | }, 97 | dir: '~', 98 | source: '3', 99 | runtimeHash: null 100 | } 101 | const four = { 102 | fileType: 'JSON', 103 | options: { 104 | plugins: ['quux'] 105 | }, 106 | dir: '~', 107 | source: '4', 108 | runtimeHash: null 109 | } 110 | const five = { 111 | fileType: 'JS', 112 | options: { 113 | plugins: ['/thud'] 114 | }, 115 | dir: '/', 116 | envName: 'foo', 117 | source: '5', 118 | runtimeDependencies: new Map([['/thud.js', '/thud']]), 119 | runtimeHash: null 120 | } 121 | 122 | test('reduces config chains', reduces, 123 | Object.assign([zero, one, two], {overrides: []}), 124 | new Map([['foo', Object.assign([one, two, three, four, five], {overrides: []})]]), 125 | { 126 | dependencies: [ 127 | {default: false, envs: new Set(['foo']), filename: '/thud.js', fromPackage: null}, 128 | {default: true, envs: new Set(['foo']), filename: '~/babel-plugin-foo/index.js', fromPackage: '~/babel-plugin-foo'}, 129 | {default: false, envs: new Set(['foo']), filename: '~/babel-plugin-quux/index.js', fromPackage: '~/babel-plugin-quux'}, 130 | {default: true, envs: new Set(['foo']), filename: '~/babel-preset-qux/index.js', fromPackage: '~/babel-preset-qux'}, 131 | {default: true, envs: new Set(['foo']), filename: '~/bar.js', fromPackage: null}, 132 | {default: true, envs: new Set(['foo']), filename: '~/baz.js', fromPackage: null} 133 | ], 134 | envNames: new Set(['foo']), 135 | sources: [ 136 | {default: true, envs: new Set(), source: '0', runtimeHash: null}, 137 | {default: true, envs: new Set(['foo']), source: '1', runtimeHash: null}, 138 | {default: true, envs: new Set(['foo']), source: '2', runtimeHash: null}, 139 | {default: false, envs: new Set(['foo']), source: '3', runtimeHash: null}, 140 | {default: false, envs: new Set(['foo']), source: '4', runtimeHash: null}, 141 | {default: false, envs: new Set(['foo']), source: '5', runtimeHash: null} 142 | ], 143 | unflattenedDefaultOptions: Object.assign([{ 144 | fileType: 'JSON', 145 | options: { 146 | parserOpts: {foo: 1}, 147 | plugins: [ 148 | {dirname: '~', filename: '~/babel-plugin-foo/index.js', name: '🤡🎪🎟.0'}, 149 | {dirname: '~', filename: '~/baz.js', options: {}, name: '🤡🎪🎟.4'} 150 | ], 151 | presets: [ 152 | {dirname: '~', filename: '~/bar.js', options: {hello: 'world'}, name: '🤡🎪🎟.2'}, 153 | {dirname: '~', filename: '~/babel-preset-qux/index.js', name: '🤡🎪🎟.6'} 154 | ], 155 | sourceMaps: false 156 | } 157 | }], {overrides: []}), 158 | unflattenedEnvOptions: new Map([ 159 | ['foo', Object.assign([{ 160 | fileType: 'JSON', 161 | options: { 162 | parserOpts: {foo: 2}, 163 | plugins: [ 164 | {dirname: '~', filename: '~/babel-plugin-foo/index.js', name: '🤡🎪🎟.0'}, 165 | {dirname: '~', filename: '~/baz.js', options: {}, name: '🤡🎪🎟.4'}, 166 | {dirname: '~', filename: '~/babel-plugin-quux/index.js', name: '🤡🎪🎟.8'} 167 | ], 168 | presets: [ 169 | {dirname: '~', filename: '~/bar.js', options: {goodbye: true}, name: '🤡🎪🎟.2'}, 170 | {dirname: '~', filename: '~/babel-preset-qux/index.js', name: '🤡🎪🎟.6'} 171 | ], 172 | sourceMaps: false 173 | } 174 | }, { 175 | dir: '/', 176 | envName: 'foo', 177 | fileType: 'JS', 178 | source: '5' 179 | }], {overrides: []})] 180 | ]) 181 | }) 182 | } 183 | 184 | test('removes non-array plugins and presets values', reduces, Object.assign([ 185 | { 186 | fileType: 'JSON', 187 | options: { 188 | plugins: 'plugins', 189 | presets: 'presets' 190 | } 191 | } 192 | ], {overrides: []}), new Map(), { 193 | unflattenedDefaultOptions: Object.assign([{ 194 | fileType: 'JSON', 195 | options: { 196 | plugins: [], 197 | presets: [] 198 | } 199 | }], {overrides: []}) 200 | }) 201 | 202 | test('fileType becomes JSON5 if some of the configs were parsed using JSON5', reduces, Object.assign([ 203 | { 204 | fileType: 'JSON', 205 | options: {} 206 | }, 207 | { 208 | fileType: 'JSON5', 209 | options: {} 210 | } 211 | ], {overrides: []}), new Map(), { 212 | unflattenedDefaultOptions: Object.assign([{ 213 | fileType: 'JSON5', 214 | options: { 215 | plugins: [], 216 | presets: [] 217 | } 218 | }], {overrides: []}) 219 | }) 220 | 221 | test('fileType becomes JSON5 if some of the configs were parsed using JSON5, after encountering a JS config', reduces, 222 | Object.assign([ 223 | { 224 | dir: '~', 225 | envName: null, 226 | fileType: 'JS', 227 | options: {}, 228 | source: '~/config.js' 229 | }, 230 | { 231 | fileType: 'JSON', 232 | options: {} 233 | }, 234 | { 235 | fileType: 'JSON5', 236 | options: {} 237 | } 238 | ], {overrides: []}), new Map(), { 239 | unflattenedDefaultOptions: Object.assign([ 240 | { 241 | dir: '~', 242 | envName: null, 243 | fileType: 'JS', 244 | source: '~/config.js' 245 | }, 246 | { 247 | fileType: 'JSON5', 248 | options: { 249 | plugins: [], 250 | presets: [] 251 | } 252 | } 253 | ], {overrides: []}) 254 | }) 255 | 256 | { 257 | const ignore = {ignore: true} 258 | const only = {only: true} 259 | const passPerPreset = {passPerPreset: true} 260 | const sourceMap = {sourceMap: true} 261 | 262 | test('normalizes options', reduces, Object.assign([ 263 | { 264 | fileType: 'JSON', 265 | options: { 266 | ignore, 267 | only, 268 | passPerPreset, 269 | sourceMaps: null 270 | } 271 | }, 272 | { 273 | fileType: 'JSON', 274 | options: { 275 | // These should be stripped before options are merged with base 276 | ignore: null, 277 | only: null, 278 | passPerPreset: null, 279 | // `sourceMap` should be merged as `sourceMaps` 280 | sourceMap 281 | } 282 | } 283 | ], {overrides: []}), new Map(), { 284 | unflattenedDefaultOptions: Object.assign([{ 285 | fileType: 'JSON', 286 | options: { 287 | ignore, 288 | only, 289 | passPerPreset, 290 | sourceMaps: sourceMap, 291 | plugins: [], 292 | presets: [] 293 | } 294 | }], {overrides: []}) 295 | }) 296 | } 297 | 298 | { 299 | const pluginTarget1 = {[Symbol('plugin1')]: true} 300 | const pluginTarget2 = {[Symbol('plugin2')]: true} 301 | const pluginTarget3 = {[Symbol('plugin3')]: true} 302 | const pluginTarget4 = {[Symbol('plugin4')]: true} 303 | const presetTarget1 = {[Symbol('preset1')]: true} 304 | const presetTarget2 = {[Symbol('preset2')]: true} 305 | const presetTarget3 = {[Symbol('preset3')]: true} 306 | const presetTarget4 = {[Symbol('preset4')]: true} 307 | test('preserves object and function values for plugin and preset targets', reduces, Object.assign([ 308 | { 309 | fileType: 'JSON', 310 | dir: '~', 311 | options: { 312 | plugins: [ 313 | pluginTarget1, 314 | [pluginTarget2], 315 | [pluginTarget3, {}], 316 | [pluginTarget4, {}, 'name'] 317 | ], 318 | presets: [ 319 | presetTarget1, 320 | [presetTarget2], 321 | [presetTarget3, {}], 322 | [presetTarget4, {}, 'name'] 323 | ] 324 | } 325 | } 326 | ], {overrides: []}), new Map(), { 327 | unflattenedDefaultOptions: Object.assign([{ 328 | fileType: 'JSON', 329 | options: { 330 | plugins: [ 331 | {dirname: '~', target: pluginTarget1, name: '🤡🎪🎟.0'}, 332 | {dirname: '~', target: pluginTarget2, name: '🤡🎪🎟.1'}, 333 | {dirname: '~', target: pluginTarget3, options: {}, name: '🤡🎪🎟.2'}, 334 | {dirname: '~', target: pluginTarget4, options: {}, name: '🤡🎪🎟.3.name'} 335 | ], 336 | presets: [ 337 | {dirname: '~', target: presetTarget1, name: '🤡🎪🎟.4'}, 338 | {dirname: '~', target: presetTarget2, name: '🤡🎪🎟.5'}, 339 | {dirname: '~', target: presetTarget3, options: {}, name: '🤡🎪🎟.6'}, 340 | {dirname: '~', target: presetTarget4, options: {}, name: '🤡🎪🎟.7.name'} 341 | ] 342 | } 343 | }], {overrides: []}) 344 | }) 345 | } 346 | 347 | test('collects fixed source hashes', reduces, Object.assign([ 348 | { 349 | options: {}, 350 | source: 'foo', 351 | hash: 'hash of foo' 352 | }, 353 | { 354 | options: {}, 355 | source: 'bar', 356 | hash: 'hash of bar' 357 | } 358 | ], {overrides: []}), new Map(), { 359 | fixedSourceHashes: new Map([ 360 | ['foo', 'hash of foo'], 361 | ['bar', 'hash of bar'] 362 | ]) 363 | }) 364 | 365 | test('throws when a config contains repeated plugins', t => { 366 | const plugin = () => {} 367 | const chains = makeChainsObj(Object.assign([ 368 | { 369 | options: { 370 | plugins: [ 371 | plugin, 372 | plugin 373 | ] 374 | }, 375 | source: 'foo' 376 | } 377 | ], {overrides: []}), new Map()) 378 | 379 | const err = t.throws(() => reduceChains(chains)) 380 | t.is(err.name, 'InvalidFileError') 381 | t.is(err.source, 'foo') 382 | }) 383 | 384 | test('throws when a config contains repeated presets', t => { 385 | const preset = () => {} 386 | const chains = makeChainsObj(Object.assign([ 387 | { 388 | options: { 389 | presets: [ 390 | preset, 391 | preset 392 | ] 393 | }, 394 | source: 'foo' 395 | } 396 | ], {overrides: []}), new Map()) 397 | 398 | const err = t.throws(() => reduceChains(chains)) 399 | t.is(err.name, 'InvalidFileError') 400 | t.is(err.source, 'foo') 401 | }) 402 | -------------------------------------------------------------------------------- /test/resolvePluginsAndPresets.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | import test from 'ava' 4 | import proxyquire from 'proxyquire' 5 | import td from 'testdouble' 6 | 7 | import pkgDirMock from './helpers/pkgDirMock' 8 | 9 | { 10 | const {default: resolvePluginsAndPresets} = proxyquire('../build/resolvePluginsAndPresets', { 11 | 'pkg-dir': pkgDirMock, 12 | 'resolve-from': { 13 | silent (dir, ref) { 14 | if (path.isAbsolute(ref)) { 15 | return ref 16 | } 17 | 18 | if (ref.startsWith('./') || ref.startsWith('../')) { 19 | return path.resolve(dir, ref + '.js') 20 | } 21 | 22 | if (ref.includes('exact') && ref.replace(/^@.+?\//, '').startsWith('babel-')) { 23 | return null 24 | } 25 | 26 | return path.resolve('node_modules', ref, 'index.js') 27 | } 28 | } 29 | }) 30 | 31 | const resolves = (t, chains, expected) => { 32 | t.deepEqual(resolvePluginsAndPresets(chains), expected) 33 | } 34 | 35 | { 36 | const config = { 37 | options: { 38 | plugins: [ 39 | 'plugin', 40 | 'babel-plugin-plugin', 41 | '@babel/plugin', 42 | '@babel/plugin-plugin', 43 | ['plugin-with-options', {}], 44 | '../relative-plugin', 45 | 'module:exact-plugin', 46 | '@scope/plugin', 47 | 'module:@scope/exact-plugin' 48 | ], 49 | presets: [ 50 | 'preset', 51 | 'babel-preset-preset', 52 | '@babel/preset', 53 | '@babel/preset-preset', 54 | ['preset-with-options', {}], 55 | '../relative-preset', 56 | 'module:exact-preset', 57 | '@scope/preset', 58 | 'module:@scope/exact-preset' 59 | ] 60 | }, 61 | dir: path.resolve('my-configs') 62 | } 63 | 64 | test('resolves plugins and presets', resolves, [ 65 | Object.assign([config], {overrides: []}) 66 | ], new Map([ 67 | [config, { 68 | plugins: new Map([ 69 | ['plugin', { 70 | filename: path.resolve('node_modules/babel-plugin-plugin/index.js'), 71 | fromPackage: path.resolve('node_modules/babel-plugin-plugin') 72 | }], 73 | ['babel-plugin-plugin', { 74 | filename: path.resolve('node_modules/babel-plugin-plugin/index.js'), 75 | fromPackage: path.resolve('node_modules/babel-plugin-plugin') 76 | }], 77 | ['@babel/plugin', { 78 | filename: path.resolve('node_modules/@babel/plugin-plugin/index.js'), 79 | fromPackage: path.resolve('node_modules/@babel/plugin-plugin') 80 | }], 81 | ['@babel/plugin-plugin', { 82 | filename: path.resolve('node_modules/@babel/plugin-plugin/index.js'), 83 | fromPackage: path.resolve('node_modules/@babel/plugin-plugin') 84 | }], 85 | ['plugin-with-options', { 86 | filename: path.resolve('node_modules/babel-plugin-plugin-with-options/index.js'), 87 | fromPackage: path.resolve('node_modules/babel-plugin-plugin-with-options') 88 | }], 89 | ['../relative-plugin', { 90 | filename: path.resolve('relative-plugin.js'), 91 | fromPackage: null 92 | }], 93 | ['module:exact-plugin', { 94 | filename: path.resolve('node_modules/exact-plugin/index.js'), 95 | fromPackage: path.resolve('node_modules/exact-plugin') 96 | }], 97 | ['@scope/plugin', { 98 | filename: path.resolve('node_modules/@scope/babel-plugin-plugin/index.js'), 99 | fromPackage: path.resolve('node_modules/@scope/babel-plugin-plugin') 100 | }], 101 | ['module:@scope/exact-plugin', { 102 | filename: path.resolve('node_modules/@scope/exact-plugin/index.js'), 103 | fromPackage: path.resolve('node_modules/@scope/exact-plugin') 104 | }] 105 | ]), 106 | presets: new Map([ 107 | ['preset', { 108 | filename: path.resolve('node_modules/babel-preset-preset/index.js'), 109 | fromPackage: path.resolve('node_modules/babel-preset-preset') 110 | }], 111 | ['babel-preset-preset', { 112 | filename: path.resolve('node_modules/babel-preset-preset/index.js'), 113 | fromPackage: path.resolve('node_modules/babel-preset-preset') 114 | }], 115 | ['@babel/preset', { 116 | filename: path.resolve('node_modules/@babel/preset-preset/index.js'), 117 | fromPackage: path.resolve('node_modules/@babel/preset-preset') 118 | }], 119 | ['@babel/preset-preset', { 120 | filename: path.resolve('node_modules/@babel/preset-preset/index.js'), 121 | fromPackage: path.resolve('node_modules/@babel/preset-preset') 122 | }], 123 | ['preset-with-options', { 124 | filename: path.resolve('node_modules/babel-preset-preset-with-options/index.js'), 125 | fromPackage: path.resolve('node_modules/babel-preset-preset-with-options') 126 | }], 127 | ['../relative-preset', { 128 | filename: path.resolve('relative-preset.js'), 129 | fromPackage: null 130 | }], 131 | ['module:exact-preset', { 132 | filename: path.resolve('node_modules/exact-preset/index.js'), 133 | fromPackage: path.resolve('node_modules/exact-preset') 134 | }], 135 | ['@scope/preset', { 136 | filename: path.resolve('node_modules/@scope/babel-preset-preset/index.js'), 137 | fromPackage: path.resolve('node_modules/@scope/babel-preset-preset') 138 | }], 139 | ['module:@scope/exact-preset', { 140 | filename: path.resolve('node_modules/@scope/exact-preset/index.js'), 141 | fromPackage: path.resolve('node_modules/@scope/exact-preset') 142 | }] 143 | ]) 144 | }] 145 | ])) 146 | } 147 | 148 | { 149 | const first = { 150 | options: { 151 | plugins: [ 152 | 'plugin' 153 | ] 154 | }, 155 | dir: path.resolve('my-configs') 156 | } 157 | const second = { 158 | options: { 159 | presets: [ 160 | 'preset' 161 | ] 162 | }, 163 | dir: path.resolve('my-configs') 164 | } 165 | 166 | const expected = new Map([ 167 | [first, { 168 | plugins: new Map([ 169 | ['plugin', { 170 | filename: path.resolve('node_modules/babel-plugin-plugin/index.js'), 171 | fromPackage: path.resolve('node_modules/babel-plugin-plugin') 172 | }] 173 | ]), 174 | presets: new Map() 175 | }], 176 | [second, { 177 | plugins: new Map(), 178 | presets: new Map([ 179 | ['preset', { 180 | filename: path.resolve('node_modules/babel-preset-preset/index.js'), 181 | fromPackage: path.resolve('node_modules/babel-preset-preset') 182 | }] 183 | ]) 184 | }] 185 | ]) 186 | 187 | test('resolves multiple configs in a chain', resolves, [Object.assign([first, second], {overrides: []})], expected) 188 | test('resolves multiple chains', resolves, [ 189 | Object.assign([first], {overrides: []}), 190 | Object.assign([second], {overrides: []}) 191 | ], expected) 192 | } 193 | 194 | { 195 | const config = { 196 | options: { 197 | plugins: [{}], 198 | presets: [{}] 199 | }, 200 | dir: path.resolve('my-configs') 201 | } 202 | test('ignores non-string targets', resolves, [Object.assign([config], {overrides: []})], new Map([ 203 | [config, { 204 | plugins: new Map(), 205 | presets: new Map() 206 | }] 207 | ])) 208 | } 209 | } 210 | 211 | test('caches results', t => { 212 | const resolveFrom = td.object({silent () {}}) 213 | td.when(resolveFrom.silent(td.matchers.anything(), td.matchers.anything())).thenReturn('/stubbed/path') 214 | 215 | const {default: resolvePluginsAndPresets} = proxyquire('../build/resolvePluginsAndPresets', { 216 | 'pkg-dir': pkgDirMock, 217 | 'resolve-from': resolveFrom 218 | }) 219 | 220 | const config = { 221 | options: { 222 | plugins: [ 223 | 'foo' 224 | ], 225 | presets: [ 226 | 'foo' 227 | ] 228 | }, 229 | dir: path.resolve('bar') 230 | } 231 | resolvePluginsAndPresets([ 232 | Object.assign([ 233 | config, 234 | { 235 | options: { 236 | plugins: [ 237 | 'foo', 238 | 'baz' 239 | ] 240 | }, 241 | dir: path.resolve('bar') 242 | } 243 | ], {overrides: []}), 244 | Object.assign([ 245 | { 246 | options: { 247 | plugins: [ 248 | 'foo', 249 | 'baz' 250 | ] 251 | }, 252 | dir: path.resolve('qux') 253 | }, 254 | config 255 | ], {overrides: []}) 256 | ]) 257 | 258 | const {callCount, calls} = td.explain(resolveFrom.silent) 259 | t.is(callCount, 5) 260 | t.deepEqual(calls.shift().args, [path.resolve('bar'), 'babel-plugin-foo']) 261 | t.deepEqual(calls.shift().args, [path.resolve('bar'), 'babel-preset-foo']) 262 | t.deepEqual(calls.shift().args, [path.resolve('bar'), 'babel-plugin-baz']) 263 | t.deepEqual(calls.shift().args, [path.resolve('qux'), 'babel-plugin-foo']) 264 | t.deepEqual(calls.shift().args, [path.resolve('qux'), 'babel-plugin-baz']) 265 | }) 266 | 267 | test('caches can be shared', t => { 268 | const resolveFrom = td.object({silent () {}}) 269 | td.when(resolveFrom.silent(td.matchers.anything(), td.matchers.anything())).thenReturn('/stubbed/path') 270 | 271 | const sharedCache = { 272 | pluginsAndPresets: new Map() 273 | } 274 | 275 | const {default: resolvePluginsAndPresets} = proxyquire('../build/resolvePluginsAndPresets', { 276 | 'resolve-from': resolveFrom 277 | }) 278 | 279 | const config = { 280 | options: { 281 | plugins: [ 282 | 'foo' 283 | ], 284 | presets: [ 285 | 'foo' 286 | ] 287 | }, 288 | dir: path.resolve('bar') 289 | } 290 | 291 | ;[1, 2].forEach(() => { 292 | resolvePluginsAndPresets([ 293 | Object.assign([ 294 | config, 295 | { 296 | options: { 297 | plugins: [ 298 | 'foo', 299 | 'baz' 300 | ] 301 | }, 302 | dir: path.resolve('bar') 303 | } 304 | ], {overrides: []}), 305 | Object.assign([ 306 | { 307 | options: { 308 | plugins: [ 309 | 'foo', 310 | 'baz' 311 | ] 312 | }, 313 | dir: path.resolve('qux') 314 | }, 315 | config 316 | ], {overrides: []}) 317 | ], sharedCache) 318 | }) 319 | 320 | const {callCount, calls} = td.explain(resolveFrom.silent) 321 | t.is(callCount, 5) 322 | t.deepEqual(calls.shift().args, [path.resolve('bar'), 'babel-plugin-foo']) 323 | t.deepEqual(calls.shift().args, [path.resolve('bar'), 'babel-preset-foo']) 324 | t.deepEqual(calls.shift().args, [path.resolve('bar'), 'babel-plugin-baz']) 325 | t.deepEqual(calls.shift().args, [path.resolve('qux'), 'babel-plugin-foo']) 326 | t.deepEqual(calls.shift().args, [path.resolve('qux'), 'babel-plugin-baz']) 327 | }) 328 | 329 | { 330 | const resolveFrom = td.object({silent () {}}) 331 | td.when(resolveFrom.silent(td.matchers.anything(), td.matchers.anything())).thenReturn(null) 332 | 333 | const {default: resolvePluginsAndPresets} = proxyquire('../build/resolvePluginsAndPresets', { 334 | 'pkg-dir': pkgDirMock, 335 | 'resolve-from': resolveFrom 336 | }) 337 | 338 | const throws = (t, kind, ref) => { 339 | const dir = path.resolve('foo') 340 | const source = path.join(dir, 'source.js') 341 | const config = { 342 | options: { 343 | [`${kind}s`]: [ref] 344 | }, 345 | dir, 346 | source 347 | } 348 | 349 | const err = t.throws(() => resolvePluginsAndPresets([[config]])) 350 | t.is(err.name, 'ResolveError') 351 | t.is(err.source, source) 352 | t.is(err.ref, ref) 353 | if (kind === 'plugin') { 354 | t.true(err.isPlugin) 355 | t.false(err.isPreset) 356 | } 357 | if (kind === 'preset') { 358 | t.false(err.isPlugin) 359 | t.true(err.isPreset) 360 | } 361 | } 362 | 363 | test('throws if a plugin cannot be resolved', throws, 'plugin', 'my-plugin') 364 | test('throws if a preset cannot be resolved', throws, 'preset', 'my-preset') 365 | } 366 | -------------------------------------------------------------------------------- /test/snapshots/codegen.js.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `test/codegen.js` 2 | 3 | The actual snapshot is saved in `codegen.js.snap`. 4 | 5 | Generated by [AVA](https://ava.li). 6 | 7 | ## generates a nicely indented module 8 | 9 | > Snapshot 1 10 | 11 | `"use strict"␊ 12 | ␊ 13 | const process = require("process")␊ 14 | const helpers = require("~/build/helpers.js")␊ 15 | ␊ 16 | const defaultOptions = (envName, cache) => {␊ 17 | const wrapperFns = new Map()␊ 18 | return Object.assign(helpers.mergeOptions([␊ 19 | helpers.loadCachedModule(cache, "~/test/fixtures/compare/dir/subdir", "~/test/fixtures/compare/dir/subdir/extended-by-babelrc.js", envName, false, undefined),␊ 20 | {␊ 21 | sourceMaps: false,␊ 22 | plugins: [␊ 23 | {␊ 24 | dirname: '~/test/fixtures/compare',␊ 25 | filename: '~/test/fixtures/compare/node_modules/plugin/index.js',␊ 26 | options: {␊ 27 | label: 'plugin@babelrc.1',␊ 28 | },␊ 29 | name: '🤡🎪🎟.0',␊ 30 | },␊ 31 | {␊ 32 | dirname: '~/test/fixtures/compare',␊ 33 | filename: '~/test/fixtures/compare/node_modules/plugin/index.js',␊ 34 | options: {␊ 35 | label: 'plugin@babelrc.2',␊ 36 | },␊ 37 | name: '🤡🎪🎟.0.plugin@babelrc.2',␊ 38 | },␊ 39 | {␊ 40 | dirname: '~/test/fixtures/compare',␊ 41 | filename: '~/test/fixtures/compare/node_modules/plugin/index.js',␊ 42 | options: {␊ 43 | label: 'plugin@babelrc.3',␊ 44 | },␊ 45 | name: '🤡🎪🎟.0.plugin@babelrc.3',␊ 46 | },␊ 47 | {␊ 48 | dirname: '~/test/fixtures/compare',␊ 49 | filename: '~/test/fixtures/compare/node_modules/plugin/index.js',␊ 50 | options: {␊ 51 | label: 'plugin-not-copied',␊ 52 | },␊ 53 | name: '🤡🎪🎟.0.copy-or-not',␊ 54 | },␊ 55 | ],␊ 56 | presets: [␊ 57 | {␊ 58 | dirname: '~/test/fixtures/compare',␊ 59 | filename: '~/test/fixtures/compare/node_modules/preset/index.js',␊ 60 | options: {␊ 61 | label: 'preset@babelrc',␊ 62 | },␊ 63 | name: '🤡🎪🎟.4.preset@babelrc',␊ 64 | },␊ 65 | ],␊ 66 | }␊ 67 | ], wrapperFns), {␊ 68 | babelrc: false,␊ 69 | envName,␊ 70 | overrides: [␊ 71 | ␊ 72 | ]␊ 73 | })␊ 74 | }␊ 75 | ␊ 76 | const envOptions = Object.create(null)␊ 77 | ␊ 78 | envOptions["foo"] = (envName, cache) => {␊ 79 | const wrapperFns = new Map()␊ 80 | return Object.assign(helpers.mergeOptions([␊ 81 | helpers.loadCachedModule(cache, "~/test/fixtures/compare/dir/subdir", "~/test/fixtures/compare/dir/subdir/extended-by-babelrc.js", envName, false, undefined),␊ 82 | helpers.loadCachedModule(cache, "~/test/fixtures/compare/dir/subdir", "~/test/fixtures/compare/dir/subdir/extended-by-babelrc.js", envName, true, undefined),␊ 83 | {␊ 84 | sourceMaps: false,␊ 85 | plugins: [␊ 86 | {␊ 87 | dirname: '~/test/fixtures/compare',␊ 88 | filename: '~/test/fixtures/compare/node_modules/plugin/index.js',␊ 89 | options: {␊ 90 | label: 'plugin@babelrc.1.foo',␊ 91 | },␊ 92 | name: '🤡🎪🎟.0',␊ 93 | },␊ 94 | {␊ 95 | dirname: '~/test/fixtures/compare',␊ 96 | filename: '~/test/fixtures/compare/node_modules/plugin/index.js',␊ 97 | options: {␊ 98 | label: 'plugin@babelrc.2.foo',␊ 99 | },␊ 100 | name: '🤡🎪🎟.0.plugin@babelrc.2',␊ 101 | },␊ 102 | {␊ 103 | dirname: '~/test/fixtures/compare',␊ 104 | filename: '~/test/fixtures/compare/node_modules/plugin/index.js',␊ 105 | options: {␊ 106 | label: 'plugin@babelrc.3',␊ 107 | },␊ 108 | name: '🤡🎪🎟.0.plugin@babelrc.3',␊ 109 | },␊ 110 | {␊ 111 | dirname: '~/test/fixtures/compare',␊ 112 | filename: '~/test/fixtures/compare/node_modules/plugin/index.js',␊ 113 | options: {␊ 114 | label: 'plugin-not-copied',␊ 115 | },␊ 116 | name: '🤡🎪🎟.0.copy-or-not',␊ 117 | },␊ 118 | {␊ 119 | dirname: '~/test/fixtures/compare',␊ 120 | filename: '~/test/fixtures/compare/node_modules/env-plugin/index.js',␊ 121 | options: {␊ 122 | label: 'env-plugin@babelrc.foo',␊ 123 | },␊ 124 | name: '🤡🎪🎟.0.plugin@babelrc.foo',␊ 125 | },␊ 126 | {␊ 127 | dirname: '~/test/fixtures/compare',␊ 128 | filename: '~/test/fixtures/compare/node_modules/plugin-default-opts/index.js',␊ 129 | name: '🤡🎪🎟.7',␊ 130 | },␊ 131 | ],␊ 132 | presets: [␊ 133 | {␊ 134 | dirname: '~/test/fixtures/compare',␊ 135 | filename: '~/test/fixtures/compare/node_modules/preset/index.js',␊ 136 | options: {␊ 137 | label: 'preset@babelrc',␊ 138 | },␊ 139 | name: '🤡🎪🎟.4.preset@babelrc',␊ 140 | },␊ 141 | {␊ 142 | dirname: '~/test/fixtures/compare',␊ 143 | filename: '~/test/fixtures/compare/node_modules/preset/index.js',␊ 144 | options: {␊ 145 | label: 'preset@babelrc.foo',␊ 146 | },␊ 147 | name: '🤡🎪🎟.4.preset@babelrc.foo',␊ 148 | },␊ 149 | ],␊ 150 | }␊ 151 | ], wrapperFns), {␊ 152 | babelrc: false,␊ 153 | envName,␊ 154 | overrides: [␊ 155 | ␊ 156 | ]␊ 157 | })␊ 158 | }␊ 159 | ␊ 160 | exports.getOptions = (envName, cache) => {␊ 161 | if (typeof envName !== "string") {␊ 162 | envName = process.env.BABEL_ENV || process.env.NODE_ENV || "development"␊ 163 | }␊ 164 | return envName in envOptions␊ 165 | ? envOptions[envName](envName, cache)␊ 166 | : defaultOptions(envName, cache)␊ 167 | }␊ 168 | ` 169 | -------------------------------------------------------------------------------- /test/snapshots/codegen.js.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/novemberborn/hullabaloo-config-manager/7c6a877ee4659b19a56652ad003e3856c64290e7/test/snapshots/codegen.js.snap -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "alwaysStrict": true, 5 | "declaration": true, 6 | "module": "CommonJS", 7 | "moduleResolution": "node", 8 | "newLine": "lf", 9 | "noImplicitAny": true, 10 | "noImplicitReturns": true, 11 | "noImplicitThis": true, 12 | "outDir": "build", 13 | "pretty": true, 14 | "sourceMap": true, 15 | "strictFunctionTypes": true, 16 | "strictNullChecks": true, 17 | "target": "es2015", 18 | "typeRoots": [ 19 | "./typings", 20 | "./node_modules/@types" 21 | ] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /typings/json5.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'json5' { 2 | namespace json5 { 3 | type Options = { 4 | quote?: string 5 | space?: string | number 6 | replacer?: (key: string, value: any) => string 7 | } 8 | 9 | function parse (text: string, reviver?: (key: string, value: any) => any): any 10 | function stringify (value: any, replacer?: (key: string, value: any) => string, space?: string | number): string 11 | function stringify (value: any, options?: Options): string 12 | } 13 | export = json5 14 | } 15 | -------------------------------------------------------------------------------- /typings/lodash.isequal.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'lodash.isequal' { 2 | function isEqual (lhs: any, rhs: any): boolean 3 | export = isEqual 4 | } 5 | -------------------------------------------------------------------------------- /typings/lodash.merge.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'lodash.merge' { 2 | function merge (object: T, ...sources: object[]): T 3 | export = merge 4 | } 5 | -------------------------------------------------------------------------------- /typings/md5-hex.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'md5-hex' { 2 | function md5Hex (input: Buffer | string | Array): string 3 | export = md5Hex 4 | } 5 | -------------------------------------------------------------------------------- /typings/package-hash.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'package-hash' { 2 | function packageHash (paths: string | string[], salt?: any[] | Buffer | object | string): Promise 3 | export = packageHash 4 | } 5 | -------------------------------------------------------------------------------- /typings/pirates.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'pirates' { 2 | namespace pirates { 3 | function addHook( 4 | hook: (code: string, filename: string) => string, 5 | opts: { 6 | exts: string[] 7 | matcher: (filename: string) => boolean 8 | } 9 | ): () => void 10 | } 11 | export = pirates 12 | } 13 | -------------------------------------------------------------------------------- /typings/pkg-dir.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'pkg-dir' { 2 | namespace pkgDir { 3 | export function sync (filename: string): string | null 4 | } 5 | export = pkgDir 6 | } 7 | -------------------------------------------------------------------------------- /typings/resolve-from.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'resolve-from' { 2 | function resolveFrom (fromDir: string, name: string): string 3 | namespace resolveFrom { 4 | export function silent (fromDir: string, name: string): string | null 5 | } 6 | export = resolveFrom 7 | } 8 | --------------------------------------------------------------------------------