Skip to content

Commit

Permalink
Add support for <script setup lang=ts generic="..."> (#182)
Browse files Browse the repository at this point in the history
* Add support for `<script setup lang=ts generic="...">`

* update

* update

* fix unused import

* add test cases

* rename node type and internal method

* fix and change node def

* add test case
  • Loading branch information
ota-meshi authored May 14, 2023
1 parent 92c0d88 commit 195d46d
Show file tree
Hide file tree
Showing 71 changed files with 23,524 additions and 281 deletions.
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,7 @@
"test:mocha": "mocha --require ts-node/register \"test/*.js\" --reporter dot --timeout 60000",
"test:cover": "nyc mocha \"test/*.js\" --reporter dot --timeout 60000",
"test:debug": "mocha --require ts-node/register/transpile-only \"test/*.js\" --reporter dot --timeout 60000",
"preupdate-fixtures": "npm run -s build",
"update-fixtures": "node scripts/update-fixtures-ast.js && node scripts/update-fixtures-document-fragment.js",
"update-fixtures": "ts-node --transpile-only scripts/update-fixtures-ast.js && ts-node --transpile-only scripts/update-fixtures-document-fragment.js",
"preversion": "npm test",
"version": "npm run -s build",
"postversion": "git push && git push --tags",
Expand Down
11 changes: 6 additions & 5 deletions scripts/update-fixtures-ast.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

const fs = require("fs")
const path = require("path")
const parser = require("../")
const parser = require("../src")
const escope = require("eslint-scope")
const semver = require("semver")

Expand Down Expand Up @@ -222,16 +222,17 @@ for (const name of TARGETS) {
continue
}
const sourcePath = path.join(ROOT, `${name}/source.vue`)
const optionsPath = path.join(ROOT, `${name}/parser-options.json`)
const optionsPath = [
path.join(ROOT, `${name}/parser-options.json`),
path.join(ROOT, `${name}/parser-options.js`),
].find((fp) => fs.existsSync(fp))
const astPath = path.join(ROOT, `${name}/ast.json`)
const tokenRangesPath = path.join(ROOT, `${name}/token-ranges.json`)
const treePath = path.join(ROOT, `${name}/tree.json`)
const scopePath = path.join(ROOT, `${name}/scope.json`)
const servicesPath = path.join(ROOT, `${name}/services.json`)
const source = fs.readFileSync(sourcePath, "utf8")
const parserOptions = fs.existsSync(optionsPath)
? JSON.parse(fs.readFileSync(optionsPath, "utf8"))
: {}
const parserOptions = optionsPath ? require(optionsPath) : {}
const options = Object.assign(
{ filePath: sourcePath },
PARSER_OPTIONS,
Expand Down
15 changes: 8 additions & 7 deletions scripts/update-fixtures-document-fragment.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

const fs = require("fs")
const path = require("path")
const parser = require("../")
const parser = require("../src")

//------------------------------------------------------------------------------
// Helpers
Expand Down Expand Up @@ -53,14 +53,15 @@ for (const name of TARGETS) {
.readdirSync(path.join(ROOT, name))
.find((f) => f.startsWith("source."))
const sourcePath = path.join(ROOT, `${name}/${sourceFileName}`)
const optionsPath = path.join(ROOT, `${name}/parser-options.json`)
const optionsPath = [
path.join(ROOT, `${name}/parser-options.json`),
path.join(ROOT, `${name}/parser-options.js`),
].find((fp) => fs.existsSync(fp))
const source = fs.readFileSync(sourcePath, "utf8")
const options = Object.assign(
{ filePath: sourcePath },
PARSER_OPTIONS,
fs.existsSync(optionsPath)
? JSON.parse(fs.readFileSync(optionsPath, "utf8"))
: {}
optionsPath ? require(optionsPath) : {},
)
const result = parser.parseForESLint(source, options)
const actual = result.services.getDocumentFragment()
Expand All @@ -72,7 +73,7 @@ for (const name of TARGETS) {
console.log("Update:", name)

const tokenRanges = getAllTokens(actual).map((t) =>
source.slice(t.range[0], t.range[1])
source.slice(t.range[0], t.range[1]),
)
const tree = getTree(source, actual)

Expand Down Expand Up @@ -106,7 +107,7 @@ function getTree(source, fgAst) {
type: node.type,
text: source.slice(node.range[0], node.range[1]),
children: [],
})
}),
)
},
leaveNode() {
Expand Down
16 changes: 15 additions & 1 deletion src/ast/nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import type { ScopeManager } from "eslint-scope"
import type { ParseError } from "./errors"
import type { HasLocation } from "./locations"
import type { Token } from "./tokens"
// eslint-disable-next-line node/no-extraneous-import -- ignore
import type { TSESTree } from "@typescript-eslint/utils"

//------------------------------------------------------------------------------
// Common
Expand All @@ -28,6 +30,7 @@ export type Node =
| VForExpression
| VOnExpression
| VSlotScopeExpression
| VGenericExpression
| VFilterSequenceExpression
| VFilter

Expand Down Expand Up @@ -742,7 +745,7 @@ export type Namespace =
*/
export interface Variable {
id: ESLintIdentifier
kind: "v-for" | "scope"
kind: "v-for" | "scope" | "generic"
references: Reference[]
}

Expand Down Expand Up @@ -787,6 +790,16 @@ export interface VSlotScopeExpression extends HasLocation, HasParent {
params: ESLintPattern[]
}

/**
* The node of `generic` directives.
*/
export interface VGenericExpression extends HasLocation, HasParent {
type: "VGenericExpression"
parent: VExpressionContainer
params: TSESTree.TSTypeParameterDeclaration["params"]
rawParams: string[]
}

/**
* The node of a filter sequence which is separated by `|`.
*/
Expand Down Expand Up @@ -845,6 +858,7 @@ export interface VExpressionContainer extends HasLocation, HasParent {
| VForExpression
| VOnExpression
| VSlotScopeExpression
| VGenericExpression
| null
references: Reference[]
}
Expand Down
33 changes: 33 additions & 0 deletions src/common/ast-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import type {
VDirective,
VDocumentFragment,
VElement,
VExpressionContainer,
VGenericExpression,
VNode,
} from "../ast"

Expand Down Expand Up @@ -80,3 +82,34 @@ export function getLang(element: VElement | undefined): string | null {
const lang = langAttr && langAttr.value && langAttr.value.value
return lang || null
}
/**
* Check whether the given script element has `lang="ts"`.
* @param element The element to check.
* @returns The given script element has `lang="ts"`.
*/
export function isTSLang(element: VElement | undefined): boolean {
const lang = getLang(element)
// See https://github.com/vuejs/core/blob/28e30c819df5e4fc301c98f7be938fa13e8be3bc/packages/compiler-sfc/src/compileScript.ts#L179
return lang === "ts" || lang === "tsx"
}

export type GenericDirective = VDirective & {
value: VExpressionContainer & {
expression: VGenericExpression
}
}

/**
* Find `generic` directive from given `<script>` element
*/
export function findGenericDirective(
element: VElement,
): GenericDirective | null {
return (
element.startTag.attributes.find(
(attr): attr is GenericDirective =>
attr.directive &&
attr.value?.expression?.type === "VGenericExpression",
) || null
)
}
76 changes: 54 additions & 22 deletions src/html/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import type {
CustomTemplateTokenizer,
CustomTemplateTokenizerConstructor,
} from "./custom-tokenizer"
import { isScriptSetupElement, isTSLang } from "../common/ast-utils"

const DIRECTIVE_NAME = /^(?:v-|[.:@#]).*[^.:@#]$/u
const DT_DD = /^d[dt]$/u
Expand Down Expand Up @@ -178,8 +179,10 @@ export class Parser {
private document: VDocumentFragment
private elementStack: VElement[]
private vPreElement: VElement | null
private postProcessesForScript: ((parserOptions: ParserOptions) => void)[] =
[]
private postProcessesForScript: ((
htmlParserOptions: ParserOptions,
scriptParserOptions: ParserOptions,
) => void)[] = []

/**
* The source code text.
Expand Down Expand Up @@ -290,7 +293,7 @@ export class Parser {

const doc = this.document

const parserOptions = {
const htmlParserOptions = {
...this.baseParserOptions,
parser: getScriptParser(
this.baseParserOptions.parser,
Expand All @@ -300,8 +303,14 @@ export class Parser {
},
),
}
const scriptParserOptions = {
...this.baseParserOptions,
parser: getScriptParser(this.baseParserOptions.parser, () =>
getParserLangFromSFC(doc),
),
}
for (const proc of this.postProcessesForScript) {
proc(parserOptions)
proc(htmlParserOptions, scriptParserOptions)
}
this.postProcessesForScript = []

Expand Down Expand Up @@ -449,24 +458,18 @@ export class Parser {
* @param namespace The current namespace.
*/
private processAttribute(node: VAttribute, namespace: Namespace): void {
const tagName = this.getTagName(node.parent.parent)
const attrName = this.getTagName(node.key)

if (
(this.expressionEnabled ||
(attrName === "v-pre" && !this.isInVPreElement)) &&
(DIRECTIVE_NAME.test(attrName) ||
attrName === "slot-scope" ||
(tagName === "template" && attrName === "scope"))
) {
this.postProcessesForScript.push((parserOptions) => {
convertToDirective(
this.text,
parserOptions,
this.locationCalculator,
node,
)
})
if (this.needConvertToDirective(node)) {
this.postProcessesForScript.push(
(parserOptions, scriptParserOptions) => {
convertToDirective(
this.text,
parserOptions,
scriptParserOptions,
this.locationCalculator,
node,
)
},
)
return
}

Expand All @@ -480,6 +483,35 @@ export class Parser {
this.reportParseError(node, "x-invalid-namespace")
}
}
/**
* Checks whether the given attribute node is need convert to directive.
* @param node The node to check
*/
private needConvertToDirective(node: VAttribute) {
const element = node.parent.parent
const tagName = this.getTagName(element)
const attrName = this.getTagName(node.key)

if (
attrName === "generic" &&
element.parent.type === "VDocumentFragment" &&
isScriptSetupElement(element) &&
isTSLang(element)
) {
return true
}
const expressionEnabled =
this.expressionEnabled ||
(attrName === "v-pre" && !this.isInVPreElement)
if (!expressionEnabled) {
return false
}
return (
DIRECTIVE_NAME.test(attrName) ||
attrName === "slot-scope" ||
(tagName === "template" && attrName === "scope")
)
}

/**
* Process the given template text token with a configured template tokenizer, based on language.
Expand Down
Loading

0 comments on commit 195d46d

Please sign in to comment.