Skip to content

Commit

Permalink
feat: vite build
Browse files Browse the repository at this point in the history
  • Loading branch information
yyx990803 committed Apr 24, 2020
1 parent 86147d1 commit 0ea7970
Show file tree
Hide file tree
Showing 8 changed files with 1,204 additions and 31 deletions.
39 changes: 22 additions & 17 deletions bin/vite.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
#!/usr/bin/env node
const argv = require('minimist')(process.argv.slice(2))
const server = require('../dist').createServer(argv)

let port = argv.port || 3000
if (argv._[0] === 'build') {
require('../dist').build(argv)
} else {
const server = require('../dist').createServer(argv)

server.on('error', (e) => {
if (e.code === 'EADDRINUSE') {
console.log(`port ${port} is in use, trying another one...`)
setTimeout(() => {
server.close()
server.listen(++port)
}, 100)
} else {
console.error(e)
}
})
let port = argv.port || 3000

server.on('listening', () => {
console.log(`Running at http://localhost:${port}`)
})
server.on('error', (e) => {
if (e.code === 'EADDRINUSE') {
console.log(`port ${port} is in use, trying another one...`)
setTimeout(() => {
server.close()
server.listen(++port)
}, 100)
} else {
console.error(e)
}
})

server.listen(port)
server.on('listening', () => {
console.log(`Running at http://localhost:${port}`)
})

server.listen(port)
}
8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,14 @@
},
"dependencies": {
"@babel/parser": "^7.9.4",
"@rollup/plugin-alias": "^3.1.0",
"@rollup/plugin-node-resolve": "^7.1.3",
"@rollup/plugin-replace": "^2.3.2",
"@types/koa": "^2.11.3",
"@vue/compiler-sfc": "^3.0.0-beta.3",
"chalk": "^4.0.0",
"chokidar": "^3.3.1",
"cssnano": "^4.1.10",
"debug": "^4.1.1",
"es-module-lexer": "^0.3.18",
"hash-sum": "^2.0.0",
Expand All @@ -53,7 +57,11 @@
"lru-cache": "^5.1.1",
"magic-string": "^0.25.7",
"minimist": "^1.2.5",
"postcss": "^7.0.27",
"resolve-from": "^5.0.0",
"rollup": "^2.7.2",
"rollup-plugin-terser": "^5.3.0",
"rollup-plugin-vue": "^6.0.0-alpha.1",
"vue": "^3.0.0-beta.3",
"ws": "^7.2.3"
},
Expand Down
184 changes: 183 additions & 1 deletion src/node/build.ts
Original file line number Diff line number Diff line change
@@ -1 +1,183 @@
export function build() {}
import path from 'path'
import { promises as fs } from 'fs'
import { rollup as Rollup, Plugin } from 'rollup'
import { resolveVue } from './resolveVue'
import { hmrClientPublicPath } from './serverPluginHmr'
import chalk from 'chalk'

export interface BuildOptions {
root?: string
inlineVue?: boolean
}

export async function build({
root = process.cwd(),
inlineVue = resolveVue(root).hasLocalVue
}: BuildOptions = {}) {
const start = Date.now()

// lazy require rollup so that we don't load it when only using the dev server
// importing it just for the types
const rollup = require('rollup').rollup as typeof Rollup

const outDir = path.resolve(root, 'dist')
const indexPath = path.resolve(root, 'index.html')
const scriptRE = /<script\b[^>]*>([\s\S]*?)<\/script>/gm
const indexContent = await fs.readFile(indexPath, 'utf-8')

const cssFilename = 'style.css'

// make sure to use the same verison of vue as the one bundled with
// this version of vite.
const vueVersion = require('vue/package.json').version
const cdnLink = `https://unpkg.com/vue@${vueVersion}/dist/vue.esm-browser.prod.js`

const vitePlugin: Plugin = {
name: 'vite',
resolveId(id: string) {
if (id.startsWith('/')) {
if (id === hmrClientPublicPath) {
return hmrClientPublicPath
} else {
return id.startsWith(root) ? id : path.resolve(root, id.slice(1))
}
} else if (id === 'vue') {
if (inlineVue) {
return resolveVue(root, true).vue
} else {
return {
id: cdnLink,
external: true
}
}
}
},
load(id: string) {
if (id === hmrClientPublicPath) {
return `export function hot() {}`
} else if (id === indexPath) {
let script = ''
let match
while ((match = scriptRE.exec(indexContent))) {
// TODO handle <script type="module" src="..."/>
// just add it as an import
script += match[1]
}
return script
}
}
}

const styles: Map<string, string> = new Map()

const cssExtractPlugin: Plugin = {
name: 'vite-css',
transform(code: string, id: string) {
if (id.endsWith('.css')) {
styles.set(id, code)
return '/* css extracted by vite */'
}
}
}

const bundle = await rollup({
// TODO
// parse index.html
// find entry file or script
// if inline script, create a temp main file next to it before bundling
input: path.resolve(root, 'index.html'),
plugins: [
vitePlugin,
require('rollup-plugin-vue')(),
require('@rollup/plugin-node-resolve')({
rootDir: root
}),
require('@rollup/plugin-replace')({
'process.env.NODE_ENV': '"production"'
}),
cssExtractPlugin,
require('rollup-plugin-terser').terser()
]
})

const { output } = await bundle.generate({
dir: outDir,
format: 'es'
})

await fs.rmdir(outDir, { recursive: true })
await fs.mkdir(outDir)

let generatedIndex = indexContent.replace(scriptRE, '').trim()

// TODO handle public path for injections?
// this would also affect paths in templates and css.

// inject css link
generatedIndex = injectCSS(generatedIndex, cssFilename)
// write css
let css = ''
styles.forEach((s) => {
css += s
})
const cssFilepath = path.join(outDir, cssFilename)
console.log(
`write ${chalk.magenta(path.relative(process.cwd(), cssFilepath))}`
)
await fs.writeFile(
cssFilepath,
// minify with cssnano
(
await require('postcss')([require('cssnano')]).process(css, {
from: undefined
})
).css
)

if (!inlineVue) {
// if not inlining vue, inject cdn link so it can start the fetch early
generatedIndex = injectScript(generatedIndex, cdnLink)
}

// inject chunks
for (const chunk of output) {
if (chunk.type === 'chunk') {
if (chunk.isEntry) {
// inject chunk to html
generatedIndex = injectScript(generatedIndex, chunk.fileName)
}
// write chunk
const filepath = path.join(outDir, chunk.fileName)
console.log(`write ${chalk.cyan(path.relative(process.cwd(), filepath))}`)
await fs.writeFile(filepath, chunk.code)
}
}

// write html
const indexOutPath = path.join(outDir, 'index.html')
console.log(
`write ${chalk.green(path.relative(process.cwd(), indexOutPath))}`
)
await fs.writeFile(indexOutPath, generatedIndex)

console.log(`done in ${((Date.now() - start) / 1000).toFixed(2)}s.`)
}

function injectCSS(html: string, filename: string) {
const tag = `<link rel="stylesheet" href="/${filename}">`
if (/<\/head>/.test(html)) {
return html.replace(/<\/head>/, `${tag}\n</head>`)
} else {
return tag + '\n' + html
}
}

function injectScript(html: string, filename: string) {
filename = /^https?:\/\//.test(filename) ? filename : `/${filename}`
const tag = `<script type="module" src="${filename}"></script>`
if (/<\/body>/.test(html)) {
return html.replace(/<\/body>/, `${tag}\n</body>`)
} else {
return html + '\n' + tag
}
}
1 change: 1 addition & 0 deletions src/node/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './server'
export * from './build'
18 changes: 14 additions & 4 deletions src/node/resolveVue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,27 @@ import sfcCompiler from '@vue/compiler-sfc'

interface ResolvedVuePaths {
vue: string
hasLocalVue: boolean
compiler: string
}

let resolved: ResolvedVuePaths | undefined = undefined

const toBuildPaths = (p: ResolvedVuePaths) => ({
...p,
vue: p.vue.replace('esm-browser', 'esm-bundler')
})

// Resolve the correct `vue` and `@vue.compiler-sfc` to use.
// If the user project has local installations of these, they should be used;
// otherwise, fallback to the dependency of Vite itself.
export function resolveVue(root: string): ResolvedVuePaths {
export function resolveVue(root: string, isBuild = false): ResolvedVuePaths {
if (resolved) {
return resolved
return isBuild ? toBuildPaths(resolved) : resolved
}
let vuePath: string
let compilerPath: string
let hasLocalVue = true
try {
// see if user has local vue installation
const userVuePkg = resolve(root, 'vue/package.json')
Expand Down Expand Up @@ -45,13 +52,16 @@ export function resolveVue(root: string): ResolvedVuePaths {
}
} catch (e) {
// user has no local vue, use vite's dependency version
hasLocalVue = false
vuePath = require.resolve('vue/dist/vue.runtime.esm-browser.js')
compilerPath = require.resolve('@vue/compiler-sfc')
}
return (resolved = {
resolved = {
vue: vuePath,
hasLocalVue,
compiler: compilerPath
})
}
return isBuild ? toBuildPaths(resolved) : resolved
}

export function resolveCompiler(cwd: string): typeof sfcCompiler {
Expand Down
2 changes: 1 addition & 1 deletion src/node/serverPluginHmr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import { importerMap, hmrBoundariesMap } from './serverPluginModules'
const debug = require('debug')('vite:hmr')

// client and node files are placed flat in the dist folder
const hmrClientFilePath = path.resolve(__dirname, './client.js')
export const hmrClientFilePath = path.resolve(__dirname, './client.js')
export const hmrClientPublicPath = '/@hmr'

interface HMRPayload {
Expand Down
1 change: 1 addition & 0 deletions src/node/serverPluginVue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ function compileSFCMain(
code += `\n__script.render = __render`
}
code += `\n__script.__hmrId = ${JSON.stringify(pathname)}`
code += `\n__script.__file = ${JSON.stringify(filename)}`
code += `\nexport default __script`

cached = cached || { styles: [] }
Expand Down
Loading

0 comments on commit 0ea7970

Please sign in to comment.