diff --git a/README.md b/README.md index 976fc4c..177f81a 100644 --- a/README.md +++ b/README.md @@ -1,158 +1,161 @@ -[![npm][npm]][npm-url] -[![build][build]][build-url] - -
- - - -
- -# One Loader - -A webpack loader to enable single-file React components. -Inspired by `vue-loader`. - -## Features - -* CSS and JavaScript code co-located in a single `.one` file (extension is configurable) -* Configurable loaders for JavaScript and CSS -* Support for scoped styles through CSS Modules (using `css-loader`) - -## Installation - -```bash -$ npm i --save-dev one-loader -``` - -## Example - -In `webpack.config.js`: - -```javascript -{ - module: { - loaders: [ - { - test: /\.one$/, - loader: 'one-loader', - options: { - map: { - 'text/css': ['style-loader', 'css-loader'], - 'javascript': 'babel-loader' - } - } - } - ] - } -} -``` - -In `ExampleComponent.one`: - -```html - - - -``` - -More examples are available in [examples](examples) directory: - -* [Simple Counter](examples/01_counter) -* [Redux Todo List with extracted CSS file](examples/02_redux-todos) -* [Redux Todo List with scoped CSS](examples/03_redux-todos-scoped) - -## Configuration - -The `map` object in `one-loader` options is responsible for assigning loaders to code types in your single-file components. - -If no mapping is provided ` - -``` - -There are no restrictions on type naming, so any string will work, however descriptive values are recommended. - -## Known issues - -The internal architecture of the loader requires passing the options object to sub-loaders through a `require` string. -This is currently causing issues when defining `map` object loaders in strings with a `!` separator. -Thus array syntax is recommended for defining mapped loaders. - -This will **NOT** work: - -```javascript -{ - module: { - loaders: [ - { - test: /\.one$/, - loader: 'one-loader', - options: { - map: { - 'text/css': 'style-loader!css-loader', - 'javascript': 'babel-loader' - } - } - } - ] - } -} -``` - -This will work: - -```javascript -{ - module: { - loaders: [ - { - test: /\.one$/, - loader: 'one-loader', - options: { - map: { - 'text/css': ['style-loader', 'css-loader'], - 'javascript': 'babel-loader' - } - } - } - ] - } -} -``` - - -## License - -MIT - -[npm]: https://img.shields.io/npm/v/one-loader.svg -[npm-url]: https://npmjs.com/package/one-loader - -[build]: https://travis-ci.org/digitalie/one-loader.svg?branch=master -[build-url]: https://travis-ci.org/digitalie/one-loader +[![npm][npm]][npm-url] +[![build][build]][build-url] + +
+ + + +
+ +# One Loader + +A webpack loader to enable single-file components, inspired by `vue-loader`. + +Originally it was built for react, but will work for almost any type of content. +In fact, if you wanted to, you could even use it for php, html and css using file-loaders without any javascript part. + + +## Features + +* CSS, JavaScript code and other parts co-located in a single `.one` file (extension is configurable) +* Configurable loaders for JavaScript and CSS +* Support for scoped styles through CSS Modules (using `css-loader`) + +## Installation + +```bash +$ npm i --save-dev one-loader +``` + +## Example + +In `webpack.config.js`: + +```javascript +{ + module: { + loaders: [ + { + test: /\.one$/, + loader: 'one-loader', + options: { + map: { + 'text/css': ['style-loader', 'css-loader'], + 'javascript': 'babel-loader' + } + } + } + ] + } +} +``` + +In `ExampleComponent.one`: + +```html + + + +``` + +More examples are available in [examples](examples) directory: + +* [Simple Counter](examples/01_counter) +* [Redux Todo List with extracted CSS file](examples/02_redux-todos) +* [Redux Todo List with scoped CSS](examples/03_redux-todos-scoped) + +## Configuration + +The `map` object in `one-loader` options is responsible for assigning loaders to code types in your single-file components. + +If no mapping is provided ` + +``` + +There are no restrictions on type naming, so any string will work, however descriptive values are recommended. + +## Known issues + +The internal architecture of the loader requires passing the options object to sub-loaders through a `require` string. +This is currently causing issues when defining `map` object loaders in strings with a `!` separator. +Thus array syntax is recommended for defining mapped loaders. + +This will **NOT** work: + +```javascript +{ + module: { + loaders: [ + { + test: /\.one$/, + loader: 'one-loader', + options: { + map: { + 'text/css': 'style-loader!css-loader', + 'javascript': 'babel-loader' + } + } + } + ] + } +} +``` + +This will work: + +```javascript +{ + module: { + loaders: [ + { + test: /\.one$/, + loader: 'one-loader', + options: { + map: { + 'text/css': ['style-loader', 'css-loader'], + 'javascript': 'babel-loader' + } + } + } + ] + } +} +``` + + +## License + +MIT + +[npm]: https://img.shields.io/npm/v/one-loader.svg +[npm-url]: https://npmjs.com/package/one-loader + +[build]: https://travis-ci.org/digitalie/one-loader.svg?branch=master +[build-url]: https://travis-ci.org/digitalie/one-loader diff --git a/package-lock.json b/package-lock.json index f482a40..3b28625 100644 --- a/package-lock.json +++ b/package-lock.json @@ -715,8 +715,7 @@ "@types/node": { "version": "14.6.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.6.0.tgz", - "integrity": "sha512-mikldZQitV94akrc4sCcSjtJfsTKt4p+e/s0AGscVA6XArQ9kFclP+ZiYUMnq987rc6QlYxXv/EivqlfSLxpKA==", - "dev": true + "integrity": "sha512-mikldZQitV94akrc4sCcSjtJfsTKt4p+e/s0AGscVA6XArQ9kFclP+ZiYUMnq987rc6QlYxXv/EivqlfSLxpKA==" }, "@types/normalize-package-data": { "version": "2.4.0", @@ -1057,6 +1056,11 @@ "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==" }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1157,6 +1161,78 @@ "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", "dev": true }, + "cheerio": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.3.tgz", + "integrity": "sha512-0td5ijfUPuubwLUu0OBoe98gZj8C/AA+RW3v67GPlGOrvxWjZmBXiBCRU+I8VEiNyJzjth40POfHiz2RB3gImA==", + "requires": { + "css-select": "~1.2.0", + "dom-serializer": "~0.1.1", + "entities": "~1.1.1", + "htmlparser2": "^3.9.1", + "lodash": "^4.15.0", + "parse5": "^3.0.1" + }, + "dependencies": { + "dom-serializer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", + "integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==", + "requires": { + "domelementtype": "^1.3.0", + "entities": "^1.1.1" + } + }, + "domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" + }, + "domhandler": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", + "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "requires": { + "domelementtype": "1" + } + }, + "domutils": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" + }, + "htmlparser2": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", + "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", + "requires": { + "domelementtype": "^1.3.1", + "domhandler": "^2.3.0", + "domutils": "^1.5.1", + "entities": "^1.1.1", + "inherits": "^2.0.1", + "readable-stream": "^3.1.1" + } + }, + "parse5": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-3.0.3.tgz", + "integrity": "sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA==", + "requires": { + "@types/node": "*" + } + } + } + }, "ci-info": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", @@ -1297,6 +1373,54 @@ "which": "^1.2.9" } }, + "css-select": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", + "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", + "requires": { + "boolbase": "~1.0.0", + "css-what": "2.1", + "domutils": "1.5.1", + "nth-check": "~1.0.1" + }, + "dependencies": { + "dom-serializer": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", + "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", + "requires": { + "domelementtype": "^2.0.1", + "entities": "^2.0.0" + }, + "dependencies": { + "domelementtype": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz", + "integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==" + } + } + }, + "domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" + }, + "domutils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + } + } + }, + "css-what": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", + "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==" + }, "cssom": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", @@ -1438,32 +1562,6 @@ "integrity": "sha512-5j5vdRcw3CNctePNYN0Wy2e/JbWT6cAYnXv5OuqPhDpyCGc0uLu2TK0zOCJWNB9kOIfYMSpIulRaDgIi4HJ6Ig==", "dev": true }, - "dom-serializer": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", - "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", - "requires": { - "domelementtype": "^2.0.1", - "entities": "^2.0.0" - }, - "dependencies": { - "domelementtype": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz", - "integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==" - }, - "entities": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz", - "integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==" - } - } - }, - "domelementtype": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", - "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" - }, "domexception": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", @@ -1481,23 +1579,6 @@ } } }, - "domhandler": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", - "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", - "requires": { - "domelementtype": "1" - } - }, - "domutils": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", - "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", - "requires": { - "dom-serializer": "0", - "domelementtype": "1" - } - }, "ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -1535,9 +1616,9 @@ } }, "entities": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", - "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz", + "integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==" }, "error-ex": { "version": "1.3.2", @@ -2035,19 +2116,6 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, - "htmlparser2": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", - "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", - "requires": { - "domelementtype": "^1.3.1", - "domhandler": "^2.3.0", - "domutils": "^1.5.1", - "entities": "^1.1.1", - "inherits": "^2.0.1", - "readable-stream": "^3.1.1" - } - }, "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -2925,6 +2993,14 @@ "whatwg-url": "^8.0.0", "ws": "^7.2.3", "xml-name-validator": "^3.0.0" + }, + "dependencies": { + "parse5": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", + "dev": true + } } }, "jsesc": { @@ -3040,24 +3116,13 @@ "lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", - "dev": true + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" }, "lodash.defaultsdeep": { "version": "4.6.1", "resolved": "https://registry.npmjs.org/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.1.tgz", "integrity": "sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA==" }, - "lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" - }, - "lodash.set": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", - "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=" - }, "lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", @@ -3288,6 +3353,14 @@ "path-key": "^2.0.0" } }, + "nth-check": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", + "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "requires": { + "boolbase": "~1.0.0" + } + }, "nwsapi": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", @@ -3429,12 +3502,6 @@ "lines-and-columns": "^1.1.6" } }, - "parse5": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", - "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", - "dev": true - }, "pascalcase": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", @@ -3501,14 +3568,6 @@ "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", "dev": true }, - "posthtml-parser": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/posthtml-parser/-/posthtml-parser-0.4.2.tgz", - "integrity": "sha512-BUIorsYJTvS9UhXxPTzupIztOMVNPa/HtAm9KHni9z6qEfiJ1bpOBL5DfUOL9XAc3XkLIEzBzpph+Zbm4AdRAg==", - "requires": { - "htmlparser2": "^3.9.2" - } - }, "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", diff --git a/package.json b/package.json index 5f01d38..ce5bb47 100644 --- a/package.json +++ b/package.json @@ -31,10 +31,8 @@ "jest": "^26.4.1" }, "dependencies": { + "cheerio": "^1.0.0-rc.3", "loader-utils": "^2.0.0", - "lodash.defaultsdeep": "^4.6.1", - "lodash.get": "^4.4.2", - "lodash.set": "^4.3.2", - "posthtml-parser": "^0.4.2" + "lodash.defaultsdeep": "^4.6.1" } } diff --git a/src/index.js b/src/index.js index 28398fe..6118d86 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,7 @@ const defaultsDeep = require('lodash.defaultsdeep'); const loaderUtils = require('loader-utils'); const parse = require('./parse').default; -const { getRequire } = require('./require'); +const {getRequirePrimary} = require('./require'); const defaultOptions = require('./options'); /** @@ -10,22 +10,35 @@ const defaultOptions = require('./options'); * @param {string} content */ module.exports = function (content) { - let output = ''; - - const cb = this.async(); - const parts = parse(content); - const options = defaultsDeep(loaderUtils.getOptions(this), defaultOptions); const resource = loaderUtils.getRemainingRequest(this); - const scriptKeys = Object.keys(parts['script']); - - if (scriptKeys.length === 1) { - output = getRequire(this, options, 'script', scriptKeys.pop(), resource) - } else if (scriptKeys.length > 1) { - this.emitWarning(`Only one type script tag is allowed per component. The last one will be used.`); - } else { - this.emitError('At least one script tag per component is required!'); + // parse content for validation, no transpiling or processing of parts is actually done here + const parts = parse(content); + const scriptParts = []; + let primaryPart; + parts.forEach((part) => { + // validate options, making sure we have loaders for each mimeType + if (typeof options.map[part.mimeType] === 'undefined') { + this.emitError(`Missing loader for tag <${part.tag} type="${part.mimeType}"> in one-loader`); + } + // look for scripts + if (part.tag === 'script') { + if (part.id === 'component') { + primaryPart = part; + } else { + scriptParts.push(part); + } + } + }); + if (!primaryPart) { + if (scriptParts.length > 1) { + this.emitWarning(`Multiple script tags found, using the first as the component. Set id="component" on the primary tag to avoid this warning`); + } else if (scriptParts.length < 1) { + this.emitError(`A script tag is required in every .one file`); + } + primaryPart = scriptParts[0]; } - - cb(null, output); + // require just the javascript part of the .one file here. + // The other components will be stuffed into the content of the javascript part by the partLoaderPrimaryScript.js loader + return 'module.exports = ' + getRequirePrimary(this, primaryPart, options, resource); } diff --git a/src/loader.js b/src/loader.js deleted file mode 100644 index a3ee5b9..0000000 --- a/src/loader.js +++ /dev/null @@ -1,32 +0,0 @@ -const loaderUtils = require('loader-utils'); -const parse = require('./parse').default; -const { getRequire } = require('./require'); - -/** - * Helper loader that returns content based on requested tag and type - * - * @param {string} content - */ -module.exports = function (content) { - let output = ''; - - const cb = this.async() - const parts = parse(content); - const query = loaderUtils.parseQuery(this.query); - const resource = loaderUtils.getRemainingRequest(this); - const styleKeys = Object.keys(parts['style']); - - if (query.tag === 'script') { - if (styleKeys.length === 1) { - output += getRequire(this, query.options, 'style', styleKeys.pop(), resource); - } else if (styleKeys.length > 1) { - this.emitWarning(`Only one type of style tag is allowed per component. The last one will be used.`); - } - - output += parts['script'][query.type]; - } else if (query.tag === 'style') { - output = parts['style'][query.type]; - } - - cb(null, output) -} diff --git a/src/parse.js b/src/parse.js index b8579dd..9712024 100644 --- a/src/parse.js +++ b/src/parse.js @@ -1,6 +1,4 @@ -const get = require('lodash.get'); -const set = require('lodash.set'); -const parser = require('posthtml-parser'); +const cheerio = require('cheerio'); /** * Parse file content (html) and categorize it by tag and type @@ -9,72 +7,22 @@ const parser = require('posthtml-parser'); * @returns {object} */ module.exports.default = function (content) { - let output = {}; - - const nodes = parser(content); - - nodes.forEach((node) => { - if (typeof node == 'object' && isCorrectTag(node)) { - append(output, [node.tag, getType(node)], getContent(node)); - } + const defaultTagTypes = { + script: 'javascript', + style: 'text/css', + template: 'text/html', + } + const $ = cheerio.load(`${content}`, {decodeEntities: false}); + let output = []; + $('body > script, body > style, body > template').each(function (i) { + const node = $(this); + output.push({ + id: node.attr('id') || `__one_loader_internal__part-${i}`, + tag: this.tagName, + mimeType: node.attr('type') || defaultTagTypes[this.tagName], + content: node.html(), + }) }); - return output; } -/** - * Add property of given path to the object - * - * @param {object} object - * @param {array} path - * @param {string} value - */ -function append(object, path, value) { - set(object, path, get(object, path, '') + `\r\n${value}`); -} - -/** - * Get code content of given tag (node) - * @param {object} node - * @returns {string} - */ -function getContent(node) { - return (node.content || []).join(' '); -} - -/** - * Get tag type property - * @param {object} node - * @returns {string} - */ -function getType(node) { - const tagLoaders = { - script: 'javascript', - style: 'text/css' - }; - - if(typeof node.attrs == 'undefined' || typeof node.attrs.type == 'undefined') - return tagLoaders[node.tag]; - - return node.attrs.type; -} - -/** - * Verify tag is either `script` or `style` - * - * @param {object} node - * @returns {boolean} - */ -function isCorrectTag(node) { - return node.tag === 'script' || node.tag === 'style'; -} - -/* - * Export private functions for testing purposes - */ -if (process.env.NODE_ENV === 'test') { - module.exports.append = append; - module.exports.getContent = getContent; - module.exports.getType = getType; - module.exports.isCorrectTag = isCorrectTag; -} diff --git a/src/partLoaderAdditional.js b/src/partLoaderAdditional.js new file mode 100644 index 0000000..62d263f --- /dev/null +++ b/src/partLoaderAdditional.js @@ -0,0 +1,20 @@ +const loaderUtils = require('loader-utils'); +const parse = require('./parse').default; + +/** + * Helper loader that returns content based on requested part id + * + * @param {string} content + */ +module.exports = function (content) { + const parts = parse(content); + const query = loaderUtils.parseQuery(this.query); + let output; + parts.forEach((part) => { + if (part.id === query.id) { + output = part.content; + return false; + } + }); + return output; +} diff --git a/src/partLoaderPrimaryScript.js b/src/partLoaderPrimaryScript.js new file mode 100644 index 0000000..cb01098 --- /dev/null +++ b/src/partLoaderPrimaryScript.js @@ -0,0 +1,26 @@ +const loaderUtils = require('loader-utils'); +const parse = require('./parse').default; +const {getRequireAdditional} = require('./require'); + +/** + * Helper loader that returns content based on requested part id + * + * @param {string} content + */ +module.exports = function (content) { + const parts = parse(content); + const query = loaderUtils.parseQuery(this.query); + const resource = loaderUtils.getRemainingRequest(this); + + let primaryPart; + let output = ''; + parts.forEach((part) => { + if (part.id === query.id) { + primaryPart = part; + } else { + // add each additional resource as a require() to the primary part + output += getRequireAdditional(this, part, query.options, resource); + } + }); + return output + primaryPart.content; +} diff --git a/src/require.js b/src/require.js index 8d4115e..b279cff 100644 --- a/src/require.js +++ b/src/require.js @@ -1,26 +1,41 @@ const loaderUtils = require('loader-utils'); /** - * Return full require() statement for given resource and type + * Return full require() statement for the first script tag which is used as the component part * * @param {object} context + * @param {object} part * @param {object} options - * @param {string} tag - * @param {string} type * @param {string} resource * @returns {string} */ -function getRequire(context, options, tag, type, resource) { - const loaders = normalizeLoaders(options.map[type]); - const selectLoader = require.resolve('./loader.js'); +function getRequirePrimary(context, part, options, resource) { + const fileContentPartLoader = require.resolve('./partLoaderPrimaryScript.js'); + const loaders = normalizeLoaders(options.map[part.mimeType]); const request = { - tag, - type, + id: part.id, options }; - const url = loaderUtils.stringifyRequest(context, `!${loaders}!${selectLoader}?${JSON.stringify(request)}!${resource}`) - const prefix = tag === 'script' ? 'module.exports = ' : 'const $style = '; - return `${prefix}require(${url});\r\n`; + const url = loaderUtils.stringifyRequest(context, `!${loaders}!${fileContentPartLoader}?${JSON.stringify(request)}!${resource}`); + return `require(${url});\r\n`; +} +/** + * Return full require() statement for given resource and type + * + * @param {object} context + * @param {object} part + * @param {object} options + * @param {string} resource + * @returns {string} + */ +function getRequireAdditional(context, part, options, resource) { + const fileContentPartLoader = require.resolve('./partLoaderAdditional.js'); + const loaders = normalizeLoaders(options.map[part.mimeType]); + const request = { + id: part.id, + }; + const url = loaderUtils.stringifyRequest(context, `!${loaders}!${fileContentPartLoader}?${JSON.stringify(request)}!${resource}`) + return `require(${url});\r\n`; } /** @@ -47,7 +62,7 @@ function stringifyLoaders(loaders) { }).join('!') } -module.exports = { getRequire }; +module.exports = { getRequirePrimary, getRequireAdditional }; /* * Export private functions for testing purposes