Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
wip: port rollup-plugin-vue to vite plugin
  • Loading branch information
yyx990803 committed Dec 17, 2020
1 parent 9eef197 commit bb0c105
Show file tree
Hide file tree
Showing 13 changed files with 1,027 additions and 3 deletions.
2 changes: 1 addition & 1 deletion LICENSE
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2019-present, Yuxi (Evan) You
Copyright (c) 2019-present, Yuxi (Evan) You and Vite contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
3 changes: 2 additions & 1 deletion package.json
Expand Up @@ -5,7 +5,8 @@
"packages/*"
],
"scripts": {
"lint": "eslint --ext .ts packages/*/src/**"
"lint": "eslint --ext .ts packages/*/src/**",
"bundle": "esbuild packages/vite/src/node/index.ts packages/vite/src/node/cli.ts --bundle --platform=node --target=node12 --external:fsevents --external:sugarss --external:bufferutil --external:utf-8-validate --outdir=esbuild"
},
"devDependencies": {
"@types/node": "^14.14.10",
Expand Down
34 changes: 33 additions & 1 deletion packages/plugin-vue/package.json
@@ -1,4 +1,36 @@
{
"name": "@vitejs/plugin-vue",
"version": "1.0.0"
"version": "1.0.0",
"license": "MIT",
"files": [
"dist"
],
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"dev": "tsc -w --incremental -p ."
},
"engines": {
"node": ">=12.0.0"
},
"repository": {
"type": "git",
"url": "git+https://github.com/vitejs/vite.git"
},
"bugs": {
"url": "https://github.com/vitejs/vite/issues"
},
"homepage": "https://github.com/vitejs/vite/tree/master/#readme",
"peerDependencies": {
"@vue/compiler-sfc": "^3.0.4"
},
"devDependencies": {
"@rollup/pluginutils": "^4.1.0",
"@types/hash-sum": "^1.0.0",
"@vue/compiler-sfc": "^3.0.4",
"debug": "^4.3.1",
"hash-sum": "^2.0.0",
"rollup": "^2.35.1",
"source-map": "^0.6.1"
}
}
194 changes: 194 additions & 0 deletions packages/plugin-vue/src/handleHotUpdate.ts
@@ -0,0 +1,194 @@
import fs from 'fs'
import _debug from 'debug'
import { parse, SFCBlock, SFCDescriptor } from '@vue/compiler-sfc'
import {
getDescriptor,
setDescriptor,
setPrevDescriptor
} from './utils/descriptorCache'
import { getResolvedScript, setResolvedScript } from './script'
import { ModuleNode } from 'vite'

const debug = _debug('vite:hmr')

/**
* Vite-specific HMR handling
*/
export async function handleHotUpdate(
file: string,
modules: ModuleNode[]
): Promise<ModuleNode[] | void> {
if (!file.endsWith('.vue')) {
return
}

const prevDescriptor = getDescriptor(file)
if (!prevDescriptor) {
// file hasn't been requested yet (e.g. async component)
return
}

let content = fs.readFileSync(file, 'utf-8')
if (!content) {
await untilModified(file)
content = fs.readFileSync(file, 'utf-8')
}

const { descriptor } = parse(content, {
filename: file,
sourceMap: true
})
setDescriptor(file, descriptor)
setPrevDescriptor(file, prevDescriptor)

let needRerender = false
const affectedModules = new Set<ModuleNode | undefined>()
const mainModule = modules.find(
(m) => !/type=/.test(m.url) || /type=script/.test(m.url)
)
const templateModule = modules.find((m) => /type=template/.test(m.url))

if (
!isEqualBlock(descriptor.script, prevDescriptor.script) ||
!isEqualBlock(descriptor.scriptSetup, prevDescriptor.scriptSetup)
) {
affectedModules.add(mainModule)
}

if (!isEqualBlock(descriptor.template, prevDescriptor.template)) {
// when a <script setup> component's template changes, it will need correct
// binding metadata. However, when reloading the template alone the binding
// metadata will not be available since the script part isn't loaded.
// in this case, reuse the compiled script from previous descriptor.
if (mainModule && !affectedModules.has(mainModule)) {
setResolvedScript(descriptor, getResolvedScript(prevDescriptor)!)
}
affectedModules.add(templateModule)
needRerender = true
}

let didUpdateStyle = false
const prevStyles = prevDescriptor.styles || []
const nextStyles = descriptor.styles || []

// force reload if CSS vars injection changed
if (prevDescriptor.cssVars.join('') !== descriptor.cssVars.join('')) {
affectedModules.add(mainModule)
}

// force reload if scoped status has changed
if (prevStyles.some((s) => s.scoped) !== nextStyles.some((s) => s.scoped)) {
// template needs to be invalidated as well
affectedModules.add(templateModule)
affectedModules.add(mainModule)
}

// only need to update styles if not reloading, since reload forces
// style updates as well.
for (let i = 0; i < nextStyles.length; i++) {
const prev = prevStyles[i]
const next = nextStyles[i]
if (!prev || !isEqualBlock(prev, next)) {
didUpdateStyle = true
const mod = modules.find((m) => m.url.includes(`type=style&index=${i}`))
if (mod) {
affectedModules.add(mod)
} else {
// new style block - force reload
affectedModules.add(mainModule)
}
}
}
if (prevStyles.length > nextStyles.length) {
// style block removed - force reload
affectedModules.add(mainModule)
}

const prevCustoms = prevDescriptor.customBlocks || []
const nextCustoms = descriptor.customBlocks || []

// custom blocks update causes a reload
// because the custom block contents is changed and it may be used in JS.
for (let i = 0; i < nextCustoms.length; i++) {
const prev = prevCustoms[i]
const next = nextCustoms[i]
if (!prev || !isEqualBlock(prev, next)) {
const mod = modules.find((m) =>
m.url.includes(`type=${prev.type}&index=${i}`)
)
if (mod) {
affectedModules.add(mod)
} else {
affectedModules.add(mainModule)
}
}
}
if (prevCustoms.length > nextCustoms.length) {
// block rmeoved, force reload
affectedModules.add(mainModule)
}

let updateType = []
if (needRerender) {
updateType.push(`template`)
// template is inlined into main, add main module instead
if (!templateModule) {
affectedModules.add(mainModule)
}
}
if (didUpdateStyle) {
updateType.push(`style`)
}
if (updateType.length) {
debug(`[vue:update(${updateType.join('&')})] ${file}`)
}
return [...affectedModules].filter(Boolean) as ModuleNode[]
}

// vitejs/vite#610 when hot-reloading Vue files, we read immediately on file
// change event and sometimes this can be too early and get an empty buffer.
// Poll until the file's modified time has changed before reading again.
async function untilModified(file: string) {
const mtime = fs.statSync(file).mtimeMs
return new Promise((r) => {
let n = 0
const poll = async () => {
n++
const newMtime = fs.statSync(file).mtimeMs
if (newMtime !== mtime || n > 10) {
r(0)
} else {
setTimeout(poll, 10)
}
}
setTimeout(poll, 10)
})
}

function isEqualBlock(a: SFCBlock | null, b: SFCBlock | null) {
if (!a && !b) return true
if (!a || !b) return false
// src imports will trigger their own updates
if (a.src && b.src && a.src === b.src) return true
if (a.content !== b.content) return false
const keysA = Object.keys(a.attrs)
const keysB = Object.keys(b.attrs)
if (keysA.length !== keysB.length) {
return false
}
return keysA.every((key) => a.attrs[key] === b.attrs[key])
}

export function isOnlyTemplateChanged(
prev: SFCDescriptor,
next: SFCDescriptor
) {
return (
isEqualBlock(prev.script, next.script) &&
isEqualBlock(prev.scriptSetup, next.scriptSetup) &&
prev.styles.length === next.styles.length &&
prev.styles.every((s, i) => isEqualBlock(s, next.styles[i])) &&
prev.customBlocks.length === next.customBlocks.length &&
prev.customBlocks.every((s, i) => isEqualBlock(s, next.customBlocks[i]))
)
}

0 comments on commit bb0c105

Please sign in to comment.