├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── package-lock.json ├── package.json ├── src └── index.js └── test ├── .eslintrc.json ├── babel-register.js ├── fixtures ├── invalid-design-props │ ├── entry.js │ └── props.json ├── invalid-json │ ├── entry.js │ └── props.json ├── nested-imports │ ├── entry-commonjs.js │ ├── entry-scss.js │ ├── entry.js │ ├── import.json │ ├── nested-import.json │ └── props.json ├── no-imports │ ├── entry.js │ └── props.json ├── non-json-import │ ├── entry.js │ └── props.hson └── non-nested-imports │ ├── entry-query-options.js │ ├── entry.js │ ├── import.json │ └── props.json └── loaderTest.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015" 4 | ], 5 | "plugins": [ 6 | "transform-object-rest-spread" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs. 2 | # More information at http://EditorConfig.org 3 | 4 | # No .editorconfig files above the root directory 5 | root = true 6 | 7 | [*] 8 | indent_style = space 9 | indent_size = 4 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | 14 | [*.{json,yml,babelrc}] 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | test/temp 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends" : "airbnb", 4 | "rules" : { 5 | "indent": [2, 4], 6 | "max-len": [2, 120, 4] 7 | }, 8 | "settings": { 9 | "import/resolver": "webpack" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | npm-debug.log 4 | dist 5 | test/temp 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /* 2 | /*/ 3 | !/dist/ 4 | !README.md 5 | !LICENSE.md 6 | !package.json 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "7" 4 | - "6" 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | ## [3.0.0] 8 | 9 | The following changes and additions were made to configuring theo-loader: 10 | 11 | - Theo options can now be passed as JSON via query string, e.g. `theo-loader?{“transform”:{“type”:”web”},”format”:{“type”:”sass”}}!./tokens.json` 12 | - The options format passed to the webpack `LoaderOptionsPlugin` has changed: Theo options are specified as they would be to the`theo.convert` function. These options apply to all instances of theo-loader. 13 | - For options that vary on a per-transform, per-format or per-invocation basis, the new `getOptions` function option should be used. `getOptions` receives an initial options object that is the merged contents of the constant options supplied to the LoaderOptionsPlugin and any options supplied via query string. The final options object should be returned. 14 | 15 | ### :bangbang: Breaking changes with 2.x 16 | 17 | - The previous configuration format is no longer supported 18 | 19 | ## [2.0.0] 20 | 21 | - Updated minimum `theo` peerDependency package version to `6.0.0-beta`. ([#87](https://github.com/Autodesk/theo-loader/issues/87)) 22 | 23 | ### :bangbang: Breaking changes with 1.x 24 | 25 | #### Updated Theo to v6 26 | 27 | Support for previous versions has been dropped. For information on the changes included with theo@6 and how to migrate from previous versions, please see [the Theo documentation](https://raw.githubusercontent.com/salesforce-ux/theo). 28 | 29 | **Additional Notes**: 30 | 31 | - Node.js v6 or above is required 32 | - Theo no longer has a `json` format, so the default format for theo-loader has been changed to `common.js` which behaves similarly. 33 | 34 | ## [1.0.0] 35 | 36 | - Updated minimum `webpack` peerDependency package version to `2.4.1` 37 | 38 | ### :bangbang: Breaking changes with 0.x 39 | 40 | #### Webpack v2 now required 41 | 42 | Webpack 2 is now required for theo-loader. For instructions on upgrading from previous versions of webpack, please see [the migration guide](https://webpack.js.org/guides/migrating/). 43 | 44 | **Additional Notes**: 45 | 46 | - Options for theo-loader are now specified in the `LoaderOptionsPlugin` 47 | - Props passed to `propsFilter` and `propsMap` format options are now [Immutable.js Maps](https://facebook.github.io/immutable-js/docs/#/Map) instead of plain javascript objects. 48 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | 1. Fork the repository 4 | 2. Create your feature branch 5 | 3. Commit and push your work to the feature branch 6 | 4. Create a new Pull Request 7 | 8 | # Guidelines 9 | 10 | * Please make sure to follow existing conventions and style in order to keep the code as readable as possible. `npm run lint` should not print any errors. 11 | * Please make sure your changes did not break the unit tests 12 | * Please make sure to comment your changes 13 | * Please make sure to document public APIs for other users to understand their purpose 14 | * Please make sure to update the README.md file if needed. 15 | * Please add unit tests for your changes 16 | 17 | 18 | Before your code can be accepted into the project you must also sign the Contributor License Agreement (CLA). 19 | Please contact Beau Roberts for a copy of the CLA. 20 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2016 Autodesk Inc. http://www.autodesk.com 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # theo loader for webpack 2 | 3 | [![Greenkeeper badge](https://badges.greenkeeper.io/Autodesk/theo-loader.svg)](https://greenkeeper.io/) 4 | 5 | A webpack loader that transforms Design Tokens files using [Salesforce's theo](https://github.com/salesforce-ux/theo). 6 | 7 | [![Build Status](https://img.shields.io/travis/Autodesk/theo-loader/master.svg)](https://travis-ci.org/Autodesk/theo-loader) 8 | [![NPM Version](https://img.shields.io/npm/v/theo-loader.svg)](https://www.npmjs.com/package/theo-loader) 9 | [![Dependencies](https://david-dm.org/Autodesk/theo-loader.svg)](https://david-dm.org/Autodesk/theo-loader) 10 | 11 | ## Installation 12 | 13 | ```bash 14 | npm install --save-dev webpack theo theo-loader 15 | ``` 16 | 17 | __Note:__ [npm](https://npmjs.com) deprecated 18 | [auto-installing of peerDependencies](https://github.com/npm/npm/issues/6565) from npm@3, so required peer dependencies like theo and webpack must be listed explicitly in your `package.json`. 19 | 20 | ## Usage 21 | 22 | `props.json` 23 | ```json 24 | { 25 | "aliases": { 26 | "WHITE": "#FFFFFF", 27 | "LINK_WATER": "#F4F6F9" 28 | }, 29 | "props": { 30 | "COLOR_BACKGROUND": { 31 | "value": "{!LINK_WATER}", 32 | "comment": "Default background color for the whole app." 33 | }, 34 | "COLOR_BACKGROUND_ALT": { 35 | "value": "{!WHITE}", 36 | "comment": "Second default background color for the app." 37 | } 38 | }, 39 | "global": { 40 | "type": "color", 41 | "category": "background" 42 | } 43 | } 44 | ``` 45 | 46 | ``` javascript 47 | import designTokens from 'theo-loader!./props.json' 48 | // => { 49 | // COLOR_BACKGROUND: "rgb(244, 246, 249)", 50 | // COLOR_BACKGROUND_ALT: "rgb(255, 255, 255)" 51 | // } 52 | ``` 53 | 54 | [Documentation: Using loaders](http://webpack.github.io/docs/using-loaders.html) 55 | 56 | ## Formats and Transforms 57 | 58 | The loader uses the `web` transform and `common.js` format by default. You can specify another transform or format in the query parameters: 59 | 60 | ```javascript 61 | import designTokens from 'theo-loader?{"transform":{"type":"web"},"format":{"type":"scss"}!./props.json'; 62 | // => "$color-background: rgb(244, 246, 249);\n$color-background-alt: rgb(255, 255, 255);" 63 | ``` 64 | 65 | or you can use the shorthand: 66 | 67 | ```javascript 68 | import designTokens from 'theo-loader?transform=web&format=scss!./props.json'; 69 | // => "$color-background: rgb(244, 246, 249);\n$color-background-alt: rgb(255, 255, 255);" 70 | ``` 71 | 72 | You can specify other options to pass to theo via the `LoaderOptionsPlugin` in your webpack configuration: 73 | 74 | `webpack.config.js` 75 | ```javascript 76 | module.exports = { 77 | ... 78 | module: { 79 | rules: [ 80 | { 81 | test: /\.json$/, 82 | loader: "theo-loader" 83 | } 84 | ] 85 | }, 86 | 87 | plugins: [ 88 | new webpack.LoaderOptionsPlugin({ 89 | options: { 90 | theo: { 91 | // These options will be passed to Theo in all instances of theo-loader 92 | transform: { 93 | type: 'web' 94 | }, 95 | 96 | // `getOptions` will be called per import 97 | // `prevOptions` will be a merged object of the options specified 98 | // above, as well as any passed to the loader via query string 99 | getOptions: (prevOptions) => { 100 | let newOptions = prevOptions; 101 | 102 | const formatOptions = (prevOptions && prevOptions.format) || {}; 103 | const formatType = format.type; 104 | 105 | if (formatType === 'scss') { 106 | newOptions = { 107 | ...prevOptions, 108 | format: { 109 | ...formatOptions, 110 | // SCSS variables will be named by applying 'PREFIX_' to the 111 | // front of the token name 112 | propsMap: prop => prop.update('name', name => `PREFIX_${name}`) 113 | }, 114 | }; 115 | } 116 | 117 | return newOptions; 118 | } 119 | } 120 | } 121 | }) 122 | ] 123 | }; 124 | ``` 125 | 126 | See the [theo documentation](https://github.com/salesforce-ux/theo) for more information about the Theo options format. 127 | 128 | ## theo Initialization 129 | 130 | You can perform any initialization for theo, like registering custom transforms or formatters using `registerTransform`, `registerValueTransform` or `registerFormat`, in your webpack configuration: 131 | 132 | ```javascript 133 | import theo from 'theo'; 134 | 135 | // Do any theo initialization here 136 | theo.registerValueTransform( 137 | 'animation/web/curve', 138 | prop => prop.get('type') === 'animation-curve', 139 | prop => 'cubic-bezier(' + prop.get('value').join(', ') + ')' 140 | ); 141 | 142 | module.exports = { 143 | ... 144 | module: { 145 | rules: [ 146 | { 147 | test: /\.json$/, 148 | loader: "theo-loader" 149 | } 150 | ] 151 | }, 152 | 153 | plugins: { 154 | new webpack.LoaderOptionsPlugin({ 155 | options: { 156 | theo: { 157 | // Configure theo-loader here 158 | ... 159 | } 160 | } 161 | }) 162 | } 163 | } 164 | ``` 165 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "theo-loader", 3 | "version": "4.0.0", 4 | "description": "A webpack loader that transforms Design Tokens files using Salesforce's theo", 5 | "license": "Apache-2.0", 6 | "main": "dist/index.js", 7 | "scripts": { 8 | "test:js": "mocha --compilers js:test/babel-register", 9 | "lint:js": "eslint .", 10 | "test": "npm run test:js && npm run lint:js", 11 | "prepublish": "babel src --out-dir dist" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/Autodesk/theo-loader.git" 16 | }, 17 | "keywords": [ 18 | "theo", 19 | "json", 20 | "webpack", 21 | "loader", 22 | "design" 23 | ], 24 | "author": "Beau Roberts ", 25 | "bugs": { 26 | "url": "https://github.com/Autodesk/theo-loader/issues" 27 | }, 28 | "homepage": "https://github.com/Autodesk/theo-loader#readme", 29 | "peerDependencies": { 30 | "theo": "^6.0.0 || ^6.0.0-beta.7", 31 | "webpack": "^3.7.1" 32 | }, 33 | "devDependencies": { 34 | "babel-cli": "^6.24.1", 35 | "babel-core": "^6.18.2", 36 | "babel-eslint": "^8.0.0", 37 | "babel-loader": "^7.0.0", 38 | "babel-plugin-transform-object-rest-spread": "^6.6.5", 39 | "babel-preset-es2015": "^6.6.0", 40 | "bluebird": "^3.3.4", 41 | "eslint": "^4.8.0", 42 | "eslint-config-airbnb": "^16.0.0", 43 | "eslint-import-resolver-webpack": "^0.8.3", 44 | "eslint-plugin-import": "^2.7.0", 45 | "eslint-plugin-jsx-a11y": "^6.0.2", 46 | "eslint-plugin-react": "^7.4.0", 47 | "hanson": "^1.1.1", 48 | "hson-loader": "^2.0.0", 49 | "mocha": "^4.0.1", 50 | "node-sass": "^4.3.0", 51 | "rimraf": "^2.5.2", 52 | "should": "^13.1.2", 53 | "theo": "^6.0.0 || ^6.0.0-beta.7", 54 | "webpack": "^3.7.1" 55 | }, 56 | "dependencies": { 57 | "loader-utils": "^1.1.0" 58 | }, 59 | "engines": { 60 | "node": ">=6" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Autodesk Inc. http://www.autodesk.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import theo from 'theo'; 18 | import loaderUtils from 'loader-utils'; 19 | import path from 'path'; 20 | import fs from 'fs'; 21 | 22 | const DEFAULT_TRANSFORM = 'web'; 23 | const DEFAULT_FORMAT = 'common.js'; 24 | 25 | module.exports = function theoLoader(content) { 26 | // Return any options to pass to the theo transform and format plugins for the given transform/format pair. 27 | const mergeOptions = (loaderOptions, queryOptions) => { 28 | const { getOptions, ...otherLoaderOptions } = loaderOptions; 29 | let options = { 30 | ...otherLoaderOptions, 31 | ...(queryOptions || {}), 32 | }; 33 | 34 | if (typeof options.transform === 'string') { 35 | options.transform = { 36 | type: options.transform, 37 | }; 38 | } 39 | 40 | if (typeof options.format === 'string') { 41 | options.format = { 42 | type: options.format, 43 | }; 44 | } 45 | 46 | if (typeof getOptions === 'function') { 47 | options = getOptions(options); 48 | } 49 | 50 | return options; 51 | }; 52 | 53 | // Recursively add dependencies on imported Design Tokens files 54 | const addImportDependencies = (jsonString, filePath) => { 55 | const { imports } = JSON.parse(jsonString); 56 | 57 | if (!imports) { 58 | return; 59 | } 60 | 61 | imports.forEach((importPath) => { 62 | const importPathAbs = path.resolve(path.dirname(filePath), importPath); 63 | this.addDependency(importPathAbs); 64 | 65 | // Now add *this* file's dependencies 66 | addImportDependencies(fs.readFileSync(importPathAbs, 'utf8'), importPathAbs); 67 | }); 68 | }; 69 | 70 | // Return the output of theo as a Javascript module definition. 71 | const moduleize = (theoOutput, formatType) => { 72 | let moduleized; 73 | if (/js$/.test(formatType)) { 74 | // These are already javascripts modules, either CommonJS or AMD 75 | moduleized = theoOutput; 76 | } else { 77 | let moduleContent; 78 | if (/json$/.test(formatType)) { 79 | moduleContent = theoOutput; 80 | } else { 81 | // Export everything else as a string 82 | const escaped = theoOutput.replace(/\n/g, '\\n').replace(/"/g, '\\"'); 83 | moduleContent = `"${escaped}"`; 84 | } 85 | moduleized = `module.exports = ${moduleContent};`; 86 | } 87 | return moduleized; 88 | }; 89 | 90 | this.cacheable(); 91 | const callback = this.async(); 92 | 93 | let jsonContent; 94 | try { 95 | // Assume the content is a serialized module 96 | jsonContent = JSON.stringify(this.exec(content, this.resourcePath)); 97 | } catch (e) { 98 | // Fall back to assuming its serialized JSON 99 | jsonContent = content; 100 | } 101 | 102 | // Add a dependency on each of the imported Design Tokens files, recursively 103 | try { 104 | addImportDependencies(jsonContent, this.resourcePath); 105 | } catch (e) { 106 | process.nextTick(() => { 107 | callback(e); 108 | }); 109 | return; 110 | } 111 | 112 | // Parse the transform and format from the query in the request 113 | const query = this.query && loaderUtils.parseQuery(this.query); 114 | const { format, transform, ...otherMergedOptions } = mergeOptions(this.options.theo || {}, query); 115 | const transformType = (transform && transform.type) || DEFAULT_TRANSFORM; 116 | const formatType = (format && format.type) || DEFAULT_FORMAT; 117 | 118 | theo 119 | .convert({ 120 | ...otherMergedOptions, 121 | transform: { 122 | ...(transform || {}), 123 | // theo will choke if file path does not end with ".json" 124 | file: this.resourcePath.replace(/\.[^.]+$/, '.json'), 125 | data: jsonContent, 126 | type: transformType, 127 | }, 128 | format: { 129 | ...(format || {}), 130 | type: formatType, 131 | }, 132 | }) 133 | .then((result) => { 134 | // Convert the result into a JS module 135 | callback(null, moduleize(result, formatType)); 136 | }) 137 | .catch(callback); 138 | }; 139 | -------------------------------------------------------------------------------- /test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "rules": { 6 | "global-require": 0, 7 | "import/no-extraneous-dependencies": ["error", { "devDependencies": true }] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/babel-register.js: -------------------------------------------------------------------------------- 1 | // Disable the babel cache 2 | require('babel-core/register')({ 3 | cache: false, 4 | }); 5 | -------------------------------------------------------------------------------- /test/fixtures/invalid-design-props/entry.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-webpack-loader-syntax 2 | import theoContent from '../../../src/index.js!./props.json'; 3 | 4 | export default theoContent; 5 | -------------------------------------------------------------------------------- /test/fixtures/invalid-design-props/props.json: -------------------------------------------------------------------------------- 1 | { 2 | "props": { 3 | "one": { 4 | "value": "rgb(0, 0, 0)" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/invalid-json/entry.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-webpack-loader-syntax 2 | import theoContent from '../../../src/index.js!./props.json'; 3 | 4 | export default theoContent; 5 | -------------------------------------------------------------------------------- /test/fixtures/invalid-json/props.json: -------------------------------------------------------------------------------- 1 | I'm not JSON} 2 | -------------------------------------------------------------------------------- /test/fixtures/nested-imports/entry-commonjs.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-webpack-loader-syntax 2 | import theoContent from '../../../src/index.js?transform=web&format=common.js!./props.json'; 3 | 4 | export default theoContent; 5 | -------------------------------------------------------------------------------- /test/fixtures/nested-imports/entry-scss.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-webpack-loader-syntax 2 | import theoContent from '../../../src/index.js?transform=web&format=scss!./props.json'; 3 | 4 | export default theoContent; 5 | -------------------------------------------------------------------------------- /test/fixtures/nested-imports/entry.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-webpack-loader-syntax 2 | import theoContent from '../../../src/index.js!./props.json'; 3 | 4 | export default theoContent; 5 | -------------------------------------------------------------------------------- /test/fixtures/nested-imports/import.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": [ 3 | "./nested-import.json" 4 | ], 5 | "aliases": { 6 | "BIG_NUMBER": "123456789" 7 | }, 8 | "props": { 9 | "three": { 10 | "value": "20rem", 11 | "type": "number", 12 | "category": "foo" 13 | }, 14 | "four": { 15 | "value": "{!BIG_NUMBER}", 16 | "type": "number", 17 | "category": "bar" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/fixtures/nested-imports/nested-import.json: -------------------------------------------------------------------------------- 1 | { 2 | "props": { 3 | "five": { 4 | "value": "rgb(255, 0, 0)" 5 | } 6 | }, 7 | "global": { 8 | "type": "color", 9 | "category": "background" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures/nested-imports/props.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": [ 3 | "./import.json" 4 | ], 5 | "props": { 6 | "one": { 7 | "value": "rgb(0, 0, 0)", 8 | "type": "color", 9 | "category": "background" 10 | }, 11 | "two": { 12 | "value": "rgb(255, 255, 255)", 13 | "type": "color", 14 | "category": "background" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/fixtures/no-imports/entry.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-webpack-loader-syntax 2 | import theoContent from '../../../src/index.js!./props.json'; 3 | 4 | export default theoContent; 5 | -------------------------------------------------------------------------------- /test/fixtures/no-imports/props.json: -------------------------------------------------------------------------------- 1 | { 2 | "props": { 3 | "five": { 4 | "value": "rgb(255, 0, 0)" 5 | } 6 | }, 7 | "global": { 8 | "type": "color", 9 | "category": "background" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures/non-json-import/entry.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-webpack-loader-syntax 2 | import theoContent from '../../../src/index.js!hson-loader!./props.hson'; 3 | 4 | export default theoContent; 5 | -------------------------------------------------------------------------------- /test/fixtures/non-json-import/props.hson: -------------------------------------------------------------------------------- 1 | { 2 | props: { 3 | five: { 4 | value: "rgb(255, 0, 0)" 5 | } 6 | }, 7 | global: { 8 | type: "color", 9 | category: "background" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures/non-nested-imports/entry-query-options.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-webpack-loader-syntax 2 | import theoContent from '../../../src/index.js?{"format":{"type":"raw.json"},"propToDelete":"three"}!./props.json'; 3 | 4 | export default theoContent; 5 | -------------------------------------------------------------------------------- /test/fixtures/non-nested-imports/entry.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-webpack-loader-syntax 2 | import theoContent from '../../../src/index.js!./props.json'; 3 | 4 | export default theoContent; 5 | -------------------------------------------------------------------------------- /test/fixtures/non-nested-imports/import.json: -------------------------------------------------------------------------------- 1 | { 2 | "props": { 3 | "five": { 4 | "value": "rgb(255, 0, 0)" 5 | } 6 | }, 7 | "global": { 8 | "type": "color", 9 | "category": "background" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures/non-nested-imports/props.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": [ 3 | "./import.json" 4 | ], 5 | "aliases": { 6 | "BIG_NUMBER": "123456789" 7 | }, 8 | "props": { 9 | "three": { 10 | "value": "20rem", 11 | "type": "number", 12 | "category": "foo" 13 | }, 14 | "four": { 15 | "value": "{!BIG_NUMBER}", 16 | "type": "number", 17 | "category": "bar" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/loaderTest.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-dynamic-require */ 2 | import path from 'path'; 3 | import fs from 'fs'; 4 | import rimraf from 'rimraf'; 5 | import sass from 'node-sass'; 6 | import webpack from 'webpack'; 7 | import Promise from 'bluebird'; 8 | import should from 'should'; 9 | 10 | const FIXTURE_PATH = path.resolve(__dirname, 'fixtures'); 11 | const OUTPUT_DIR = path.resolve(__dirname, 'temp'); 12 | const OUTPUT_BASENAME = 'main.js'; 13 | const OUTPUT_PATH = path.resolve(OUTPUT_DIR, OUTPUT_BASENAME); 14 | 15 | const webpackConfigBase = { 16 | context: __dirname, 17 | output: { 18 | filename: OUTPUT_BASENAME, 19 | path: OUTPUT_DIR, 20 | libraryTarget: 'commonjs2', 21 | }, 22 | module: { 23 | rules: [ 24 | { 25 | test: /\.js$/, 26 | exclude: /node_modules/, 27 | loader: 'babel-loader', 28 | }, 29 | ], 30 | }, 31 | }; 32 | 33 | const fixtureAbsPath = relPath => path.resolve(FIXTURE_PATH, relPath); 34 | const loadFixture = relPath => fs.readFileSync(fixtureAbsPath(relPath)).toString(); 35 | 36 | describe('[theo-loader]', () => { 37 | beforeEach((done) => { 38 | delete require.cache[OUTPUT_PATH]; 39 | rimraf(OUTPUT_PATH, done); 40 | }); 41 | 42 | it('should error on an invalid json file', (done) => { 43 | const config = { 44 | ...webpackConfigBase, 45 | entry: fixtureAbsPath('./invalid-json/entry.js'), 46 | }; 47 | webpack(config, () => { 48 | (() => { 49 | require(OUTPUT_PATH); 50 | }).should.throw(/SyntaxError: Unexpected token/); 51 | done(); 52 | }); 53 | }); 54 | 55 | it('should error on an invalid Design Tokens file', (done) => { 56 | // The file may be valid JSON... 57 | const fixtureFileName = './invalid-design-props/props.json'; 58 | const json = JSON.parse(loadFixture(fixtureFileName)); 59 | json.should.be.a.Object(); 60 | 61 | const config = { 62 | ...webpackConfigBase, 63 | entry: fixtureAbsPath('./invalid-design-props/entry.js'), 64 | }; 65 | webpack(config, () => { 66 | // ...but it's not a valid design tokens file 67 | (() => { 68 | require(OUTPUT_PATH); 69 | }).should.throw(/Property "one" contained no "category" key/); 70 | done(); 71 | }); 72 | }); 73 | 74 | it('should recursively add dependencies', (done) => { 75 | const promise1 = new Promise((resolve) => { 76 | const config = { 77 | ...webpackConfigBase, 78 | entry: fixtureAbsPath('./no-imports/entry.js'), 79 | }; 80 | webpack(config, (err, stats) => { 81 | should.not.exist(err); 82 | stats.compilation.missingDependencies.should.be.empty(); 83 | stats.compilation.fileDependencies.should.containDeep([fixtureAbsPath('./no-imports/props.json')]); 84 | resolve(); 85 | }); 86 | }); 87 | 88 | const promise2 = new Promise((resolve) => { 89 | const config = { 90 | ...webpackConfigBase, 91 | entry: fixtureAbsPath('./non-nested-imports/entry.js'), 92 | }; 93 | webpack(config, (err, stats) => { 94 | should.not.exist(err); 95 | stats.compilation.missingDependencies.should.be.empty(); 96 | stats.compilation.fileDependencies.should.containDeep([ 97 | fixtureAbsPath('./non-nested-imports/props.json'), 98 | fixtureAbsPath('./non-nested-imports/import.json'), 99 | ]); 100 | resolve(); 101 | }); 102 | }); 103 | 104 | const promise3 = new Promise((resolve) => { 105 | const config = { 106 | ...webpackConfigBase, 107 | entry: fixtureAbsPath('./nested-imports/entry.js'), 108 | }; 109 | webpack(config, (err, stats) => { 110 | should.not.exist(err); 111 | stats.compilation.missingDependencies.should.be.empty(); 112 | stats.compilation.fileDependencies.should.containDeep([ 113 | fixtureAbsPath('./nested-imports/props.json'), 114 | fixtureAbsPath('./nested-imports/import.json'), 115 | fixtureAbsPath('./nested-imports/nested-import.json'), 116 | ]); 117 | resolve(); 118 | }); 119 | }); 120 | 121 | Promise.all([promise1, promise2, promise3]).then(() => { 122 | done(); 123 | }); 124 | }); 125 | 126 | it('should generate an importable javascript module', (done) => { 127 | const config = { 128 | ...webpackConfigBase, 129 | entry: fixtureAbsPath('./nested-imports/entry.js'), 130 | }; 131 | webpack(config, () => { 132 | let result; 133 | (() => { 134 | result = require(OUTPUT_PATH).default; 135 | }).should.not.throw(); 136 | result.should.be.ok(); 137 | done(); 138 | }); 139 | }); 140 | 141 | it('should use transform: "web", format: "common.js" by default', (done) => { 142 | const config = { 143 | ...webpackConfigBase, 144 | entry: fixtureAbsPath('./nested-imports/entry.js'), 145 | }; 146 | webpack(config, () => { 147 | const result = require(OUTPUT_PATH).default; 148 | result.should.be.a.Object(); 149 | done(); 150 | }); 151 | }); 152 | 153 | it('should be able to specify transform, format in the query parameters', (done) => { 154 | const config = { 155 | ...webpackConfigBase, 156 | entry: fixtureAbsPath('./nested-imports/entry-scss.js'), 157 | }; 158 | webpack(config, () => { 159 | const result = require(OUTPUT_PATH).default; 160 | result.should.be.a.String(); 161 | done(); 162 | }); 163 | }); 164 | 165 | it('should successfully import "common.js" format', (done) => { 166 | const config = { 167 | ...webpackConfigBase, 168 | entry: fixtureAbsPath('./nested-imports/entry-commonjs.js'), 169 | }; 170 | webpack(config, () => { 171 | const result = require(OUTPUT_PATH).default; 172 | result.should.be.a.Object(); 173 | done(); 174 | }); 175 | }); 176 | 177 | it('should successfully import "scss" format', (done) => { 178 | const config = { 179 | ...webpackConfigBase, 180 | entry: fixtureAbsPath('./nested-imports/entry-scss.js'), 181 | }; 182 | webpack(config, () => { 183 | const sassContent = require(OUTPUT_PATH).default; 184 | const css = sass 185 | .renderSync({ 186 | data: `${sassContent}a { background-color: $two; }`, 187 | }) 188 | .css.toString(); 189 | css.should.match(/^a\s+{\s+background-color: white;\s+}\s+$/); 190 | done(); 191 | }); 192 | }); 193 | 194 | it('should accept format and transform options', (done) => { 195 | const config = { 196 | ...webpackConfigBase, 197 | entry: fixtureAbsPath('./nested-imports/entry.js'), 198 | plugins: [ 199 | new webpack.LoaderOptionsPlugin({ 200 | options: { 201 | theo: { 202 | transform: 'web', 203 | format: { 204 | type: 'common.js', 205 | // Only return props with the type of 'color' 206 | propsFilter: prop => prop.get('type') === 'color', 207 | // Prefix each prop name with 'PREFIX_' 208 | propsMap: prop => prop.update('name', name => `PREFIX_${name}`), 209 | }, 210 | }, 211 | }, 212 | }), 213 | ], 214 | }; 215 | webpack(config, () => { 216 | const js = require(OUTPUT_PATH).default; 217 | js.prefixFive.should.equal('rgb(255, 0, 0)'); 218 | done(); 219 | }); 220 | }); 221 | 222 | it('should be able to modify options based on those passed via query', (done) => { 223 | const config = { 224 | ...webpackConfigBase, 225 | entry: fixtureAbsPath('./non-nested-imports/entry-query-options.js'), 226 | plugins: [ 227 | new webpack.LoaderOptionsPlugin({ 228 | options: { 229 | theo: { 230 | transform: 'web', 231 | getOptions: (options) => { 232 | options.format.type.should.eql('raw.json'); 233 | options.propToDelete.should.eql('three'); 234 | 235 | return { 236 | ...options, 237 | transform: { 238 | ...(options.transform || {}), 239 | preprocess: def => def.deleteIn(['props', options.propToDelete]), 240 | }, 241 | }; 242 | }, 243 | }, 244 | }, 245 | }), 246 | ], 247 | }; 248 | webpack(config, () => { 249 | const js = require(OUTPUT_PATH).default; 250 | js.props.should.have.properties(['four', 'five']); 251 | js.props.should.not.have.property('three'); 252 | done(); 253 | }); 254 | }); 255 | 256 | it('should play nice with upstream loaders', (done) => { 257 | const config = { 258 | ...webpackConfigBase, 259 | entry: fixtureAbsPath('./non-json-import/entry.js'), 260 | }; 261 | webpack(config, () => { 262 | const result = require(OUTPUT_PATH).default; 263 | result.should.have.property('five'); 264 | done(); 265 | }); 266 | }); 267 | }); 268 | --------------------------------------------------------------------------------