├── .babelrc ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── LICENSE ├── Logo.png ├── README.md ├── illustration.png ├── index.js ├── lib ├── addScopeIDToCSS │ ├── index.js │ └── scopeIdPlugin.js ├── buildTemplate.js ├── core.js ├── createOutput.js ├── findScopedSelectors.js ├── genScopeID.js ├── parsePage.js ├── rewriteTemplate.js └── utils │ └── reportError.js ├── package.json ├── test ├── buildTemplate.test.js ├── createOutput.test.js ├── findScopedSelectors.test.js ├── fixture │ ├── componentB │ │ ├── componentB.css │ │ ├── componentB.js │ │ └── componentB.tpl │ ├── include │ │ ├── componentA │ │ │ ├── componentA.css │ │ │ ├── componentA.js │ │ │ └── componentA.tpl │ │ └── componentD │ │ │ ├── componentD.css │ │ │ ├── componentD.js │ │ │ └── componentD.tpl │ ├── pageC.entry.js │ ├── pageC │ │ ├── pageC.css │ │ ├── pageC.js │ │ └── pageC.tpl │ ├── pageE.entry.js │ ├── pageE │ │ ├── pageE.css │ │ ├── pageE.js │ │ └── pageE.tpl │ └── scopeCompo │ │ ├── scopeCompo.css │ │ └── scopeCompo.tpl ├── genScopeID.test.js ├── parsePage.test.js ├── setScope.test.js ├── testInWebpack.test.js └── util │ ├── buildOption.normal.js │ ├── components.normal.js │ └── webpack.config.normal.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | test/* -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "airbnb-base", 3 | "env": { 4 | "browser": true, 5 | "node": true 6 | } 7 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | test/assets 3 | test/assetsCoreTest 4 | test/coverage 5 | .idea 6 | jest_0 7 | package-lock.json 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" 4 | 5 | after_success: 6 | - npm i coveralls 7 | - cat ./test/coverage/lcov.info | coveralls 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Nicholas Lee 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 | -------------------------------------------------------------------------------- /Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicholaslee119/webpack-component-loader/06a4bd8a625e72afce2ae6734f58bf4df5135877/Logo.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![NPM][npm]][npm-url] 2 | [![Tests][build]][build-url] 3 | [![Deps][deps]][deps-url] 4 | [![Coverage][cover]][cover-url] 5 | 6 |
7 | 8 | 9 | 10 |
11 | 12 | # webpack-component-loader 13 | A webpack loader to componentify CSS/JS/HTML without framework 14 | 15 | ![illustration](https://github.com/nicholaslee119/webpack-component-loader/blob/improve-document/illustration.png) 16 | 17 | ## Conception 18 | 19 | 简体中文: [如何在没有前端框架的情况下实现组件化](https://juejin.im/post/59df7e76f265da431e15c4fe) 20 | 21 | 日本語: [Frontend Framework無しでComponent化を導入する](https://qiita.com/nicholaslee/items/6c7643b9e6be12531ac3) 22 | 23 | ## highlight features 24 | 25 | 1. Scoped CSS 26 | 27 | ## Install 28 | ```javascript 29 | 30 | $npm install webpack-component-loader 31 | 32 | ``` 33 | ## Test 34 | ``` 35 | $npm test 36 | ``` 37 | 38 | ## Usage 39 | 40 | ### webpack 41 | 42 | ```js 43 | 44 | import {extractor, injector, addScopeAttr} from 'webpack-component-loader-smarty-parser'; 45 | // or create the injector, extractor or addScopeAttr by yourself 46 | function extractor (template) { 47 | // extract the including component path from the plain text of template 48 | } 49 | function injector (template, component, buildOption) { 50 | // inject the url of assets to template 51 | } 52 | function addScopeAttr (template, component) { 53 | // add data-s-[hash] to tags in template for scope css, and must return Promise type 54 | return new Promise(); 55 | } 56 | 57 | module.exports = { 58 | entry: { 59 | entryA: './test/fixture/entryA.js', 60 | entryB: './test/fixture/entryB.js' 61 | }, 62 | output: { 63 | path: path.resolve(__dirname, "../assets/"), 64 | filename : 'js/[name].js', 65 | chunkFilename: 'js/[name].chunk.js', 66 | }, 67 | module: { 68 | rules: [ 69 | { 70 | test : /\.tpl?$/, 71 | exclude: /(node_modules)/, 72 | use: [ 73 | { 74 | loader: 'webpack-component-loader', 75 | options: { 76 | isCodeSplit: false, 77 | extractor, 78 | injector, 79 | addScopeAttr, 80 | ext: '.tpl', 81 | srcPath : path.resolve(__dirname, '.'), 82 | builtTemplatePath : path.resolve(__dirname, '../assets/templates'), 83 | }, 84 | }, 85 | ], 86 | }, 87 | { 88 | test: /\.css$/, 89 | exclude: /(node_modules)/, 90 | enforce: "post", 91 | use: ExtractTextPlugin.extract({ 92 | fallback: "style-loader", 93 | use: "css-loader" 94 | }) 95 | } 96 | ], 97 | }, 98 | plugins: [ 99 | new ExtractTextPlugin({ 100 | filename: "css/[name].css", 101 | // allChunks: true 102 | }), 103 | new webpack.optimize.CommonsChunkPlugin({ 104 | name: "commons", 105 | filename: "js/commons.js", 106 | }) 107 | ] 108 | } 109 | ``` 110 | 111 | ## Ecosystem 112 | 113 | | Name | Status | Description | 114 | |:----:|:------:|:-----------:| 115 | |[component-smarty-parser][smarty]|[![npm][smarty-badge]][smarty-npm]| Parser to extract and inject smarty template| 116 | |[component-pug-parser][pug]|[![npm][pug-badge]][pug-npm]| Parser to extract and inject pug template | 117 | 118 | [smarty]: https://github.com/nicholaslee119/webpack-component-loader-smarty-parser 119 | [smarty-badge]: https://img.shields.io/npm/v/webpack-component-loader-smarty-parser.svg 120 | [smarty-npm]: https://npmjs.com/package/webpack-component-loader-smarty-parser 121 | 122 | [pug]: https://github.com/nicholaslee119/webpack-component-loader-smarty-parser 123 | [pug-badge]: https://img.shields.io/npm/v/webpack-component-loader-smarty-parser.svg 124 | [pug-npm]: https://npmjs.com/package/webpack-component-loader-smarty-parser 125 | 126 | ## RoadMap 127 | 128 | [RoadMap](https://github.com/nicholaslee119/webpack-component-loader/projects/1) 129 | 130 | ## License 131 | 132 | [MIT](http://opensource.org/licenses/MIT) 133 | 134 | 135 | 136 | [npm]: https://img.shields.io/npm/v/webpack-component-loader.svg 137 | [npm-url]: https://www.npmjs.com/package/webpack-component-loader 138 | 139 | [deps]: https://david-dm.org/nicholaslee119/webpack-component-loader/dev-status.svg 140 | [deps-url]: https://david-dm.org/nicholaslee119/webpack-component-loader?type=dev 141 | 142 | [cover]: https://coveralls.io/repos/github/nicholaslee119/webpack-component-loader/badge.svg?branch=master 143 | [cover-url]: https://coveralls.io/github/nicholaslee119/webpack-component-loader?branch=master 144 | 145 | 146 | [build]: https://travis-ci.org/nicholaslee119/webpack-component-loader.svg?branch=master 147 | [build-url]: https://travis-ci.org/nicholaslee119/webpack-component-loader 148 | -------------------------------------------------------------------------------- /illustration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicholaslee119/webpack-component-loader/06a4bd8a625e72afce2ae6734f58bf4df5135877/illustration.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const isValidPath = require('is-valid-path'); 2 | 3 | const loaderUtils = require('loader-utils'); 4 | const core = require('./lib/core'); 5 | const reportError = require('./lib/utils/reportError'); 6 | 7 | function checkPath(...urls) { 8 | return urls.every((url) => { 9 | const checkRes = typeof url === 'string' && isValidPath(url); 10 | if (!checkRes) { 11 | reportError(`${url} is not validate path`); 12 | } 13 | return checkRes; 14 | }); 15 | } 16 | 17 | function checkSource(source) { 18 | const res = typeof source === 'string' && source; 19 | if (!res) reportError('something wrong with the source'); 20 | } 21 | 22 | function checkFunction(extractor) { 23 | const res = typeof extractor === 'function'; 24 | if (!res) reportError('something wrong with the extractor'); 25 | } 26 | 27 | 28 | module.exports = function loaderEntry(source) { 29 | this.cacheable(); 30 | const options = loaderUtils.getOptions(this); 31 | 32 | checkPath(options.srcPath, options.builtTemplatePath, this.resourcePath); 33 | checkFunction(options.extractor, options.addScopeAttr); 34 | checkFunction(options.addScopeAttr); 35 | checkFunction(options.injector); 36 | checkSource(source); 37 | 38 | const buildOption = { 39 | ext: options.ext, 40 | srcPath: options.srcPath, 41 | builtTemplatePath: options.builtTemplatePath, 42 | builtAssetsPublicPath: options.builtAssetsPublicPath, 43 | isCodeSplit: options.isCodeSplit, 44 | extractor: options.extractor, 45 | injector: options.injector, 46 | addScopeAttr: options.addScopeAttr, 47 | currentPagePath: this.resourcePath, 48 | publicPath: this.options.output.publicPath || '', 49 | }; 50 | 51 | return core(source, buildOption); 52 | }; 53 | -------------------------------------------------------------------------------- /lib/addScopeIDToCSS/index.js: -------------------------------------------------------------------------------- 1 | // this is a entry of webpack-loader 2 | const postcss = require('postcss'); 3 | const loaderUtils = require('loader-utils'); 4 | const scopeId = require('./scopeIdPlugin'); 5 | 6 | module.exports = function addScopeIDToCSS(css) { 7 | this.cacheable(); 8 | const cb = this.async(); 9 | const query = loaderUtils.getOptions(this) || {}; 10 | const id = query.scopeID; 11 | 12 | const options = { 13 | from: this.resourcePath, 14 | to: this.resourcePath, 15 | map: false, 16 | }; 17 | 18 | postcss([scopeId({ id })]) 19 | .process(css, options) 20 | .then(res => cb(null, res.css)); 21 | }; 22 | -------------------------------------------------------------------------------- /lib/addScopeIDToCSS/scopeIdPlugin.js: -------------------------------------------------------------------------------- 1 | const postcss = require('postcss'); 2 | const selectorParser = require('postcss-selector-parser'); 3 | 4 | module.exports = postcss.plugin('add-id', ({ id }) => (cssNodes) => { 5 | 6 | const isScoped = cssNodes.some(node => node.type === 'comment' && node.text === 'scoped'); 7 | 8 | if (isScoped) { 9 | cssNodes.some((node) => { 10 | if (!node.selector) { 11 | return false; 12 | } 13 | node.selector = selectorParser((selectors) => { 14 | selectors.each((selector) => { 15 | let insertNode = null; 16 | selector.each((n) => { 17 | insertNode = n; 18 | }) 19 | selector.insertAfter(insertNode, selectorParser.attribute({ 20 | attribute: `data-s-${id}`, 21 | })); 22 | }); 23 | }).process(node.selector).result; 24 | return false; 25 | }); 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /lib/buildTemplate.js: -------------------------------------------------------------------------------- 1 | const reportError = require('./utils/reportError'); 2 | 3 | const rewriteTemplate = require('./rewriteTemplate'); 4 | 5 | module.exports = function buildTemplate(components, buildOption) { 6 | if (!Array.isArray(components)) { 7 | reportError('something wrong with building Template: components is not an array'); 8 | } 9 | components.forEach(rewriteTemplate, buildOption); 10 | }; 11 | -------------------------------------------------------------------------------- /lib/core.js: -------------------------------------------------------------------------------- 1 | const parsePage = require('./parsePage'); 2 | const createOutput = require('./createOutput'); 3 | const buildTemplate = require('./buildTemplate'); 4 | const genScopeID = require('./genScopeID'); 5 | const findScopedSelectors = require('./findScopedSelectors'); 6 | 7 | module.exports = function core(source, buildOption) { 8 | const components = parsePage(source, buildOption); 9 | genScopeID(components); 10 | findScopedSelectors(components, buildOption); 11 | buildTemplate(components, buildOption); 12 | return createOutput(components, buildOption); 13 | } 14 | -------------------------------------------------------------------------------- /lib/createOutput.js: -------------------------------------------------------------------------------- 1 | const fsx = require('fs-extra'); 2 | const path = require('path'); 3 | 4 | const reportError = require('./utils/reportError'); 5 | 6 | module.exports = function createOutput(components, buildOption) { 7 | let output = ''; 8 | if (!Array.isArray(components)) { 9 | reportError('the component is not an array'); 10 | } 11 | 12 | components.forEach((component) => { 13 | const compoPath = path.resolve(buildOption.srcPath, component.dir, component.name, component.name); 14 | const addScopeIDToCSSPath = path.resolve(__dirname, './addScopeIDToCSS/index'); 15 | if (fsx.existsSync(`${compoPath}.js`)) { 16 | if (buildOption.isCodeSplit) { 17 | output += `import('babel-loader!${compoPath}.js');\n`; 18 | } else { 19 | output += `require('babel-loader!${compoPath}.js');\n`; 20 | } 21 | } 22 | if (fsx.existsSync(`${compoPath}.css`)) { 23 | output += `require('${addScopeIDToCSSPath}?{"scopeID":"${component.scopeID}"}!${compoPath}.css');\n`; 24 | } 25 | }); 26 | 27 | return output; 28 | } -------------------------------------------------------------------------------- /lib/findScopedSelectors.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fsx = require('fs-extra'); 3 | 4 | // TODO: should improve, maybe deal with postcss 5 | function findSelectors(component, buildOption) { 6 | const cssFilePath = path.resolve(buildOption.srcPath, component.dir, component.name, `${component.name}.css`); 7 | const css = fsx.readFileSync(cssFilePath, 'utf8'); 8 | const selectorNameReg = new RegExp("([^\r\n,{}]+)(,(?=[^}]*{)|\s*{)", 'g'); 9 | if (css.indexOf('/* scoped */') === -1) return []; 10 | const res = css.match(selectorNameReg); 11 | return res.map(item => item.slice(1, item.length - 1).trim()); 12 | } 13 | 14 | module.exports = function findScopedSelectors(components, buildOption) { 15 | components.forEach((component) => { 16 | component.scopedSelectors = findSelectors(component, buildOption); 17 | return component; 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /lib/genScopeID.js: -------------------------------------------------------------------------------- 1 | // TODO: make it more reliable 2 | const hash = require('hash-sum'); 3 | 4 | const cache = Object.create(null); 5 | 6 | function genID(component) { 7 | const filePath = `${component.dir}/${component.base}`; 8 | const res = cache[filePath] || (cache[filePath] = hash(filePath)); 9 | return res; 10 | } 11 | 12 | module.exports = function genScopeID(components) { 13 | components.forEach((component) => { 14 | component.scopeID = genID(component); 15 | return component; 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /lib/parsePage.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fsx = require('fs-extra'); 3 | 4 | const reportError = require('./utils/reportError'); 5 | 6 | let components; 7 | const pushedComponents = new Map(); 8 | 9 | function recursiveParse(source, buildOption) { 10 | let includes; 11 | try { 12 | includes = buildOption.extractor(source); 13 | } catch (e) { 14 | reportError(`something wrong with the extractor: ${e}`); 15 | } 16 | if (!Array.isArray(includes)) { 17 | reportError('the result of extractor is not an array'); 18 | } 19 | includes.forEach((componentPath) => { 20 | if (pushedComponents.get(componentPath)) return; 21 | pushedComponents.set(componentPath, true); 22 | const parsed = path.parse(componentPath); 23 | const parsedPath = path.resolve(buildOption.srcPath, parsed.dir, parsed.name, parsed.base); 24 | if (fsx.existsSync(parsedPath)) { 25 | components.push(parsed); 26 | recursiveParse(fsx.readFileSync(parsedPath, 'utf8'), buildOption); 27 | } 28 | }); 29 | } 30 | 31 | module.exports = function parsePage(source, buildOption) { 32 | components = []; 33 | recursiveParse(source, buildOption); 34 | const pageSelf = path.parse(buildOption.currentPagePath); 35 | pageSelf.dir = pageSelf.dir.replace(pageSelf.name, '').replace(`${buildOption.srcPath}/`, ''); 36 | pageSelf.root = ''; 37 | pageSelf.pageFlag = true; 38 | components.push(pageSelf); 39 | return components; 40 | }; 41 | -------------------------------------------------------------------------------- /lib/rewriteTemplate.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fsx = require('fs-extra'); 3 | const reportError = require('./utils/reportError'); 4 | 5 | module.exports = async function rewriteTemplate(component) { 6 | const buildOption = this; 7 | const templatePath = path.resolve(buildOption.srcPath, component.dir, component.name, `${component.base}`); 8 | if (fsx.existsSync(templatePath)) { 9 | try { 10 | const template = fsx.readFileSync(templatePath, 'utf8'); 11 | let res = await buildOption.addScopeAttr(template, component); 12 | if (component.pageFlag) res = buildOption.injector(res, component, buildOption); 13 | fsx.ensureDirSync(path.resolve(buildOption.builtTemplatePath, component.dir)); 14 | fsx.writeFileSync(path.resolve(buildOption.builtTemplatePath, component.dir, `${component.base}`), res); 15 | } catch (e) { 16 | reportError(e); 17 | } 18 | } else { 19 | reportError(`something wrong with building Template: ${templatePath} is non existence`); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/utils/reportError.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function reportError(message) { 3 | throw new Error(`[webpack-component-loader]: ${message}\n`); 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-component-loader", 3 | "version": "1.0.10", 4 | "description": "A webpack loader to componentify CSS/JS/HTML without framework", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/nicholaslee119/webpack-component-loader.git" 12 | }, 13 | "keywords": [ 14 | "webpack", 15 | "loader", 16 | "component" 17 | ], 18 | "author": "nicholas lee", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/nicholaslee119/webpack-component-loader/issues" 22 | }, 23 | "homepage": "https://github.com/nicholaslee119/webpack-component-loader#readme", 24 | "devDependencies": { 25 | "babel-jest": "^21.2.0", 26 | "babel-loader": "^7.1.2", 27 | "babel-preset-es2015": "^6.24.1", 28 | "css-loader": "^0.28.7", 29 | "eslint": "^5.15.2", 30 | "eslint-config-airbnb-base": "^12.0.0", 31 | "eslint-plugin-import": "^2.7.0", 32 | "extract-text-webpack-plugin": "^3.0.1", 33 | "jest": "^21.2.0", 34 | "klaw-sync": "^3.0.0", 35 | "regenerator-runtime": "^0.11.0", 36 | "style-loader": "^0.19.0", 37 | "webpack": "^3.5.6", 38 | "webpack-component-loader-smarty-parser": "0.0.4" 39 | }, 40 | "jest": { 41 | "collectCoverage": true, 42 | "collectCoverageFrom": [ 43 | "**/lib/**" 44 | ], 45 | "coverageDirectory": "./test/coverage/" 46 | }, 47 | "dependencies": { 48 | "fs-extra": "^4.0.2", 49 | "hash-sum": "^1.0.2", 50 | "is-valid-path": "^0.1.1", 51 | "loader-utils": "^1.1.0", 52 | "postcss": "^6.0.13", 53 | "postcss-selector-parser": "^2.2.3" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/buildTemplate.test.js: -------------------------------------------------------------------------------- 1 | import fsx from 'fs-extra'; 2 | import path from 'path'; 3 | import klawSync from 'klaw-sync'; 4 | 5 | import buildTemplate from '../lib/buildTemplate'; 6 | import buildOptionNormal from './util/buildOption.normal'; 7 | 8 | describe('test buildTemplate', function() { 9 | 10 | it('should built successfully', function(cb) { 11 | const normalComponents = 12 | [ 13 | { 14 | "root": "", 15 | "dir": "include", 16 | "base": "componentA.tpl", 17 | "ext": ".tpl", 18 | "name": "componentA" 19 | }, 20 | { 21 | "root": "", 22 | "dir": "include", 23 | "base": "componentD.tpl", 24 | "ext": ".tpl", 25 | "name": "componentD" 26 | }, 27 | { 28 | "root": "", 29 | "dir": "", 30 | "base": "componentB.tpl", 31 | "ext": ".tpl", 32 | "name": "componentB" 33 | }, 34 | { 35 | "root": "/", 36 | "dir": "", 37 | "base": "pageC.tpl", 38 | "ext": ".tpl", 39 | "name": "pageC" 40 | } 41 | ]; 42 | fsx.removeSync(path.join(__dirname, './assetsCoreTest')); 43 | buildTemplate(normalComponents, buildOptionNormal); 44 | 45 | setTimeout(()=>{ 46 | const dirs = klawSync(path.join(__dirname, './assetsCoreTest'), {nodir: true}); 47 | expect(dirs).toHaveLength(normalComponents.length); 48 | fsx.removeSync(path.join(__dirname, './assetsCoreTest')); 49 | cb(); 50 | }, 0) 51 | 52 | }); 53 | 54 | // TODO: should improve the test for async 55 | // it('should throw error when component is not exist', function() { 56 | // const inexistentComponents = 57 | // [ 58 | // { 59 | // "root": "", 60 | // "dir": "include", 61 | // "base": "noA.tpl", 62 | // "ext": ".tpl", 63 | // "name": "noA" 64 | // }, 65 | // { 66 | // "root": "/", 67 | // "dir": "", 68 | // "base": "noB.tpl", 69 | // "ext": ".tpl", 70 | // "name": "noB" 71 | // } 72 | // ]; 73 | // 74 | // const errorPath = path.resolve(buildOptionNormal.srcPath, inexistentComponents[0].dir, inexistentComponents[0].name, `${inexistentComponents[0].name}${buildOptionNormal.ext}`); 75 | // 76 | // setTimeout(()=>{ 77 | // const dirs = klawSync(path.join(__dirname, './assetsCoreTest'), {nodir: true}); 78 | // expect(dirs).toHaveLength(normalComponents.length); 79 | // fsx.removeSync(path.join(__dirname, './assetsCoreTest')); 80 | // cb(); 81 | // }, 0) 82 | // 83 | // expect(()=>{ 84 | // buildTemplate(inexistentComponents, buildOptionNormal); 85 | // }).toThrowError(`[webpack-component-loader]: something wrong with building Template: ${errorPath} is non existence`); 86 | // }) 87 | // 88 | // it('should throw error when on-array component was passed in', function() { 89 | // const noArray = 'NOT A ARRAY'; 90 | // expect(()=>{ 91 | // buildTemplate(noArray, buildOptionNormal); 92 | // }).toThrowError('[webpack-component-loader]: something wrong with building Template: components is not an array') 93 | // }) 94 | // 95 | // it('should throw error when was passed in a broken component', function() { 96 | // const brokenComponents = [ 97 | // 1,2, 98 | // { 99 | // "dir": "include", 100 | // "base": "noA.tpl", 101 | // "ext": ".tpl", 102 | // "name": "noA" 103 | // }, 104 | // { 105 | // "root": "/", 106 | // "dir": "", 107 | // "base": "noB.tpl", 108 | // "name": "noB" 109 | // }, 110 | // {} 111 | // ]; 112 | // expect(()=>{ 113 | // buildTemplate(brokenComponents, buildOptionNormal); 114 | // }).toThrow(); 115 | // }) 116 | 117 | }) 118 | 119 | 120 | -------------------------------------------------------------------------------- /test/createOutput.test.js: -------------------------------------------------------------------------------- 1 | import createOutput from '../lib/createOutput'; 2 | import normalBuildOption from './util/buildOption.normal'; 3 | import normalComponents from './util/components.normal'; 4 | 5 | 6 | describe('test createOutput', function(){ 7 | it('should pass', function(){ 8 | const res = createOutput(normalComponents, normalBuildOption); 9 | expect(res).toMatch(/require\('.*'\)/); 10 | }); 11 | 12 | it('should throw error when pass in a bad Array', function(){ 13 | expect(()=>createOutput('NOT AN ARRAY', normalBuildOption)).toThrow('[webpack-component-loader]: the component is not an array'); 14 | }) 15 | }) -------------------------------------------------------------------------------- /test/findScopedSelectors.test.js: -------------------------------------------------------------------------------- 1 | import findScopedSelectors from '../lib/findScopedSelectors'; 2 | import buildOption from './util/buildOption.normal'; 3 | 4 | describe('test genScopeID', function(){ 5 | it('should pass with normal input', function () { 6 | let components = [ 7 | { 8 | "root": "/", 9 | "dir": "", 10 | "base": "scopeCompo.tpl", 11 | "ext": ".tpl", 12 | "name": "scopeCompo" 13 | } 14 | ]; 15 | findScopedSelectors(components, buildOption); 16 | // TODD: should improve 17 | expect(components.every(component=>{ 18 | return component.hasOwnProperty('scopedSelectors') && component.scopedSelectors !== undefined; 19 | })).toBeTruthy(); 20 | }) 21 | }) -------------------------------------------------------------------------------- /test/fixture/componentB/componentB.css: -------------------------------------------------------------------------------- 1 | .componentB { 2 | /* I came from B*/ 3 | } 4 | -------------------------------------------------------------------------------- /test/fixture/componentB/componentB.js: -------------------------------------------------------------------------------- 1 | function B () { 2 | /* I came from B*/ 3 | } 4 | -------------------------------------------------------------------------------- /test/fixture/componentB/componentB.tpl: -------------------------------------------------------------------------------- 1 |

I am component B

2 |
{include file='include/componentD.tpl'}
-------------------------------------------------------------------------------- /test/fixture/include/componentA/componentA.css: -------------------------------------------------------------------------------- 1 | /* scoped */ 2 | .componentA { 3 | /* I come from A*/ 4 | } 5 | -------------------------------------------------------------------------------- /test/fixture/include/componentA/componentA.js: -------------------------------------------------------------------------------- 1 | function A () { 2 | /* I came from A*/ 3 | } 4 | -------------------------------------------------------------------------------- /test/fixture/include/componentA/componentA.tpl: -------------------------------------------------------------------------------- 1 |

I am component A

2 |
{include file='include/componentD.tpl'}
-------------------------------------------------------------------------------- /test/fixture/include/componentD/componentD.css: -------------------------------------------------------------------------------- 1 | .componentD .sss. ddffff{ 2 | /* I come from D*/ 3 | } -------------------------------------------------------------------------------- /test/fixture/include/componentD/componentD.js: -------------------------------------------------------------------------------- 1 | function D () { 2 | /* I came from D*/ 3 | } 4 | -------------------------------------------------------------------------------- /test/fixture/include/componentD/componentD.tpl: -------------------------------------------------------------------------------- 1 |

I am component D

-------------------------------------------------------------------------------- /test/fixture/pageC.entry.js: -------------------------------------------------------------------------------- 1 | import './pageC/pageC.tpl'; -------------------------------------------------------------------------------- /test/fixture/pageC/pageC.css: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | /*************************************************** 4 | * 見出し 5 | ***************************************************/ 6 | h2.sectionHeadRtop { 7 | border-top: none; 8 | border-bottom: none; 9 | color: #333; 10 | font-size: 13px; 11 | padding-top: 11px; 12 | padding-bottom: 10px; 13 | background: #e4f1d7; 14 | text-shadow: none; 15 | } 16 | 17 | h2.sectionHeadRtop span { 18 | display: block; 19 | border-left: 3px solid #61aa12; 20 | padding-left: 5px; 21 | margin-left: 5px; 22 | border-radius: 1px; 23 | } 24 | 25 | /*************************************************** 26 | * 都道府県選択 27 | ***************************************************/ 28 | .todofuken { 29 | border-bottom: 1px solid #eee; 30 | } 31 | 32 | .todofuken .area { 33 | display: block; 34 | border-top: 1px solid #eee; 35 | } 36 | 37 | .todofuken .area .areaName { 38 | display: block; 39 | position: relative; 40 | background: #fff; 41 | padding: 15px 10px; 42 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 43 | } 44 | 45 | .todofuken .area .areaName h3 { 46 | font-size: 15px; 47 | font-weight: bold; 48 | color: #333; 49 | } 50 | 51 | .todofuken .area .areaName { 52 | border-bottom: 3px solid #eee; 53 | background: #f2f2f2; 54 | } 55 | 56 | .todofuken .area .pref li:first-child .prefecture { 57 | border-top: none; 58 | } 59 | 60 | .todofuken .area .pref { 61 | height: auto; 62 | overflow: hidden; 63 | background: #fff; 64 | padding-left: 20px; 65 | } 66 | 67 | .todofuken .area .pref .prefecture { 68 | display: block; 69 | padding: 17px 17px 17px 10px; 70 | font-size: 15px; 71 | color: #333; 72 | border-top: 1px solid #eee; 73 | position: relative; 74 | } 75 | 76 | .todofuken .area .pref .prefecture.ui-hover { 77 | background: #f9f9f9; 78 | } 79 | 80 | .todofuken .area .pref .prefecture:after { 81 | display: block; 82 | content: " "; 83 | position: absolute; 84 | width: 9px; 85 | height: 17px; 86 | background-image: url(/img/chintai/top/kensentaku/sprite/icon.png?id=CACHE_REVISION); 87 | background-repeat: no-repeat; 88 | background-size: 20px 57px; 89 | -webkit-background-size: 20px 57px; 90 | background-position: 0 0; 91 | top: 50%; 92 | margin-top: -8px; 93 | right: 15px; 94 | } -------------------------------------------------------------------------------- /test/fixture/pageC/pageC.js: -------------------------------------------------------------------------------- 1 | function C () { 2 | /* I came from C*/ 3 | let a = 1; 4 | } 5 | -------------------------------------------------------------------------------- /test/fixture/pageC/pageC.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
{include file='include/componentA.tpl'}
7 | 8 |
{include file='componentB.tpl'}
9 | 10 | 11 | -------------------------------------------------------------------------------- /test/fixture/pageE.entry.js: -------------------------------------------------------------------------------- 1 | import './pageE/pageE.tpl'; -------------------------------------------------------------------------------- /test/fixture/pageE/pageE.css: -------------------------------------------------------------------------------- 1 | .pageE { 2 | /* I come from E*/ 3 | } 4 | 5 | .common { 6 | /* common */ 7 | } -------------------------------------------------------------------------------- /test/fixture/pageE/pageE.js: -------------------------------------------------------------------------------- 1 | function E () { 2 | /* I came from E*/ 3 | let a = 1; 4 | } 5 | -------------------------------------------------------------------------------- /test/fixture/pageE/pageE.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
{include file='include/componentD.tpl'}
7 | 8 |
{include file='componentB.tpl'}
9 | 10 | 11 | -------------------------------------------------------------------------------- /test/fixture/scopeCompo/scopeCompo.css: -------------------------------------------------------------------------------- 1 | .scopedClass { 2 | background: moccasin; 3 | } 4 | 5 | .scopedClassS { 6 | background: moccasin; 7 | } -------------------------------------------------------------------------------- /test/fixture/scopeCompo/scopeCompo.tpl: -------------------------------------------------------------------------------- 1 |
2 |

I am scopeCompo

3 |

I am scopeCompo

4 |

without className

5 |

element with global namespace

6 |

without className

7 |
-------------------------------------------------------------------------------- /test/genScopeID.test.js: -------------------------------------------------------------------------------- 1 | import genScopeID from '../lib/genScopeID'; 2 | import components from './util/components.normal'; 3 | 4 | describe('test genScopeID', function(){ 5 | it('should pass with normal input', function () { 6 | genScopeID(components); 7 | expect(components.every(component=>{ 8 | return component.hasOwnProperty('scopeID') && component.scopeID !== undefined; 9 | })).toBeTruthy(); 10 | }) 11 | }) -------------------------------------------------------------------------------- /test/parsePage.test.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | 4 | import buildOptionNormal from './util/buildOption.normal'; 5 | import parsePage from '../lib/parsePage'; 6 | 7 | describe('test parsePage', function() { 8 | it('should build successfully', function() { 9 | const source = fs.readFileSync(path.join(__dirname, './fixture/pageC/pageC.tpl'), 'utf8'); 10 | const components = parsePage(source, buildOptionNormal); 11 | expect(components).toHaveLength(4); 12 | }); 13 | 14 | it('should throw error when something wrong with extractor', function() { 15 | const source = fs.readFileSync(path.join(__dirname, './fixture/pageC/pageC.tpl'), 'utf8'); 16 | const badOptions = Object.create(buildOptionNormal); 17 | badOptions.extractor = ()=>{ throw 'the extractor is down!'}; 18 | expect(()=>{ 19 | parsePage(source, badOptions); 20 | }).toThrowError('[webpack-component-loader]: something wrong with the extractor: the extractor is down!'); 21 | }); 22 | 23 | it('should throw error when something wrong with extractor', function() { 24 | const source = fs.readFileSync(path.join(__dirname, './fixture/pageC/pageC.tpl'), 'utf8'); 25 | const badOptions = Object.create(buildOptionNormal); 26 | badOptions.extractor = ()=>{ return null}; 27 | expect(()=>{ 28 | parsePage(source, badOptions) 29 | }).toThrowError('[webpack-component-loader]: the result of extractor is not an array'); 30 | }); 31 | }) 32 | -------------------------------------------------------------------------------- /test/setScope.test.js: -------------------------------------------------------------------------------- 1 | import {addScopeAttr} from 'webpack-component-loader-smarty-parser'; 2 | // import normalComponent from "./fixture/scopeCompo/scopeCompo.tpl"; 3 | 4 | describe('test setScope', function(){ 5 | 6 | it('should pass with normal', function(){ 7 | expect.assertions(1); 8 | const normalTemplate = 9 | `
`+ 10 | `

I am scopeCompo

`+ 11 | `

I am scopeCompo

`+ 12 | `

without className

`+ 13 | `

element with global namespace

`+ 14 | `

without className

`+ 15 | `
`; 16 | 17 | const component = { 18 | scopedSelectors: ['scopedClass', 'scopedClassS'], 19 | scopeID: 'dr2343d' 20 | }; 21 | 22 | return addScopeAttr(normalTemplate, component).then((res) => { 23 | expect(res).toMatch( 24 | `
`+ 25 | `

I am scopeCompo

`+ 26 | `

I am scopeCompo

`+ 27 | `

without className

`+ 28 | `

element with global namespace

`+ 29 | `

without className

`+ 30 | `
`); 31 | }); 32 | 33 | }) 34 | }) -------------------------------------------------------------------------------- /test/testInWebpack.test.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import fsx from 'fs-extra'; 3 | import path from 'path'; 4 | 5 | import config from './util/webpack.config.normal'; 6 | 7 | describe('test in webpack', function () { 8 | it('should be built successfully in webpack', function(done) { 9 | function cb (err, stats) { 10 | if (err) { 11 | console.error(err.stack || err); 12 | if (err.details) { 13 | console.error(err.details); 14 | } 15 | } 16 | expect(err).toBeNull(); 17 | const builtTemplates = fsx.pathExistsSync(path.resolve(__dirname, './assets/templates')); 18 | const builtJS = fsx.pathExistsSync(path.resolve(__dirname, './assets/js')); 19 | const builtCSS = fsx.pathExistsSync(path.resolve(__dirname, './assets/css')); 20 | expect(builtTemplates).toBeTruthy(); 21 | expect(builtJS).toBeTruthy(); 22 | expect(builtCSS).toBeTruthy(); 23 | fsx.removeSync(path.resolve(__dirname, './assets')); 24 | done(); 25 | }; 26 | fsx.removeSync(path.resolve(__dirname, './assets')); 27 | webpack (config, cb); 28 | }); 29 | 30 | 31 | it('unvalidate path should not be passed', function(done){ 32 | function cb (err, stats) { 33 | if (err) { 34 | console.error(err.stack || err); 35 | if (err.details) { 36 | console.error(err.details); 37 | } 38 | } 39 | const builtTemplates = fsx.pathExistsSync(path.resolve(__dirname, './assets/templates')); 40 | // const builtJS = fsx.pathExistsSync(path.resolve(__dirname, './assets/js')); 41 | const builtCSS = fsx.pathExistsSync(path.resolve(__dirname, './assets/css')); 42 | expect(builtTemplates).toBeFalsy(); 43 | // expect(builtJS).toBeFalsy(); 44 | expect(builtCSS).toBeFalsy(); 45 | fsx.removeSync(path.resolve(__dirname, './assets')); 46 | done(); 47 | }; 48 | const badConfig = Object.create(config); 49 | badConfig.module.rules[0].use[0].options.srcPath = '/!!!!'; 50 | fsx.removeSync(path.resolve(__dirname, './assets')); 51 | webpack(badConfig, cb ); 52 | }); 53 | 54 | 55 | it('unvalidate extractor should not be passed', function(done){ 56 | function cb (err, stats) { 57 | if (err) { 58 | console.error(err.stack || err); 59 | if (err.details) { 60 | console.error(err.details); 61 | } 62 | } 63 | const builtTemplates = fsx.pathExistsSync(path.resolve(__dirname, './assets/templates')); 64 | // const builtJS = fsx.pathExistsSync(path.resolve(__dirname, './assets/js')); 65 | const builtCSS = fsx.pathExistsSync(path.resolve(__dirname, './assets/css')); 66 | expect(builtTemplates).toBeFalsy(); 67 | // expect(builtJS).toBeFalsy(); 68 | expect(builtCSS).toBeFalsy(); 69 | fsx.removeSync(path.resolve(__dirname, './assets')); 70 | done(); 71 | }; 72 | const badConfig = Object.create(config); 73 | badConfig.module.rules[0].use[0].options.extractor = {}; 74 | fsx.removeSync(path.resolve(__dirname, './assets')); 75 | webpack(badConfig, cb ); 76 | }); 77 | }) -------------------------------------------------------------------------------- /test/util/buildOption.normal.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const {extractor, addScopeAttr} = require('webpack-component-loader-smarty-parser'); 3 | 4 | export default { 5 | extractor, 6 | addScopeAttr, 7 | ext: '.tpl', 8 | isCodeSplit: false, 9 | srcPath: path.join(__dirname, '../fixture'), 10 | builtJSPath: path.join(__dirname, '../assetsCoreTest/js'), 11 | builtCSSPath: path.join(__dirname, '../assetsCoreTest/css'), 12 | builtTemplatePath: path.join(__dirname, '../assetsCoreTest/templates'), 13 | currentPagePath: path.join(__dirname, '../fixture/pageC/pageC.tpl'), 14 | }; -------------------------------------------------------------------------------- /test/util/components.normal.js: -------------------------------------------------------------------------------- 1 | export default 2 | [ 3 | { 4 | "root": "", 5 | "dir": "include", 6 | "base": "componentA.tpl", 7 | "ext": ".tpl", 8 | "name": "componentA" 9 | }, 10 | { 11 | "root": "", 12 | "dir": "include", 13 | "base": "componentD.tpl", 14 | "ext": ".tpl", 15 | "name": "componentD" 16 | }, 17 | { 18 | "root": "", 19 | "dir": "", 20 | "base": "componentB.tpl", 21 | "ext": ".tpl", 22 | "name": "componentB" 23 | }, 24 | { 25 | "root": "/", 26 | "dir": "", 27 | "base": "pageC.tpl", 28 | "ext": ".tpl", 29 | "name": "pageC" 30 | } 31 | ]; -------------------------------------------------------------------------------- /test/util/webpack.config.normal.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 3 | const webpack = require('webpack'); 4 | 5 | const {extractor, addScopeAttr, injector} = require('webpack-component-loader-smarty-parser'); 6 | 7 | module.exports = { 8 | entry: { 9 | 'pageC': path.resolve(__dirname, '../fixture/pageC.entry.js'), 10 | 'pageE': path.resolve(__dirname, '../fixture/pageE.entry.js'), 11 | }, 12 | output: { 13 | path: path.resolve(__dirname, '../assets/'), 14 | publicPath: '/assets/', 15 | filename : 'js/[name].js', 16 | chunkFilename: 'js/[name].chunk.js', 17 | }, 18 | resolveLoader: { 19 | alias: { 20 | 'webpack-component-loader': path.resolve(__dirname, '../../index.js'), 21 | }, 22 | }, 23 | module: { 24 | rules: [ 25 | { 26 | test : /\.tpl?$/, 27 | exclude: /(node_modules)/, 28 | use: [ 29 | { 30 | loader: 'webpack-component-loader', 31 | options: { 32 | isCodeSplit: false, 33 | extractor, 34 | injector, 35 | addScopeAttr, 36 | ext: '.tpl', 37 | srcPath : path.resolve(__dirname, '../fixture'), 38 | builtTemplatePath : path.resolve(__dirname, '../assets/templates'), 39 | }, 40 | }, 41 | ], 42 | }, 43 | { 44 | test: /\.css$/, 45 | exclude: /(node_modules)/, 46 | enforce: 'post', 47 | use: ExtractTextPlugin.extract({ 48 | fallback: 'style-loader', 49 | use: 'css-loader' 50 | }) 51 | } 52 | ], 53 | }, 54 | plugins: [ 55 | new ExtractTextPlugin({ 56 | filename: 'css/[name].css', 57 | // allChunks: true 58 | }), 59 | new webpack.optimize.CommonsChunkPlugin({ 60 | name: 'commons', 61 | filename: 'js/commons.js', 62 | }) 63 | ] 64 | } --------------------------------------------------------------------------------