├── .gitignore ├── .gitattributes ├── babel.js ├── .prettierrc ├── lib ├── compileTemplate.js ├── style-compilers │ ├── stylus.js │ ├── postcss.js │ └── sass.js ├── importLocal.js ├── script-compilers │ ├── ts.js │ └── babel.js ├── compileScript.js ├── utils.js ├── compileStyles.js ├── writeSFC.js ├── babel │ └── preset.js └── index.js ├── .editorconfig ├── circle.yml ├── LICENSE ├── package.json ├── bin └── cli.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /babel.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/babel/preset') 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /lib/compileTemplate.js: -------------------------------------------------------------------------------- 1 | module.exports = template => { 2 | return template 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /lib/style-compilers/stylus.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { promisify } = require('util') 3 | const importLocal = require('../importLocal') 4 | 5 | module.exports = (code, { filename }) => { 6 | const stylus = importLocal(path.dirname(filename), 'stylus') 7 | return promisify(stylus.render.bind(stylus))(code, { filename }) 8 | } 9 | -------------------------------------------------------------------------------- /lib/importLocal.js: -------------------------------------------------------------------------------- 1 | const importFrom = require('import-from') 2 | 3 | module.exports = (dir, name, fallback) => { 4 | const found = importFrom.silent(dir, name) || importFrom.silent(dir, fallback) 5 | if (!found) { 6 | throw new Error( 7 | `You need to install "${name}"${ 8 | fallback ? ` or "${fallback}"` : '' 9 | } in current directory!` 10 | ) 11 | } 12 | return found 13 | } 14 | -------------------------------------------------------------------------------- /lib/script-compilers/ts.js: -------------------------------------------------------------------------------- 1 | // TODO: use `typescript` module to compile typescript code 😅 2 | // So that we have proper type checking at compile time 3 | module.exports = (code, { filename }) => { 4 | return require('@babel/core').transform(code, { 5 | filename, 6 | babelrc: false, 7 | presets: [ 8 | require.resolve('@babel/preset-typescript'), 9 | require.resolve('../babel/preset') 10 | ] 11 | }).code 12 | } 13 | -------------------------------------------------------------------------------- /lib/compileScript.js: -------------------------------------------------------------------------------- 1 | const { notSupportedLang } = require('./utils') 2 | 3 | module.exports = async (script, ctx) => { 4 | if (!script) return script 5 | 6 | const code = script.content.replace(/^\/\/$/mg, '') 7 | 8 | if (script.lang === 'ts' || script.lang === 'typescript') { 9 | script.content = await require('./script-compilers/ts')(code, ctx) 10 | } else if (!script.lang || script.lang === 'esnext' || script.lang === 'babel') { 11 | script.content = await require('./script-compilers/babel')(code, ctx) 12 | } else { 13 | throw new Error(notSupportedLang(script.lang, 'script')) 14 | } 15 | 16 | return script 17 | } 18 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/node:latest 6 | branches: 7 | ignore: 8 | - gh-pages # list of branches to ignore 9 | - /release\/.*/ # or ignore regexes 10 | steps: 11 | - checkout 12 | - restore_cache: 13 | key: dependency-cache-{{ checksum "yarn.lock" }} 14 | - run: 15 | name: install dependences 16 | command: yarn 17 | - save_cache: 18 | key: dependency-cache-{{ checksum "yarn.lock" }} 19 | paths: 20 | - ./node_modules 21 | - run: 22 | name: test 23 | command: yarn test 24 | - run: 25 | name: release 26 | command: npx semantic-release 27 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | exports.humanlizePath = p => path.relative(process.cwd(), p) 4 | 5 | exports.notSupportedLang = (lang, tag) => { 6 | return `"${lang}" is not supported for <${tag}> tag currently, wanna contribute this feature?` 7 | } 8 | 9 | function escapeRe(str) { 10 | return str.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&') 11 | } 12 | 13 | exports.replaceContants = (content, constants) => { 14 | if (!constants) return content 15 | 16 | const RE = new RegExp( 17 | `\\b(${Object.keys(constants) 18 | .map(escapeRe) 19 | .join('|')})\\b`, 20 | 'g' 21 | ) 22 | content = content.replace(RE, (_, p1) => { 23 | return constants[p1] 24 | }) 25 | 26 | return content 27 | } 28 | 29 | exports.cssExtensionsRe = /\.(css|s[ac]ss|styl(us)?)$/ 30 | -------------------------------------------------------------------------------- /lib/script-compilers/babel.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const debug = require('debug')('vue-compile:script') 3 | 4 | const cache = new Map() 5 | 6 | module.exports = async (code, { filename, modern, babelrc }) => { 7 | const cwd = path.dirname(filename) 8 | const file = 9 | babelrc === false ? 10 | null : 11 | cache.get(cwd) || 12 | (await require('find-babel-config')(cwd).then(res => res.file)) 13 | 14 | cache.set(cwd, file) 15 | 16 | const config = { 17 | filename, 18 | presets: [ 19 | [ 20 | require.resolve('../babel/preset'), 21 | { 22 | modern 23 | } 24 | ] 25 | ] 26 | } 27 | if (file) { 28 | config.babelrc = true 29 | debug(`Using Babel config file at ${file}`) 30 | } else { 31 | config.babelrc = false 32 | } 33 | 34 | return require('@babel/core').transform(code, config).code 35 | } 36 | -------------------------------------------------------------------------------- /lib/compileStyles.js: -------------------------------------------------------------------------------- 1 | const { notSupportedLang } = require('./utils') 2 | 3 | module.exports = (styles, { filename }) => { 4 | return Promise.all( 5 | styles.map(async style => { 6 | // Do not handle "src" import 7 | // Until we figure out how to handle it 8 | if (style.src) return style 9 | 10 | if (style.lang === 'stylus') { 11 | style.content = await require('./style-compilers/stylus')( 12 | style.content, 13 | { filename } 14 | ) 15 | } else if (!style.lang || style.lang === 'postcss') { 16 | style.content = await require('./style-compilers/postcss')( 17 | style.content, 18 | { filename } 19 | ) 20 | } else if (style.lang === 'scss' || style.lang === 'sass') { 21 | style.content = await require('./style-compilers/sass')(style.content, { 22 | filename, 23 | indentedSyntax: style.lang === 'sass' 24 | }) 25 | } else if (style.lang) { 26 | throw new Error(notSupportedLang(style.lang, 'style')) 27 | } 28 | 29 | return style 30 | }) 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /lib/style-compilers/postcss.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const debug = require('debug')('vue-compile:style') 3 | 4 | const cache = new Map() 5 | 6 | module.exports = async (code, { filename }) => { 7 | const ctx = { 8 | file: { 9 | extname: path.extname(filename), 10 | dirname: path.dirname(filename), 11 | basename: path.basename(filename) 12 | }, 13 | options: {} 14 | } 15 | 16 | const cwd = path.dirname(filename) 17 | const config = 18 | cache.get(cwd) || 19 | (await require('postcss-load-config')(ctx, cwd, { 20 | argv: false 21 | }).catch(err => { 22 | if (err.message.includes('No PostCSS Config found in')) { 23 | return {} 24 | } 25 | throw err 26 | })) 27 | cache.set(cwd, config) 28 | 29 | if (config.file) { 30 | debug(`Using PostCSS config file at ${config.file}`) 31 | } 32 | 33 | const options = Object.assign( 34 | { 35 | from: filename, 36 | map: false 37 | }, 38 | config.options 39 | ) 40 | 41 | return require('postcss')(config.plugins || []) 42 | .process(code, options) 43 | .then(res => res.css) 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) egoist <0x142857@gmail.com> (https://github.com/egoist) 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/writeSFC.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const fs = require('fs-extra') 3 | const stringifyAttrs = require('stringify-attributes') 4 | const { cssExtensionsRe } = require('./utils') 5 | 6 | module.exports = async ({ script, styles, template }, outFile) => { 7 | const parts = [] 8 | 9 | if (template) { 10 | parts.push( 11 | `${template.content 12 | .replace(/\n$/, '') 13 | .replace(/^/gm, ' ')}\n` 14 | ) 15 | } 16 | 17 | if (script) { 18 | parts.push(``) 19 | } 20 | 21 | if (styles.length > 0) { 22 | for (const style of styles) { 23 | const attrs = Object.assign({}, style.attrs) 24 | delete attrs.lang 25 | 26 | if (style.src) { 27 | attrs.src = attrs.src.replace(cssExtensionsRe, '.css') 28 | parts.push(``) 29 | } else { 30 | parts.push( 31 | `\n${style.content.trim()}\n` 32 | ) 33 | } 34 | } 35 | } 36 | 37 | await fs.ensureDir(path.dirname(outFile)) 38 | await fs.writeFile(outFile, parts.join('\n\n'), 'utf8') 39 | } 40 | -------------------------------------------------------------------------------- /lib/babel/preset.js: -------------------------------------------------------------------------------- 1 | const { cssExtensionsRe } = require('../utils') 2 | 3 | module.exports = (_, { modern } = {}) => { 4 | return { 5 | presets: [ 6 | [ 7 | require.resolve('babel-preset-minimal'), 8 | { 9 | jsx: 'vue', 10 | mode: modern ? 'modern' : undefined 11 | } 12 | ] 13 | ], 14 | plugins: [replaceExtensionInImports] 15 | } 16 | } 17 | 18 | function replaceExtensionInImports({ types: t }) { 19 | return { 20 | name: 'replace-extension-in-imports', 21 | visitor: { 22 | ImportDeclaration(path) { 23 | if (cssExtensionsRe.test(path.node.source.value)) { 24 | path.node.source.value = path.node.source.value.replace( 25 | cssExtensionsRe, 26 | '.css' 27 | ) 28 | } 29 | }, 30 | CallExpression(path) { 31 | if (path.node.callee.name === 'require') { 32 | const arg = path.get('arguments.0') 33 | if (arg) { 34 | const res = arg.evaluate() 35 | if (res.confident && cssExtensionsRe.test(res.value)) { 36 | path.node.arguments = [ 37 | t.stringLiteral(res.value.replace(cssExtensionsRe, '.css')) 38 | ] 39 | } 40 | } 41 | } 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-compile", 3 | "version": "0.5.2", 4 | "description": "Pre-compile each blocks of your Vue single-file components.", 5 | "repository": { 6 | "url": "egoist/vue-compile", 7 | "type": "git" 8 | }, 9 | "main": "lib/index.js", 10 | "bin": "bin/cli.js", 11 | "files": [ 12 | "bin/", 13 | "lib/", 14 | "babel.js" 15 | ], 16 | "scripts": { 17 | "test": "npm run lint && echo 'no tests!'", 18 | "lint": "xo", 19 | "postinstall": "node -e \"console.log('\\u001b[35m\\u001b[1mLove vue-compile? You can now donate to support the author:\\u001b[22m\\u001b[39m\\n> \\u001b[36mhttps://patreon.com/egoist\\u001b[39m')\"" 20 | }, 21 | "author": "egoist <0x142857@gmail.com>", 22 | "license": "MIT", 23 | "dependencies": { 24 | "@babel/core": "^7.0.0", 25 | "@babel/preset-typescript": "^7.0.0", 26 | "@vue/component-compiler-utils": "^2.3.0", 27 | "babel-preset-minimal": "^0.1.2", 28 | "cac": "^6.4.2", 29 | "chalk": "^2.4.1", 30 | "debug": "^3.1.0", 31 | "fast-glob": "^2.2.1", 32 | "find-babel-config": "^1.1.0", 33 | "fs-extra": "^6.0.0", 34 | "import-from": "^2.1.0", 35 | "is-binary-path": "^2.0.0", 36 | "joycon": "^2.1.2", 37 | "postcss": "^7.0.6", 38 | "postcss-load-config": "^2.0.0", 39 | "resolve": "^1.8.1", 40 | "stringify-attributes": "^1.0.0", 41 | "vue-template-compiler": "^2.6.10" 42 | }, 43 | "devDependencies": { 44 | "eslint-config-rem": "^4.0.0", 45 | "husky": "^1.0.0-rc.4", 46 | "lint-staged": "^7.1.0", 47 | "stylus": "^0.54.5", 48 | "xo": "^0.18.0" 49 | }, 50 | "xo": { 51 | "extends": "rem", 52 | "rules": { 53 | "unicorn/filename-case": "off" 54 | } 55 | }, 56 | "husky": { 57 | "hooks": { 58 | "pre-commit": "lint-staged" 59 | } 60 | }, 61 | "lint-staged": { 62 | "{lib,bin}/**/*.js": [ 63 | "xo --fix", 64 | "git add" 65 | ] 66 | }, 67 | "release": { 68 | "branch": "master" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/style-compilers/sass.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { promisify } = require('util') 3 | const importLocal = require('../importLocal') 4 | 5 | const moduleRe = /^~([a-z0-9]|@).+/i 6 | 7 | const getUrlOfPartial = url => { 8 | const parsedUrl = path.parse(url) 9 | return `${parsedUrl.dir}${path.sep}_${parsedUrl.base}` 10 | } 11 | 12 | module.exports = async (code, { filename, indentedSyntax }) => { 13 | const sass = importLocal(path.dirname(filename), 'sass', 'node-sass') 14 | const res = await promisify(sass.render.bind(sass))({ 15 | file: filename, 16 | data: code, 17 | indentedSyntax, 18 | sourceMap: false, 19 | importer: [ 20 | (url, importer, done) => { 21 | if (!moduleRe.test(url)) return done({ file: url }) 22 | 23 | const moduleUrl = url.slice(1) 24 | const partialUrl = getUrlOfPartial(moduleUrl) 25 | 26 | const options = { 27 | basedir: path.dirname(importer), 28 | extensions: ['.scss', '.sass', '.css'] 29 | } 30 | const finishImport = id => { 31 | done({ 32 | // Do not add `.css` extension in order to inline the file 33 | file: id.endsWith('.css') ? id.replace(/\.css$/, '') : id 34 | }) 35 | } 36 | 37 | const next = () => { 38 | // Catch all resolving errors, return the original file and pass responsibility back to other custom importers 39 | done({ file: url }) 40 | } 41 | 42 | const resolvePromise = promisify(require('resolve')) 43 | 44 | // Give precedence to importing a partial 45 | resolvePromise(partialUrl, options) 46 | .then(finishImport) 47 | .catch(err => { 48 | if (err.code === 'MODULE_NOT_FOUND' || err.code === 'ENOENT') { 49 | resolvePromise(moduleUrl, options) 50 | .then(finishImport) 51 | .catch(next) 52 | } else { 53 | next() 54 | } 55 | }) 56 | } 57 | ] 58 | }) 59 | 60 | return res.css.toString() 61 | } 62 | -------------------------------------------------------------------------------- /bin/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const chalk = require('chalk') 3 | 4 | if (parseInt(process.versions.node, 10) < 8) { 5 | console.error( 6 | chalk.red(`The "vue-compile" module requires Node.js 8 or above!`) 7 | ) 8 | console.error(chalk.dim(`Current version: ${process.versions.node}`)) 9 | process.exit(1) 10 | } 11 | 12 | const cac = require('cac') 13 | const pkg = require('../package.json') 14 | 15 | const cli = cac('vue-compile') 16 | 17 | cli 18 | .command('[input]', 'Normalize input file or directory', { 19 | ignoreOptionDefaultValue: true 20 | }) 21 | .usage(`[input] [options]`) 22 | .action((input, flags) => { 23 | const options = Object.assign( 24 | { 25 | input 26 | }, 27 | flags 28 | ) 29 | 30 | if (!options.input) { 31 | delete options.input 32 | } 33 | 34 | if (options.debug === true) { 35 | process.env.DEBUG = 'vue-compile:*' 36 | } else if (typeof options.debug === 'string') { 37 | process.env.DEBUG = `vue-compile:${options.debug}` 38 | } 39 | 40 | const vueCompile = require('../lib')(options) 41 | 42 | if (!vueCompile.options.input) { 43 | return cli.outputHelp() 44 | } 45 | 46 | vueCompile.on('normalized', (input, output) => { 47 | if (!vueCompile.options.debug) { 48 | const { humanlizePath } = require('../lib/utils') 49 | 50 | console.log( 51 | `${chalk.magenta(humanlizePath(input))} ${chalk.dim( 52 | '->' 53 | )} ${chalk.green(humanlizePath(output))}` 54 | ) 55 | } 56 | }) 57 | 58 | return vueCompile.normalize().catch(handleError) 59 | }) 60 | .option('-o, --output ', 'Output path') 61 | .option( 62 | '-i, --include ', 63 | 'A glob pattern to include from input directory' 64 | ) 65 | .option( 66 | '-e, --exclude ', 67 | 'A glob pattern to exclude from input directory' 68 | ) 69 | .option('--no-babelrc', 'Disable .babelrc file') 70 | .option( 71 | '--modern', 72 | 'Only supports browsers that support 69 | 70 | 78 | ``` 79 | 80 | Out: 81 | 82 | ```vue 83 | 88 | 89 | 98 | 99 | 106 | ``` 107 | 108 | 109 | ### Compile Standalone CSS Files 110 | 111 | CSS files like `.css` `.scss` `.sass` `.styl` will be compiled to output directory with `.css` extension, all relevant `import` statements in `.js` `.ts` or `