├── .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 | ``
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 |
84 |
85 | {{ count }}
86 |
87 |
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 `