├── .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 |
--------------------------------------------------------------------------------