+
diff --git a/packages/client/components/StateGraph.vue b/packages/client/components/StateGraph.vue
new file mode 100644
index 00000000..476d9465
--- /dev/null
+++ b/packages/client/components/StateGraph.vue
@@ -0,0 +1,108 @@
+
+
+
+
+
diff --git a/packages/client/composables/highlight.ts b/packages/client/composables/highlight.ts
new file mode 100644
index 00000000..19c00661
--- /dev/null
+++ b/packages/client/composables/highlight.ts
@@ -0,0 +1,37 @@
+import type { Highlighter } from 'shiki'
+import { getHighlighter, setCDN } from 'shiki'
+
+const extToLang = {
+ vue: 'vue',
+ js: 'javascript',
+ ts: 'typescript',
+ jsx: 'jsx',
+ tsx: 'tsx',
+}
+
+setCDN('https://unpkg.com/shiki/')
+
+let highlighter: Highlighter
+
+async function initHighlighter() {
+ if (highlighter)
+ return highlighter
+ return await getHighlighter({
+ themes: ['vitesse-dark', 'vitesse-light'],
+ langs: ['vue', 'javascript', 'typescript', 'jsx', 'tsx'],
+ }).then(h => highlighter = h)
+}
+
+export async function useHighlight() {
+ const isDark = useDark()
+ if (!highlighter)
+ await initHighlighter()
+ return {
+ highlightedCode: (code: string, ext: string) => highlighter.codeToHtml(code,
+ {
+ lang: extToLang[ext],
+ theme: isDark.value ? 'vitesse-dark' : 'vitesse-light',
+ },
+ ),
+ }
+}
diff --git a/packages/client/logic/state-graph.ts b/packages/client/logic/state-graph.ts
new file mode 100644
index 00000000..0d120d66
--- /dev/null
+++ b/packages/client/logic/state-graph.ts
@@ -0,0 +1,101 @@
+import type { AcceptableLang } from 'esm-analyzer'
+import { Project } from 'esm-analyzer'
+import { rpc } from './rpc'
+
+export enum StateGraphStateEnum {
+ NOT_READY, // graph is not ready yet
+ READY, // graph is ready
+ NOT_SELECT_FILE, // graph is ready, but no file is selected
+ NOT_COLLECTED, // graph is ready, but the selected file is not collected
+ NO_STATE, // graph is ready, but the selected file has no state
+ HAS_STATE, // everything is ready
+}
+
+const codeData = ref<{
+ code: string
+ filename: string
+}[]>([])
+const rawAnalyzeData = ref<{
+ code: string
+ lang: AcceptableLang
+ path: string
+ offsetContent: string
+}[]>([])
+const project = shallowRef
(new Project('state-analyze'))
+export const stateGraphState = ref(StateGraphStateEnum.NOT_READY)
+
+export async function initRawData() {
+ if (stateGraphState.value !== StateGraphStateEnum.NOT_READY)
+ return
+ const rawData = await rpc.getStateAnalyzeCollectedData() ?? []
+ rawData.forEach((item) => {
+ rawAnalyzeData.value.push({
+ code: item.code,
+ lang: item.lang as AcceptableLang,
+ path: item.filename,
+ offsetContent: item.offsetContent,
+ })
+ codeData.value.push({
+ code: item.fullCode,
+ filename: item.filename,
+ })
+ })
+ project.value.addFiles(rawAnalyzeData.value)
+ await project.value.prepare()
+ stateGraphState.value = StateGraphStateEnum.READY
+}
+
+export const currentSelectedFile = ref* file name */string>()
+watch(currentSelectedFile, () => {
+ stateGraphState.value = getState()
+})
+
+export function useStateGraph() {
+ const [drawerVisible, toggleDrawerVisible] = useToggle(false)
+
+ return {
+ drawerVisible,
+ toggleDrawerVisible,
+ enable: async () => {
+ toggleDrawerVisible()
+ await initRawData()
+ stateGraphState.value = getState()
+ },
+ currentSelectedFile,
+ }
+}
+
+function getState() {
+ // if not ready
+ if (stateGraphState.value === StateGraphStateEnum.NOT_READY)
+ return StateGraphStateEnum.NOT_READY
+ const selectFile = currentSelectedFile.value
+ // if not selected file
+ if (!selectFile)
+ return StateGraphStateEnum.NOT_SELECT_FILE
+ // if not collected
+ if (!project.value.getFilePaths().includes(selectFile))
+ return StateGraphStateEnum.NOT_COLLECTED
+ // if no state
+ const data = project.value.getAnalyzeResults(selectFile)
+ if (!data || !data.size)
+ return StateGraphStateEnum.NO_STATE
+ return StateGraphStateEnum.HAS_STATE
+}
+
+function getData() {
+ return stateGraphState.value === StateGraphStateEnum.HAS_STATE
+ ? project.value.getAnalyzeResults(currentSelectedFile.value!)!
+ : null
+}
+
+export const stateGraphRawData = computed(() => {
+ return getData()
+})
+
+export const currentFullCodeAndFilename = computed(() => {
+ return codeData.value.find(item => item.filename === currentSelectedFile.value) ?? {
+ code: '',
+ filename: '',
+ }
+})
diff --git a/packages/client/package.json b/packages/client/package.json
index 8b6aaca1..c021932a 100644
--- a/packages/client/package.json
+++ b/packages/client/package.json
@@ -34,12 +34,15 @@
"@vueuse/integrations": "^10.3.0",
"@webfansplz/vuedoc-parser": "^0.0.3",
"algoliasearch": "^4.19.1",
+ "esm-analyzer": "^0.3.5",
"json-editor-vue": "^0.10.6",
"minimatch": "^9.0.3",
"nanoid": "^4.0.2",
+ "shiki": "^0.14.3",
"splitpanes": "^3.1.5",
"vanilla-jsoneditor": "^0.17.8",
"vite-hot-client": "^0.2.1",
+ "vue-resizables": "^0.3.4",
"vue-router": "^4.2.4",
"vuedraggable": "^4.1.0",
"xterm": "^5.2.1",
diff --git a/packages/client/pages/graph.vue b/packages/client/pages/graph.vue
index 6c4d2a05..f6125aa8 100644
--- a/packages/client/pages/graph.vue
+++ b/packages/client/pages/graph.vue
@@ -2,6 +2,7 @@
import type { Data, Options } from 'vis-network'
import { Network } from 'vis-network'
import { matchedKeys } from '../logic/graph'
+import { useStateGraph } from '../logic/state-graph'
import { searchResults as modules } from '~/logic/graph'
import { useDevToolsClient } from '~/logic/client'
import { rootPath } from '~/logic/global'
@@ -86,6 +87,8 @@ const data = computed(() => {
}
})
+const { drawerVisible, enable: enableStateGraph, toggleDrawerVisible, currentSelectedFile } = useStateGraph()
+
onMounted(() => {
const options: Options = {
nodes: {
@@ -158,6 +161,7 @@ onMounted(() => {
network.on('click', (params) => {
const nodeId = params.nodes?.[0]
+ currentSelectedFile.value = nodeId
if (!nodeId)
return resetNodeStyle()
if (settings.graphSettings.value.clickOpenInEditor && metaKeyPressed.value)
@@ -191,12 +195,21 @@ onMounted(() => {
})
})
const { showGraphSetting } = useGraphSettings()
+
+const navbar = ref()
-
+
+
@@ -204,5 +217,8 @@ const { showGraphSetting } = useGraphSettings()
+
+
+
diff --git a/packages/core/build.config.ts b/packages/core/build.config.ts
index 268b6001..40d41421 100644
--- a/packages/core/build.config.ts
+++ b/packages/core/build.config.ts
@@ -11,6 +11,7 @@ export default defineBuildConfig({
'@babel/parser',
'estree-walker',
'magic-string',
+ 'esm-analyzer',
],
declaration: true,
rollup: {
diff --git a/packages/core/src/compiler/__test__/analyze.test.ts b/packages/core/src/compiler/__test__/analyze.test.ts
index e2a0031e..f00bac96 100644
--- a/packages/core/src/compiler/__test__/analyze.test.ts
+++ b/packages/core/src/compiler/__test__/analyze.test.ts
@@ -3,17 +3,18 @@ import { analyzeCode } from '..'
const baseConfig: AnalyzeOptions = {
rerenderTrace: true,
+ stateAnalyze: true,
}
describe('analyzeCode - exclude', () => {
test('not acceptable lang', () => {
- expect(analyzeCode('', 'test.txt', baseConfig)).toBe('')
+ expect(analyzeCode('', 'test.txt', baseConfig)).toBeNull()
})
test('excluded path', () => {
- expect(analyzeCode('', 'node_modules/test.js', baseConfig)).toBe('')
+ expect(analyzeCode('', 'node_modules/test.js', baseConfig)).toBeNull()
})
test('not enabled', () => {
- expect(analyzeCode('', 'test.js', { rerenderTrace: false })).toBe('')
+ expect(analyzeCode('', 'test.js', { rerenderTrace: false, stateAnalyze: false })).toBeNull()
})
test('should execute', () => {
expect(analyzeCode(`
@@ -39,7 +40,7 @@ describe('analyzeCode - rerender - sfc', () => {
`
- const result = analyzeCode(code, 'test.vue', baseConfig)
+ const result = analyzeCode(code, 'test.vue', baseConfig) as { code: string } | undefined
expect(result?.code).toMatchSnapshot()
})
test('script setup with script', () => {
diff --git a/packages/core/src/compiler/__test__/common.test.ts b/packages/core/src/compiler/__test__/common.test.ts
index 5e7d7480..c787f952 100644
--- a/packages/core/src/compiler/__test__/common.test.ts
+++ b/packages/core/src/compiler/__test__/common.test.ts
@@ -25,19 +25,4 @@ describe('compiler:common:parseSFC', () => {
parseSFC(code, 'test.vue')
}).not.toThrow()
})
- test('should throw if
-
-
-
- `
- expect(() => {
- parseSFC(code, 'test.vue')
- }).toThrow()
- })
})
diff --git a/packages/core/src/compiler/common/analyze.ts b/packages/core/src/compiler/common/analyze.ts
index 998849c5..bb01dd29 100644
--- a/packages/core/src/compiler/common/analyze.ts
+++ b/packages/core/src/compiler/common/analyze.ts
@@ -8,9 +8,8 @@ function isSetupFn(node: Node): node is ObjectMethod | ObjectProperty {
return isObjectFn(node) && (node.key as Identifier).name === SETUP_FN
}
-/**
- * @returns insert code location
- */
+const VUE_SFC_SCRIPT_BLOCK_START_RE = /
+
diff --git a/packages/ui-kit/src/components/Loading.vue b/packages/ui-kit/src/components/Loading.vue
new file mode 100644
index 00000000..b4cc54d2
--- /dev/null
+++ b/packages/ui-kit/src/components/Loading.vue
@@ -0,0 +1,21 @@
+
+
+
+
+
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3853d68d..a21d3875 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -77,6 +77,9 @@ importers:
algoliasearch:
specifier: ^4.19.1
version: 4.19.1
+ esm-analyzer:
+ specifier: ^0.3.5
+ version: 0.3.5
json-editor-vue:
specifier: ^0.10.6
version: 0.10.6(vanilla-jsoneditor@0.17.8)(vue@3.3.4)
@@ -86,6 +89,9 @@ importers:
nanoid:
specifier: ^4.0.2
version: 4.0.2
+ shiki:
+ specifier: ^0.14.3
+ version: 0.14.3
splitpanes:
specifier: ^3.1.5
version: 3.1.5
@@ -95,6 +101,9 @@ importers:
vite-hot-client:
specifier: ^0.2.1
version: 0.2.1(vite@4.4.7)
+ vue-resizables:
+ specifier: ^0.3.4
+ version: 0.3.4(vue@3.3.4)
vue-router:
specifier: ^4.2.4
version: 4.2.4(vue@3.3.4)
@@ -2521,6 +2530,10 @@ packages:
engines: {node: '>=12'}
dev: true
+ /ansi-sequence-parser@1.1.1:
+ resolution: {integrity: sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==}
+ dev: false
+
/ansi-styles@3.2.1:
resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
engines: {node: '>=4'}
@@ -3868,6 +3881,16 @@ packages:
- supports-color
dev: true
+ /esm-analyzer@0.3.5:
+ resolution: {integrity: sha512-WyXk3t3ppT5mY2/ub/Rrqb6jVIOf7K0+jVYECB+0Bvcqi+foRdK/e0iCRMAztFudG3lr4VETKLNdWucAxaeYBg==}
+ dependencies:
+ '@babel/parser': 7.22.7
+ estree-walker: 2.0.2
+ mitt: 3.0.1
+ p-limit: 4.0.0
+ to-path-tree: 0.2.0
+ dev: false
+
/esno@0.16.3:
resolution: {integrity: sha512-6slSBEV1lMKcX13DBifvnDFpNno5WXhw4j/ff7RI0y51BZiDqEe5dNhhjhIQ3iCOQuzsm2MbVzmwqbN78BBhPg==}
hasBin: true
@@ -4901,7 +4924,6 @@ packages:
/jsonc-parser@3.2.0:
resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==}
- dev: true
/jsonfile@6.1.0:
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
@@ -5337,6 +5359,10 @@ packages:
resolution: {integrity: sha512-ILj2TpLiysu2wkBbWjAmww7TkZb65aiQO+DkVdUTBpBXq+MHYiETENkKFMtsJZX1Lf4pe4QOrTSjIfUwN5lRdg==}
dev: true
+ /mitt@3.0.1:
+ resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
+ dev: false
+
/mkdirp@1.0.4:
resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}
engines: {node: '>=10'}
@@ -5714,7 +5740,6 @@ packages:
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dependencies:
yocto-queue: 1.0.0
- dev: true
/p-locate@4.1.0:
resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
@@ -6486,6 +6511,15 @@ packages:
/shell-quote@1.8.1:
resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==}
+ /shiki@0.14.3:
+ resolution: {integrity: sha512-U3S/a+b0KS+UkTyMjoNojvTgrBHjgp7L6ovhFVZsXmBGnVdQ4K4U9oK0z63w538S91ATngv1vXigHCSWOwnr+g==}
+ dependencies:
+ ansi-sequence-parser: 1.1.1
+ jsonc-parser: 3.2.0
+ vscode-oniguruma: 1.7.0
+ vscode-textmate: 8.0.0
+ dev: false
+
/side-channel@1.0.4:
resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==}
dependencies:
@@ -6885,6 +6919,10 @@ packages:
resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
engines: {node: '>=4'}
+ /to-path-tree@0.2.0:
+ resolution: {integrity: sha512-TGf6IdITeelc0zNz4XCPbHb0lYagTc2Gvdm3I41vjFPsUXXFBg+c0i8oaMgftJhHLITPDTfi9KwcGDoMZ9OTIg==}
+ dev: false
+
/to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
@@ -7591,6 +7629,14 @@ packages:
engines: {node: '>=0.10.0'}
dev: false
+ /vscode-oniguruma@1.7.0:
+ resolution: {integrity: sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==}
+ dev: false
+
+ /vscode-textmate@8.0.0:
+ resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==}
+ dev: false
+
/vue-demi@0.13.11(vue@3.3.4):
resolution: {integrity: sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==}
engines: {node: '>=12'}
@@ -7661,6 +7707,14 @@ packages:
vue: 3.3.4
dev: true
+ /vue-resizables@0.3.4(vue@3.3.4):
+ resolution: {integrity: sha512-eLnVRngj0EMo4MKgAXppzdmtCiTEyQghQ5j4kgu1EjY80LYsqkEZp8mHPII2vdG3aFKnWZeMWuw5JysG8v5hUQ==}
+ peerDependencies:
+ vue: ^3.0.0
+ dependencies:
+ vue: 3.3.4
+ dev: false
+
/vue-resize@2.0.0-alpha.1(vue@3.3.4):
resolution: {integrity: sha512-7+iqOueLU7uc9NrMfrzbG8hwMqchfVfSzpVlCMeJQe4pyibqyoifDNbKTZvwxZKDvGkB+PdFeKvnGZMoEb8esg==}
peerDependencies:
@@ -7918,4 +7972,3 @@ packages:
/yocto-queue@1.0.0:
resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==}
engines: {node: '>=12.20'}
- dev: true
diff --git a/tsconfig.json b/tsconfig.json
index 10d0000d..60d2a63c 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -16,5 +16,5 @@
"vitest/globals"
]
},
- "exclude": ["node_modules", "dist", "src/node/app.js"]
+ "exclude": ["node_modules", "dist", "*/node/src/app.js", "**/*/dist"]
}