From 664b2d97b82287950f2b413beb0cd5e56d678d5b Mon Sep 17 00:00:00 2001 From: webfansplz <308241863@qq.com> Date: Wed, 19 Jul 2023 00:01:48 +0800 Subject: [PATCH 01/33] feat: init --- packages/client/App.vue | 4 +- packages/client/logic/components/data.ts | 6 +-- packages/client/logic/components/index.ts | 2 +- packages/client/logic/format-state.ts | 3 -- packages/client/logic/hook.ts | 49 +++++++++++++++-------- packages/core/src/hook.ts | 2 + packages/playground/src/App.vue | 28 ++++++++++++- 7 files changed, 68 insertions(+), 26 deletions(-) diff --git a/packages/client/App.vue b/packages/client/App.vue index d042c223..86eac81f 100644 --- a/packages/client/App.vue +++ b/packages/client/App.vue @@ -17,12 +17,12 @@ hookApi.hook.on('init:vue:app', () => { // mark client as loaded client.value.markClientLoaded() // listen hook - hookApi.produce() + hookApi.subscribe() // perf timeline // close perf timeline to avoid performance issue (#9) // initPerfTimeline(categorizedHookBuffer.perf) // consume hook buffer - hookApi.consume(categorizedHookBuffer.component ?? []) + hookApi.publish(categorizedHookBuffer.component ?? []) // init routes initRoutes(categorizedHookBuffer.router ?? []) // init pinia diff --git a/packages/client/logic/components/data.ts b/packages/client/logic/components/data.ts index e16590ac..11fe1ddb 100644 --- a/packages/client/logic/components/data.ts +++ b/packages/client/logic/components/data.ts @@ -167,7 +167,7 @@ function processState(instance: any): any { })) } -function processSetupState(instance: any) { +export function processSetupState(instance: any) { const raw = instance.devtoolsRawSetupState || {} return Object.keys(instance.setupState) .filter(key => !vueBuiltins.includes(key) && key.split(/(?=[A-Z])/)[0] !== 'use') @@ -226,14 +226,14 @@ function isReadOnly(raw: any): boolean { return !!raw.__v_isReadonly } -function toRaw(value: any) { +export function toRaw(value: any) { if (value?.__v_raw) return value.__v_raw return value } -function getSetupStateInfo(raw: any) { +export function getSetupStateInfo(raw: any) { return { ref: isRef(raw), computed: isComputed(raw), diff --git a/packages/client/logic/components/index.ts b/packages/client/logic/components/index.ts index 7b8857ba..0a72a3a9 100644 --- a/packages/client/logic/components/index.ts +++ b/packages/client/logic/components/index.ts @@ -1,4 +1,4 @@ export { ComponentWalker, InstanceMap } from './tree' -export { getInstanceState, getInstanceDetails } from './data' +export { getInstanceState, processSetupState, getInstanceDetails, getSetupStateInfo } from './data' export { getInstanceOrVnodeRect, getRootElementsFromComponentInstance } from './el' export { getInstanceName } from './util' diff --git a/packages/client/logic/format-state.ts b/packages/client/logic/format-state.ts index aba6efa1..f8da29e3 100644 --- a/packages/client/logic/format-state.ts +++ b/packages/client/logic/format-state.ts @@ -27,9 +27,6 @@ export interface StateType { rawDisplay?: string recursive: boolean } -// function isReactive(raw: any): boolean { -// return !!raw.__ob__ -// } export function formatStateType(value: unknown): StateType { // Vue diff --git a/packages/client/logic/hook.ts b/packages/client/logic/hook.ts index fa9372d2..43bb638c 100644 --- a/packages/client/logic/hook.ts +++ b/packages/client/logic/hook.ts @@ -1,14 +1,11 @@ +import { DevToolsHooks } from '@vite-plugin-vue-devtools/core' +import type { DebuggerEvent } from 'vue' import { updatePinia } from './pinia' import { instance, updateApp, app as vueApp } from './app' import { useDevToolsClient } from './client' +import { getSetupStateInfo, toRaw } from '~/logic/components/data' -enum DevtoolsHooks { - APP_INIT = 'app:init', - COMPONENT_UPDATED = 'component:updated', - COMPONENT_ADDED = 'component:added', - COMPONENT_REMOVED = 'component:removed', - COMPONENT_EMIT = 'component:emit', -} +type ComponentInstance = any // @TODO function hideInDevtools(component) { return component?.root?.type?.devtools?.hide @@ -16,10 +13,10 @@ function hideInDevtools(component) { const client = useDevToolsClient() -function produceHook() { +function subscribeHook() { const client = useDevToolsClient() const hook = client.value.hook - hook.on(DevtoolsHooks.APP_INIT, (app) => { + hook.on(DevToolsHooks.APP_INIT, (app) => { if (app?._vueDevtools_hidden_) return vueApp.value = app @@ -30,7 +27,27 @@ function produceHook() { return (!app || (typeof uid !== 'number' && !uid) || !component || hideInDevtools(component)) } - hook.on(DevtoolsHooks.COMPONENT_UPDATED, (app, uid, parentUid, component) => { + hook.on(DevToolsHooks.RENDER_TRACKED, (e: DebuggerEvent, instance: ComponentInstance) => { + // console.log(processSetupState(instance)) + // console.log(getSetupStateInfo(e.target)) + // console.log('track', e, instance.setupState, instance.devtoolsRawSetupState) + }) + + hook.on(DevToolsHooks.RENDER_TRIGGERED, (e: DebuggerEvent, instance: ComponentInstance) => { + // data type + const info = getSetupStateInfo(e.target) + const dataType = info.computed ? 'Computed' : info.ref ? 'Ref' : info.reactive ? 'Reactive' : null + // key + const index = Object.values(instance.devtoolsRawSetupState).map(i => toRaw(i)).indexOf(e.target) + const key = Object.keys(instance.devtoolsRawSetupState)[index] + // value + const value = !dataType || info.reactive ? e.target[e.key] : e.target.value + // update type + console.log('xxx', key, dataType, value, e.type) + // console.log('trigger', e, instance.setupState, instance.devtoolsRawSetupState) + }) + + hook.on(DevToolsHooks.COMPONENT_UPDATED, (app, uid, parentUid, component) => { updatePinia(component) if (skipCollect(app, uid, component)) @@ -39,7 +56,7 @@ function produceHook() { updateApp(app, component) }) - hook.on(DevtoolsHooks.COMPONENT_ADDED, (app, uid, parentUid, component) => { + hook.on(DevToolsHooks.COMPONENT_ADDED, (app, uid, parentUid, component) => { updatePinia(component) if (skipCollect(app, uid, component)) @@ -49,7 +66,7 @@ function produceHook() { updateApp(app, component) }) - hook.on(DevtoolsHooks.COMPONENT_REMOVED, (app, uid, parentUid, component) => { + hook.on(DevToolsHooks.COMPONENT_REMOVED, (app, uid, parentUid, component) => { updatePinia(component) if (skipCollect(app, uid, component)) @@ -60,7 +77,7 @@ function produceHook() { updateApp(app, component) }) - hook.on(DevtoolsHooks.COMPONENT_EMIT, (app, uid, parentUid, component) => { + hook.on(DevToolsHooks.COMPONENT_EMIT, (app, uid, parentUid, component) => { updatePinia(component) if (skipCollect(app, uid, component)) @@ -72,7 +89,7 @@ function produceHook() { }) } -function ConsumeHook(buffer: [string, Record][]) { +function publishHook(buffer: [string, Record][]) { buffer.forEach(([_, { app, component }]) => { updatePinia(component) updateApp(app, component) @@ -81,6 +98,6 @@ function ConsumeHook(buffer: [string, Record][]) { export const hookApi = { hook: client.value.hook, - produce: produceHook, - consume: ConsumeHook, + subscribe: subscribeHook, + publish: publishHook, } diff --git a/packages/core/src/hook.ts b/packages/core/src/hook.ts index 6d8cf78c..63b6f8eb 100644 --- a/packages/core/src/hook.ts +++ b/packages/core/src/hook.ts @@ -9,6 +9,8 @@ export enum DevToolsHooks { PERFORMANCE_END = 'perf:end', ADD_ROUTE = 'router:add-route', REMOVE_ROUTE = 'router:remove-route', + RENDER_TRACKED = 'render:tracked', + RENDER_TRIGGERED = 'render:triggered', } export const devtoosHook = typeof window !== 'undefined' diff --git a/packages/playground/src/App.vue b/packages/playground/src/App.vue index 0ea4edb3..1bf8c374 100644 --- a/packages/playground/src/App.vue +++ b/packages/playground/src/App.vue @@ -1,7 +1,30 @@ From b4ff7e041ba9875b8b01f76b284235b1d16518c5 Mon Sep 17 00:00:00 2001 From: webfansplz <308241863@qq.com> Date: Wed, 19 Jul 2023 10:24:36 +0800 Subject: [PATCH 02/33] chore: update --- packages/core/src/hook.ts | 7 ++++--- packages/core/src/host.ts | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/core/src/hook.ts b/packages/core/src/hook.ts index 63b6f8eb..fd255dd0 100644 --- a/packages/core/src/hook.ts +++ b/packages/core/src/hook.ts @@ -13,8 +13,8 @@ export enum DevToolsHooks { RENDER_TRIGGERED = 'render:triggered', } -export const devtoosHook = typeof window !== 'undefined' - ? window.__VUE_DEVTOOLS_GLOBAL_HOOK__ ??= { +export function createDevToolsHook() { + window.__VUE_DEVTOOLS_GLOBAL_HOOK__ ??= { events: new Map void>(), on(event: DevToolsHooks, fn: () => void) { if (!this.events.has(event)) @@ -27,7 +27,8 @@ export const devtoosHook = typeof window !== 'undefined' this.events.get(event).forEach(fn => fn(...payload)) }, } - : {} + return window.__VUE_DEVTOOLS_GLOBAL_HOOK__ +} export function collectDevToolsHookBuffer() { const hookBuffer: [string, Record][] = [] diff --git a/packages/core/src/host.ts b/packages/core/src/host.ts index 5ed61636..65c3e985 100644 --- a/packages/core/src/host.ts +++ b/packages/core/src/host.ts @@ -1,6 +1,6 @@ import { createApp, h } from 'vue' import type { Component } from 'vue' -import { devtoosHook } from './hook' +import { createDevToolsHook } from './hook' export function createDevToolsContainer(App: Component) { const CONTAINER_ID = '__vue-devtools-container__' @@ -9,7 +9,7 @@ export function createDevToolsContainer(App: Component) { el.setAttribute('data-v-inspector-ignore', 'true') document.getElementsByTagName('body')[0].appendChild(el) createApp({ - render: () => h(App, { hook: devtoosHook }), + render: () => h(App, { hook: createDevToolsHook() }), devtools: { hide: true, }, From a8e2dcc22e1063ba8e83c7dce391480bb85e1ebd Mon Sep 17 00:00:00 2001 From: webfansplz <308241863@qq.com> Date: Wed, 19 Jul 2023 20:11:30 +0800 Subject: [PATCH 03/33] feat: rerender trace page --- packages/client/logic/hook.ts | 24 --- packages/client/pages/rerender-trace.vue | 182 +++++++++++++++++++++++ packages/client/store/tab.ts | 6 + packages/client/uno.config.ts | 1 + packages/playground/src/App.vue | 3 + 5 files changed, 192 insertions(+), 24 deletions(-) create mode 100644 packages/client/pages/rerender-trace.vue diff --git a/packages/client/logic/hook.ts b/packages/client/logic/hook.ts index 43bb638c..a6b6e735 100644 --- a/packages/client/logic/hook.ts +++ b/packages/client/logic/hook.ts @@ -1,11 +1,7 @@ import { DevToolsHooks } from '@vite-plugin-vue-devtools/core' -import type { DebuggerEvent } from 'vue' import { updatePinia } from './pinia' import { instance, updateApp, app as vueApp } from './app' import { useDevToolsClient } from './client' -import { getSetupStateInfo, toRaw } from '~/logic/components/data' - -type ComponentInstance = any // @TODO function hideInDevtools(component) { return component?.root?.type?.devtools?.hide @@ -27,26 +23,6 @@ function subscribeHook() { return (!app || (typeof uid !== 'number' && !uid) || !component || hideInDevtools(component)) } - hook.on(DevToolsHooks.RENDER_TRACKED, (e: DebuggerEvent, instance: ComponentInstance) => { - // console.log(processSetupState(instance)) - // console.log(getSetupStateInfo(e.target)) - // console.log('track', e, instance.setupState, instance.devtoolsRawSetupState) - }) - - hook.on(DevToolsHooks.RENDER_TRIGGERED, (e: DebuggerEvent, instance: ComponentInstance) => { - // data type - const info = getSetupStateInfo(e.target) - const dataType = info.computed ? 'Computed' : info.ref ? 'Ref' : info.reactive ? 'Reactive' : null - // key - const index = Object.values(instance.devtoolsRawSetupState).map(i => toRaw(i)).indexOf(e.target) - const key = Object.keys(instance.devtoolsRawSetupState)[index] - // value - const value = !dataType || info.reactive ? e.target[e.key] : e.target.value - // update type - console.log('xxx', key, dataType, value, e.type) - // console.log('trigger', e, instance.setupState, instance.devtoolsRawSetupState) - }) - hook.on(DevToolsHooks.COMPONENT_UPDATED, (app, uid, parentUid, component) => { updatePinia(component) diff --git a/packages/client/pages/rerender-trace.vue b/packages/client/pages/rerender-trace.vue new file mode 100644 index 00000000..4bcea62e --- /dev/null +++ b/packages/client/pages/rerender-trace.vue @@ -0,0 +1,182 @@ + + + + + diff --git a/packages/client/store/tab.ts b/packages/client/store/tab.ts index 54e92a3d..671fd780 100644 --- a/packages/client/store/tab.ts +++ b/packages/client/store/tab.ts @@ -21,6 +21,12 @@ export const builtinTabs: BuiltinTab[] = [ icon: 'i-carbon-assembly-cluster', group: 'app', }, + { + path: 'rerender-trace', + title: 'Rerender trace', + icon: 'i-ic:outline-track-changes', + group: 'app', + }, { path: 'assets', title: 'Assets', diff --git a/packages/client/uno.config.ts b/packages/client/uno.config.ts index eea43825..4c2829e9 100644 --- a/packages/client/uno.config.ts +++ b/packages/client/uno.config.ts @@ -84,5 +84,6 @@ export default defineConfig({ 'i-mdi:eyedropper', 'i-carbon-close-outline', 'i-carbon:checkmark', + 'i-ic:outline-track-changes', ], }) diff --git a/packages/playground/src/App.vue b/packages/playground/src/App.vue index 1bf8c374..888b9209 100644 --- a/packages/playground/src/App.vue +++ b/packages/playground/src/App.vue @@ -11,6 +11,9 @@ const p = reactive({ age: 18, }) +// setInterval(() => { +// count.value++ +// }, 1000) // watch(count, () => { // p.age = count.value // }) From 2a1b89a76b0de1941912c99223b5e498d9b8ad22 Mon Sep 17 00:00:00 2001 From: alexzhang1030 <1642114555@qq.com> Date: Thu, 20 Jul 2023 12:25:08 +0800 Subject: [PATCH 04/33] feat(compiler): init --- .vscode/settings.json | 3 +- package.json | 4 +- packages/core/build.config.ts | 6 +- packages/core/package.json | 6 + .../core/src/compiler/__test__/common.test.ts | 28 ++ .../core/src/compiler/__test__/lang.test.ts | 30 ++ packages/core/src/compiler/common.ts | 67 ++++ packages/core/src/compiler/index.ts | 1 + packages/core/src/compiler/lang.ts | 7 + packages/core/src/compiler/trace-rerender.ts | 22 + packages/core/src/index.ts | 1 + packages/node/README.md | 8 +- packages/node/src/vite.ts | 36 +- pnpm-lock.yaml | 376 +++++++++++++++--- tsconfig.json | 3 +- vitest.config.ts | 7 + 16 files changed, 546 insertions(+), 59 deletions(-) create mode 100644 packages/core/src/compiler/__test__/common.test.ts create mode 100644 packages/core/src/compiler/__test__/lang.test.ts create mode 100644 packages/core/src/compiler/common.ts create mode 100644 packages/core/src/compiler/index.ts create mode 100644 packages/core/src/compiler/lang.ts create mode 100644 packages/core/src/compiler/trace-rerender.ts create mode 100644 vitest.config.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 22614a38..50d6ce2b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,5 +4,6 @@ "source.fixAll.eslint": true }, "editor.formatOnSave": false, - "typescript.tsdk": "node_modules/typescript/lib" + "typescript.tsdk": "node_modules/typescript/lib", + "references.preferredLocation": "peek" } diff --git a/package.json b/package.json index 8e4c89aa..70739bac 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,8 @@ "prepublishOnly": "npm run build", "release": "bumpp -r", "dep:up": "taze -I major", - "prepare": "simple-git-hooks" + "prepare": "simple-git-hooks", + "test": "vitest" }, "peerDependencies": { "vite": "^3.1.0 || ^4.0.0-0" @@ -76,6 +77,7 @@ "typescript": "^5.1.6", "unbuild": "^1.2.1", "vite": "^4.4.4", + "vitest": "^0.33.0", "vue": "^3.3.4" }, "simple-git-hooks": { diff --git a/packages/core/build.config.ts b/packages/core/build.config.ts index be31769f..22d4639a 100644 --- a/packages/core/build.config.ts +++ b/packages/core/build.config.ts @@ -5,11 +5,15 @@ export default defineBuildConfig({ 'src/index', ], externals: [ - // 'vue', + 'ast-kit', + '@vue/compiler-sfc', + '@babel/parser', + '@babel/type', ], declaration: true, rollup: { emitCJS: true, + cjsBridge: true, }, }) diff --git a/packages/core/package.json b/packages/core/package.json index 8b5e7505..7228a484 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -33,7 +33,13 @@ "vite": "^3.1.0 || ^4.0.0-0" }, "dependencies": { + "@babel/parser": "^7.22.7", "birpc": "^0.2.12", + "estree-walker": "^3.0.3", "vite-hot-client": "^0.2.1" + }, + "devDependencies": { + "@babel/types": "^7.22.5", + "@vue/compiler-sfc": "^3.3.4" } } diff --git a/packages/core/src/compiler/__test__/common.test.ts b/packages/core/src/compiler/__test__/common.test.ts new file mode 100644 index 00000000..c787f952 --- /dev/null +++ b/packages/core/src/compiler/__test__/common.test.ts @@ -0,0 +1,28 @@ +import { getBabelParsePlugins, parseSFC } from '../common' + +describe('compiler:common', () => { + test.each([ + ['ts', ['typescript']], + ['tsx', ['typescript', 'jsx']], + ['js', []], + ['jsx', ['jsx']], + ])('getBabelParsePlugins(%s) === %s', (lang, expected) => { + expect(getBabelParsePlugins(lang)).toEqual(expected) + }) +}) + +describe('compiler:common:parseSFC', () => { + test('should parse + + + ` + expect(() => { + parseSFC(code, 'test.vue') + }).not.toThrow() + }) +}) diff --git a/packages/core/src/compiler/__test__/lang.test.ts b/packages/core/src/compiler/__test__/lang.test.ts new file mode 100644 index 00000000..174df968 --- /dev/null +++ b/packages/core/src/compiler/__test__/lang.test.ts @@ -0,0 +1,30 @@ +import { isJSX, isTS, isVUE } from '../lang' + +describe('compiler:lang', () => { + test.each([ + ['ts', true], + ['tsx', true], + ['js', false], + ['jsx', false], + ['vue', false], + ])('isTS(%s) === %s', (lang, expected) => { + expect(isTS(lang)).toBe(expected) + }) + test.each([ + ['ts', false], + ['tsx', true], + ['js', false], + ['jsx', true], + ['vue', false], + ])('isJSX(%s) === %s', (lang, expected) => { + expect(isJSX(lang)).toBe(expected) + }) + test.each([ + ['a.vue', true], + ['vue', true], + ['avue', false], + ['a.vue.js', false], + ])('isVUE(%s) === %s', (filename, expected) => { + expect(isVUE(filename)).toBe(expected) + }) +}) diff --git a/packages/core/src/compiler/common.ts b/packages/core/src/compiler/common.ts new file mode 100644 index 00000000..76d88bf6 --- /dev/null +++ b/packages/core/src/compiler/common.ts @@ -0,0 +1,67 @@ +import { parse as sfcParse } from '@vue/compiler-sfc' +import type { ParserOptions, ParserPlugin } from '@babel/parser' +import { parse } from '@babel/parser' +import type { Node } from '@babel/types' +import { walk } from 'estree-walker' +import { isJSX, isTS } from './lang' + +export function getBabelParsePlugins(lang: string) { + const plugins: ParserPlugin[] = [] + if (isTS(lang)) + plugins.push('typescript') + if (isJSX(lang)) + plugins.push('jsx') + return plugins +} + +function babelParse(code: string, lang: string) { + const options: ParserOptions = { + plugins: getBabelParsePlugins(lang), + } + const { program, errors } = parse(code, options) + return { + ...program, + errors, + } +} + +export function parseSFC(code: string, filename: string) { + const sfc = sfcParse(code, { + filename, + }) + + const { descriptor, errors } = sfc + + const scriptLang = descriptor.script?.lang || 'js' + const scriptSetupLang = descriptor.scriptSetup?.lang || 'js' + + if (scriptLang !== scriptSetupLang) + throw new Error(`[vue-devtools] ${filename} + + + ` + expect(() => { + parseSFC(code, 'test.vue') + }).toThrow() + }) }) diff --git a/packages/core/src/compiler/__test__/import.test.ts b/packages/core/src/compiler/__test__/import.test.ts new file mode 100644 index 00000000..7b74340a --- /dev/null +++ b/packages/core/src/compiler/__test__/import.test.ts @@ -0,0 +1,92 @@ +import { IMPORT_ALIAS, PURE_IMPORT_RE, collectAllImports } from '../common' + +describe('compiler:imports', () => { + test('collect imports', () => { + const code = ` + import { ref } from 'vue' + import {useRouter,type Router} from 'vue-router' + const a = 1 + import * from 'vue-i18n' + import * as pinia from 'pinia' + import 'index.css' + import { reactive as vReactive, onMounted } from 'vue' + import { + shallowRef as sref, + shallowReactive, + } from 'vue' + ` + expect(collectAllImports(code)).toMatchInlineSnapshot(` + { + "index.css": { + "all": true, + "allAlias": null, + }, + "pinia": { + "all": true, + "allAlias": "*", + }, + "vue": [ + { + "alias": null, + "raw": "ref", + }, + { + "alias": "vReactive", + "raw": "reactive", + }, + { + "alias": null, + "raw": "onMounted", + }, + { + "alias": "sref", + "raw": "shallowRef", + }, + { + "alias": null, + "raw": "shallowReactive", + }, + ], + "vue-i18n": { + "all": true, + "allAlias": null, + }, + "vue-router": [ + { + "alias": null, + "raw": "useRouter", + }, + ], + } + `) + }) + test.each([ + ['{ref', 'ref'], + ['ref', 'ref'], + ['ref}', 'ref'], + ['{ ref', 'ref'], + ['{type ref', 'type ref'], + ['a as b', 'a as b'], + [`{ + ref }`, 'ref'], + [`{ + ref`, 'ref'], + [`{ + ref, + }`, 'ref'], + ])('pure import re <%s> <%s>', (input, expected) => { + expect(PURE_IMPORT_RE.exec(input)?.[1]).toEqual(expected) + }) +}) + +describe('compiler:import:alias', () => { + test.each([ + ['* as b', ['*', 'b']], + ['* as $Foo_aFOo', ['*', '$Foo_aFOo']], + ['a as b', ['a', 'b']], + ['a as $Foo_aFOo', ['a', '$Foo_aFOo']], + ])('IMPORT_ALL_ALIAS <%s> <%s>', (input, expected) => { + const r = IMPORT_ALIAS.exec(input)! + expect([r[1], r[2]]).toEqual(expected) + }) +}) diff --git a/packages/core/src/compiler/__test__/lang.test.ts b/packages/core/src/compiler/__test__/lang.test.ts index 174df968..09ed5e00 100644 --- a/packages/core/src/compiler/__test__/lang.test.ts +++ b/packages/core/src/compiler/__test__/lang.test.ts @@ -1,4 +1,4 @@ -import { isJSX, isTS, isVUE } from '../lang' +import { isJSX, isTS, isVUE } from '../common' describe('compiler:lang', () => { test.each([ diff --git a/packages/core/src/compiler/common/import.ts b/packages/core/src/compiler/common/import.ts new file mode 100644 index 00000000..a34af09c --- /dev/null +++ b/packages/core/src/compiler/common/import.ts @@ -0,0 +1,77 @@ +/** + * @description + * - import <{ a, b }> from 'c' + * - import <* as d> from 'e' + * - import from 'g' + * - import 'h' + * will ignore import type + */ +const importRE = /import(?:[\s]+([\w*{}\n\r\t, ]+)[\s]+from)?[\s]+['"]([^'"]+)['"]/g + +/** + * @description + * - {id + * - { id } + * - id } + * - type a + * - a as b + * - { \nid\n} + */ +export const PURE_IMPORT_RE = /{?\s?([$A-Za-z_](\s?[$A-Za-z0-9_])+)\s?}?/ + +/** + * @description + * - `import { a as b } from 'c'` + * - `import * as d from 'e'` + */ +export const IMPORT_ALIAS = /(.+)\sas\s(.+)/ + +interface AnalyzeImportAll { all: true; allAlias: string | null } +interface AnalyzeImportItem { + raw: string + alias: string | null +} +type AnalyzeImportResult = AnalyzeImportAll | AnalyzeImportItem[] + +export function collectAllImports(code: string): Record { + // k: source, v: imports + const imports: Record = {} + + let match: RegExpExecArray | null + // eslint-disable-next-line no-cond-assign + while (match = importRE.exec(code)) { + const [, importsString, source] = match + // figure out if it is import all + const isAll = !importsString || importsString.startsWith('*') + if (isAll) { + const alias = IMPORT_ALIAS.exec(importsString) + imports[source] = { all: true, allAlias: alias?.[1] || null } + continue + } + const result = importsString.split(',').map((i) => { + const [, raw] = PURE_IMPORT_RE.exec(i.trim()) || [] + // resolve import alias + const r = IMPORT_ALIAS.exec(raw) + return { + raw: r ? r[1] : raw, + alias: r ? r[2] : null, + } + }).filter(item => item.raw && !item.raw.startsWith('type ')) // <-- filter `import { type foo }` case + if (!imports[source]) { + imports[source] = result + continue + } + if (Reflect.get(imports[source], 'isAll')) + continue + imports[source] = imports[source] + ? [...(imports[source] as AnalyzeImportItem[]), ...result] + : result + } + return imports +} + +// export function supplementImport(nodes: Node[]) { +// for (const node of nodes) { + +// } +// } diff --git a/packages/core/src/compiler/common.ts b/packages/core/src/compiler/common/index.ts similarity index 82% rename from packages/core/src/compiler/common.ts rename to packages/core/src/compiler/common/index.ts index 76d88bf6..aac8b3bc 100644 --- a/packages/core/src/compiler/common.ts +++ b/packages/core/src/compiler/common/index.ts @@ -3,8 +3,12 @@ import type { ParserOptions, ParserPlugin } from '@babel/parser' import { parse } from '@babel/parser' import type { Node } from '@babel/types' import { walk } from 'estree-walker' +import type { WalkerContext } from 'estree-walker/types/walker' import { isJSX, isTS } from './lang' +export * from './lang' +export * from './import' + export function getBabelParsePlugins(lang: string) { const plugins: ParserPlugin[] = [] if (isTS(lang)) @@ -61,7 +65,12 @@ export function parseSFC(code: string, filename: string) { } } -export function walkAST(node: Node, handlers: Parameters[1]) { +export type WalkCallback = (this: WalkerContext, node: Node, parent: Node | null, key: string | number | symbol | null | undefined, index: number | null | undefined) => void + +export function walkAST(node: Node, handlers: { + enter?: WalkCallback + leave?: WalkCallback +}) { // @ts-expect-error estree-walker types are not compatible with babel types return walk(node, handlers) } diff --git a/packages/core/src/compiler/lang.ts b/packages/core/src/compiler/common/lang.ts similarity index 100% rename from packages/core/src/compiler/lang.ts rename to packages/core/src/compiler/common/lang.ts diff --git a/packages/core/src/compiler/trace-rerender.ts b/packages/core/src/compiler/trace-rerender.ts index 3698a6cb..f0e3563e 100644 --- a/packages/core/src/compiler/trace-rerender.ts +++ b/packages/core/src/compiler/trace-rerender.ts @@ -1,11 +1,11 @@ -import { parseSFC, walkAST } from './common' -import { isVUE } from './lang' +import { isVUE, parseSFC, walkAST } from './common' +// TODO: support more, currently only analyze - +