Skip to content
50 changes: 36 additions & 14 deletions packages/vue-note/src/compilor/component.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import type { RawComponent } from './parse'
import process from 'node:process'
import { compileScript, parse } from 'vue/compiler-sfc'
import { compileScript, compileTemplate, parse } from 'vue/compiler-sfc'
import { getUniqueFilename, getUniqueID } from './utils/id'

export interface CompiledComponent {
code: string
code: {
main: string
template?: string
}
id: string
uniqueId: string
uniqueFilename: string
}

export function parseComponents(filename: string, rawComponents: RawComponent[]): CompiledComponent[] {
export function parseComponents(filename: string, rawComponents: RawComponent[], detached: boolean): CompiledComponent[] {
return rawComponents.map((e) => {
const _vueSFC = generateSFC(e)

Expand All @@ -18,20 +22,38 @@ export function parseComponents(filename: string, rawComponents: RawComponent[])

const { descriptor } = parse(_vueSFC, { ignoreEmpty: false, sourceMap: false, filename: uniqueFilename })

return {
id: e.id,
uniqueFilename,
code: compileScript(descriptor, {
isProd: process.env.NODE_ENV === 'production',
id: uniqueId,
inlineTemplate: true,
sourceMap: false,
templateOptions: {
const script = compileScript(descriptor, {
isProd: process.env.NODE_ENV === 'production',
id: uniqueId,
sourceMap: false,
inlineTemplate: !detached,
templateOptions: !detached
? {
source: descriptor.template!.content,
filename: descriptor.filename,
id: uniqueId,
}
: undefined,
})
const code = {
main: script.content,
template: detached
? compileTemplate({
source: descriptor.template!.content,
filename: descriptor.filename,
id: uniqueId,
},
}).content,
compilerOptions: {
bindingMetadata: script.bindings, // Bind script
},
}).code
: undefined,
}

return {
id: e.id,
uniqueId,
uniqueFilename,
code,
}
})
}
Expand Down
3 changes: 2 additions & 1 deletion packages/vue-note/src/compilor/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,11 @@ export function getRawComponents(astResult: ParseResult, ctx: Rollup.TransformPl
template = (_templateExpression as ExpressionStatement).expression as CallExpression
}

const id = getID(rawComponents.length)
rawComponents.push({
script: `import {${variablesScopeTracker.getVariablesInScope().join(',')}} from 'vue-note';${print(component).code.slice(1, -1)}`,
template: template ? getTemplate(template.callee.end, template.end, astResult.comments) : '',
id: getID(node),
id,
})
}
else {
Expand Down
37 changes: 29 additions & 8 deletions packages/vue-note/src/compilor/resolve.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,52 @@
import type { ExportDefaultDeclarationKind, ImportDeclaration, ImportDefaultSpecifier, ImportNamespaceSpecifier, ImportSpecifier, Program } from 'oxc-parser'
import type { Expression, ImportDeclaration, ImportDefaultSpecifier, ImportNamespaceSpecifier, ImportSpecifier, Program, Statement } from 'oxc-parser'
import type { Rollup } from 'vite'
import type { CompiledComponent } from './component'
import type { CacheHash } from './utils/hmr'
import { parseSync, Visitor } from 'oxc-parser'
import { walk } from 'oxc-walker'
import { print } from 'recast'
import { getComponentHmrCode } from './utils/hmr'
import { getID } from './utils/id'
import { wrapperComponent } from './utils/wrapper'

export function resolve(program: Program, compiledComponents: CompiledComponent[], ctx: Rollup.TransformPluginContext): string {
export function resolve(program: Program, compiledComponents: CompiledComponent[], ctx: Rollup.TransformPluginContext, hmrCache: [CacheHash | undefined, CacheHash] | false): string {
const imports: ImportDeclaration[] = []

let index = 0
walk(program, {
enter(node) {
// replace defineCommentComponent to compiled component
if (node.type === 'CallExpression' && node.callee.type === 'Identifier' && node.callee.name === 'defineCommentComponent') {
const _script = compiledComponents.find(e => e.id === getID(node))!
const component = compiledComponents.find(e => e.id === getID(index))!

let script: ExportDefaultDeclarationKind | undefined
let script: Expression | undefined
let template: Statement | undefined

new Visitor({
ExportDefaultDeclaration(decl) {
script = decl.declaration
script = decl.declaration as Expression
},
ImportDeclaration(decl) {
// collect imports
imports.push(decl)
},
}).visit(parseSync('foo.ts', _script.code).program)
}).visit(parseSync('foo.ts', component.code.main).program)

this.replace(script!)
if (component.code.template) {
new Visitor({
FunctionDeclaration(decl) {
template = decl
},
ImportDeclaration(decl) {
// collect imports
imports.push(decl)
},
}).visit(parseSync('foo.ts', component.code.template).program)
}

this.replace(wrapperComponent(script!, hmrCache ? getComponentHmrCode(component.uniqueId, hmrCache) : [], template))

index++
}

// remove macro imports
Expand All @@ -44,7 +63,9 @@ export function resolve(program: Program, compiledComponents: CompiledComponent[

program.body.unshift(...dedupeImports(imports, ctx))

return print(program).code
return `const __componentsMap = new Map();
${print(program).code}
export { __componentsMap }`
}

function dedupeImports(rawImports: ImportDeclaration[], ctx: Rollup.TransformPluginContext): ImportDeclaration[] {
Expand Down
35 changes: 35 additions & 0 deletions packages/vue-note/src/compilor/utils/hmr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
export interface CacheHash {
ast: string
template: Map<string, string>
}

export function getComponentHmrCode(uniqueId: string, cache: [CacheHash | undefined, CacheHash]): string {
// const range = { start: 0, end: 0 }
const _templateChanged = cache[1].template.get(uniqueId) !== cache[0]?.template.get(uniqueId)
const _scriptChanged = cache[0]?.ast !== cache[1].ast

// return ''
return `
_component.__hmrId = '${uniqueId}';
_component._templateChanged = ${_templateChanged};
_component._scriptChanged = ${_scriptChanged};
typeof __VUE_HMR_RUNTIME__ !== "undefined" && __VUE_HMR_RUNTIME__.createRecord(_component.__hmrId, _component);
__componentsMap.set(_component.__hmrId, _component)
if (import.meta.hot) {
import.meta.hot.accept((mod) => {
if(!mod) {
return
}
const __component = mod.__componentsMap.get(_component.__hmrId)
if(!__component) {
return
}
if (__component._scriptChanged) {
window.location.reload();
} else if(__component._templateChanged) {
__VUE_HMR_RUNTIME__.rerender(__component.__hmrId, __component.render);
}
})
}
`
}
8 changes: 4 additions & 4 deletions packages/vue-note/src/compilor/utils/id.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { Node } from 'oxc-parser'
import { createHash } from 'node:crypto'

export function getID(node: Node): string {
return `${node.start}-${node.end}`
export function getID(componentIndex: number): string {
return createHash('md5').update(`${componentIndex}`).digest('hex').slice(0, 8)
}

export function getUniqueID(filename: string, id: string): string {
return `${filename}_${id}`
return createHash('md5').update(`${filename}_${id}`).digest('hex').slice(0, 8)
}

export function getUniqueFilename(filename: string, id: string): string {
Expand Down
128 changes: 128 additions & 0 deletions packages/vue-note/src/compilor/utils/wrapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import type { Directive, Expression, Statement } from 'oxc-parser'
import { parseSync } from 'oxc-parser'

export function wrapperComponent(definention: Expression, injectedCode: (Directive | Statement)[] | string, template?: Statement): Expression {
const range = { start: 0, end: 0 }

let inject: (Directive | Statement)[]
if (typeof injectedCode === 'string') {
const { program } = parseSync('foo.ts', injectedCode)
inject = program.body
}
else {
inject = injectedCode
}

return {
type: 'CallExpression',
...range,
typeArguments: null,
arguments: [],
optional: false,
callee: {
type: 'ParenthesizedExpression',
...range,
expression: {
type: 'ArrowFunctionExpression',
...range,
body: {
type: 'BlockStatement',
...range,
body: [
{
type: 'VariableDeclaration',
...range,
declarations: [
{
type: 'VariableDeclarator',
...range,
id: {
type: 'Identifier',
...range,
decorators: [],
name: '_component',
optional: false,
typeAnnotation: null,
},
init: definention,
definite: false,
},
],
kind: 'const',
declare: false,
},
...getTemplatInject(template, range),
...inject,
{
type: 'ReturnStatement',
...range,
argument: {
type: 'Identifier',
...range,
decorators: [],
name: '_component',
optional: false,
typeAnnotation: null,
},
},
],
},
expression: false,
async: false,
typeParameters: null,
params: [],
returnType: null,
id: null,
generator: false,
},
},
}
}

function getTemplatInject(template: Statement | undefined, range = { start: 0, end: 0 }): Statement[] {
return (template
? [
template,
{
type: 'ExpressionStatement',
...range,
expression: {
type: 'AssignmentExpression',
...range,
operator: '=',
left: {
type: 'MemberExpression',
...range,
object: {
type: 'Identifier',
...range,
decorators: [],
name: '_component',
optional: false,
typeAnnotation: null,
},
property: {
type: 'Identifier',
...range,
decorators: [],
name: 'render',
optional: false,
typeAnnotation: null,
},
optional: false,
computed: false,
},
right: {
type: 'Identifier',
...range,
decorators: [],
name: 'render',
optional: false,
typeAnnotation: null,
},
},
directive: null,
},
]
: [])
}
25 changes: 24 additions & 1 deletion packages/vue-note/src/vite/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import type { PluginOption } from 'vite'
import type { CacheHash } from '../compilor/utils/hmr'
import type { TransformOption } from './transform'
import { parseQuery } from './query'
import { transform } from './transform'

export function VueNote(): PluginOption {
const transformOption: Partial<TransformOption> = {}

let lastTransformCache: CacheHash | undefined

return {
name: 'vue-note',
async transform(src, id, opt) {
Expand All @@ -12,7 +18,24 @@ export function VueNote(): PluginOption {
if (!filename.endsWith('.ts'))
return

return transform(src, filename, this, query, ssr)
const { result, cache } = await transform(
src,
filename,
this,
query,
ssr,
transformOption as TransformOption,
lastTransformCache,
)
if (cache)
lastTransformCache = cache
return result
},
configureServer(server) {
transformOption.server = server
},
configResolved(config) {
transformOption.isProduction = config.isProduction
},
config(config) { // disable esbuild (oxc) to process typescript
if (!config.esbuild) {
Expand Down
Loading