Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
rework sfc-inspector to provide modular API
- Loading branch information
Showing
15 changed files
with
964 additions
and
201 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import { Context, Plugin } from './types'; | ||
import { parse, SFCBlock } from '@vue/compiler-sfc'; | ||
import { createComponentInfoFactory } from './component'; | ||
|
||
const parsers: Context['parsers'] = { | ||
sfc: { | ||
sourceMap: false, | ||
pad: 'space', | ||
}, | ||
|
||
babel: { | ||
sourceType: 'module', | ||
plugins: ['bigInt', 'optionalChaining', 'optionalCatchBinding', 'nullishCoalescingOperator', 'objectRestSpread'], | ||
}, | ||
}; | ||
|
||
export function createAnalyzer(plugins: Plugin[], options: Partial<Context['parsers']> = {}) { | ||
function createContext(fileName: string, content: string): Context { | ||
const { descriptor } = parse(content, { ...parsers.sfc, ...options.sfc, filename: fileName }); | ||
|
||
return { | ||
fileName, | ||
component: createComponentInfoFactory(), | ||
descriptor, | ||
plugins, | ||
parsers: { | ||
sfc: { ...parsers.sfc, ...options.sfc }, | ||
babel: { ...parsers.babel, ...options.babel }, | ||
}, | ||
}; | ||
} | ||
|
||
function analyze(content: string, fileName: string = 'component.vue') { | ||
const context = createContext(fileName, content); | ||
|
||
processSFC(context); | ||
|
||
return context.component.info(); | ||
} | ||
|
||
function analyzeScript(content: string, fileName?: string) { | ||
return analyze(`<script>${content}</script>`, fileName); | ||
} | ||
|
||
function analyzeTemplate(content: string, fileName?: string) { | ||
return analyze(`<template>${content}</template>`, fileName); | ||
} | ||
|
||
return { analyze, analyzeScript, analyzeTemplate }; | ||
} | ||
|
||
function processSFC(context: Context) { | ||
const { script, template, styles, customBlocks } = context.descriptor; | ||
|
||
function call<T extends SFCBlock>(kind: string, block: T) { | ||
context.plugins.forEach(({ blocks }) => { | ||
if (blocks && kind in blocks) { | ||
blocks[kind](block, context); | ||
} | ||
}); | ||
} | ||
|
||
if (script) call('script', script); | ||
if (template) call('script', template); | ||
styles.forEach(call.bind(null, 'style')); | ||
customBlocks.forEach((block) => call(block.type, block)); | ||
} |
174 changes: 174 additions & 0 deletions
174
packages/@vuedx/sfc-inspector/src/analyzers/blockScriptAnalyzer.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
import { parse } from '@babel/parser'; | ||
import traverse, { NodePath } from '@babel/traverse'; | ||
import { isIdentifier, ObjectExpression } from '@babel/types'; | ||
import { SFCScriptBlock } from '@vue/compiler-sfc'; | ||
import { Context, Plugin, ScriptAnalyzerContext } from '../types'; | ||
import { isNotNull } from '../utilities'; | ||
|
||
export const blockScriptAnalyzer: Plugin = { | ||
blocks: { | ||
script: (block, ctx) => { | ||
if (block.content) { | ||
processScript(createScriptContext(block.content, ctx, block)); | ||
} | ||
}, | ||
}, | ||
}; | ||
|
||
export function createScriptContext(content: string, context: Context, block?: SFCScriptBlock): ScriptAnalyzerContext { | ||
const script = block || { | ||
type: 'script', | ||
content: content, | ||
setup: false, | ||
attrs: {}, | ||
// TODO: Create loc object as if javascript file. | ||
loc: { start: { offset: 0, line: 1, column: 1 }, end: { offset: 0, line: 1, column: 1 }, source: content }, | ||
}; | ||
|
||
const plugins = context.parsers.babel.plugins || []; | ||
|
||
const ast = parse(content, { | ||
...context.parsers.babel, | ||
plugins: Array.from(new Set(plugins)), | ||
}); | ||
|
||
return { | ||
...context, | ||
mode: script.setup ? 'setup' : 'module', | ||
ast: ast, | ||
block: script, | ||
}; | ||
} | ||
|
||
function processScript(context: ScriptAnalyzerContext) { | ||
const enterHandlers = context.plugins | ||
.map((plugin) => { | ||
if (plugin.babel) { | ||
if (typeof plugin.babel === 'function') { | ||
return plugin.babel; | ||
} | ||
if ('enter' in plugin.babel) { | ||
return plugin.babel.enter; | ||
} | ||
} | ||
}) | ||
.filter(isNotNull); | ||
|
||
const exitHandlers = context.plugins | ||
.map((plugin) => { | ||
if (plugin.babel && 'exit' in plugin.babel) { | ||
return plugin.babel.exit; | ||
} | ||
}) | ||
.filter(isNotNull); | ||
|
||
const setupHandlers = context.plugins | ||
.map((plugin) => plugin.setup) | ||
.filter(isNotNull) | ||
.flat(); | ||
const optionsHandlers = context.plugins | ||
.map((plugin) => (Array.isArray(plugin.options) ? plugin.options : null)) | ||
.filter(isNotNull) | ||
.flat(); | ||
const optionsByNameHandlers = context.plugins | ||
.map((plugin) => (Array.isArray(plugin.options) ? null : plugin.options)) | ||
.filter(isNotNull); | ||
const declarationHandlers = context.plugins | ||
.map((plugin) => plugin.declaration) | ||
.filter(isNotNull) | ||
.flat(); | ||
|
||
function call<T>(fns: ((node: T, context: ScriptAnalyzerContext) => void)[], node: T) { | ||
fns.forEach((fn) => { | ||
try { | ||
fn(node, context); | ||
} catch { | ||
// TODO: Handle error. | ||
} | ||
}); | ||
} | ||
|
||
function processOptions(options$: NodePath<ObjectExpression>) { | ||
const properties$ = options$.get('properties'); | ||
|
||
properties$.forEach((property$) => { | ||
if (property$.isObjectMember()) { | ||
const { key } = property$.node; | ||
|
||
if (isIdentifier(key)) { | ||
const name = key.name; | ||
optionsByNameHandlers.forEach((options) => { | ||
const fn = options[name]; | ||
|
||
if (fn) { | ||
try { | ||
fn(property$, context); | ||
} catch { | ||
// TODO: Handler error. | ||
} | ||
} | ||
}); | ||
|
||
if (property$.isObjectMethod() && name === 'setup') { | ||
call(setupHandlers, property$ as any); | ||
} | ||
} | ||
} | ||
}); | ||
} | ||
|
||
traverse(context.ast, { | ||
enter(path) { | ||
call(enterHandlers, path); | ||
}, | ||
exit(path) { | ||
call(exitHandlers, path); | ||
}, | ||
ExportDefaultDeclaration(path) { | ||
if (context.mode === 'setup') return; | ||
const declaration$ = path.get('declaration'); | ||
/** | ||
* Matches: | ||
* export default {} | ||
*/ | ||
if (declaration$.isObjectExpression()) { | ||
call(declarationHandlers, declaration$); | ||
call(optionsHandlers, declaration$); | ||
processOptions(declaration$); | ||
} else if (declaration$.isCallExpression()) { | ||
/** | ||
* Matches: | ||
* export default fn(...) | ||
*/ | ||
const { callee, arguments: args } = declaration$.node; | ||
const args$ = declaration$.get('arguments'); | ||
let options$ = Array.isArray(args$) ? args$[0] : args$; | ||
|
||
/** | ||
* Matches: | ||
* export default defineComponent(...) | ||
*/ | ||
if (isIdentifier(callee) && callee.name === 'defineComponent') { | ||
if (options$.isObjectExpression()) { | ||
/** | ||
* Matches: | ||
* export default defineComponent({ ... }) | ||
*/ | ||
call(declarationHandlers, declaration$); | ||
call(optionsHandlers, options$); | ||
processOptions(options$); | ||
} else if (options$.isFunctionExpression() || options$.isArrowFunctionExpression()) { | ||
/** | ||
* Matches: | ||
* export default defineComponent(() => {...}) | ||
* export default defineComponent(function setup() {...}) | ||
*/ | ||
call(setupHandlers, options$ as any); | ||
} | ||
} | ||
} | ||
}, | ||
}); | ||
} | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export * from './blockScriptAnalyzer'; | ||
|
||
export * from './scriptOptionsComponentsAnalyzer'; |
95 changes: 95 additions & 0 deletions
95
packages/@vuedx/sfc-inspector/src/analyzers/scriptOptionsComponentsAnalyzer.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
import { createAnalyzer } from '../analyzer'; | ||
import { blockScriptAnalyzer as ScriptBlockAnalyzer } from './blockScriptAnalyzer'; | ||
import { ComponentsOptionAnalyzer } from './scriptOptionsComponentsAnalyzer'; | ||
|
||
describe('script/options/components', () => { | ||
const analyzer = createAnalyzer([ScriptBlockAnalyzer, ComponentsOptionAnalyzer]); | ||
|
||
test('imported component in object export', () => { | ||
const info = analyzer.analyzeScript(` | ||
import Foo from './foo.vue' | ||
import { Bar } from 'external-library' | ||
import Baz from './baz.vue' | ||
export default { | ||
components: { Foo, Bar } | ||
} | ||
`); | ||
|
||
expect(info.components).toHaveLength(2); | ||
expect(info.components[0]).toMatchObject({ | ||
name: 'Foo', | ||
kind: 'local', | ||
source: { | ||
moduleName: './foo.vue', | ||
}, | ||
}); | ||
expect(info.components[1]).toMatchObject({ | ||
name: 'Bar', | ||
kind: 'local', | ||
source: { | ||
moduleName: 'external-library', | ||
exportName: 'Bar', | ||
}, | ||
}); | ||
}); | ||
|
||
test('imported component in defineComponent', () => { | ||
const info = analyzer.analyzeScript(` | ||
import { defineComponent } from 'vue' | ||
import Foo from './foo.vue' | ||
import { Bar } from 'external-library' | ||
import Baz from './baz.vue' | ||
export default defineComponent({ | ||
components: { Foo, Bar } | ||
}) | ||
`); | ||
|
||
expect(info.components).toHaveLength(2); | ||
expect(info.components[0]).toMatchObject({ | ||
name: 'Foo', | ||
kind: 'local', | ||
source: { | ||
moduleName: './foo.vue', | ||
}, | ||
}); | ||
expect(info.components[1]).toMatchObject({ | ||
name: 'Bar', | ||
kind: 'local', | ||
source: { | ||
moduleName: 'external-library', | ||
exportName: 'Bar', | ||
}, | ||
}); | ||
}); | ||
|
||
test('imported component and registered with different name', () => { | ||
const info = analyzer.analyzeScript(` | ||
import Foo from './foo.vue' | ||
import { Bar as LocalBar } from 'external-library' | ||
import Baz from './baz.vue' | ||
export default { | ||
components: { MyFoo: Foo, MyBar: LocalBar } | ||
} | ||
`); | ||
|
||
expect(info.components).toHaveLength(2); | ||
expect(info.components[0]).toMatchObject({ | ||
name: 'MyFoo', | ||
kind: 'local', | ||
source: { | ||
moduleName: './foo.vue', | ||
}, | ||
}); | ||
expect(info.components[1]).toMatchObject({ | ||
name: 'MyBar', | ||
kind: 'local', | ||
source: { | ||
moduleName: 'external-library', | ||
exportName: 'Bar', | ||
}, | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.