├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmignore ├── .npmrc ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package.json ├── src ├── NO_COMPONENT.js ├── bin.js ├── commands │ ├── componentPaths.js │ ├── syncConfig.js │ └── validate.js ├── helpers │ ├── addComponentAliases.js │ ├── getComponentPaths.js │ ├── getComponents.js │ ├── getDefaultOrModule.js │ ├── getDescriptorFromProvider.js │ ├── getModuleFromPath.js │ ├── getProjectExtras.js │ ├── getProjectRootConfig.js │ ├── getValidationErrors.js │ ├── getVariationProviders.js │ ├── getVariations.js │ ├── globToFiles.js │ ├── interopRequireDefault.js │ ├── isValidProjectName.js │ ├── normalizeConfig.js │ ├── requireFile.js │ ├── requireFiles.js │ ├── requireProperties.js │ ├── requirePropertyPaths.js │ ├── runSyncModules.js │ ├── stripMatchingPrefix.js │ ├── validateCommand.js │ ├── validateProject.js │ └── validateProjects.js ├── projectConfig.js ├── schema.json └── traversal │ ├── forEachDescriptor.js │ ├── forEachProject.js │ ├── forEachProjectVariation.js │ └── forEachVariation.js └── test ├── fixtures ├── defaultExportObject.js ├── namedExports.js └── projectConfig.json ├── helpers ├── getComponentPaths.js ├── getComponents.js ├── getDefaultOrModule.js ├── getDescriptorFromProvider.js ├── getModuleFromPath.js ├── getVariationProviders.js ├── isValidProjectName.js ├── requirePropertyPaths.js ├── runSyncModules.js ├── stripMatchingPrefix.js └── validateProjects.js └── traversal ├── forEachDescriptor.js ├── forEachProject.js ├── forEachProjectVariation.js └── forEachVariation.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["airbnb"], 3 | "plugins": [ 4 | ["transform-replace-object-assign", { "moduleSpecifier": "object.assign" }], 5 | ["add-module-exports"] 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": "airbnb-base", 4 | "rules": { 5 | "max-len": 0, 6 | "no-console": 0, 7 | }, 8 | "overrides": [{ 9 | "files": ["test/**/*.js"], 10 | "env": { 11 | "jest": true, 12 | }, 13 | "rules": { 14 | "global-require": "off", 15 | }, 16 | }], 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Ignore top-level built files 9 | /*.js 10 | /*.json 11 | /commands 12 | /helpers 13 | /traversal 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (http://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # Typescript v1 declaration files 47 | typings/ 48 | 49 | # Optional npm cache directory 50 | .npm 51 | 52 | # Optional eslint cache 53 | .eslintcache 54 | 55 | # Optional REPL history 56 | .node_repl_history 57 | 58 | # Output of 'npm pack' 59 | *.tgz 60 | 61 | # Yarn Integrity file 62 | .yarn-integrity 63 | 64 | # dotenv environment variables file 65 | .env 66 | 67 | # Only apps should have lockfiles 68 | npm-shrinkwrap.json 69 | package-lock.json 70 | yarn.lock 71 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # Only apps should have lockfiles 61 | npm-shrinkwrap.json 62 | package-lock.json 63 | yarn.lock 64 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | allow-same-version=true 3 | message=v%s 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | os: 3 | - linux 4 | node_js: 5 | - "10" 6 | - "8" 7 | - "6" 8 | before_install: 9 | - 'case "${TRAVIS_NODE_VERSION}" in 0.*) export NPM_CONFIG_STRICT_SSL=false ;; esac' 10 | - 'nvm install-latest-npm' 11 | install: 12 | - 'if [ "${TRAVIS_NODE_VERSION}" = "0.6" ] || [ "${TRAVIS_NODE_VERSION}" = "0.9" ]; then nvm install --latest-npm 0.8 && npm install && nvm use "${TRAVIS_NODE_VERSION}"; else npm install; fi;' 13 | script: 14 | - 'if [ -n "${PRETEST-}" ]; then npm run pretest ; fi' 15 | - 'if [ -n "${POSTTEST-}" ]; then npm run posttest ; fi' 16 | - 'if [ -n "${TEST-}" ]; then npm run tests-only ; fi' 17 | sudo: false 18 | env: 19 | - TEST=true 20 | matrix: 21 | fast_finish: true 22 | include: 23 | - node_js: "lts/*" 24 | env: PRETEST=true 25 | allow_failures: 26 | - os: osx 27 | - env: TEST=true ALLOW_FAILURE=true 28 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### Changelog 2 | 3 | All notable changes to this project will be documented in this file. Dates are displayed in UTC. 4 | 5 | Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). 6 | 7 | #### [v0.3.4](https://github.com/airbnb/react-component-variations/compare/v0.3.3...v0.3.4) 8 | 9 | > 26 April 2019 10 | 11 | - [New] allow multiple `project` config items in `normalizeConfig` [`d7ea2c8`](https://github.com/airbnb/react-component-variations/commit/d7ea2c80873daaa8ad2198e53ed6678b7c1b9346) 12 | 13 | #### [v0.3.3](https://github.com/airbnb/react-component-variations/compare/v0.3.2...v0.3.3) 14 | 15 | > 11 March 2019 16 | 17 | - [Fix] ensure requireable arrays are not normalized to objects [`a32ac6a`](https://github.com/airbnb/react-component-variations/commit/a32ac6af246fcaeb4591fc408cae88470f0d4564) 18 | 19 | #### [v0.3.2](https://github.com/airbnb/react-component-variations/compare/v0.3.1...v0.3.2) 20 | 21 | > 11 March 2019 22 | 23 | - [fix] Make the `sync` field optional in projectConfig [`3ac61b4`](https://github.com/airbnb/react-component-variations/commit/3ac61b4605c5d5554efd8296badce8fd9b0c5b37) 24 | - [Deps] update `airbnb-prop-types`, `resolve` [`df15e06`](https://github.com/airbnb/react-component-variations/commit/df15e0654ef74a1258be063b25a021a29d3d2241) 25 | - [Dev Deps] update `eslint`, `eslint-plugin-import` [`9f5984c`](https://github.com/airbnb/react-component-variations/commit/9f5984cf6df45873480e2e2d9c6f308aee2aedc1) 26 | 27 | #### [v0.3.1](https://github.com/airbnb/react-component-variations/compare/v0.3.0...v0.3.1) 28 | 29 | > 1 March 2019 30 | 31 | - Add a configuration sync script [`#15`](https://github.com/airbnb/react-component-variations/pull/15) 32 | - auto-changelog script [`d88de95`](https://github.com/airbnb/react-component-variations/commit/d88de95a626a78712a8f7bb8ec20a93a654c1658) 33 | - [New] Add project config sync hooks [`7e508dc`](https://github.com/airbnb/react-component-variations/commit/7e508dcfbb8842c050b5c1747d0d1da8df8c8d74) 34 | - [Tests] fix eslint issues [`f2fe177`](https://github.com/airbnb/react-component-variations/commit/f2fe17721dd1d8b757bf9dcc870df6c4c47c1c24) 35 | - Move builder functions to a common helper [`f10ab41`](https://github.com/airbnb/react-component-variations/commit/f10ab41e903edd042d3e09c373d21ffc14460d72) 36 | - [Refactor] add `getModuleFromPath` helper [`5f7e628`](https://github.com/airbnb/react-component-variations/commit/5f7e628beff908d5239a9ba485dcf19b3234ea21) 37 | - [meta] Add contribution instructions to the README [`4404b67`](https://github.com/airbnb/react-component-variations/commit/4404b6715ebdd3023b794fb78ee7a827475ee191) 38 | - Use nicer syntax for builder exports in command files [`73b9e46`](https://github.com/airbnb/react-component-variations/commit/73b9e463fe64246b33890f7b412007e4f514351c) 39 | - [meta] Copy all src into the root during a build [`1c73aa1`](https://github.com/airbnb/react-component-variations/commit/1c73aa10273ca1e7802edbde2c637c1c0032e616) 40 | 41 | #### [v0.3.0](https://github.com/airbnb/react-component-variations/compare/v0.2.15...v0.3.0) 42 | 43 | > 14 January 2019 44 | 45 | - [Breaking] switch component dependency injection to regular imports [`#10`](https://github.com/airbnb/react-component-variations/pull/10) 46 | - [Breaking] use a real components object instead of the Proxy mock madness [`278a7aa`](https://github.com/airbnb/react-component-variations/commit/278a7aa7473413ca89b54584316b144d1ac17a07) 47 | - [Breaking] move from component dependency injection, to normal requires/imports [`36c4e45`](https://github.com/airbnb/react-component-variations/commit/36c4e4517c192201285637df74e9a035eeec8aeb) 48 | - [New] add explicit `NO_COMPONENT` export [`8282a64`](https://github.com/airbnb/react-component-variations/commit/8282a6496175e316b6fbc12254cc262d16d898e3) 49 | - [Deps] update `chalk`, `object.entries`, `object.values`, `resolve`, `yargs` [`2ba7bf7`](https://github.com/airbnb/react-component-variations/commit/2ba7bf7c9f45a37397dc223fa6bda63f7d6bf26e) 50 | - [Dev Deps] update `eslint`, `rimraf` [`0cedde6`](https://github.com/airbnb/react-component-variations/commit/0cedde6d2d24e98544a88f572a9e639ccf5f51a1) 51 | 52 | #### [v0.2.15](https://github.com/airbnb/react-component-variations/compare/v0.2.14...v0.2.15) 53 | 54 | > 5 December 2018 55 | 56 | - [Fix] `requirePropertyPaths`: bail out on a falsy object [`05f9377`](https://github.com/airbnb/react-component-variations/commit/05f9377531080861b32cfbef7ea7d6d41236495c) 57 | 58 | #### [v0.2.14](https://github.com/airbnb/react-component-variations/compare/v0.2.13...v0.2.14) 59 | 60 | > 5 December 2018 61 | 62 | - [New] `getDescriptorFromProvider`: accept `projectMetadata`, and merge it into the descriptor’s metadata. [`8f12bdf`](https://github.com/airbnb/react-component-variations/commit/8f12bdff2b2fbe0018b3b1c6184748b626b3f8d5) 63 | - [New] add `requirePropertyPaths` helper; use it in `getProjectExtras` [`14ce416`](https://github.com/airbnb/react-component-variations/commit/14ce416f8fe154e45b2e94ebdf940ff65f73de33) 64 | - [New] allow `metadata` in project and root configs. [`5d67c11`](https://github.com/airbnb/react-component-variations/commit/5d67c11f75c1821bcbef5b09ae5deeff38fa3c31) 65 | - [Refactor] pick a better variable name [`5a49dfa`](https://github.com/airbnb/react-component-variations/commit/5a49dfade506a052137eea5fe897cf40777d56ce) 66 | 67 | #### [v0.2.13](https://github.com/airbnb/react-component-variations/compare/v0.2.12...v0.2.13) 68 | 69 | > 4 December 2018 70 | 71 | - [Fix] normalize extras more properly across project configs [`cbebb32`](https://github.com/airbnb/react-component-variations/commit/cbebb32c8486de70e96e2074a37cf94d3ea51222) 72 | - [Tests] fix tests and linter [`a5bef95`](https://github.com/airbnb/react-component-variations/commit/a5bef9501e138e6654855991a7eb3f1acb4aecbb) 73 | - [Fix] properly handle top-level "extras" values, like "action" [`82e36e0`](https://github.com/airbnb/react-component-variations/commit/82e36e05d7c819d568fa86114f82a1e06915a4fa) 74 | - Improve console error message [`1e46373`](https://github.com/airbnb/react-component-variations/commit/1e46373413af3ed0e263819c4cc18804d28210ba) 75 | 76 | #### [v0.2.12](https://github.com/airbnb/react-component-variations/compare/v0.2.11...v0.2.12) 77 | 78 | > 26 November 2018 79 | 80 | - [New] add `renderWrapper` [`5830e98`](https://github.com/airbnb/react-component-variations/commit/5830e986c68d83260c4f158c479660f844bfdd79) 81 | - [New] add `requireInteropWrapper` option to config; default to babel‘s `interopRequireDefault` [`38eb946`](https://github.com/airbnb/react-component-variations/commit/38eb946981fa519a673e66383e1dc99b33132761) 82 | - [New] add `requireProperties` helper [`9318d06`](https://github.com/airbnb/react-component-variations/commit/9318d064f6b6cedca21427a784f6fee5bf8e55c1) 83 | - [New] `requireFile`: add `lazyRequire` option. [`d21c079`](https://github.com/airbnb/react-component-variations/commit/d21c079c81884512dab0ad11de56d63bea7d5956) 84 | - [New] `normalizeConfig`: add `projectRoot` [`17583af`](https://github.com/airbnb/react-component-variations/commit/17583af735d1accca4ba5cca4d3525d09f91952c) 85 | 86 | #### [v0.2.11](https://github.com/airbnb/react-component-variations/compare/v0.2.10...v0.2.11) 87 | 88 | > 19 September 2018 89 | 90 | - [Fix] `validate`: extract project extras even if no extras are ever used [`64d6711`](https://github.com/airbnb/react-component-variations/commit/64d671167c63758a55ba555551b68d51a409d12e) 91 | - [Dev Deps] update `babel-preset-airbnb`, `eslint`, `jest`, `babel-jest` [`2b5f049`](https://github.com/airbnb/react-component-variations/commit/2b5f049a7894f972ede9b96d061a88ec8e5be163) 92 | - [Dev Deps] update `eslint`, `eslint-config-plugin-airbnb-base`, `eslint-plugin-import`, `jest` [`85c463f`](https://github.com/airbnb/react-component-variations/commit/85c463f32b869700598082b40d4c283a5b01efd8) 93 | - [Deps] update `airbnb-prop-types`, `glob` [`089e548`](https://github.com/airbnb/react-component-variations/commit/089e5484b2c5de709eb421d5c6e68db5f5ca3da0) 94 | 95 | #### [v0.2.10](https://github.com/airbnb/react-component-variations/compare/v0.2.9...v0.2.10) 96 | 97 | > 10 August 2018 98 | 99 | - [New] add `getComponentPaths` helper; `componentPaths` subcommand [`bf2339e`](https://github.com/airbnb/react-component-variations/commit/bf2339ea0a8a1d9506f426ee132f890069f172a4) 100 | - [New] add `getDefaultOrModule` [`9408bd7`](https://github.com/airbnb/react-component-variations/commit/9408bd7f42f96c4434da4b070db721b5b22ae29e) 101 | - [Deps] update `object.fromentries` [`b80b9d2`](https://github.com/airbnb/react-component-variations/commit/b80b9d28b4b95733710383d4bac71fc90ca813fd) 102 | - [Tests] tests should use the node env, not browser [`a76b455`](https://github.com/airbnb/react-component-variations/commit/a76b455a539ea1d6dc88c02736f495be5798b52e) 103 | 104 | #### [v0.2.9](https://github.com/airbnb/react-component-variations/compare/v0.2.8...v0.2.9) 105 | 106 | > 6 August 2018 107 | 108 | - [Fix] `validate`: use `getVariationProviders` [`a6f59c3`](https://github.com/airbnb/react-component-variations/commit/a6f59c38a5743314997edb9a0fa241c9534a9a02) 109 | 110 | #### [v0.2.8](https://github.com/airbnb/react-component-variations/compare/v0.2.7...v0.2.8) 111 | 112 | > 6 August 2018 113 | 114 | - [New] `forEachDescriptor`: provide `variationProvider` info to callback [`dd9a6a9`](https://github.com/airbnb/react-component-variations/commit/dd9a6a94c3e0a9217262d0575e7d33de093440c4) 115 | - [New] `getVariationProviders`: add `fileMapOnly` option [`303cceb`](https://github.com/airbnb/react-component-variations/commit/303ccebdeb5d0f89f84105b62ad2ab8772f422fe) 116 | - [Fix] `forEachVariation`: add tests; properly throw on an invalid callback [`b627ff1`](https://github.com/airbnb/react-component-variations/commit/b627ff14f09e5a591efd562d710ae4d099161b74) 117 | - [New] `forEachProjectVariation`: add `variationProvider` info to descriptor [`2c02ac8`](https://github.com/airbnb/react-component-variations/commit/2c02ac83f6877970c67a7952d048c27783c7e10b) 118 | - [Refactor] rename `getVariations` helper to `getVariationProviers` [`d7bd243`](https://github.com/airbnb/react-component-variations/commit/d7bd2433d7e778eb14c84c7270544190665c304b) 119 | - [New] `forEachVariation`: add `projectName` to descriptor [`d0171f3`](https://github.com/airbnb/react-component-variations/commit/d0171f3ec094a407f62683b48c3233898b851f50) 120 | - [Fix] `forEachDescriptor`: `variationProvider` info: `hierarchy` [`3e06d41`](https://github.com/airbnb/react-component-variations/commit/3e06d419a4b0156f93f668ebc3c061de215505ab) 121 | - [Tests] add travis.yml [`96f7690`](https://github.com/airbnb/react-component-variations/commit/96f769038bb3c6ca74b5f662abf2163d83f98973) 122 | - [New] add `stripMatchingPrefix` helper; use it in `forEachDescriptor` [`c00fb35`](https://github.com/airbnb/react-component-variations/commit/c00fb3593fd01b7b8f4f231374df311aec928e66) 123 | - [Fix] `forEachDescriptor`: fix variationsRoot prefix stripping [`30951ec`](https://github.com/airbnb/react-component-variations/commit/30951ecaa58aec5e57078a82552cf967c16d503c) 124 | - [Dev Deps] update `babel-jest`, `babel-preset-airbnb`, `jest` [`9428e58`](https://github.com/airbnb/react-component-variations/commit/9428e585bc0bc80dbfab8d836255c1730935fdd3) 125 | - [New] `forEachVariation`: add `variationProvider` info to descriptor [`d323dba`](https://github.com/airbnb/react-component-variations/commit/d323dba328293455363314b72beb3abad33c75fc) 126 | - Restore `./helpers/getVariations` to make the previous commit non-breaking. [`e6ff016`](https://github.com/airbnb/react-component-variations/commit/e6ff016c02c98b9c3998c19093467bce04b26407) 127 | - [Tests] properly work around the bug, pending https://github.com/facebook/jest/pull/6792 [`17a1777`](https://github.com/airbnb/react-component-variations/commit/17a17775841268b153dbc192606999cc201ee58f) 128 | - [Tests] pin jsdom to v11.1, until https://github.com/facebook/jest/pull/6792 is released [`4684e2d`](https://github.com/airbnb/react-component-variations/commit/4684e2dc2193615945a54dc654d9f1f89ad1a0ae) 129 | 130 | #### [v0.2.7](https://github.com/airbnb/react-component-variations/compare/v0.2.6...v0.2.7) 131 | 132 | > 1 August 2018 133 | 134 | - [Fix] `forEachProjectVariation`: require needed files in project root config [`aeabcd2`](https://github.com/airbnb/react-component-variations/commit/aeabcd2591592b6680475172ade6b632148cc1be) 135 | 136 | #### [v0.2.6](https://github.com/airbnb/react-component-variations/compare/v0.2.5...v0.2.6) 137 | 138 | > 31 July 2018 139 | 140 | - [Fix] `validate`: properly `getComponents`; better logging [`0d49ed3`](https://github.com/airbnb/react-component-variations/commit/0d49ed3c488b5b06ef2041bf87cff3d784ecb7ec) 141 | 142 | #### [v0.2.5](https://github.com/airbnb/react-component-variations/compare/v0.2.4...v0.2.5) 143 | 144 | > 26 July 2018 145 | 146 | - [New] `forEachDescriptor`: provide `variationPath` to callback [`ef4aab7`](https://github.com/airbnb/react-component-variations/commit/ef4aab72b51a740f306b7c0779ececf255ccd525) 147 | - [New] `projectConfig`/`getComponents`: add `componentsRoot` [`8992d35`](https://github.com/airbnb/react-component-variations/commit/8992d35087f68af9b9ff45cecf2d9c73647e2eae) 148 | - [Fix] `normalizeConfig`: merge extras [`fdd81dc`](https://github.com/airbnb/react-component-variations/commit/fdd81dcdbe236571313c0df4111b38bd49de81e5) 149 | - [Fix] `globToFiles`: take a projectRoot [`b2c001f`](https://github.com/airbnb/react-component-variations/commit/b2c001f9077f6c9a7e504636891482880cf4b573) 150 | 151 | #### [v0.2.4](https://github.com/airbnb/react-component-variations/compare/v0.2.3...v0.2.4) 152 | 153 | > 17 July 2018 154 | 155 | - [Fix] `getProjectExtras`: entries are a tuple, not a struct [`6558afd`](https://github.com/airbnb/react-component-variations/commit/6558afdfa19f98b08f39c76116b69717073f9f9c) 156 | 157 | #### [v0.2.3](https://github.com/airbnb/react-component-variations/compare/v0.2.2...v0.2.3) 158 | 159 | > 16 July 2018 160 | 161 | - [New] add `extras` to project config [`cd26cc0`](https://github.com/airbnb/react-component-variations/commit/cd26cc0dfb9609bbe3ba8cb6c44c754c79ee9b06) 162 | 163 | #### [v0.2.2](https://github.com/airbnb/react-component-variations/compare/v0.2.1...v0.2.2) 164 | 165 | > 15 July 2018 166 | 167 | - [Dev Deps] update `eslint-config-airbnb-base`, `eslint-plugin-import`, `jest` [`e820898`](https://github.com/airbnb/react-component-variations/commit/e820898f8a582c771baac184084991ba8cfd4458) 168 | - `validate`: clean up Proxy code [`dd24377`](https://github.com/airbnb/react-component-variations/commit/dd243777ac547343387db3d2e23663c9b98641f8) 169 | - [Dev Deps] update `babel-preset-airbnb` [`426be26`](https://github.com/airbnb/react-component-variations/commit/426be260d6c6b3fd588e99015d52bb66e6728112) 170 | 171 | #### [v0.2.1](https://github.com/airbnb/react-component-variations/compare/v0.2.0...v0.2.1) 172 | 173 | > 15 July 2018 174 | 175 | - [Tests] initial tests [`ddfedf4`](https://github.com/airbnb/react-component-variations/commit/ddfedf451ecbb69e2117fa86fc657590a50f8ebb) 176 | - [Refactor] convert scripts to modules, require to import [`45d1963`](https://github.com/airbnb/react-component-variations/commit/45d196323ab5e55ea4da7fe74d5587cce4053a4d) 177 | - [Breaking] add `flattenComponentTree`; alias paths in `getComponents` [`0b82259`](https://github.com/airbnb/react-component-variations/commit/0b8225926e3ff0a4bfb6f69ebd91cfbe05bfc765) 178 | - [Refactor] `requireFiles`: improve readability [`8691143`](https://github.com/airbnb/react-component-variations/commit/86911430ab2c8f461fbbcb7d80317caefd51a6f5) 179 | - [Fix] allow extensions in project configs; pass projectRoot around. [`1c0c551`](https://github.com/airbnb/react-component-variations/commit/1c0c551eee59bf22481ae06b8e11e08e306b892d) 180 | - `forEachDescriptor`: throw when no components or variations are found [`1980195`](https://github.com/airbnb/react-component-variations/commit/198019513056b82ef96edfeea5bbe7b079921995) 181 | - [Fix] `forEachProject`: fix logic caught by tests [`4dd73fb`](https://github.com/airbnb/react-component-variations/commit/4dd73fb79d0b967958aba81ec1cc8bcee13e93ba) 182 | - `forEachDescriptor`: improve error message [`77b385b`](https://github.com/airbnb/react-component-variations/commit/77b385b1e2bd05c3684250047894e3ac7931e33d) 183 | - [Fix] `requireFiles`: use babel interopRequireDefault helper [`1ec9c96`](https://github.com/airbnb/react-component-variations/commit/1ec9c9604f96046539e93a02e1e7aa3dbafdb009) 184 | - [Fix] `getProjectRootConfig`: return the normalized config [`1e17d17`](https://github.com/airbnb/react-component-variations/commit/1e17d174eb3ee2e4a1f2e30f19e77bedd25398c7) 185 | - [Fix] `globToFiles`: `path.normalize` strips off the leading dot [`e99c467`](https://github.com/airbnb/react-component-variations/commit/e99c467ae5acef8f8a95273fad42a1e3514e6cca) 186 | - [Fix] `forEachDescriptor`: call the passed-in `getDescriptor` [`1a622ff`](https://github.com/airbnb/react-component-variations/commit/1a622ff4354b82d0409ba3ea13680193e739fa38) 187 | 188 | #### [v0.2.0](https://github.com/airbnb/react-component-variations/compare/v0.1.1...v0.2.0) 189 | 190 | > 11 July 2018 191 | 192 | - [Breaking] move traversal helpers into traversal/; add new traversal helpers [`816f446`](https://github.com/airbnb/react-component-variations/commit/816f4463435fad89c56e3140c83aa858640162af) 193 | - [Fix] `normalizeConfig`: strip out yargs junk; fix schema. [`22a4c52`](https://github.com/airbnb/react-component-variations/commit/22a4c524fd975d801864eceb56170a641750b9f3) 194 | - [Refactor] use object spread instead of `Object.assign` [`cdaacfa`](https://github.com/airbnb/react-component-variations/commit/cdaacfae94708c495325963c5feca59e73470b36) 195 | - [Refactor] `forEachVariation`: use `getComponentName` from `airbnb-prop-types` [`2368156`](https://github.com/airbnb/react-component-variations/commit/23681564ee81cb46ac1bc9555d154149f3e32d9c) 196 | - [Breaking] remove `noVisualSignificance` [`baae4aa`](https://github.com/airbnb/react-component-variations/commit/baae4aa7c0582402478c4f25a2269301d99ff880) 197 | - Fix eslint errors [`f65750a`](https://github.com/airbnb/react-component-variations/commit/f65750a1ac7fe947ab58f12565b731f1b24a169c) 198 | - [New] allow a shortcut of `true` as well as `false` for consumer options [`b5b0b99`](https://github.com/airbnb/react-component-variations/commit/b5b0b99a0f02c90d5819ba47cf5efab27c6fd6bd) 199 | 200 | #### [v0.1.1](https://github.com/airbnb/react-component-variations/compare/v0.1.0...v0.1.1) 201 | 202 | > 19 June 2018 203 | 204 | - [Refactor] `validate`: extract out "getValidationErrors" logic [`b3e72a7`](https://github.com/airbnb/react-component-variations/commit/b3e72a7a00f15a1a4027ba121af231b4dc2f264b) 205 | - [Refactor] `validate`: make "get overall errors" logic reusable [`55e6b8c`](https://github.com/airbnb/react-component-variations/commit/55e6b8cd88e8e94b133a865924365b0480a32e35) 206 | - [New] add `--all` option [`17d5d4e`](https://github.com/airbnb/react-component-variations/commit/17d5d4e01d1dac2364211e41917e02ae7be95771) 207 | - [Refactor] `validate`: create globToFiles and requireFiles helpers [`7eb1701`](https://github.com/airbnb/react-component-variations/commit/7eb1701c7ad179ca89ee424e3e6f51bb2b786c19) 208 | - [Fix] `validate`: restore `console.error` after every validation [`a45779e`](https://github.com/airbnb/react-component-variations/commit/a45779e62fc9b02933a314ec769e033f4ad1cc0d) 209 | 210 | #### [v0.1.0](https://github.com/airbnb/react-component-variations/compare/v0.0.15...v0.1.0) 211 | 212 | > 19 June 2018 213 | 214 | - [Breaking] change “paths” to “variations”; allow for either “no projects” or “specific projects” in the config. [`01bc0db`](https://github.com/airbnb/react-component-variations/commit/01bc0dbb65f1d829a8c2530122aa068edf70e03b) 215 | - [New] add Components config, and minor validation [`c95fb8f`](https://github.com/airbnb/react-component-variations/commit/c95fb8f847eb74fa452901ce5545fc775b979469) 216 | - [Deps] update `resolve`, `yargs` [`261b1f3`](https://github.com/airbnb/react-component-variations/commit/261b1f3b809e4f2d40c531ad2772e2f8b5474bcb) 217 | - [Dev Deps] update `babel-preset-airbnb` [`0b73d12`](https://github.com/airbnb/react-component-variations/commit/0b73d12166641dfd8ec3567f59b83bc9309fb295) 218 | - [patch] improve “require” arg description [`b22cf7d`](https://github.com/airbnb/react-component-variations/commit/b22cf7de7acdca406bf2cf434e719442b71e8ad2) 219 | - [patch] Improve the `demandCommand` error message. [`6979b31`](https://github.com/airbnb/react-component-variations/commit/6979b31eb3bc92968dc1bceda7ddd874a83082d7) 220 | - [minor] `validate`: add printout of how many providers were found [`c910880`](https://github.com/airbnb/react-component-variations/commit/c9108806c9858feb35c0edfb40db22cb2cff858d) 221 | 222 | #### [v0.0.15](https://github.com/airbnb/react-component-variations/compare/v0.0.14...v0.0.15) 223 | 224 | > 30 May 2018 225 | 226 | - [Fix] use .npmignore [`3e20353`](https://github.com/airbnb/react-component-variations/commit/3e203534d703ff58f4db5470c499977d1ed3c2dd) 227 | 228 | #### [v0.0.14](https://github.com/airbnb/react-component-variations/compare/v0.0.13...v0.0.14) 229 | 230 | > 26 May 2018 231 | 232 | - [Refactor] add babel to transpile forEachVariation with build step [`a2cddab`](https://github.com/airbnb/react-component-variations/commit/a2cddab07d9000a4ef5fc6f42cc09d516a672417) 233 | - [New] transpile everything [`57d8bf9`](https://github.com/airbnb/react-component-variations/commit/57d8bf9a12c47ec98f8fd9598455ed6c0541d005) 234 | - [Deps] update `chalk` [`8c20855`](https://github.com/airbnb/react-component-variations/commit/8c208557b2cf9a7c51634a57b18d88265a2d51ff) 235 | - [Dev Deps] update `eslint-plugin-import` [`d9cd019`](https://github.com/airbnb/react-component-variations/commit/d9cd0198cbb78e4d11481d524ca4804bcef25711) 236 | 237 | #### [v0.0.13](https://github.com/airbnb/react-component-variations/compare/v0.0.12...v0.0.13) 238 | 239 | > 17 April 2018 240 | 241 | - [Fix] only return the Components object when a path ending in slash is destructured [`4e1c0d0`](https://github.com/airbnb/react-component-variations/commit/4e1c0d0e943a50d3db085c9d0f71fa27df7156d6) 242 | 243 | #### [v0.0.12](https://github.com/airbnb/react-component-variations/compare/v0.0.11...v0.0.12) 244 | 245 | > 17 April 2018 246 | 247 | - [Deps] update `chalk`, `jsonschema`, `resolve` [`de94583`](https://github.com/airbnb/react-component-variations/commit/de945830d8df12c51f19d2ec55699df3dce40200) 248 | - [Dev Deps] update `eslint`, `eslint-plugin-import` [`ba1ec61`](https://github.com/airbnb/react-component-variations/commit/ba1ec61d4d4c15822074fd01fbbd502e7ba537c9) 249 | - [New] allow one level of nesting in the Components object [`43fd4ed`](https://github.com/airbnb/react-component-variations/commit/43fd4edea9f17dab7891966198320de13a964875) 250 | 251 | #### [v0.0.11](https://github.com/airbnb/react-component-variations/compare/v0.0.10...v0.0.11) 252 | 253 | > 13 April 2018 254 | 255 | - [Fix] actually add metadata [`0a68810`](https://github.com/airbnb/react-component-variations/commit/0a688103b5d44fafd625b61f77a233f9424302d3) 256 | 257 | #### [v0.0.10](https://github.com/airbnb/react-component-variations/compare/v0.0.9...v0.0.10) 258 | 259 | > 13 April 2018 260 | 261 | - [New] add top-level `metadata` [`e314787`](https://github.com/airbnb/react-component-variations/commit/e314787d859fc0da408386945f524f697ed77872) 262 | - [Fix] improve validation error output [`b62d7e6`](https://github.com/airbnb/react-component-variations/commit/b62d7e6eedadbc05cc869f54ccd1106b1aaeb14a) 263 | 264 | #### [v0.0.9](https://github.com/airbnb/react-component-variations/compare/v0.0.8...v0.0.9) 265 | 266 | > 12 April 2018 267 | 268 | - [eslint] fix some linter violations [`fa01aaa`](https://github.com/airbnb/react-component-variations/commit/fa01aaa7079ff072b2a5acfa3a3ea8bfa62e1f10) 269 | - [Breaking] "paths" must be an array [`c864b0e`](https://github.com/airbnb/react-component-variations/commit/c864b0e5eda805397e9d0151fbce74eaeb637d3d) 270 | 271 | #### [v0.0.8](https://github.com/airbnb/react-component-variations/compare/v0.0.7...v0.0.8) 272 | 273 | > 2 January 2018 274 | 275 | - [Fix] ensure ExtraMock is also a recursive Proxy [`df31c30`](https://github.com/airbnb/react-component-variations/commit/df31c3008734583a8015f2b3c73431058ddb0ef3) 276 | 277 | #### [v0.0.7](https://github.com/airbnb/react-component-variations/compare/v0.0.6...v0.0.7) 278 | 279 | > 13 December 2017 280 | 281 | - [Deps] add missing `object.entries` dep [`54d4064`](https://github.com/airbnb/react-component-variations/commit/54d40645f930e06b27722e51accf339c133cd5b5) 282 | 283 | #### [v0.0.6](https://github.com/airbnb/react-component-variations/compare/v0.0.5...v0.0.6) 284 | 285 | > 12 December 2017 286 | 287 | - [Fix] ensure noVisualSignificance option is merged properly [`48d793d`](https://github.com/airbnb/react-component-variations/commit/48d793dfef91d34d886c28f51c3e0375d6525bcb) 288 | 289 | #### [v0.0.5](https://github.com/airbnb/react-component-variations/compare/v0.0.4...v0.0.5) 290 | 291 | > 11 December 2017 292 | 293 | - [Fix] merge all root options into `rootOptions` [`190fdb3`](https://github.com/airbnb/react-component-variations/commit/190fdb3038345962a08d38d3f07a476734acbfa0) 294 | - [New] Add `noVisualSignificance` as a top-level and per-variation attribute [`a651ce2`](https://github.com/airbnb/react-component-variations/commit/a651ce2d07b9d67fab81de806217ce3623794717) 295 | 296 | #### [v0.0.4](https://github.com/airbnb/react-component-variations/compare/v0.0.3...v0.0.4) 297 | 298 | > 10 December 2017 299 | 300 | - Ensure `rootOptions` is an object [`66fc1d9`](https://github.com/airbnb/react-component-variations/commit/66fc1d9eb5b3c94e948ecd112b1a63359266b77c) 301 | 302 | #### [v0.0.3](https://github.com/airbnb/react-component-variations/compare/v0.0.2...v0.0.3) 303 | 304 | > 9 December 2017 305 | 306 | - Add `—require`, remove auto-require of babel-register` [`54d6317`](https://github.com/airbnb/react-component-variations/commit/54d63177f4549ca6ebaf3a98dd6eac7670d63f52) 307 | 308 | #### [v0.0.2](https://github.com/airbnb/react-component-variations/compare/v0.0.1...v0.0.2) 309 | 310 | > 9 December 2017 311 | 312 | - [Fix] Remove trailing comma [`966c931`](https://github.com/airbnb/react-component-variations/commit/966c9312e4b1c60586182485aaaf1aa878629468) 313 | 314 | #### v0.0.1 315 | 316 | > 8 December 2017 317 | 318 | - Validation code. [`665e844`](https://github.com/airbnb/react-component-variations/commit/665e8446977b0bb6fc93568de2f170c6b283c49c) 319 | - Initial commit [`8f82ddb`](https://github.com/airbnb/react-component-variations/commit/8f82ddb7038f4983e435ea0d51b3c19c07d7a98f) 320 | - [Tests] add eslint [`87c877f`](https://github.com/airbnb/react-component-variations/commit/87c877fc043737f0af89765a652faf490cb8ad22) 321 | - Add variations schema. [`a357de5`](https://github.com/airbnb/react-component-variations/commit/a357de5bd17ad9cc38d62530d8b8657896e51d84) 322 | - Reorganize to take subcommands. [`9ca498d`](https://github.com/airbnb/react-component-variations/commit/9ca498dacd65af744277b70bf2db834a3ea98579) 323 | - Add `forEachVariation` [`5173da2`](https://github.com/airbnb/react-component-variations/commit/5173da26b937f003f91672fb007b2d24765457e1) 324 | - `validate`: allow `package.json`/`--config` configuration; improve yargs usage. [`fee0dee`](https://github.com/airbnb/react-component-variations/commit/fee0dee77e33e09cea18f83171910854af3c7cd4) 325 | - package.json [`ef905a8`](https://github.com/airbnb/react-component-variations/commit/ef905a8fc343f95208808088aa39082c87aa82ec) 326 | - React warns if a [[Get]] of `getDefaultProps` returns a function. [`7fb0907`](https://github.com/airbnb/react-component-variations/commit/7fb0907d626328ff065f6660c42c77ee91025014) 327 | - Allow `component` to be a scalar or an array of components. [`70ee1a6`](https://github.com/airbnb/react-component-variations/commit/70ee1a64eefdcdd7e92e9160181b60cd27a693e6) 328 | - Only apps should have lockfiles [`2d47cc5`](https://github.com/airbnb/react-component-variations/commit/2d47cc5f5c582266893687593da4382cfc65f82d) 329 | - Add warning note to readme. [`d5d2425`](https://github.com/airbnb/react-component-variations/commit/d5d2425da64380d75f25e54ca304762d7d2c91d0) 330 | - validate: fix proxy mock properties [`f3ae5c5`](https://github.com/airbnb/react-component-variations/commit/f3ae5c56382d64c5337c20f0191cca592d0b9f25) 331 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Airbnb 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | | :exclamation: Deprecation Notice | 2 | |:-| 3 | |We want to express our sincere gratitude for your support and contributions to this open source project. As we are no longer using this technology internally, we have come to the decision to archive this repository. While we won't be providing further updates or support, the existing code and resources will remain accessible for your reference. We encourage anyone interested to fork the repository and continue the project's legacy independently. Thank you for being a part of this journey and for your patience and understanding.| 4 | 5 | # react-component-variations 6 | 7 | ⚠️ **Warning: this project is still very alpha** 8 | 9 | This project is: 10 | * A format called "variations" for specifying React component **_examples_** and providing **_documentation_** for them. 11 | * Tools for finding, validating, and iterating over variations. 12 | 13 | A variation looks like: 14 | 15 | ```jsx 16 | // ButtonVariationProvider.jsx 17 | 18 | import React from 'react'; 19 | import Button from '../components/Button'; 20 | 21 | export default function ButtonVariationProvider({ action }) { 22 | return { 23 | component: Button, 24 | 25 | usage: 'Buttons are things you click on!', 26 | 27 | variations: [ 28 | { 29 | title: 'Default', 30 | render: () => , 31 | }, 32 | { 33 | title: 'Disabled', 34 | render: () => 35 | } 36 | ], 37 | }; 38 | } 39 | ``` 40 | 41 | We also have "consumers" that use the variation provider to perform tasks. Example consumers are: 42 | * Rendering variations in [Storybook](https://storybook.js.org/) 43 | * Running in tests with [Enzyme](https://github.com/airbnb/react-component-variations-consumer-enzyme) 44 | * Taking screenshots with [Happo](https://happo.io/) 45 | * Checking for accessibility violations with [Axe](https://www.deque.com/axe/) 46 | 47 | ## Contribution Guidelines: 48 | 49 | 1. Fork this Repo 50 | 2. Install dependencies by running `npm install` in the root of the project directory. 51 | 3. To run lint and test run locally, in the project root run `npm test`. To run tests only, `npm run tests-only`. 52 | 53 | Make sure new helpers and traversal functions have related tests. 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-component-variations", 3 | "version": "0.3.4", 4 | "description": "", 5 | "main": "index.js", 6 | "bin": { 7 | "react-component-variations": "./bin.js" 8 | }, 9 | "scripts": { 10 | "prebuild": "npm run clean", 11 | "build": "babel src -d . --copy-files", 12 | "build:watch": "npm run build -- --watch", 13 | "clean": "rimraf *.js {commands,helpers,traversal} schema.json", 14 | "lint": "eslint --ext=js,jsx --ignore-path=.gitignore .", 15 | "version": "auto-changelog && git add CHANGELOG.md", 16 | "postversion": "auto-changelog && git add CHANGELOG.md && git commit --no-edit --amend && git tag -f \"v$(node -e \"console.log(require('./package.json').version)\")\"", 17 | "prepublish": "npm run build", 18 | "pretest": "npm run lint", 19 | "tests-only": "jest --coverage", 20 | "test": "npm run tests-only" 21 | }, 22 | "author": "Jordan Harband ", 23 | "license": "MIT", 24 | "auto-changelog": { 25 | "output": "CHANGELOG.md", 26 | "unreleased": false, 27 | "commitLimit": false, 28 | "backfillLimit": false 29 | }, 30 | "dependencies": { 31 | "airbnb-prop-types": "^2.12.0", 32 | "array.prototype.find": "^2.0.4", 33 | "array.prototype.flat": "^1.2.1", 34 | "array.prototype.flatmap": "^1.2.1", 35 | "chalk": "^2.4.2", 36 | "find-up": "^3.0.0", 37 | "glob": "^7.1.3", 38 | "has": "^1.0.3", 39 | "jsonschema": "^1.2.4", 40 | "object.assign": "^4.1.0", 41 | "object.entries": "^1.1.0", 42 | "object.fromentries": "^2.0.0", 43 | "object.values": "^1.1.0", 44 | "promise.try": "^1.0.0", 45 | "resolve": "^1.10.0", 46 | "yargs": "^10.1.2" 47 | }, 48 | "devDependencies": { 49 | "auto-changelog": "^1.11.0", 50 | "babel-cli": "^6.26.0", 51 | "babel-jest": "^23.6.0", 52 | "babel-plugin-add-module-exports": "^0.2.1", 53 | "babel-plugin-transform-replace-object-assign": "^1.0.0", 54 | "babel-preset-airbnb": "^2.6.0", 55 | "eslint": "^5.15.1", 56 | "eslint-config-airbnb-base": "^13.1.0", 57 | "eslint-plugin-import": "^2.16.0", 58 | "jest": "^23.6.0", 59 | "react": "*", 60 | "rimraf": "^2.6.3" 61 | }, 62 | "peerDependencies": { 63 | "react": "*" 64 | }, 65 | "jest": { 66 | "collectCoverageFrom": [ 67 | "src/**", 68 | "!**/*.json" 69 | ], 70 | "coverageReporters": [ 71 | "json-summary", 72 | "lcov", 73 | "html" 74 | ], 75 | "moduleFileExtensions": [ 76 | "js", 77 | "jsx", 78 | "json" 79 | ], 80 | "modulePathIgnorePatterns": [ 81 | "/test/fixtures" 82 | ], 83 | "testEnvironment": "node", 84 | "testRegex": "test/.*\\.jsx?$" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/NO_COMPONENT.js: -------------------------------------------------------------------------------- 1 | export default { 2 | then() { throw new SyntaxError('how can you await nothing'); }, 3 | toJSON() { throw new SyntaxError('how can you serialize nothing'); }, 4 | toString() { return 'placeholder hack to represent "no component"'; }, 5 | valueOf() { return NaN; }, 6 | }; 7 | -------------------------------------------------------------------------------- /src/bin.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import globToFiles from './helpers/globToFiles'; 4 | import requireFiles from './helpers/requireFiles'; 5 | 6 | require('yargs') 7 | .commandDir('./commands') 8 | .demandCommand(1, 'a subcommand is required') 9 | .option('project', { 10 | type: 'string', 11 | describe: 'project name', 12 | implies: 'projects', 13 | }) 14 | .option('all', { 15 | type: 'boolean', 16 | default: undefined, // necessary because "conflicts" is stupid with booleans 17 | describe: 'include all projects', 18 | implies: 'projects', 19 | }) 20 | .conflicts('all', 'project') 21 | .conflicts('project', 'all') 22 | .option('projects', { 23 | describe: 'Object mapping project names to project configs', 24 | hidden: true, 25 | }) 26 | .option('require', { 27 | type: 'string', 28 | describe: 'Optional path(s) to require, like a shim or custom loader', 29 | coerce: (x) => { requireFiles(x); return x; }, 30 | }) 31 | .option('components', { 32 | type: 'string', 33 | describe: 'glob path to React components', 34 | coerce: globToFiles, 35 | }) 36 | .option('variations', { 37 | type: 'string', 38 | describe: 'glob path to Variation Providers', 39 | coerce: globToFiles, 40 | }) 41 | .config() 42 | .pkgConf('react-component-variations') 43 | .help() 44 | .parse(); 45 | -------------------------------------------------------------------------------- /src/commands/componentPaths.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import path from 'path'; 3 | import fromEntries from 'object.fromentries'; 4 | import getComponentName from 'airbnb-prop-types/build/helpers/getComponentName'; 5 | 6 | import forEachProject from '../traversal/forEachProject'; 7 | import forEachDescriptor from '../traversal/forEachDescriptor'; 8 | import getProjectRootConfig from '../helpers/getProjectRootConfig'; 9 | import requireFiles from '../helpers/requireFiles'; 10 | import getComponentPaths from '../helpers/getComponentPaths'; 11 | import normalizeConfig from '../helpers/normalizeConfig'; 12 | import stripMatchingPrefix from '../helpers/stripMatchingPrefix'; 13 | 14 | export const command = 'componentPaths'; 15 | export const desc = 'print out component paths for the given project'; 16 | 17 | export { default as builder } from '../helpers/validateCommand'; 18 | 19 | export const handler = (config) => { 20 | const projectRoot = process.cwd(); 21 | const { projectNames } = normalizeConfig(config); 22 | const { 23 | projects, 24 | require: requires, 25 | requireInteropWrapper, 26 | } = getProjectRootConfig(projectRoot); 27 | 28 | if (requires) { requireFiles(requires, { requireInteropWrapper }); } 29 | 30 | forEachProject(projects, projectNames, (projectName, projectConfig) => { 31 | const { variationsRoot } = projectConfig; 32 | const actualVariationRoot = variationsRoot ? path.join(projectRoot, variationsRoot) : projectRoot; 33 | 34 | console.log(`${chalk.inverse(chalk.blue(`Project “${projectName}”`))}:`); 35 | 36 | forEachDescriptor(projectConfig)((descriptor, { variationProvider }) => { 37 | const Components = [].concat(descriptor.component); 38 | const pathMap = fromEntries(Components.map(x => [getComponentName(x), getComponentPaths(projectConfig, projectRoot, x)])); 39 | console.log( 40 | chalk.bold(`${stripMatchingPrefix(actualVariationRoot, variationProvider.resolvedPath)}:`), 41 | JSON.stringify(pathMap, null, 2), 42 | ); 43 | }); 44 | }); 45 | 46 | process.exit(); 47 | }; 48 | -------------------------------------------------------------------------------- /src/commands/syncConfig.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import normalizeConfig from '../helpers/normalizeConfig'; 3 | import forEachProject from '../traversal/forEachProject'; 4 | 5 | import runSyncModules from '../helpers/runSyncModules'; 6 | import requireFiles from '../helpers/requireFiles'; 7 | 8 | export { default as builder } from '../helpers/validateCommand'; 9 | export const desc = 'Sync variation consumers.'; 10 | export const command = 'sync'; 11 | 12 | export const handler = (config) => { 13 | const { sync, projectNames, projects } = normalizeConfig(config); 14 | 15 | forEachProject(projects, projectNames, (projectName, projectConfig) => { 16 | const { 17 | require: requires, 18 | requireInteropWrapper, 19 | } = projectConfig; 20 | 21 | if (requires) { 22 | requireFiles(requires, { requireInteropWrapper }); 23 | } 24 | 25 | runSyncModules({ projectName, sync, projectConfig }).catch((err) => { 26 | console.error(chalk.red(err)); 27 | process.exit(1); 28 | }); 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /src/commands/validate.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | import getComponents from '../helpers/getComponents'; 4 | import getVariationProviders from '../helpers/getVariationProviders'; 5 | import getValidationErrors from '../helpers/getValidationErrors'; 6 | import requireFiles from '../helpers/requireFiles'; 7 | import forEachProject from '../traversal/forEachProject'; 8 | import normalizeConfig from '../helpers/normalizeConfig'; 9 | 10 | 11 | function getOverallErrors({ 12 | variations = {}, 13 | components = {}, 14 | log, 15 | warn, 16 | error, 17 | projectConfig, 18 | projectRoot, 19 | }) { 20 | const variationCount = Object.keys(variations).length; 21 | if (variationCount === 0) { 22 | error(chalk.red(chalk.bold('No Variation Providers found.'))); 23 | return 1; 24 | } 25 | 26 | const componentCount = Object.keys(components).length; 27 | if (componentCount === 0) { 28 | error(chalk.red(chalk.bold('No Components found.'))); 29 | return 1; 30 | } 31 | 32 | log(chalk.blue(`${chalk.bold(componentCount)} Components found...`)); 33 | log(chalk.green(`${chalk.bold(variationCount)} Variation Providers found...`)); 34 | 35 | if (componentCount < variationCount) { 36 | error(chalk.red(chalk.bold('Found fewer Components than Variation Providers.'))); 37 | return 1; 38 | } 39 | 40 | const errors = getValidationErrors(variations, { 41 | componentMap: components, 42 | projectConfig, 43 | projectRoot, 44 | }); 45 | 46 | if (errors.length > 0) { 47 | errors.forEach((e) => { warn(e); }); 48 | return errors.length; 49 | } 50 | log(chalk.green(`${chalk.bold('Success!')} All Variation Providers appear to be valid.`)); 51 | return 0; 52 | } 53 | 54 | export const command = 'validate [variations]'; 55 | export const desc = 'validate Variation Providers'; 56 | export { default as builder } from '../helpers/validateCommand'; 57 | 58 | export const handler = (config) => { 59 | const projectRoot = process.cwd(); 60 | const { projects, projectNames } = normalizeConfig(config, { projectRoot }); 61 | 62 | const exitCodes = []; 63 | 64 | forEachProject(projects, projectNames, (project, projectConfig) => { 65 | const { 66 | require: requires, 67 | requireInteropWrapper, 68 | } = projectConfig; 69 | const log = x => console.log(`${chalk.inverse(chalk.blue(`Project “${project}”`))}: ${x}`); 70 | log(chalk.yellow('validating...')); 71 | 72 | if (requires) { requireFiles(requires, { requireInteropWrapper }); } 73 | // the purpose of the try/catch here is so that when an error is encountered, we can continue showing useful output rather than terminating the process. 74 | try { 75 | const exitCode = getOverallErrors({ 76 | variations: getVariationProviders(projectConfig, projectRoot, { fileMapOnly: true }), 77 | components: getComponents(projectConfig, projectRoot, { fileMapOnly: true }), 78 | log, 79 | warn: x => console.warn(`${chalk.inverse(chalk.yellow(`Project “${project}”`))}: ${x}`), 80 | error: x => console.error(`${chalk.inverse(chalk.red(`Project “${project}”`))}: ${x}`), 81 | projectConfig, 82 | projectRoot, 83 | }); 84 | exitCodes.push(exitCode); 85 | } catch (e) { 86 | console.error(`${chalk.inverse(chalk.red(`Project “${project}”`))}: ${e.message}`); 87 | exitCodes.push(1); // however, we don't want to exit 0 later if anything has errored 88 | } 89 | }); 90 | 91 | process.exit(Math.max(...exitCodes)); 92 | }; 93 | -------------------------------------------------------------------------------- /src/helpers/addComponentAliases.js: -------------------------------------------------------------------------------- 1 | import has from 'has'; 2 | import path from 'path'; 3 | 4 | function getNameAndPath(actualPath) { 5 | const componentName = path.basename(actualPath, path.extname(actualPath)); 6 | const componentPath = path.dirname(actualPath).slice(2); 7 | 8 | return { componentName, componentPath }; 9 | } 10 | 11 | function getFlattenedAlias(componentName, componentPath) { 12 | // Handle ComponentName/index.jsx: 13 | if (componentName === 'index') { 14 | const componentKey = componentPath.slice(componentPath.lastIndexOf('/') + 1); 15 | return componentKey; 16 | } 17 | 18 | return componentName; 19 | } 20 | 21 | /* eslint no-param-reassign: 0 */ 22 | export default function addComponentAliases(Components, actualPath, value, flattenComponentTree = false) { 23 | const { componentName, componentPath } = getNameAndPath(actualPath); 24 | 25 | const actualPathNoExtension = actualPath.slice(0, -path.extname(actualPath).length); 26 | Components[actualPathNoExtension] = value; 27 | 28 | if (flattenComponentTree) { 29 | const alias = getFlattenedAlias(componentName, componentPath); 30 | if (has(Components, alias)) { 31 | throw new TypeError(` 32 | There is more than one component with the name “${alias}”! 33 | Please rename one of these components, or set the \`flattenComponentTree\` option to \`false\`. 34 | `); 35 | } 36 | 37 | Components[alias] = value; 38 | } else { 39 | // `foo/bar/baz` gets stored as `foo/bar/:baz` 40 | // `foo/bar/index` gets stored as `foo/bar/:index` 41 | const dirKey = `${componentPath}/`; 42 | if (!Components[dirKey]) { Components[dirKey] = {}; } 43 | Components[dirKey][componentName] = value; 44 | 45 | // Handle ComponentName/index.jsx: 46 | if (componentName === 'index') { 47 | // `foo/bar/index` gets stored as `foo/bar` 48 | Components[componentPath] = value; 49 | } else { 50 | // `foo/bar/baz` gets stored as `foo/bar/baz` 51 | const directPathKey = `${componentPath}/${componentName}`; 52 | Components[directPathKey] = value; 53 | } 54 | } 55 | 56 | return Components; 57 | } 58 | -------------------------------------------------------------------------------- /src/helpers/getComponentPaths.js: -------------------------------------------------------------------------------- 1 | import flatMap from 'array.prototype.flatmap'; 2 | import find from 'array.prototype.find'; 3 | import entries from 'object.entries'; 4 | 5 | import getComponents from './getComponents'; 6 | import getDefaultOrModule from './getDefaultOrModule'; 7 | 8 | export default function getComponentPaths(projectConfig, projectRoot, Component) { 9 | const fileMap = getComponents(projectConfig, projectRoot, { fileMapOnly: true }); 10 | 11 | const found = flatMap(entries(fileMap), ([requirePath, { Module, actualPath }]) => { 12 | if (Component === getDefaultOrModule(Module)) { 13 | const { componentsRoot } = projectConfig; 14 | return { 15 | actualPath, 16 | requirePath, 17 | projectRoot, 18 | ...(componentsRoot && { componentsRoot }), 19 | }; 20 | } 21 | return []; 22 | }); 23 | return find(found, Boolean); 24 | } 25 | -------------------------------------------------------------------------------- /src/helpers/getComponents.js: -------------------------------------------------------------------------------- 1 | import values from 'object.values'; 2 | import path from 'path'; 3 | 4 | import validateProject from './validateProject'; 5 | import globToFiles from './globToFiles'; 6 | import requireFiles from './requireFiles'; 7 | import addComponentAliases from './addComponentAliases'; 8 | import getDefaultOrModule from './getDefaultOrModule'; 9 | 10 | function stripRoot(filePath, projectRoot) { 11 | return filePath.startsWith(projectRoot) 12 | ? filePath.slice(projectRoot.length).replace(/^\/?/, './') 13 | : filePath; 14 | } 15 | 16 | export default function getComponents(projectConfig, projectRoot, { 17 | fileMapOnly = false, 18 | } = {}) { 19 | validateProject(projectConfig); 20 | 21 | const { 22 | components, 23 | componentsRoot, 24 | extensions, 25 | flattenComponentTree, 26 | requireInteropWrapper, 27 | } = projectConfig; 28 | const actualRoot = componentsRoot ? path.join(projectRoot, componentsRoot) : projectRoot; 29 | const files = globToFiles(components, actualRoot); 30 | const fileMap = requireFiles(files, { 31 | projectRoot: actualRoot, 32 | extensions, 33 | requireInteropWrapper, 34 | }); 35 | 36 | if (fileMapOnly) { 37 | return fileMap; 38 | } 39 | 40 | return values(fileMap).reduce(( 41 | Components, 42 | { actualPath, Module }, 43 | ) => addComponentAliases( 44 | Components, 45 | stripRoot(actualPath, actualRoot), 46 | getDefaultOrModule(Module), 47 | flattenComponentTree, 48 | ), {}); 49 | } 50 | -------------------------------------------------------------------------------- /src/helpers/getDefaultOrModule.js: -------------------------------------------------------------------------------- 1 | import has from 'has'; 2 | 3 | export default function getDefaultOrModule(Module) { 4 | return has(Module, 'default') ? Module.default : Module; 5 | } 6 | -------------------------------------------------------------------------------- /src/helpers/getDescriptorFromProvider.js: -------------------------------------------------------------------------------- 1 | import getProjectExtras from './getProjectExtras'; 2 | 3 | export default function getDescriptorFromProvider(provider, { 4 | projectConfig, 5 | projectRoot, 6 | getExtras = undefined, 7 | projectMetadata = undefined, 8 | }) { 9 | const descriptor = provider(getProjectExtras({ 10 | projectConfig, 11 | projectRoot, 12 | getExtras, 13 | })); 14 | return { 15 | ...descriptor, 16 | ...(projectMetadata && { 17 | metadata: { 18 | ...projectMetadata, 19 | ...descriptor.metadata, 20 | }, 21 | }), 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /src/helpers/getModuleFromPath.js: -------------------------------------------------------------------------------- 1 | import requireFile from './requireFile'; 2 | 3 | export default function getModuleFromPath(path, options) { 4 | const { Module: { default: requiredModule } } = requireFile(path, options); 5 | return requiredModule; 6 | } 7 | -------------------------------------------------------------------------------- /src/helpers/getProjectExtras.js: -------------------------------------------------------------------------------- 1 | import validateProject from './validateProject'; 2 | import requirePropertyPaths from './requirePropertyPaths'; 3 | 4 | export default function getProjectExtras({ 5 | projectConfig, 6 | projectRoot, 7 | getExtras = () => {}, 8 | }) { 9 | validateProject(projectConfig); 10 | 11 | const { extras = {}, extensions } = projectConfig; 12 | 13 | return { 14 | ...requirePropertyPaths(extras, { projectRoot, extensions }), 15 | ...getExtras(), 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/helpers/getProjectRootConfig.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import yargs from 'yargs'; 4 | import getModuleFromPath from './getModuleFromPath'; 5 | 6 | import normalizeConfig from './normalizeConfig'; 7 | import interopRequireDefault from './interopRequireDefault'; 8 | 9 | export default function getProjectRootConfig(projectRoot = process.cwd(), configPath = undefined) { 10 | const config = configPath 11 | ? JSON.parse(fs.readFileSync(path.join(projectRoot, configPath))) 12 | : yargs.pkgConf('react-component-variations', projectRoot).parse(''); 13 | 14 | const { 15 | extensions, 16 | requireInteropWrapper: requireInteropWrapperPath, 17 | } = config; 18 | 19 | if (requireInteropWrapperPath) { 20 | const requireInteropWrapper = getModuleFromPath(requireInteropWrapperPath, { 21 | extensions, 22 | projectRoot, 23 | requireInteropWrapper: interopRequireDefault, 24 | }); 25 | 26 | return normalizeConfig({ ...config, requireInteropWrapper }, { 27 | projectRoot, 28 | requireInteropWrapper, 29 | }); 30 | } 31 | 32 | return normalizeConfig(config, { projectRoot }); 33 | } 34 | -------------------------------------------------------------------------------- /src/helpers/getValidationErrors.js: -------------------------------------------------------------------------------- 1 | import has from 'has'; 2 | import values from 'object.values'; 3 | 4 | import { validate } from 'jsonschema'; 5 | import schema from '../schema.json'; 6 | import getProjectExtras from './getProjectExtras'; 7 | import getDefaultOrModule from './getDefaultOrModule'; 8 | import NO_COMPONENT from '../NO_COMPONENT'; 9 | 10 | function getProxy(mock) { 11 | const properties = []; 12 | return new Proxy(this || {}, { 13 | get(target, property) { 14 | if (properties.indexOf(property) < 0) { 15 | properties.push(property); 16 | } 17 | return mock.call(target, property); 18 | }, 19 | ownKeys() { 20 | return properties; 21 | }, 22 | }); 23 | } 24 | 25 | function getStaticProperty(p) { 26 | if (this && p in this) { return Reflect.get(this, p); } 27 | if (p !== 'getDefaultProps') { // avoid a React warning 28 | return () => p; 29 | } 30 | return undefined; 31 | } 32 | 33 | function ExtraMock(extra, property) { 34 | this.extra = extra; 35 | this.property = property; 36 | return getProxy.call(this, getStaticProperty); 37 | } 38 | function ExtrasMock(extra, { 39 | projectExtras, 40 | }) { 41 | return getProxy.call(projectExtras[extra], property => ( 42 | has(projectExtras[extra], property) ? projectExtras[extra][property] : new ExtraMock(extra, property) 43 | )); 44 | } 45 | 46 | function formatMsg(file, msg) { 47 | return `${file}:\n ${msg}\n`; 48 | } 49 | 50 | function validateDescriptorProvider(file, provider, { 51 | actualComponents, 52 | projectConfig, 53 | projectRoot, 54 | }) { 55 | if (typeof provider !== 'function') { 56 | throw new TypeError('provider must be a function'); 57 | } 58 | 59 | if (provider.length !== 0 && provider.length !== 1) { 60 | throw new RangeError(`provider function must take exactly 0 or 1 arguments: takes ${provider.length}`); 61 | } 62 | 63 | const projectExtras = getProjectExtras({ 64 | projectConfig, 65 | projectRoot, 66 | }); 67 | const Extras = getProxy(extra => new ExtrasMock(extra, { projectExtras })); 68 | 69 | const descriptor = provider(Extras); 70 | 71 | const components = Array.isArray(descriptor.component) 72 | ? descriptor.component 73 | : [descriptor.component]; 74 | components.forEach((component) => { 75 | if (!actualComponents.has(component)) { 76 | throw new TypeError('descriptor must have a "component" property, with a value exported from one of the files in the "components" glob (or an array of them)'); 77 | } 78 | }); 79 | 80 | const { errors } = validate(descriptor, schema); 81 | if (errors.length > 0) { 82 | throw new SyntaxError(`received invalid descriptor object:\n - ${errors.join('\n - ')}`); 83 | } 84 | 85 | descriptor.variations.forEach((variation, i) => { 86 | if (typeof variation.render !== 'function') { 87 | throw new TypeError(`variation index ${i}: 'render' must be a function`); 88 | } 89 | if (variation.render.length !== 0) { 90 | throw new RangeError(`variation index ${i}: 'render' function takes no arguments`); 91 | } 92 | // TODO: assert on return 93 | }); 94 | } 95 | 96 | export default function getValidationErrors(variations, { 97 | componentMap, 98 | projectConfig, 99 | projectRoot, 100 | }) { 101 | const origError = console.error; 102 | 103 | const componentValues = values(componentMap) 104 | .map(({ Module }) => getDefaultOrModule(Module)) 105 | .concat(NO_COMPONENT); 106 | const actualComponents = new Set(componentValues); 107 | 108 | return values(variations).map(({ actualPath, Module }) => { 109 | console.error = function throwError(msg) { throw new Error(`${actualPath}: “${msg}”`); }; 110 | try { 111 | validateDescriptorProvider(actualPath, getDefaultOrModule(Module), { 112 | actualComponents, 113 | projectConfig, 114 | projectRoot, 115 | }); 116 | console.error = origError; 117 | return null; 118 | } catch (e) { 119 | return formatMsg(actualPath, e.message); 120 | } 121 | }).filter(Boolean); 122 | } 123 | -------------------------------------------------------------------------------- /src/helpers/getVariationProviders.js: -------------------------------------------------------------------------------- 1 | import entries from 'object.entries'; 2 | import fromEntries from 'object.fromentries'; 3 | import path from 'path'; 4 | 5 | import validateProject from './validateProject'; 6 | import globToFiles from './globToFiles'; 7 | import requireFiles from './requireFiles'; 8 | import getDefaultOrModule from './getDefaultOrModule'; 9 | 10 | export default function getVariationProviders(projectConfig, projectRoot, { 11 | fileMapOnly = false, 12 | } = {}) { 13 | validateProject(projectConfig); 14 | 15 | const { 16 | variations, 17 | variationsRoot, 18 | extensions, 19 | requireInteropWrapper, 20 | } = projectConfig; 21 | const actualRoot = variationsRoot ? path.join(projectRoot, variationsRoot) : projectRoot; 22 | const files = globToFiles(variations, actualRoot); 23 | const fileMap = requireFiles(files, { 24 | projectRoot: actualRoot, 25 | extensions, 26 | requireInteropWrapper, 27 | }); 28 | 29 | if (fileMapOnly) { 30 | return fileMap; 31 | } 32 | 33 | return fromEntries(entries(fileMap).map(([requirePath, { Module }]) => [ 34 | requirePath, 35 | getDefaultOrModule(Module), 36 | ])); 37 | } 38 | -------------------------------------------------------------------------------- /src/helpers/getVariations.js: -------------------------------------------------------------------------------- 1 | console.warn('`getVariations` is deprecated; use `getVariationProviders` instead.'); 2 | 3 | export { default } from './getVariationProviders'; 4 | -------------------------------------------------------------------------------- /src/helpers/globToFiles.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import glob from 'glob'; 3 | 4 | export default function globToFiles(arg, projectRoot = process.cwd()) { 5 | if (Array.isArray(arg)) { 6 | return arg; 7 | } 8 | return glob.sync(arg, { cwd: projectRoot }).map(x => `./${path.normalize(x)}`); 9 | } 10 | -------------------------------------------------------------------------------- /src/helpers/interopRequireDefault.js: -------------------------------------------------------------------------------- 1 | export default function interopRequireDefault(obj) { 2 | // eslint-disable-next-line no-underscore-dangle 3 | return obj && obj.__esModule ? obj : { default: obj }; 4 | } 5 | -------------------------------------------------------------------------------- /src/helpers/isValidProjectName.js: -------------------------------------------------------------------------------- 1 | import has from 'has'; 2 | 3 | export default function isValidProjectName(projects, project) { 4 | return typeof project === 'string' && project.length > 0 && has(projects, project); 5 | } 6 | -------------------------------------------------------------------------------- /src/helpers/normalizeConfig.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import findUp from 'find-up'; 3 | import fromEntries from 'object.fromentries'; 4 | import entries from 'object.entries'; 5 | import flat from 'array.prototype.flat'; 6 | 7 | import requireProperties from './requireProperties'; 8 | 9 | function normalizeProjectConfig(config, { 10 | extensions, 11 | projectRoot = process.cwd(), 12 | requireInteropWrapper, 13 | } = {}) { 14 | const c = requireProperties(config, ['renderWrapper'], { 15 | extensions, 16 | projectRoot, 17 | requireInteropWrapper, 18 | }); 19 | return c; 20 | } 21 | 22 | function normalizeConfigArrays(rootConfig, projectConfig, propertyName) { 23 | const set = new Set([].concat(projectConfig[propertyName] || [], rootConfig[propertyName] || [])); 24 | return [...set]; 25 | } 26 | 27 | function normalizeRequireableArrays(propertyNames, rootConfig = {}, projectConfig = {}) { 28 | return propertyNames.reduce((conf, name) => ({ 29 | ...conf, 30 | [name]: normalizeConfigArrays(rootConfig, projectConfig, name), 31 | }), projectConfig); 32 | } 33 | 34 | function normalizeRequireableBags(propertyNames, rootConfig = {}, projectConfig = {}) { 35 | return propertyNames.reduce((conf, name) => ({ 36 | ...conf, 37 | [name]: { 38 | ...rootConfig[name], 39 | ...projectConfig[name], 40 | }, 41 | }), projectConfig); 42 | } 43 | 44 | function normalizeProjects(rootConfig, projects, extraData) { 45 | return fromEntries(entries(projects).map(([name, projectConfig]) => { 46 | const obj = normalizeRequireableBags([ 47 | 'extras', 48 | 'metadata', 49 | ], rootConfig, normalizeProjectConfig(projectConfig, extraData)); 50 | 51 | const sync = normalizeRequireableArrays([ 52 | 'hooks', 53 | ], rootConfig.sync, projectConfig.sync); 54 | 55 | const value = { ...obj, sync }; 56 | return [name, value]; 57 | })); 58 | } 59 | 60 | export default function normalizeConfig({ 61 | all, 62 | /* these are provided by yargs */ 63 | version, 64 | help, 65 | $0, 66 | config: __, 67 | _, 68 | /* ^ these were provided by yargs */ 69 | ...config 70 | }, extraData = {}) { 71 | if (all) { 72 | const { project, ...rest } = config; 73 | return { 74 | ...normalizeProjectConfig(rest, extraData), 75 | projects: normalizeProjects(rest, config.projects, extraData), 76 | projectNames: Object.keys(config.projects), 77 | }; 78 | } 79 | if (config.project) { 80 | const { project, ...rest } = config; 81 | return { 82 | ...normalizeProjectConfig(rest, extraData), 83 | projects: normalizeProjects(config, config.projects, extraData), 84 | projectNames: flat([project], 1), 85 | }; 86 | } 87 | if (!config.projects) { 88 | const { project: ___, projects, ...rest } = config; 89 | const packagePath = findUp.sync('package.json', { normalize: false }); 90 | const { name: packageName } = JSON.parse(fs.readFileSync(packagePath)); 91 | const project = packageName || 'root'; 92 | 93 | return { 94 | ...rest, 95 | projects: normalizeProjects(config, { 96 | [project]: normalizeProjectConfig(rest, extraData), 97 | }, extraData), 98 | projectNames: [project], 99 | }; 100 | } 101 | 102 | const { project, ...rest } = config; 103 | return { 104 | ...normalizeProjectConfig(rest), 105 | projectNames: Object.keys(config.projects), 106 | projects: normalizeProjects(config, config.projects, extraData), 107 | }; 108 | } 109 | -------------------------------------------------------------------------------- /src/helpers/requireFile.js: -------------------------------------------------------------------------------- 1 | import resolve from 'resolve'; 2 | import interopRequireDefault from './interopRequireDefault'; 3 | 4 | const defaultExtensionKeys = Object.keys(require.extensions); 5 | const defaultExtensions = defaultExtensionKeys.length > 0 ? defaultExtensionKeys : ['.js', '.jsx']; 6 | 7 | export default function requireFile(requirePath, { 8 | extensions = defaultExtensions, 9 | projectRoot = process.cwd(), 10 | requireInteropWrapper = interopRequireDefault, 11 | lazyRequire = false, 12 | }) { 13 | const getModule = () => { 14 | const actualPath = resolve.sync(requirePath, { basedir: projectRoot, extensions }); 15 | return { 16 | actualPath, 17 | // eslint-disable-next-line global-require, import/no-dynamic-require 18 | Module: requireInteropWrapper(require(actualPath)), 19 | }; 20 | }; 21 | 22 | return lazyRequire ? getModule : getModule(); 23 | } 24 | -------------------------------------------------------------------------------- /src/helpers/requireFiles.js: -------------------------------------------------------------------------------- 1 | import fromEntries from 'object.fromentries'; 2 | 3 | import requireFile from './requireFile'; 4 | 5 | export default function requireFiles(arg, { 6 | projectRoot = process.cwd(), 7 | requireInteropWrapper, 8 | extensions = undefined, 9 | } = {}) { 10 | if (arg && !Array.isArray(arg) && typeof arg === 'object') { 11 | return arg; 12 | } 13 | 14 | const entries = [].concat(arg).map(requirePath => [ 15 | requirePath, 16 | requireFile(requirePath, { projectRoot, requireInteropWrapper, extensions }), 17 | ]); 18 | 19 | return fromEntries(entries); 20 | } 21 | -------------------------------------------------------------------------------- /src/helpers/requireProperties.js: -------------------------------------------------------------------------------- 1 | import fromEntries from 'object.fromentries'; 2 | import entries from 'object.entries'; 3 | 4 | import requireFile from './requireFile'; 5 | 6 | export default function requireProperties(obj, propertyNames, { 7 | extensions, 8 | projectRoot, 9 | requireInteropWrapper, 10 | } = {}) { 11 | const newEntries = propertyNames.map((name) => { 12 | const { [name]: path } = obj; 13 | if (path) { 14 | const getModule = requireFile(path, { 15 | extensions, 16 | projectRoot, 17 | requireInteropWrapper, 18 | lazyRequire: true, 19 | }); 20 | return [ 21 | name, 22 | (...args) => { 23 | const m = getModule(); 24 | const { Module: { default: required } } = m; 25 | return required(...args); 26 | }, 27 | ]; 28 | } 29 | return null; 30 | }).filter(Boolean); 31 | return fromEntries(entries(obj).concat(newEntries)); 32 | } 33 | -------------------------------------------------------------------------------- /src/helpers/requirePropertyPaths.js: -------------------------------------------------------------------------------- 1 | import fromEntries from 'object.fromentries'; 2 | import entries from 'object.entries'; 3 | 4 | import requireFile from './requireFile'; 5 | import getDefaultOrModule from './getDefaultOrModule'; 6 | 7 | export default function requirePropertyPaths(obj, { projectRoot, extensions }) { 8 | if (!obj) { 9 | return obj; 10 | } 11 | const newEntries = entries(obj).map(([key, filePath]) => { 12 | const { Module } = requireFile(filePath, { projectRoot, extensions }); 13 | return [key, getDefaultOrModule(Module)]; 14 | }); 15 | return fromEntries(newEntries); 16 | } 17 | -------------------------------------------------------------------------------- /src/helpers/runSyncModules.js: -------------------------------------------------------------------------------- 1 | import promiseTry from 'promise.try'; 2 | 3 | import interopRequireDefault from './interopRequireDefault'; 4 | import getModuleFromPath from './getModuleFromPath'; 5 | 6 | function getSyncModule(config, syncScriptPath) { 7 | const { 8 | projectRoot = process.cwd(), 9 | } = config; 10 | 11 | return getModuleFromPath(syncScriptPath, { 12 | projectRoot, 13 | requireInteropWrapper: interopRequireDefault, 14 | }); 15 | } 16 | 17 | export default function runSyncModules(config) { 18 | const { 19 | sync, 20 | } = config; 21 | 22 | return sync.hooks.reduce( 23 | (prev, syncScriptPath) => prev.then(() => { 24 | const module = getSyncModule(config, syncScriptPath); 25 | return promiseTry(() => module(config)); 26 | }), 27 | Promise.resolve(), 28 | ).then(() => {}); 29 | } 30 | -------------------------------------------------------------------------------- /src/helpers/stripMatchingPrefix.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | function stripExtension(actualPath) { 4 | return path.join(path.dirname(actualPath), path.basename(actualPath, path.extname(actualPath))); 5 | } 6 | 7 | export default function stripMatchingPrefix(prefix, toStrip) { 8 | return stripExtension(toStrip.startsWith(prefix) ? toStrip.slice(prefix.length) : toStrip); 9 | } 10 | -------------------------------------------------------------------------------- /src/helpers/validateCommand.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import has from 'has'; 3 | 4 | import normalizeConfig from './normalizeConfig'; 5 | import validateProjects from './validateProjects'; 6 | import validateProject from './validateProject'; 7 | 8 | export default function validateCommand(yargs) { 9 | const config = normalizeConfig(yargs.argv); 10 | const { project, projects, all } = config; 11 | const allProjectNames = projects ? Object.keys(projects) : []; 12 | 13 | if (all && allProjectNames.length <= 0) { 14 | throw chalk.red('`--all` requires a non-empty “projects” config'); 15 | } 16 | if (all && project) { 17 | throw chalk.red('`--all` and `--project` are mutually exclusive'); 18 | } 19 | if (project && !has(projects, project)) { 20 | throw chalk.red(`Project "${project}" missing from “projects” config`); 21 | } 22 | 23 | if (projects) { 24 | validateProjects(projects, allProjectNames, 'in the “projects” config'); 25 | } else { 26 | validateProject(config); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/helpers/validateProject.js: -------------------------------------------------------------------------------- 1 | import { validate } from 'jsonschema'; 2 | import projectSchema from '../projectConfig'; 3 | 4 | export default function validateProject(projectConfig) { 5 | const { errors } = validate(projectConfig, projectSchema); 6 | if (errors.length > 0) { 7 | throw new SyntaxError(`invalid project config:\n - ${errors.join('\n - ')}`); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/helpers/validateProjects.js: -------------------------------------------------------------------------------- 1 | import isValidProjectName from './isValidProjectName'; 2 | import validateProject from './validateProject'; 3 | 4 | export default function validateProjects(projects, projectNames, extraMsg = '') { 5 | const areProjectNamesValid = Array.isArray(projectNames) 6 | && projectNames.length > 0 7 | && projectNames.every(x => isValidProjectName(projects, x)); 8 | 9 | if (!areProjectNamesValid) { 10 | throw new TypeError(`All project names ${extraMsg ? `${extraMsg} ` : ''}must be strings, and present in the “projects” config. 11 | Existing projects: 12 | - ${Object.keys(projects).join('\n - ')} 13 | `); 14 | } 15 | 16 | projectNames.forEach((project) => { 17 | const projectConfig = projects[project]; 18 | validateProject(projectConfig); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /src/projectConfig.js: -------------------------------------------------------------------------------- 1 | import VariationDescriptorSchema from './schema.json'; 2 | 3 | const { 4 | consumerOptionsObject, 5 | consumerOptions, 6 | defaultConsumerOptions, 7 | options, 8 | } = VariationDescriptorSchema.definitions; 9 | 10 | export default { 11 | $ref: '#/definitions/project', 12 | definitions: { 13 | project: { 14 | type: 'object', 15 | properties: { 16 | componentsRoot: { $ref: '#/definitions/rootDir' }, 17 | components: { $ref: '#/definitions/components' }, 18 | variationsRoot: { $ref: '#/definitions/rootDir' }, 19 | variations: { $ref: '#/definitions/variations' }, 20 | options: { $ref: '#/definitions/options' }, 21 | require: { $ref: '#/definitions/require' }, 22 | extensions: { $ref: '#/definitions/extensions' }, 23 | flattenComponentTree: { type: 'boolean' }, 24 | extras: { $ref: '#/definitions/extras' }, 25 | metadata: { $ref: '#/definitions/metadata' }, 26 | sync: { $ref: '#/definitions/sync' }, 27 | renderWrapper: { 28 | oneOf: [{ 29 | type: 'string', 30 | minLength: 1, 31 | }, { 32 | type: 'function', 33 | }], 34 | }, 35 | }, 36 | required: ['components', 'variations'], 37 | additionalProperties: false, 38 | }, 39 | components: { 40 | oneOf: [{ 41 | $ref: '#/definitions/relativeGlobPath', 42 | }, { 43 | type: 'array', 44 | items: { $ref: '#/definitions/relativeGlobPath' }, 45 | uniqueItems: true, 46 | }], 47 | }, 48 | sync: { 49 | type: 'object', 50 | properties: { 51 | hooks: { $ref: '#/definitions/require' }, 52 | additionalProperties: false, 53 | }, 54 | }, 55 | rootDir: { 56 | type: 'string', 57 | pattern: '^[^*]+/$', 58 | minLength: 1, 59 | }, 60 | variations: { 61 | oneOf: [{ 62 | $ref: '#/definitions/relativeGlobPath', 63 | }, { 64 | type: 'array', 65 | items: { $ref: '#/definitions/relativeGlobPath' }, 66 | uniqueItems: true, 67 | }], 68 | }, 69 | require: { 70 | type: 'array', 71 | items: { $ref: '#/definitions/requiredFile' }, 72 | uniqueItems: true, 73 | }, 74 | relativeGlobPath: { 75 | type: 'string', 76 | minLength: 1, 77 | }, 78 | requiredFile: { 79 | type: 'string', 80 | minLength: 1, 81 | }, 82 | extensions: { 83 | type: 'array', 84 | items: { $ref: '#/definitions/extension' }, 85 | uniqueItems: true, 86 | minLength: 1, 87 | }, 88 | extension: { 89 | type: 'string', 90 | minLength: 1, 91 | pattern: '^\\..+$', 92 | }, 93 | extras: { 94 | type: 'object', 95 | properties: {}, 96 | additionalProperties: { $ref: '#/definitions/requiredFile' }, 97 | }, 98 | metadata: { 99 | type: 'object', 100 | properties: {}, 101 | additionalProperties: { $ref: '#/definitions/requiredFile' }, 102 | }, 103 | consumerOptionsObject, 104 | consumerOptions, 105 | defaultConsumerOptions, 106 | options, 107 | }, 108 | }; 109 | -------------------------------------------------------------------------------- /src/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$ref": "#/definitions/descriptor", 3 | "definitions": { 4 | "usage": { 5 | "type": "string" 6 | }, 7 | "variation": { 8 | "type": "object", 9 | "properties": { 10 | "title": { 11 | "type": "string", 12 | "minLength": 1 13 | }, 14 | "options": { "$ref": "#/definitions/options" }, 15 | "render": { "type": "function" } 16 | }, 17 | "required": ["title", "render"], 18 | "additionalProperties": false 19 | }, 20 | "variations": { 21 | "type": "array", 22 | "items": { "$ref": "#/definitions/variation" }, 23 | "minItems": 1, 24 | "uniqueItems": true 25 | }, 26 | "consumerOptionsObject": { 27 | "allOf": [ 28 | { "$ref": "#/definitions/defaultConsumerOptions" }, 29 | { 30 | "type": "object", 31 | "properties": { 32 | }, 33 | "minProperties": 0 34 | } 35 | ] 36 | }, 37 | "consumerOptions": { 38 | "oneOf": [ 39 | { "$ref": "#/definitions/consumerOptionsObject" }, 40 | { "type": "boolean" } 41 | ] 42 | }, 43 | "defaultConsumerOptions": { 44 | "type": "object", 45 | "properties": { 46 | "disabled": { "type": "boolean" } 47 | } 48 | }, 49 | "options": { 50 | "type": "object", 51 | "properties": { 52 | "default": { "$ref": "#/definitions/defaultConsumerOptions" } 53 | }, 54 | "patternProperties": { 55 | "^[a-zA-Z_]$": { "$ref": "#/definitions/consumerOptions" } 56 | }, 57 | "minProperties": 1 58 | }, 59 | "metadata": { 60 | "type": "object", 61 | "minProperties": 1 62 | }, 63 | "descriptor": { 64 | "type": "object", 65 | "properties": { 66 | "component": { "type": "function" }, 67 | "usage": { "$ref": "#/definitions/usage" }, 68 | "createdAt": { "type": "time-date" }, 69 | "variations": { "$ref": "#/definitions/variations" }, 70 | "options": { "$ref": "#/definitions/options" }, 71 | "metadata": { "$ref": "#/definitions/metadata" } 72 | }, 73 | "required": ["component", "variations"], 74 | "additionalProperties": false 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/traversal/forEachDescriptor.js: -------------------------------------------------------------------------------- 1 | import entries from 'object.entries'; 2 | import path from 'path'; 3 | 4 | import validateProject from '../helpers/validateProject'; 5 | import getComponents from '../helpers/getComponents'; 6 | import getVariationProviders from '../helpers/getVariationProviders'; 7 | import getDescriptorFromProvider from '../helpers/getDescriptorFromProvider'; 8 | import stripMatchingPrefix from '../helpers/stripMatchingPrefix'; 9 | import getDefaultOrModule from '../helpers/getDefaultOrModule'; 10 | import requirePropertyPaths from '../helpers/requirePropertyPaths'; 11 | 12 | export default function forEachDescriptor( 13 | projectConfig, 14 | { 15 | getExtras = () => {}, 16 | getDescriptor = getDescriptorFromProvider, 17 | projectRoot = process.cwd(), 18 | } = {}, 19 | ) { 20 | validateProject(projectConfig); 21 | 22 | if (typeof getDescriptor !== 'function' || getDescriptor.length < 1 || getDescriptor.length > 2) { 23 | throw new TypeError('`getDescriptor` must be a function that accepts exactly 1 or 2 arguments'); 24 | } 25 | 26 | const Components = getComponents(projectConfig, projectRoot); 27 | const variations = getVariationProviders(projectConfig, projectRoot, { fileMapOnly: true }); 28 | 29 | if (Object.keys(Components).length === 0) { 30 | throw new RangeError('Zero components found'); 31 | } 32 | if (Object.keys(variations).length === 0) { 33 | throw new RangeError('Zero variations found'); 34 | } 35 | 36 | const { 37 | variationsRoot, 38 | metadata: projectMetadataPaths, 39 | extensions, 40 | } = projectConfig; 41 | 42 | const projectMetadata = requirePropertyPaths(projectMetadataPaths, { projectRoot, extensions }); 43 | const actualRoot = variationsRoot ? path.join(projectRoot, variationsRoot) : projectRoot; 44 | 45 | return function traverseVariationDescriptors(callback) { 46 | if (typeof callback !== 'function' || callback.length < 1 || callback.length > 2) { 47 | throw new TypeError('a callback that accepts exactly 1 or 2 arguments is required'); 48 | } 49 | entries(variations).forEach(([filePath, { actualPath, Module }]) => { 50 | const provider = getDefaultOrModule(Module); 51 | if (typeof provider !== 'function') { 52 | throw new TypeError(`“${filePath}” does not export default a function; got ${typeof provider}`); 53 | } 54 | const hierarchy = stripMatchingPrefix(actualRoot, actualPath); 55 | const descriptor = getDescriptor(provider, { 56 | Components, 57 | variations, 58 | getExtras, 59 | projectConfig, 60 | projectMetadata, 61 | }); 62 | callback(descriptor, { 63 | variationProvider: { 64 | path: filePath, 65 | resolvedPath: actualPath, 66 | hierarchy, 67 | }, 68 | get variationPath() { 69 | console.warn('this property is deprecated in favor of the `variationProvider` object’s `path` property'); 70 | return filePath; 71 | }, 72 | }); 73 | }); 74 | }; 75 | } 76 | -------------------------------------------------------------------------------- /src/traversal/forEachProject.js: -------------------------------------------------------------------------------- 1 | import validateProjects from '../helpers/validateProjects'; 2 | 3 | export default function forEachProject(projects, projectNames, callback) { 4 | if (!Array.isArray(projectNames) || projectNames.length === 0) { 5 | throw new TypeError('`projectNames` must be a non-empty array'); 6 | } 7 | 8 | validateProjects(projects, projectNames); 9 | 10 | if (typeof callback !== 'function' || (callback.length < 1 || callback.length > 2)) { 11 | throw new TypeError('a callback that accepts exactly 1 or 2 arguments is required'); 12 | } 13 | 14 | projectNames.forEach((project) => { 15 | const projectConfig = projects[project]; 16 | callback(project, projectConfig); 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /src/traversal/forEachProjectVariation.js: -------------------------------------------------------------------------------- 1 | import getProjectRootConfig from '../helpers/getProjectRootConfig'; 2 | import validateProjects from '../helpers/validateProjects'; 3 | import requireFiles from '../helpers/requireFiles'; 4 | 5 | import forEachVariation from './forEachVariation'; 6 | import forEachProject from './forEachProject'; 7 | import forEachDescriptor from './forEachDescriptor'; 8 | 9 | export default function forEachProjectVariation(consumer, { 10 | projectRoot = process.cwd(), 11 | selectProjectNames = x => x, 12 | getDescriptor = undefined, 13 | getExtras = undefined, 14 | } = {}) { 15 | // adds 1 "projects" and "project" of package.json name, normalizes each project config and merges down 16 | const { 17 | projects, 18 | require: requires, 19 | requireInteropWrapper, 20 | renderWrapper: rootRenderWrapper, 21 | } = getProjectRootConfig(projectRoot); 22 | if (rootRenderWrapper && typeof rootRenderWrapper !== 'function') { 23 | throw new TypeError(`"renderWrapper" in the project root config, if provided, must resolve to a module that exports a function. Got: ${typeof rootRenderWrapper}`); 24 | } 25 | 26 | if (requires) { requireFiles(requires, { requireInteropWrapper }); } 27 | 28 | const allProjectNames = Object.keys(projects); 29 | const filteredProjectNames = [].concat(selectProjectNames(allProjectNames)); 30 | 31 | validateProjects(projects, filteredProjectNames, 'returned from `selectProjectNames`'); 32 | 33 | return function traverseVariations(callback) { 34 | if (typeof callback !== 'function' || callback.length !== 1) { 35 | throw new TypeError('a callback that accepts exactly 1 argument is required'); 36 | } 37 | 38 | forEachProject(projects, filteredProjectNames, (projectName, projectConfig) => { 39 | const { renderWrapper: projectRenderWrapper } = projectConfig; 40 | if (projectRenderWrapper && typeof projectRenderWrapper !== 'function') { 41 | throw new TypeError(`"renderWrapper" in the project config “${projectName}”, if provided, must resolve to a module that exports a function.`); 42 | } 43 | 44 | const renderWrapper = projectRenderWrapper || rootRenderWrapper; 45 | 46 | forEachDescriptor( 47 | projectConfig, 48 | { getExtras, getDescriptor, projectRoot }, 49 | )((descriptor, { variationProvider }) => { 50 | forEachVariation( 51 | { 52 | projectName, 53 | variationProvider, 54 | ...descriptor, 55 | ...(renderWrapper && { renderWrapper }), 56 | }, 57 | consumer, 58 | callback, 59 | ); 60 | }); 61 | }); 62 | }; 63 | } 64 | -------------------------------------------------------------------------------- /src/traversal/forEachVariation.js: -------------------------------------------------------------------------------- 1 | import entries from 'object.entries'; 2 | import getComponentName from 'airbnb-prop-types/build/helpers/getComponentName'; 3 | 4 | export default function forEachVariation(descriptor, consumer, callback) { 5 | if (typeof callback !== 'function' || callback.length !== 1) { 6 | throw new TypeError('a callback that accepts exactly 1 argument is required'); 7 | } 8 | 9 | const { 10 | component, 11 | projectName, 12 | variationProvider, 13 | createdAt: rootCreatedAt, 14 | usage, 15 | options: allRootConsumerOptions = {}, 16 | metadata = {}, 17 | variations, 18 | renderWrapper = render => render(), 19 | } = descriptor; 20 | 21 | const { [consumer]: rootConsumerOptions = {} } = allRootConsumerOptions || {}; 22 | 23 | // this consumer is disabled 24 | if (!rootConsumerOptions || rootConsumerOptions.disabled) { return; } 25 | 26 | const rootOptions = entries(allRootConsumerOptions).reduce((acc, [consumerName, opts]) => ({ 27 | ...acc, 28 | [consumerName]: opts || (typeof opts === 'boolean' ? { disabled: !opts } : {}), 29 | }), {}); 30 | 31 | variations.forEach((variation) => { 32 | const { 33 | title, 34 | createdAt: variationCreatedAt, 35 | options: allVariationConsumerOptions, 36 | render, 37 | } = variation; 38 | const { [consumer]: variationOptions = {} } = allVariationConsumerOptions || {}; 39 | 40 | // this consumer is disabled 41 | if (!variationOptions || variationOptions.disabled) { return; } 42 | 43 | const componentName = Array.isArray(component) 44 | ? component.map(x => getComponentName(x)) 45 | : getComponentName(component); 46 | 47 | const options = { ...rootConsumerOptions, ...variationOptions }; 48 | const createdAt = variationCreatedAt || rootCreatedAt; 49 | 50 | if (renderWrapper && typeof renderWrapper !== 'function') { 51 | throw new TypeError('"renderWrapper", if provided, must be a function'); 52 | } 53 | 54 | const newVariation = { 55 | componentName, 56 | variationProvider, 57 | projectName, 58 | title, 59 | component, 60 | usage, 61 | options, 62 | rootOptions, 63 | ...(createdAt && { createdAt }), 64 | ...variation, 65 | options, // eslint-disable-line no-dupe-keys 66 | metadata, 67 | originalRender: render, 68 | render: () => renderWrapper(render, metadata), 69 | }; 70 | callback(newVariation); 71 | }); 72 | } 73 | -------------------------------------------------------------------------------- /test/fixtures/defaultExportObject.js: -------------------------------------------------------------------------------- 1 | export default { 2 | key: 'value', 3 | }; 4 | -------------------------------------------------------------------------------- /test/fixtures/namedExports.js: -------------------------------------------------------------------------------- 1 | export const foo = {}; 2 | export const bar = {}; 3 | -------------------------------------------------------------------------------- /test/fixtures/projectConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": { 3 | "componentsRoot": "./app/assets/javascripts/foo/team/", 4 | "components": "./**/*.{jsx,tsx}", 5 | "variationsRoot": "./app/assets/javascripts/foo/examples/", 6 | "variations": "./**/*VariationProvider.{jsx,tsx}" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/helpers/getComponentPaths.js: -------------------------------------------------------------------------------- 1 | import getComponentPaths from '../../src/helpers/getComponentPaths'; 2 | 3 | const mockModuleA = { named: {} }; 4 | const mockActualPathA = 'path/to/componentA.js'; 5 | const mockRequirePathA = 'path/to/componentA'; 6 | 7 | const mockModuleB = { default: {} }; 8 | const mockActualPathB = 'path/to/componentB.js'; 9 | const mockRequirePathB = 'path/to/componentB'; 10 | 11 | jest.mock('../../src/helpers/getComponents', () => jest.fn(() => ({ 12 | [mockRequirePathA]: { actualPath: mockActualPathA, Module: mockModuleA }, 13 | [mockRequirePathB]: { actualPath: mockActualPathB, Module: mockModuleB }, 14 | }))); 15 | 16 | const projectConfig = {}; 17 | const projectConfigWithRoot = { 18 | componentsRoot: {}, 19 | }; 20 | const projectRoot = 'root'; 21 | 22 | describe('getComponentPaths', () => { 23 | it('finds a module by default export', () => { 24 | const paths = getComponentPaths(projectConfig, projectRoot, mockModuleB.default); 25 | expect(paths).toMatchObject({ 26 | actualPath: mockActualPathB, 27 | requirePath: mockRequirePathB, 28 | projectRoot, 29 | }); 30 | }); 31 | 32 | it('finds a module without default exports', () => { 33 | const paths = getComponentPaths(projectConfig, projectRoot, mockModuleA); 34 | expect(paths).toMatchObject({ 35 | actualPath: mockActualPathA, 36 | requirePath: mockRequirePathA, 37 | projectRoot, 38 | }); 39 | }); 40 | 41 | it('includes the componentsRoot when present', () => { 42 | const paths = getComponentPaths(projectConfigWithRoot, projectRoot, mockModuleA); 43 | expect(paths).toMatchObject({ 44 | actualPath: mockActualPathA, 45 | requirePath: mockRequirePathA, 46 | projectRoot, 47 | componentsRoot: projectConfigWithRoot.componentsRoot, 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /test/helpers/getComponents.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import getComponents from '../../src/helpers/getComponents'; 3 | 4 | jest.mock('../../src/helpers/validateProject', () => jest.fn()); 5 | jest.mock('../../src/helpers/globToFiles', () => jest.fn(() => ['a', 'b', 'n'])); 6 | jest.mock('../../src/helpers/requireFiles', () => jest.fn(paths => paths.reduce((obj, filePath) => ({ 7 | ...obj, 8 | [filePath]: { 9 | actualPath: filePath, 10 | Module: filePath >= 'foo' ? { default() {} } : { named() {} }, 11 | }, 12 | }), {}))); 13 | 14 | describe('getComponents', () => { 15 | beforeEach(() => { 16 | require('../../src/helpers/validateProject').mockClear(); 17 | require('../../src/helpers/globToFiles').mockClear(); 18 | require('../../src/helpers/requireFiles').mockClear(); 19 | }); 20 | 21 | it('validates the provided config', () => { 22 | const projectConfig = { 23 | components: 'foo', 24 | extensions: ['.js', '.jsx'], 25 | }; 26 | const mock = require('../../src/helpers/validateProject'); 27 | const projectRoot = 'a/b/c'; 28 | 29 | getComponents(projectConfig, projectRoot); 30 | 31 | expect(mock).toHaveBeenCalledTimes(1); 32 | expect(mock).toHaveBeenCalledWith(projectConfig); 33 | }); 34 | 35 | it('passes components path to `globToFiles`', () => { 36 | const projectConfig = { 37 | components: 'foo', 38 | extensions: ['.js', '.jsx'], 39 | }; 40 | const mock = require('../../src/helpers/globToFiles'); 41 | const projectRoot = 'a/b/c'; 42 | 43 | getComponents(projectConfig, projectRoot); 44 | 45 | expect(mock).toHaveBeenCalledTimes(1); 46 | expect(mock).toHaveBeenCalledWith(projectConfig.components, projectRoot); 47 | }); 48 | 49 | it('passes components path to `globToFiles` with `componentsRoot`', () => { 50 | const projectConfig = { 51 | components: 'foo', 52 | componentsRoot: 'bar', 53 | extensions: ['.js', '.jsx'], 54 | }; 55 | const mock = require('../../src/helpers/globToFiles'); 56 | const projectRoot = 'a/b/c'; 57 | 58 | getComponents(projectConfig, projectRoot); 59 | 60 | expect(mock).toHaveBeenCalledTimes(1); 61 | expect(mock).toHaveBeenCalledWith( 62 | projectConfig.components, 63 | path.join(projectRoot, projectConfig.componentsRoot), 64 | ); 65 | }); 66 | 67 | it('works with `flattenComponentTree` option', () => { 68 | const projectConfig = { 69 | components: 'foo/index', 70 | extensions: ['.js', '.jsx'], 71 | flattenComponentTree: true, 72 | }; 73 | const mock = require('../../src/helpers/globToFiles'); 74 | const projectRoot = 'a/b/c'; 75 | 76 | getComponents(projectConfig, projectRoot); 77 | 78 | expect(mock).toHaveBeenCalledTimes(1); 79 | expect(mock).toHaveBeenCalledWith(projectConfig.components, projectRoot); 80 | }); 81 | 82 | it('passes expected args to `requireFiles`', () => { 83 | const projectConfig = { 84 | components: 'foo', 85 | extensions: ['.js', '.jsx'], 86 | }; 87 | const globOutput = require('../../src/helpers/globToFiles')(); 88 | const mock = require('../../src/helpers/requireFiles'); 89 | const projectRoot = 'a/b/c'; 90 | 91 | getComponents(projectConfig, projectRoot); 92 | 93 | expect(mock).toHaveBeenCalledTimes(1); 94 | const { calls: [args] } = mock.mock; 95 | const [actualGlobOutput, requireOptions] = args; 96 | expect(actualGlobOutput).toEqual(globOutput); 97 | expect(requireOptions).toMatchObject({ 98 | projectRoot, 99 | extensions: projectConfig.extensions, 100 | }); 101 | }); 102 | 103 | it('returns the fileMap directly when `fileMapOnly`', () => { 104 | const projectConfig = { 105 | components: 'foo', 106 | extensions: ['.js', '.jsx'], 107 | }; 108 | const projectRoot = 'a/b/c'; 109 | const mock = require('../../src/helpers/requireFiles'); 110 | 111 | const sentinel = {}; 112 | mock.mockReturnValue(sentinel); 113 | const fileMap = getComponents(projectConfig, projectRoot, { fileMapOnly: true }); 114 | 115 | expect(fileMap).toBe(sentinel); 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /test/helpers/getDefaultOrModule.js: -------------------------------------------------------------------------------- 1 | import getDefaultOrModule from '../../src/helpers/getDefaultOrModule'; 2 | 3 | describe('getDefaultOrModule', () => { 4 | it('throws with a non-object', () => { 5 | expect(() => getDefaultOrModule()).toThrow(); 6 | expect(() => getDefaultOrModule(null)).toThrow(); 7 | expect(() => getDefaultOrModule(undefined)).toThrow(); 8 | }); 9 | 10 | it('returns the Module when it lacks "default"', () => { 11 | const Module = {}; 12 | expect(getDefaultOrModule(Module)).toEqual(Module); 13 | }); 14 | 15 | it('returns the default when present', () => { 16 | const Module = { default: {} }; 17 | expect(getDefaultOrModule(Module)).toEqual(Module.default); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /test/helpers/getDescriptorFromProvider.js: -------------------------------------------------------------------------------- 1 | import getDescriptorFromProvider from '../../src/helpers/getDescriptorFromProvider'; 2 | 3 | jest.mock('../../src/helpers/requireFile', () => filePath => ({ filePath })); 4 | jest.mock('../../src/helpers/getProjectExtras', () => jest.fn(({ 5 | getExtras = () => {}, 6 | }) => ({ 7 | ...getExtras(), 8 | }))); 9 | 10 | const projectConfig = { 11 | components: 'path/to/components', 12 | variations: 'path/to/variations', 13 | extensions: ['.gif', '.html'], 14 | extras: { 15 | a: 'some file', 16 | b: 'some other file', 17 | }, 18 | }; 19 | const projectMetadata = { 20 | a: 1, 21 | b: 2, 22 | }; 23 | 24 | describe('getDescriptorFromProvider', () => { 25 | beforeEach(() => { 26 | require('../../src/helpers/getProjectExtras').mockClear(); 27 | }); 28 | 29 | it('is a function', () => { 30 | expect(typeof getDescriptorFromProvider).toBe('function'); 31 | }); 32 | 33 | it('invokes the provided function with the right arguments', () => { 34 | const provider = jest.fn(() => ({})); 35 | const Components = {}; 36 | const getExtras = jest.fn(() => ({ a: true })); 37 | 38 | getDescriptorFromProvider(provider, { 39 | projectConfig, 40 | Components, 41 | getExtras, 42 | }); 43 | 44 | expect(provider).toHaveBeenCalledTimes(1); 45 | const { calls: [args] } = provider.mock; 46 | const [actualExtras] = args; 47 | 48 | expect(actualExtras).toMatchObject({ 49 | a: true, 50 | }); 51 | }); 52 | 53 | it('provides a default getExtras', () => { 54 | const provider = jest.fn(() => ({})); 55 | const Components = {}; 56 | 57 | getDescriptorFromProvider(provider, { Components, projectConfig }); 58 | expect(provider).toHaveBeenCalledTimes(1); 59 | const { calls: [args] } = provider.mock; 60 | const [actualExtras] = args; 61 | 62 | expect(actualExtras).toMatchObject({}); 63 | }); 64 | 65 | it('invokes the provided `getExtras`', () => { 66 | const provider = () => ({}); 67 | const Components = { components: '' }; 68 | const getExtras = () => {}; 69 | const projectRoot = { root: '' }; 70 | const getProjectExtras = require('../../src/helpers/getProjectExtras'); 71 | 72 | getDescriptorFromProvider(provider, { 73 | Components, 74 | projectConfig, 75 | projectRoot, 76 | getExtras, 77 | }); 78 | 79 | expect(getProjectExtras).toHaveBeenCalledTimes(1); 80 | expect(getProjectExtras).toHaveBeenCalledWith(expect.objectContaining({ 81 | projectConfig, 82 | projectRoot, 83 | getExtras, 84 | })); 85 | }); 86 | 87 | it('merges in projectMetadata when descriptor lacks metadata', () => { 88 | const descriptor = { a: 1 }; 89 | const provider = () => descriptor; 90 | const Components = { components: '' }; 91 | const projectRoot = { root: '' }; 92 | 93 | const result = getDescriptorFromProvider(provider, { 94 | Components, 95 | projectConfig, 96 | projectRoot, 97 | projectMetadata, 98 | }); 99 | 100 | expect(result).toMatchObject({ 101 | metadata: { 102 | a: 1, 103 | b: 2, 104 | }, 105 | }); 106 | }); 107 | 108 | it('merges in projectMetadata when descriptor has metadata', () => { 109 | const descriptor = { a: 1, metadata: { b: 3, c: 4 } }; 110 | const provider = () => descriptor; 111 | const Components = { components: '' }; 112 | const projectRoot = { root: '' }; 113 | 114 | const result = getDescriptorFromProvider(provider, { 115 | Components, 116 | projectConfig, 117 | projectRoot, 118 | projectMetadata, 119 | }); 120 | 121 | expect(result).toMatchObject({ 122 | metadata: { 123 | a: 1, 124 | b: 3, 125 | c: 4, 126 | }, 127 | }); 128 | }); 129 | }); 130 | -------------------------------------------------------------------------------- /test/helpers/getModuleFromPath.js: -------------------------------------------------------------------------------- 1 | import getModuleFromPath from '../../src/helpers/getModuleFromPath'; 2 | import defaultObject from '../fixtures/defaultExportObject'; 3 | import interopRequireDefault from '../../src/helpers/interopRequireDefault'; 4 | 5 | describe('getDefaultOrModule', () => { 6 | const moduleConfig = { 7 | projectRoot: process.cwd(), 8 | requireInteropWrapper: interopRequireDefault, 9 | }; 10 | 11 | it('throws with a out a string path or options', () => { 12 | expect(() => getModuleFromPath()).toThrow(); 13 | expect(() => getModuleFromPath(null)).toThrow(); 14 | expect(() => getModuleFromPath(undefined)).toThrow(); 15 | expect(() => getModuleFromPath('./test/fixtures/defaultExportObject')).toThrow(); 16 | }); 17 | 18 | it('returns the default export module from a given file', () => { 19 | expect(getModuleFromPath('./test/fixtures/defaultExportObject', moduleConfig)).toEqual(defaultObject); 20 | }); 21 | 22 | it('returns undefined if there are no default exports for a given file', () => { 23 | expect(getModuleFromPath('./test/fixtures/namedExports', moduleConfig)).toBe(undefined); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/helpers/getVariationProviders.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import getVariationProviders from '../../src/helpers/getVariationProviders'; 3 | 4 | jest.mock('../../src/helpers/validateProject', () => jest.fn()); 5 | jest.mock('../../src/helpers/globToFiles', () => jest.fn(() => ['a', 'b', 'n'])); 6 | jest.mock('../../src/helpers/requireFiles', () => jest.fn(paths => paths.reduce((obj, filePath) => ({ 7 | ...obj, 8 | [filePath]: { 9 | actualPath: filePath, 10 | Module: filePath >= 'foo' ? { default() {} } : { named() {} }, 11 | }, 12 | }), {}))); 13 | 14 | describe('getVariationProviders', () => { 15 | beforeEach(() => { 16 | require('../../src/helpers/validateProject').mockClear(); 17 | require('../../src/helpers/globToFiles').mockClear(); 18 | require('../../src/helpers/requireFiles').mockClear(); 19 | }); 20 | 21 | it('validates the provided config', () => { 22 | const projectConfig = { 23 | variations: 'foo', 24 | extensions: ['.js', '.jsx'], 25 | }; 26 | const mock = require('../../src/helpers/validateProject'); 27 | const projectRoot = 'a/b/c'; 28 | 29 | getVariationProviders(projectConfig, projectRoot); 30 | 31 | expect(mock).toHaveBeenCalledTimes(1); 32 | expect(mock).toHaveBeenCalledWith(projectConfig); 33 | }); 34 | 35 | it('passes variations path to `globToFiles`', () => { 36 | const projectConfig = { 37 | variations: 'foo', 38 | extensions: ['.js', '.jsx'], 39 | }; 40 | const mock = require('../../src/helpers/globToFiles'); 41 | const projectRoot = 'a/b/c'; 42 | 43 | getVariationProviders(projectConfig, projectRoot); 44 | 45 | expect(mock).toHaveBeenCalledTimes(1); 46 | expect(mock).toHaveBeenCalledWith(projectConfig.variations, projectRoot); 47 | }); 48 | 49 | it('passes variations path to `globToFiles` with `variationsRoot`', () => { 50 | const projectConfig = { 51 | variations: 'foo', 52 | variationsRoot: 'bar', 53 | extensions: ['.js', '.jsx'], 54 | }; 55 | const mock = require('../../src/helpers/globToFiles'); 56 | const projectRoot = 'a/b/c'; 57 | 58 | getVariationProviders(projectConfig, projectRoot); 59 | 60 | expect(mock).toHaveBeenCalledTimes(1); 61 | expect(mock).toHaveBeenCalledWith( 62 | projectConfig.variations, 63 | path.join(projectRoot, projectConfig.variationsRoot), 64 | ); 65 | }); 66 | 67 | it('passes expected args to `requireFiles`', () => { 68 | const projectConfig = { 69 | variations: 'foo', 70 | extensions: ['.js', '.jsx'], 71 | }; 72 | const globOutput = require('../../src/helpers/globToFiles')(); 73 | const mock = require('../../src/helpers/requireFiles'); 74 | const projectRoot = 'a/b/c'; 75 | 76 | getVariationProviders(projectConfig, projectRoot); 77 | 78 | expect(mock).toHaveBeenCalledTimes(1); 79 | const { calls: [args] } = mock.mock; 80 | const [actualGlobOutput, requireOptions] = args; 81 | expect(actualGlobOutput).toEqual(globOutput); 82 | expect(requireOptions).toMatchObject({ 83 | projectRoot, 84 | extensions: projectConfig.extensions, 85 | }); 86 | }); 87 | 88 | it('returns the fileMap directly when `fileMapOnly`', () => { 89 | const projectConfig = { 90 | variations: 'foo', 91 | extensions: ['.js', '.jsx'], 92 | }; 93 | const projectRoot = 'a/b/c'; 94 | const mock = require('../../src/helpers/requireFiles'); 95 | 96 | const sentinel = {}; 97 | mock.mockReturnValue(sentinel); 98 | const fileMap = getVariationProviders(projectConfig, projectRoot, { fileMapOnly: true }); 99 | 100 | expect(fileMap).toBe(sentinel); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /test/helpers/isValidProjectName.js: -------------------------------------------------------------------------------- 1 | import isValidProjectName from '../../src/helpers/isValidProjectName'; 2 | 3 | describe('isValidProjectName', () => { 4 | it('is a function', () => { 5 | expect(typeof isValidProjectName).toBe('function'); 6 | }); 7 | 8 | it('returns false if `project` is not a string', () => { 9 | [undefined, null, 42, [], {}, () => {}].forEach((x) => { 10 | expect(isValidProjectName({}, x)).toBe(false); 11 | }); 12 | }); 13 | 14 | it('returns false if `project` is an empty string', () => { 15 | expect(isValidProjectName({}, '')).toBe(false); 16 | }); 17 | 18 | it('returns false if `projects` lacks a key for `project`', () => { 19 | expect(isValidProjectName({}, 'foo')).toBe(false); 20 | }); 21 | 22 | it('returns true if `projects` has a key for `project`', () => { 23 | expect(isValidProjectName({ foo: true }, 'foo')).toBe(true); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/helpers/requirePropertyPaths.js: -------------------------------------------------------------------------------- 1 | import requirePropertyPaths from '../../src/helpers/requirePropertyPaths'; 2 | 3 | jest.mock('../../src/helpers/requireFile', () => jest.fn((...args) => ({ Module: args }))); 4 | 5 | const projectRoot = 'root'; 6 | const extensions = ['js', 'jsx']; 7 | 8 | describe('requirePropertyPaths', () => { 9 | beforeEach(() => { 10 | require('../../src/helpers/requireFile').mockClear(); 11 | }); 12 | 13 | it('returns a falsy value directly', () => { 14 | expect(requirePropertyPaths(undefined, {})).toEqual(undefined); 15 | expect(requirePropertyPaths(null, {})).toEqual(null); 16 | expect(requirePropertyPaths(false, {})).toEqual(false); 17 | expect(requirePropertyPaths(0, {})).toEqual(0); 18 | expect(requirePropertyPaths(NaN, {})).toEqual(NaN); 19 | expect(requirePropertyPaths('', {})).toEqual(''); 20 | }); 21 | 22 | it('returns an equivalent cloned object', () => { 23 | const obj = {}; 24 | expect(requirePropertyPaths(obj, {})).not.toBe(obj); 25 | expect(requirePropertyPaths(obj, {})).toEqual(obj); 26 | }); 27 | 28 | it('calls into requireFile', () => { 29 | const obj = { 30 | a: 'path to a', 31 | b: 'path to b', 32 | }; 33 | 34 | const result = requirePropertyPaths(obj, { projectRoot, extensions }); 35 | expect(result).toEqual({ 36 | a: [obj.a, { projectRoot, extensions }], 37 | b: [obj.b, { projectRoot, extensions }], 38 | }); 39 | 40 | const mockRequireFile = require('../../src/helpers/requireFile'); 41 | expect(mockRequireFile).toHaveBeenCalledTimes(2); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /test/helpers/runSyncModules.js: -------------------------------------------------------------------------------- 1 | import getModuleFromPath from '../../helpers/getModuleFromPath'; 2 | import generateConfig from '../../helpers/runSyncModules'; 3 | import interopRequireDefault from '../../helpers/interopRequireDefault'; 4 | 5 | jest.mock('../../helpers/getModuleFromPath'); 6 | getModuleFromPath.mockImplementation((path, options) => () => ({ path, options })); 7 | 8 | describe('runSyncModules', () => { 9 | const config = { 10 | sync: { hooks: ['module1', 'module2', 'module3'] }, 11 | }; 12 | const moduleConfig = { 13 | projectRoot: process.cwd(), 14 | requireInteropWrapper: interopRequireDefault, 15 | }; 16 | 17 | it('should trigger each sync config module function once', () => { 18 | const { hooks } = config.sync; 19 | expect(getModuleFromPath).toHaveBeenCalledTimes(0); 20 | 21 | return generateConfig(config).then((value) => { 22 | hooks.forEach((hook) => { 23 | expect(getModuleFromPath).toHaveBeenCalledWith( 24 | hook, 25 | moduleConfig, 26 | ); 27 | }); 28 | 29 | expect(getModuleFromPath).toHaveBeenCalledTimes(3); 30 | expect(value).toBeUndefined(); 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /test/helpers/stripMatchingPrefix.js: -------------------------------------------------------------------------------- 1 | import stripMatchingPrefix from '../../src/helpers/stripMatchingPrefix'; 2 | 3 | describe('stripMatchingPrefix', () => { 4 | it('correctly strips toStrip prefix', () => { 5 | const toStrip = '/Users/foo/bar/baz/buzz/bang/quox/stories/a/b/c/dvariationProvider.jsx'; 6 | const prefix = '/Users/foo/bar/baz/buzz/bang/quox/stories/'; 7 | expect(stripMatchingPrefix(prefix, toStrip)).toEqual('a/b/c/dvariationProvider'); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /test/helpers/validateProjects.js: -------------------------------------------------------------------------------- 1 | import validateProjects from '../../src/helpers/validateProjects'; 2 | import testProjectConfig from '../fixtures/projectConfig.json'; 3 | 4 | describe('validateProjects', () => { 5 | it('throws when projectNames is not a non-empty array', () => { 6 | [undefined, null, true, false, () => {}, {}].forEach((nonArray) => { 7 | expect(() => validateProjects({}, nonArray)).toThrow(TypeError); 8 | }); 9 | 10 | expect(() => validateProjects({}, [])).toThrow(TypeError); 11 | }); 12 | 13 | it('has a proper error message when throwing', () => { 14 | const extraMsg = 'SOME EXTRA STUFF'; 15 | let error; 16 | try { 17 | validateProjects({}, [], extraMsg); 18 | } catch (e) { 19 | error = e; 20 | } 21 | expect(error).toMatchObject({ 22 | message: expect.stringContaining(extraMsg), 23 | }); 24 | }); 25 | 26 | it('should pass on a valid projectConfig', () => { 27 | expect(() => validateProjects(testProjectConfig, ['foo'])).not.toThrowError(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /test/traversal/forEachDescriptor.js: -------------------------------------------------------------------------------- 1 | import interopRequireDefault from '../../src/helpers/interopRequireDefault'; 2 | import forEachDescriptor from '../../src/traversal/forEachDescriptor'; 3 | 4 | let mockComponents; 5 | let mockVariations; 6 | 7 | jest.mock('../../src/helpers/getComponents', () => jest.fn(() => mockComponents)); 8 | jest.mock('../../src/helpers/getVariationProviders', () => jest.fn(() => mockVariations)); 9 | 10 | describe('forEachDescriptor', () => { 11 | beforeEach(() => { 12 | require('../../src/helpers/getComponents').mockClear(); 13 | require('../../src/helpers/getVariationProviders').mockClear(); 14 | mockComponents = { 15 | 'path/to/component': { actualPath: 'path/to/component.js', Module: {} }, 16 | }; 17 | mockVariations = { 18 | 'path/to/VariationProvider': { 19 | Module: interopRequireDefault(jest.fn()), 20 | actualPath: '/full/path/to/VariationProvider.jsx', 21 | }, 22 | 'path/to/another/VariationProvider': { 23 | Module: jest.fn(), 24 | actualPath: '/full/path/to/another/VariationProvider.jsx', 25 | }, 26 | }; 27 | }); 28 | 29 | it('is a function', () => { 30 | expect(typeof forEachDescriptor).toBe('function'); 31 | }); 32 | 33 | const mockProjectConfig = { 34 | components: 'glob/path/to/components', 35 | variations: 'glob/path/to/variations', 36 | }; 37 | 38 | it('throws when `getDescriptor` is not a 1 or 2 arg function', () => { 39 | [null, true, false, 42, '', [], {}].forEach((nonFunction) => { 40 | expect(() => forEachDescriptor(mockProjectConfig, { getDescriptor: nonFunction })).toThrow(TypeError); 41 | }); 42 | 43 | expect(() => forEachDescriptor(mockProjectConfig, { getDescriptor() {} })).toThrow(TypeError); 44 | expect(() => forEachDescriptor(mockProjectConfig, { 45 | getDescriptor(a, b, c) { return (a, b, c); }, 46 | })).toThrow(TypeError); 47 | }); 48 | 49 | it('returns a function', () => { 50 | const traverse = forEachDescriptor(mockProjectConfig); 51 | expect(typeof traverse).toBe('function'); 52 | }); 53 | 54 | it('calls `getComponents` and `getVariationProviders`', () => { 55 | const getComponents = require('../../src/helpers/getComponents'); 56 | const getVariationProviders = require('../../src/helpers/getVariationProviders'); 57 | 58 | forEachDescriptor(mockProjectConfig); 59 | 60 | expect(getComponents).toHaveBeenCalledTimes(1); 61 | expect(getComponents).toHaveBeenCalledWith(mockProjectConfig, expect.any(String)); 62 | 63 | expect(getVariationProviders).toHaveBeenCalledTimes(1); 64 | expect(getVariationProviders).toHaveBeenCalledWith( 65 | mockProjectConfig, 66 | expect.any(String), 67 | expect.objectContaining({ 68 | fileMapOnly: true, 69 | }), 70 | ); 71 | }); 72 | 73 | describe('traversal function', () => { 74 | const projectRoot = 'some root'; 75 | const descriptor = {}; 76 | let getExtras; 77 | let getDescriptor; 78 | beforeEach(() => { 79 | getExtras = jest.fn(); 80 | // eslint-disable-next-line no-unused-vars 81 | getDescriptor = jest.fn(x => descriptor); 82 | }); 83 | 84 | it('throws when `callback` is not a 1-arg function', () => { 85 | const traverse = forEachDescriptor(mockProjectConfig, { 86 | getExtras, 87 | getDescriptor, 88 | projectRoot, 89 | }); 90 | 91 | [null, true, false, 42, '', [], {}].forEach((nonFunction) => { 92 | expect(() => traverse(nonFunction)).toThrow(TypeError); 93 | }); 94 | 95 | expect(() => traverse(() => {})).toThrow(TypeError); 96 | expect(() => traverse(a => ({ a }))).not.toThrow(TypeError); 97 | expect(() => traverse((a, b) => ({ a, b }))).not.toThrow(TypeError); 98 | expect(() => traverse((a, b, c) => ({ a, b, c }))).toThrow(TypeError); 99 | }); 100 | 101 | it('throws with no components', () => { 102 | mockComponents = {}; 103 | expect(() => forEachDescriptor(mockProjectConfig, { 104 | getExtras, 105 | getDescriptor, 106 | projectRoot, 107 | })).toThrow(RangeError); 108 | }); 109 | 110 | it('throws with no variations', () => { 111 | mockVariations = {}; 112 | expect(() => forEachDescriptor(mockProjectConfig, { 113 | getExtras, 114 | getDescriptor, 115 | projectRoot, 116 | })).toThrow(RangeError); 117 | }); 118 | 119 | it('iterates variations', () => { 120 | const a = jest.fn(); 121 | const b = jest.fn(); 122 | 123 | const variationsRoot = 'glob/variations/path/'; 124 | const componentsRoot = 'glob/components/path/'; 125 | const mockProjectConfigWithRoots = { 126 | componentsRoot, 127 | components: './to/components/**', 128 | variationsRoot, 129 | variations: './to/variations/**', 130 | }; 131 | 132 | const variationPathA = `${projectRoot}/${variationsRoot}path/to/a`; 133 | const variationPathB = `${projectRoot}/${variationsRoot}path/to/b`; 134 | 135 | mockVariations = { 136 | [variationPathA]: { 137 | Module: interopRequireDefault(a), 138 | actualPath: `${variationPathA}.extA`, 139 | }, 140 | [variationPathB]: { 141 | Module: interopRequireDefault(b), 142 | actualPath: `${variationPathB}.extB`, 143 | }, 144 | }; 145 | const traverse = forEachDescriptor(mockProjectConfigWithRoots, { 146 | getExtras, 147 | getDescriptor, 148 | projectRoot, 149 | }); 150 | 151 | const Components = require('../../src/helpers/getComponents')(); 152 | const variations = require('../../src/helpers/getVariationProviders')(); 153 | 154 | const callback = jest.fn(x => ({ x })); 155 | traverse(callback); 156 | 157 | expect(getDescriptor).toHaveBeenCalledTimes(2); 158 | const [firstDescriptorArgs, secondDescriptorArgs] = getDescriptor.mock.calls; 159 | expect(firstDescriptorArgs).toEqual([ 160 | a, 161 | expect.objectContaining({ 162 | projectConfig: mockProjectConfigWithRoots, 163 | Components, 164 | variations, 165 | getExtras, 166 | }), 167 | ]); 168 | expect(secondDescriptorArgs).toEqual([ 169 | b, 170 | expect.objectContaining({ 171 | projectConfig: mockProjectConfigWithRoots, 172 | Components, 173 | variations, 174 | getExtras, 175 | }), 176 | ]); 177 | 178 | expect(callback).toHaveBeenCalledTimes(2); 179 | const [first, second] = callback.mock.calls; 180 | expect(first).toEqual([descriptor, { 181 | variationProvider: { 182 | hierarchy: 'path/to/a', 183 | path: variationPathA, 184 | resolvedPath: mockVariations[variationPathA].actualPath, 185 | }, 186 | variationPath: variationPathA, 187 | }]); 188 | expect(second).toEqual([descriptor, { 189 | variationProvider: { 190 | hierarchy: 'path/to/b', 191 | path: variationPathB, 192 | resolvedPath: mockVariations[variationPathB].actualPath, 193 | }, 194 | variationPath: variationPathB, 195 | }]); 196 | }); 197 | 198 | it('throws when the provider is not a function', () => { 199 | mockVariations = { 200 | 'path/to/a': { 201 | Module: interopRequireDefault(true), 202 | }, 203 | }; 204 | const traverse = forEachDescriptor(mockProjectConfig, { 205 | getExtras, 206 | getDescriptor, 207 | projectRoot, 208 | }); 209 | 210 | const callback = jest.fn(x => ({ x })); 211 | expect(() => traverse(callback)).toThrow(TypeError); 212 | }); 213 | 214 | it('provides a default `getExtras`', () => { 215 | const a = jest.fn(); 216 | mockVariations = { 217 | 'path/to/a': { 218 | Module: interopRequireDefault(a), 219 | actualPath: 'path/to/a.extension', 220 | }, 221 | }; 222 | const traverse = forEachDescriptor(mockProjectConfig, { 223 | getDescriptor, 224 | projectRoot, 225 | }); 226 | 227 | const callback = jest.fn(x => ({ x })); 228 | traverse(callback); 229 | 230 | expect(getDescriptor).toHaveBeenCalledTimes(1); 231 | const [firstDescriptorArgs] = getDescriptor.mock.calls; 232 | const [, extras] = firstDescriptorArgs; 233 | expect(typeof extras.getExtras).toBe('function'); 234 | expect(() => extras.getExtras()).not.toThrow(); 235 | }); 236 | }); 237 | }); 238 | -------------------------------------------------------------------------------- /test/traversal/forEachProject.js: -------------------------------------------------------------------------------- 1 | import forEachProject from '../../src/traversal/forEachProject'; 2 | 3 | describe('forEachProject', () => { 4 | it('is a function', () => { 5 | expect(typeof forEachProject).toBe('function'); 6 | }); 7 | 8 | it('throws when it receives a non-empty `projects` array', () => { 9 | [undefined, null, '', 'foo', {}, () => {}, true, 42, false].forEach((nonArray) => { 10 | expect(() => forEachProject({}, nonArray, x => ({ x }))).toThrow(TypeError); 11 | }); 12 | 13 | expect(() => forEachProject({}, [], x => ({ x }))).toThrow(TypeError); 14 | }); 15 | 16 | const mockProjectConfig = { 17 | components: 'glob/path/to/components', 18 | variations: 'glob/path/to/variations', 19 | }; 20 | 21 | it('validates the project config', () => { 22 | expect(() => forEachProject({ a: {}, b: mockProjectConfig }, ['a', 'b'], x => ({ x }))).toThrow(SyntaxError); 23 | expect(() => forEachProject({ a: mockProjectConfig, b: {} }, ['a', 'b'], x => ({ x }))).toThrow(SyntaxError); 24 | expect(() => forEachProject({ a: mockProjectConfig, b: {} }, ['b'], x => ({ x }))).toThrow(SyntaxError); 25 | expect(() => forEachProject({ a: mockProjectConfig, b: {} }, ['a'], x => ({ x }))).not.toThrow(); 26 | }); 27 | 28 | it('throws if the callback is not a function with a length of 1 or 2', () => { 29 | [undefined, null, '', 'foo', {}, [], true, 42, false].forEach((nonFunction) => { 30 | expect(() => forEachProject({ a: mockProjectConfig }, ['a'], nonFunction)).toThrow(TypeError); 31 | }); 32 | 33 | expect(() => forEachProject({ a: mockProjectConfig }, ['a'], () => {})).toThrow(TypeError); 34 | expect(() => forEachProject({ a: mockProjectConfig }, ['a'], (a, b, c) => ({ a, b, c }))).toThrow(TypeError); 35 | }); 36 | 37 | it('calls the callback with the expected arguments', () => { 38 | const callback = jest.fn(x => ({ x })); 39 | 40 | const bProjectConfig = { ...mockProjectConfig, require: ['test'] }; 41 | 42 | forEachProject({ 43 | a: mockProjectConfig, 44 | b: bProjectConfig, 45 | c: mockProjectConfig, 46 | }, ['a', 'b'], callback); 47 | 48 | expect(callback).toHaveBeenCalledTimes(2); 49 | const { calls: [aArgs, bArgs] } = callback.mock; 50 | 51 | expect(aArgs).toEqual([ 52 | 'a', 53 | mockProjectConfig, 54 | ]); 55 | expect(bArgs).toEqual([ 56 | 'b', 57 | bProjectConfig, 58 | ]); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /test/traversal/forEachProjectVariation.js: -------------------------------------------------------------------------------- 1 | import mockInteropRequireDefault from '../../src/helpers/interopRequireDefault'; 2 | 3 | import forEachProjectVariation from '../../src/traversal/forEachProjectVariation'; 4 | 5 | const mockProjectConfig = { 6 | componentsRoot: 'glob/', 7 | components: './path/to/components/**', 8 | variationsRoot: 'glob/', 9 | variations: './path/to/variations/**', 10 | }; 11 | 12 | const mockProjectName = 'some awesome project'; 13 | const mockProjectName2 = 'a better project'; 14 | 15 | const mockDescriptor = { 16 | a: 'b', 17 | c: 'd', 18 | }; 19 | 20 | const mockVariationPath = 'glob/path/to/RealVariationProvider'; 21 | const mockVariationHierarchy = 'path/to/RealVariationProvider'; 22 | const mockVariationActualPath = `${mockVariationHierarchy}.js`; 23 | 24 | const consumer = 'cookie monster'; 25 | 26 | jest.mock('../../src/helpers/getComponents', () => jest.fn(() => ({ 27 | 'path/to/component': { actualPath: 'path/to/component.js', Module: {} }, 28 | }))); 29 | jest.mock('../../src/helpers/getVariationProviders', () => jest.fn(() => ({ 30 | [mockVariationPath]: { 31 | actualPath: mockVariationActualPath, 32 | Module: mockInteropRequireDefault(jest.fn(() => mockDescriptor)), 33 | }, 34 | }))); 35 | 36 | const mockProjectNames = [mockProjectName, mockProjectName2]; 37 | 38 | jest.mock('../../src/traversal/forEachVariation'); 39 | jest.mock('../../src/helpers/getProjectRootConfig', () => () => ({ 40 | projects: { 41 | [mockProjectName]: { ...mockProjectConfig }, 42 | [mockProjectName2]: { ...mockProjectConfig }, 43 | }, 44 | projectNames: mockProjectNames, 45 | })); 46 | 47 | describe('forEachProjectVariation', () => { 48 | it('is a function', () => { 49 | expect(typeof forEachProjectVariation).toBe('function'); 50 | }); 51 | 52 | it('returns a function', () => { 53 | const traverse = forEachProjectVariation(consumer); 54 | expect(typeof traverse).toBe('function'); 55 | }); 56 | 57 | describe('traverse', () => { 58 | it('requires a callback function of length 1', () => { 59 | const traverse = forEachProjectVariation(consumer); 60 | [null, true, false, 42, '', [], {}].forEach((nonFunction) => { 61 | expect(() => traverse(nonFunction)).toThrow(TypeError); 62 | }); 63 | 64 | expect(() => traverse(() => {})).toThrow(TypeError); 65 | expect(() => traverse((a, b) => ({ a, b }))).toThrow(TypeError); 66 | }); 67 | }); 68 | 69 | it('invokes `forEachVariation` with the expected arguments', () => { 70 | const traverse = forEachProjectVariation(consumer); 71 | const callback = x => ({ x }); 72 | const forEachVariation = require('../../src/traversal/forEachVariation'); 73 | 74 | traverse(callback); 75 | 76 | expect(mockProjectNames.length).toBeGreaterThan(0); 77 | expect(forEachVariation).toHaveBeenCalledTimes(mockProjectNames.length); 78 | const { calls } = forEachVariation.mock; 79 | 80 | calls.forEach((args, idx) => { 81 | expect(args).toEqual([ 82 | expect.objectContaining({ 83 | projectName: mockProjectNames[idx], 84 | ...mockDescriptor, 85 | variationProvider: expect.objectContaining({ 86 | path: mockVariationPath, 87 | resolvedPath: mockVariationActualPath, 88 | hierarchy: mockVariationHierarchy, 89 | }), 90 | }), 91 | consumer, 92 | callback, 93 | ]); 94 | }); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /test/traversal/forEachVariation.js: -------------------------------------------------------------------------------- 1 | import forEachVariation from '../../src/traversal/forEachVariation'; 2 | 3 | const mockRender = function render() {}; 4 | const mockVariation = { 5 | title: 'hi', 6 | render: mockRender, 7 | originalRender: mockRender, 8 | }; 9 | 10 | const consumer = 'foo'; 11 | 12 | const mockDescriptor = { 13 | component: 'mock-component', 14 | projectName: 'mock-project', 15 | createdAt: 'timestamp', 16 | usage: 'mock-usage', 17 | metadata: { foo: 'bar' }, 18 | variations: [mockVariation], 19 | variationProvider: { 20 | path: 'a/b/c', 21 | }, 22 | options: { 23 | [consumer]: { 24 | a: 'b', 25 | }, 26 | }, 27 | }; 28 | 29 | describe('forEachVariation', () => { 30 | it('is a function', () => { 31 | expect(typeof forEachVariation).toBe('function'); 32 | }); 33 | 34 | it('throws with an invalid callback', () => { 35 | [true, false, [], {}, '', 'a', 42, null, undefined].forEach((nonFunction) => { 36 | expect(() => forEachVariation(mockDescriptor, consumer, nonFunction)).toThrow(TypeError); 37 | }); 38 | 39 | expect(() => forEachVariation(mockDescriptor, consumer, () => {})).toThrow(TypeError); 40 | expect(() => forEachVariation(mockDescriptor, consumer, (a, b) => ({ a, b }))).toThrow(TypeError); 41 | }); 42 | 43 | describe('callback function', () => { 44 | it('passes in the correct mockDescriptor data', () => { 45 | const expectedDescriptor = { 46 | ...mockDescriptor, 47 | ...mockVariation, 48 | componentName: mockDescriptor.component, 49 | options: mockDescriptor.options[consumer], 50 | rootOptions: mockDescriptor.options, 51 | render: expect.any(Function), 52 | }; 53 | delete expectedDescriptor.variations; 54 | 55 | const callback = jest.fn((newVariation) => { 56 | expect(newVariation).toMatchObject(expectedDescriptor); 57 | }); 58 | forEachVariation(mockDescriptor, consumer, callback); 59 | expect(callback).toHaveBeenCalledTimes(1); 60 | }); 61 | }); 62 | }); 63 | --------------------------------------------------------------------------------