├── .gitignore ├── .tool-versions ├── tests ├── fixtures │ ├── empty │ │ ├── input.vue │ │ └── output.vue │ ├── propsArray │ │ ├── input.vue │ │ └── output.vue │ ├── defineComponent │ │ ├── output.vue │ │ └── input.vue │ ├── useSemisAndDoubleQuotes │ │ ├── output.vue │ │ └── input.vue │ ├── emits │ │ ├── output.vue │ │ └── input.vue │ ├── injectArray │ │ ├── input.vue │ │ └── output.vue │ ├── propTypes │ │ ├── output.vue │ │ └── input.vue │ ├── directives │ │ ├── output.vue │ │ └── input.vue │ ├── filters │ │ ├── output.vue │ │ └── input.vue │ ├── provideArray │ │ ├── input.vue │ │ └── output.vue │ ├── transformNode │ │ ├── output.vue │ │ └── input.vue │ ├── computedGetSet │ │ ├── output.vue │ │ └── input.vue │ ├── hooks │ │ ├── input.vue │ │ └── output.vue │ ├── example │ │ ├── output.vue │ │ └── input.vue │ └── issue-8 │ │ ├── output.vue │ │ └── input.vue ├── tsconfig.json └── index.ts ├── docs ├── tree-sitter-javascript.wasm ├── tree-sitter-typescript.wasm ├── index.html └── browser.js ├── pkg.d.ts ├── src ├── tsconfig.json ├── cli.ts ├── index.ts └── core.ts ├── .release-it.json ├── tsconfig.base.json ├── scripts └── updateReadme.ts ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | pnpm 7.13.4 2 | python 3.10.5 3 | -------------------------------------------------------------------------------- /tests/fixtures/empty/input.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/empty/output.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/propsArray/input.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/tree-sitter-javascript.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjk/vue-o2c/HEAD/docs/tree-sitter-javascript.wasm -------------------------------------------------------------------------------- /docs/tree-sitter-typescript.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjk/vue-o2c/HEAD/docs/tree-sitter-typescript.wasm -------------------------------------------------------------------------------- /tests/fixtures/propsArray/output.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pkg.d.ts: -------------------------------------------------------------------------------- 1 | declare module "tree-sitter-javascript" 2 | declare module "tree-sitter-typescript" { 3 | const typescript: any 4 | } 5 | -------------------------------------------------------------------------------- /tests/fixtures/defineComponent/output.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/useSemisAndDoubleQuotes/output.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "target": "es2016", 5 | "module": "commonjs", 6 | }, 7 | } -------------------------------------------------------------------------------- /tests/fixtures/useSemisAndDoubleQuotes/input.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "before:git:release": "sed -i '' 's;vue-o2c@[0-9.]*;vue-o2c@${version};' docs/index.html && git add docs/index.html" 4 | } 5 | } -------------------------------------------------------------------------------- /tests/fixtures/emits/output.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/emits/input.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "moduleResolution": "node", 5 | "module": "esnext", 6 | "target": "es6", 7 | }, 8 | } -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": ".", 4 | "esModuleInterop": true, 5 | "noEmit": true, 6 | }, 7 | "include": ["tests/index.ts", "src", "pkg.d.ts"] 8 | } -------------------------------------------------------------------------------- /tests/fixtures/injectArray/input.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/injectArray/output.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/defineComponent/input.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/propTypes/output.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/propTypes/input.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/directives/output.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/directives/input.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/filters/output.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 18 | -------------------------------------------------------------------------------- /tests/fixtures/provideArray/input.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/provideArray/output.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/transformNode/output.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/filters/input.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /tests/fixtures/computedGetSet/output.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/transformNode/input.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import * as path from "path" 3 | import { transformPath } from "./index" 4 | 5 | if (!process.argv[2]) { 6 | console.error("usage: vue-o2c ") 7 | process.exit(1) 8 | } 9 | try { 10 | const state = transformPath(path.resolve(process.argv[2])) 11 | if (!state.transformed) { 12 | process.exit(1) 13 | } 14 | console.log(state.transformed) 15 | } catch (e) { 16 | console.error(e) 17 | process.exit(1) 18 | } -------------------------------------------------------------------------------- /tests/fixtures/computedGetSet/input.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/hooks/input.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs" 2 | import Parser from "tree-sitter" 3 | import javascript from "tree-sitter-javascript" 4 | import { typescript } from "tree-sitter-typescript" 5 | import { scan, transform as _transform, type State } from "./core" 6 | 7 | export function transformPath(sfcPath: string): State { 8 | const sfc = fs.readFileSync(sfcPath, "utf8") 9 | return transform(sfc) 10 | } 11 | 12 | export function transform(sfc: string): State { 13 | const state = scan(sfc) 14 | if (!state.scan.script) { 15 | return state 16 | } 17 | const parser = new Parser() 18 | parser.setLanguage(state.scan.scriptTs ? typescript : javascript) 19 | _transform(state, parser) 20 | return state 21 | } -------------------------------------------------------------------------------- /tests/fixtures/hooks/output.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scripts/updateReadme.ts: -------------------------------------------------------------------------------- 1 | import cp from "child_process" 2 | import fs from "fs" 3 | import { fileURLToPath } from "url" 4 | import path from "path" 5 | 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = path.dirname(__filename) 8 | 9 | const readmePath = path.resolve(__dirname, "../README.md") 10 | const data = fs.readFileSync(readmePath, "utf8") 11 | const lines = data.split("\n") 12 | 13 | let newLines: string[] = [] 14 | for (let i = 0; i < lines.length; i++) { 15 | const md = lines[i].match(/^```[a-z]+ (.*)$/) 16 | if (md) { 17 | newLines.push(lines[i]) 18 | do { 19 | i++ 20 | } while (!lines[i].match(/^```/)) 21 | newLines.push(String(cp.execSync(md[1]))) 22 | } 23 | newLines.push(lines[i]) 24 | } 25 | 26 | fs.writeFileSync(readmePath, newLines.join("\n")) -------------------------------------------------------------------------------- /tests/fixtures/example/output.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 64 | 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-o2c", 3 | "version": "0.1.26", 4 | "description": "Vue Options API to Composition API converter", 5 | "main": "dist/index.js", 6 | "module": "dist/index.mjs", 7 | "bin": "dist/cli.js", 8 | "types": "dist/index.d.ts", 9 | "files": [ 10 | "/dist" 11 | ], 12 | "scripts": { 13 | "build-tsup": "rm -rf dist && tsup src/index.ts src/cli.ts --format cjs,esm --dts", 14 | "build": "pnpm run build-tsup && chmod +x dist/cli.js && pnpm run build:browser", 15 | "build:browser": "esbuild --bundle --minify --format=esm --platform=browser src/core.ts --outfile=dist/browser.js", 16 | "prepublishOnly": "pnpm run test && pnpm run build", 17 | "update:readme": "tsx scripts/updateReadme.ts", 18 | "test": "tsx --tsconfig tests/tsconfig.json tests/index.ts" 19 | }, 20 | "keywords": [], 21 | "author": "TJ Koblentz", 22 | "license": "MIT", 23 | "devDependencies": { 24 | "@types/diff": "^5.0.3", 25 | "@types/node": "^18.11.3", 26 | "diff": "^5.1.0", 27 | "esbuild": "^0.15.12", 28 | "picocolors": "^1.0.0", 29 | "release-it": "^15.5.0", 30 | "tree-sitter-cli": "^0.20.7", 31 | "tsup": "^6.7.0", 32 | "tsx": "^3.10.4", 33 | "typescript": "^4.8.4", 34 | "web-tree-sitter": "^0.20.7" 35 | }, 36 | "dependencies": { 37 | "safe-identifier": "^0.4.2", 38 | "tree-sitter": "^0.20.0", 39 | "tree-sitter-javascript": "^0.19.0", 40 | "tree-sitter-typescript": "^0.20.1" 41 | }, 42 | "repository": { 43 | "type": "git", 44 | "url": "git+https://github.com/tjk/vue-o2c.git" 45 | }, 46 | "bugs": { 47 | "url": "https://github.com/tjk/vue-o2c/issues" 48 | }, 49 | "homepage": "https://github.com/tjk/vue-o2c#readme", 50 | "peerDependencies": { 51 | "web-tree-sitter": "^0.20.7" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/fixtures/example/input.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 74 | 75 | -------------------------------------------------------------------------------- /tests/fixtures/issue-8/output.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 77 | 78 | -------------------------------------------------------------------------------- /tests/fixtures/issue-8/input.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 94 | 95 | -------------------------------------------------------------------------------- /tests/index.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs" 2 | import path from "path" 3 | import { fileURLToPath } from "url" 4 | import { diffLines } from "diff" 5 | import { default as pc } from "picocolors" 6 | import { transformPath } from "../src/index" 7 | 8 | const __filename = fileURLToPath(import.meta.url); 9 | const __dirname = path.dirname(__filename) 10 | 11 | function printDiff(from: string, to: string) { 12 | const diff = diffLines(from, to) 13 | diff.forEach(part => { 14 | let color = part.added ? "green" : part.removed ? "red" : "gray" 15 | const md = part.value.match(/(\s*)$/) 16 | let printNewline = true 17 | if (md) { 18 | if (md.index) { 19 | process.stderr.write((pc as any)[color](part.value.substring(0, md.index))) 20 | } 21 | if (md[1].length && (color === "green" || color === "red")) { 22 | printNewline = false 23 | color = `bg${color[0].toUpperCase() + color.substring(1)}` 24 | // remove one of the newlines unless whole part is whitespace 25 | let str = md[1] 26 | if (!str.match(/\n\n$/) && !part.value.match(/^\s+$/)) { 27 | str = str.slice(0, str.length - 1) 28 | printNewline = true 29 | } 30 | if (str.length) { 31 | // \n\n\n will write 4 newlines (with first one not colored)!?!?!? 32 | str.split("").forEach((s: string) => { 33 | if (s === "\n") { 34 | process.stderr.write((pc as any)[color]("\\n")) 35 | process.stderr.write("\n") 36 | } else { 37 | process.stderr.write((pc as any)[color](s)) 38 | } 39 | }) 40 | } 41 | } 42 | } 43 | if (printNewline) { 44 | process.stderr.write("\n") 45 | } 46 | }) 47 | console.log() 48 | } 49 | 50 | function main(str?: string) { 51 | let failed = false 52 | const fixturesPath = path.resolve(__dirname, "./fixtures") 53 | for (const folder of fs.readdirSync(fixturesPath)) { 54 | if (str && !folder.includes(str)) { 55 | continue 56 | } 57 | console.log(`- ${folder}`) 58 | // error.txt or output.vue 59 | const inputPath = path.join(fixturesPath, folder, "input.vue") 60 | const outputPath = path.join(fixturesPath, folder, "output.vue") 61 | if (fs.existsSync(outputPath)) { 62 | const expected = fs.readFileSync(outputPath, "utf8") 63 | const state = transformPath(inputPath) 64 | if (!state.transformed) { 65 | console.error(pc.red("Not output when there should have been")) 66 | failed = true 67 | } else if (state.transformed !== expected) { 68 | console.log() 69 | printDiff(state.transformed, expected) 70 | console.log() 71 | // /* 72 | console.log("actual:") 73 | console.log() 74 | console.log(`|${pc.red(state.transformed)}|`) 75 | console.log() 76 | console.log("expected:") 77 | console.log() 78 | console.log(`|${pc.green(expected)}|`) 79 | console.log() 80 | // */ 81 | failed = true 82 | } 83 | } else { 84 | const outputPath = path.join(fixturesPath, folder, "output.vue") 85 | const expected = fs.readFileSync(outputPath, "utf8") 86 | let errorStack 87 | try { 88 | transformPath(inputPath) 89 | } catch (e) { 90 | errorStack = (e as any).stack 91 | } 92 | if (errorStack !== expected) { 93 | console.log() 94 | printDiff(errorStack, expected) 95 | console.log() 96 | failed = true 97 | } 98 | } 99 | } 100 | if (failed) { 101 | process.exit(1) 102 | } else { 103 | console.log() 104 | console.log("Tests succeeded!") 105 | } 106 | } 107 | 108 | try { 109 | main(process.argv[2]) 110 | } catch (e) { 111 | console.error(e) 112 | process.exit(1) 113 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-o2c - Vue Options API to Composition API 2 | 3 | **[Demo / Online Playground](https://tjk.github.io/vue-o2c/)** 4 | 5 | **WORK IN PROGRESS** -- the following is not done: 6 | 7 | - bunch of stuff still not implemented (working through case by case) 8 | - publish package correctly for CLI command to work (need to check) 9 | - data() preamble -- if there is preamble maybe just create refs then use the function to set them 10 | - handle setup() in options api 11 | - allow options to configure (eg. no typescript) 12 | - $el needs to try to rewrite part of template 13 | - would like to maintain indentation 14 | 15 | **After running, check for FIXME comments** 16 | 17 | Composition API does not allow easy access of `app.config.globalProperties` like options API does. 18 | vue-o2c takes care of some basic cases (eg. `this.$router` assuming vue-router) but for others, you will 19 | see comments like the following and you must adjust the code depending on how you provide these systems. 20 | 21 | ```typescript 22 | const $primevue = inject("primevue") /* FIXME vue-o2c */ 23 | ``` 24 | 25 | ## Usage 26 | 27 | ### via CLI 28 | 29 | *This is not working due to a publishing issue I need to fix...* 30 | 31 | ```bash 32 | $ npx vue-o2c /path/to/sfc.vue 33 | ... 34 | 35 | ... 36 | ``` 37 | 38 | ### Programmatically 39 | 40 | ```bash 41 | $ pnpm add -D vue-o2c 42 | ``` 43 | 44 | *Please keep in mind, API is very experimental and likely will change!* 45 | 46 | ```typescript 47 | import { transformPath, transform, type State } from "vue-o2c" 48 | 49 | const s1: State = transformPath("/path/to/sfc.vue") 50 | const s2: State = transform("") 51 | // s1.transformed and s2.transformed will both contained transformed code 52 | ``` 53 | 54 | ## Example 55 | 56 | Given the following file: 57 | 58 | ```vue cat tests/fixtures/example/input.vue 59 | 63 | 64 | 109 | 110 | 115 | ``` 116 | 117 | ```bash 118 | $ git clone git@github.com:tjk/vue-o2c.git 119 | $ cd vue-o2c 120 | $ pnpm i 121 | $ pnpm exec tsx index.ts ./example.vue 122 | ``` 123 | 124 | Will output the following: 125 | 126 | ```vue pnpm exec tsx src/cli.ts ./tests/fixtures/example/input.vue 127 | 131 | 132 | 175 | 176 | 181 | 182 | ``` -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 13 |

vue-o2c

14 |

Vue Options API to Composition API Converter · view on GitHub

15 |

Try editing the input below to see the transformations in the output.

16 |
17 |
18 |

Input

19 |
20 |
21 |
22 |

Output

23 |
24 |
25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 149 | 150 | 151 | -------------------------------------------------------------------------------- /docs/browser.js: -------------------------------------------------------------------------------- 1 | var __create = Object.create; 2 | var __defProp = Object.defineProperty; 3 | var __getOwnPropDesc = Object.getOwnPropertyDescriptor; 4 | var __getOwnPropNames = Object.getOwnPropertyNames; 5 | var __getProtoOf = Object.getPrototypeOf; 6 | var __hasOwnProp = Object.prototype.hasOwnProperty; 7 | var __commonJS = (cb, mod) => function __require() { 8 | return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; 9 | }; 10 | var __copyProps = (to, from, except, desc) => { 11 | if (from && typeof from === "object" || typeof from === "function") { 12 | for (let key of __getOwnPropNames(from)) 13 | if (!__hasOwnProp.call(to, key) && key !== except) 14 | __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); 15 | } 16 | return to; 17 | }; 18 | var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( 19 | isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, 20 | mod 21 | )); 22 | 23 | // node_modules/.pnpm/safe-identifier@0.4.2/node_modules/safe-identifier/reserved.js 24 | var require_reserved = __commonJS({ 25 | "node_modules/.pnpm/safe-identifier@0.4.2/node_modules/safe-identifier/reserved.js"(exports, module) { 26 | var ES3 = { 27 | break: true, 28 | continue: true, 29 | delete: true, 30 | else: true, 31 | for: true, 32 | function: true, 33 | if: true, 34 | in: true, 35 | new: true, 36 | return: true, 37 | this: true, 38 | typeof: true, 39 | var: true, 40 | void: true, 41 | while: true, 42 | with: true, 43 | case: true, 44 | catch: true, 45 | default: true, 46 | do: true, 47 | finally: true, 48 | instanceof: true, 49 | switch: true, 50 | throw: true, 51 | try: true 52 | }; 53 | var ESnext = { 54 | await: true, 55 | debugger: true, 56 | class: true, 57 | enum: true, 58 | extends: true, 59 | super: true, 60 | const: true, 61 | export: true, 62 | import: true, 63 | null: true, 64 | true: true, 65 | false: true, 66 | implements: true, 67 | let: true, 68 | private: true, 69 | public: true, 70 | yield: true, 71 | interface: true, 72 | package: true, 73 | protected: true, 74 | static: true 75 | }; 76 | module.exports = { ES3, ESnext }; 77 | } 78 | }); 79 | 80 | // node_modules/.pnpm/safe-identifier@0.4.2/node_modules/safe-identifier/index.mjs 81 | var import_reserved = __toESM(require_reserved(), 1); 82 | function hashCode(str) { 83 | let hash = 0; 84 | for (let i = 0; i < str.length; ++i) { 85 | const char = str.charCodeAt(i); 86 | hash = (hash << 5) - hash + char; 87 | hash |= 0; 88 | } 89 | return hash; 90 | } 91 | function identifier(key, unique) { 92 | if (unique) 93 | key += " " + hashCode(key).toString(36); 94 | const id = key.trim().replace(/\W+/g, "_"); 95 | return import_reserved.default.ES3[id] || import_reserved.default.ESnext[id] || /^\d/.test(id) ? "_" + id : id; 96 | } 97 | 98 | // src/core.ts 99 | function fail(msg, n) { 100 | throw new Error(`${msg}${n ? ` @ (${n.startPosition.row + 1}, ${n.startPosition.column + 1})` : ""}`); 101 | } 102 | function assert(v, msg, n) { 103 | if (!v) { 104 | fail(`assertion failed: ${msg}`, n); 105 | } 106 | } 107 | var DISCOVERED = ""; 108 | function scan(sfc) { 109 | const state = { 110 | extraScript: "", 111 | scan: {}, 112 | importNodes: [], 113 | hooks: {}, 114 | props: {}, 115 | propDefaultNodes: {}, 116 | refs: {}, 117 | computeds: {}, 118 | using: {}, 119 | nonRefs: /* @__PURE__ */ new Set(), 120 | methods: {}, 121 | watchers: {} 122 | }; 123 | const { scan: scan2 } = state; 124 | scan2.lines = sfc.split("\n"); 125 | for (let i = 0; i < scan2.lines.length; i++) { 126 | const line = scan2.lines[i]; 127 | if (line.match(/\n` 557 | transformed += lines.slice(scriptEndIdx + 1, lines.length).join("\n") 558 | } else { 559 | transformed += lines.slice(0, scriptStartIdx).join("\n") 560 | transformed += `\n\n` 561 | transformed += lines.slice(scriptEndIdx + 1, templateStartIdx + 1).join("\n") 562 | transformed += `\n${template}\n` 563 | transformed += lines.slice(templateEndIdx, lines.length).join("\n") 564 | } 565 | } else { 566 | transformed += [ 567 | lines.slice(0, scriptStartIdx).join("\n"), 568 | ``, 569 | lines.slice(scriptEndIdx + 1, lines.length).join("\n"), 570 | ].filter(Boolean).join("\n") 571 | } 572 | 573 | state.transformed = transformed 574 | 575 | return state 576 | } 577 | 578 | function linePrefixSpaces(line: string): number { 579 | const md = line.match(/^(\s*)/) 580 | return (md?.[1].length || 0) 581 | } 582 | 583 | // just relative -- find smallest indent and then normalize it 584 | // TODO assumes space indent 585 | function reindent(s: string, minIndentSpaces: number) { 586 | const lines = s.split("\n") 587 | let minLineSpaces = Infinity 588 | // TODO assumes first line is inline (not indented) 589 | for (let i = 1; i < lines.length; i++) { 590 | const line = lines[i] 591 | const lineSpaces = linePrefixSpaces(line) 592 | if (lineSpaces < minLineSpaces) { 593 | minLineSpaces = lineSpaces 594 | } 595 | } 596 | const spaceIndentChange = minIndentSpaces - minLineSpaces 597 | const ret: string[] = [lines[0]] 598 | for (let i = 1; i < lines.length; i++) { 599 | const line = lines[i] 600 | if (spaceIndentChange > 0) { 601 | ret.push(" ".repeat(spaceIndentChange) + line) 602 | } else { 603 | ret.push(line.slice(-spaceIndentChange)) 604 | } 605 | } 606 | return ret.join("\n") 607 | } 608 | 609 | function propTypeIdentifierToType(s: string) { 610 | switch (s) { 611 | case "Array": 612 | return "any[]" 613 | case "Boolean": 614 | return "boolean" 615 | case "Date": 616 | return "Date" 617 | case "Function": 618 | return "Function" 619 | case "Number": 620 | return "number" 621 | case "Object": 622 | return "object" 623 | case "String": 624 | return "string" 625 | case "Symbol": 626 | return "symbol" 627 | default: 628 | throw new Error(`unhandled prop type identifier: ${s}`) 629 | } 630 | } 631 | 632 | function handleArray(n: SyntaxNode, onElement: (n: SyntaxNode) => void) { 633 | for (const c of n.children) { 634 | switch (c.type) { 635 | case "[": 636 | case ",": 637 | case "]": 638 | break 639 | default: 640 | onElement(c) 641 | } 642 | } 643 | } 644 | 645 | function handlePropType(n?: SyntaxNode): string { 646 | let ret: string 647 | if (n?.type === "array") { 648 | const types: string[] = [] 649 | handleArray(n, (c: SyntaxNode) => { 650 | // TODO check what kind of node (assumes identifier) 651 | types.push(propTypeIdentifierToType(c.text)) 652 | }) 653 | ret = types.join(" | ") 654 | } else if (n?.type === "identifier") { 655 | ret = propTypeIdentifierToType(n.text) 656 | } else { 657 | fail(`prop value type not array or identifier: ${n?.text}`, n) 658 | } 659 | // TODO tag these to be touched up after 660 | // if (ret.match(/any/)) { 661 | // ret += " // TODO fix any" 662 | // } 663 | return ret 664 | } 665 | 666 | function handleProps(state: State, s: SyntaxNode, transformPass = true) { // ObjectNode or ArrayNode 667 | if (s.type === 'array') { 668 | handleArray(s, (n: SyntaxNode) => { 669 | state.props[n.text] = {type: 'any'}; 670 | }) 671 | return; 672 | } 673 | handleObject(s, { 674 | onKeyValue(propName: string, n: SyntaxNode) { 675 | switch (n.type) { 676 | case "identifier": 677 | // same as { type: ____ } value 678 | state.props[propName] = {type: handlePropType(n)} 679 | break 680 | case "object": 681 | if (n.text === "{}") { 682 | state.props[propName] = {type: "any"} // TODO tag to be fixed 683 | break 684 | } 685 | let propType: string | undefined 686 | let propRequired: true | undefined = undefined 687 | handleObject(n, { 688 | onKeyValue(key: string, n: SyntaxNode) { 689 | switch (key) { 690 | case "default": 691 | state.propDefaultNodes[propName] = n.text 692 | break 693 | case "type": 694 | propType = handlePropType(n) // TODO do not assume this node is identifier 695 | break 696 | case "required": 697 | if (n.text === "true") { 698 | propRequired = true 699 | } else if (n.text === "false") { 700 | // do nothing, optional by default 701 | } else { 702 | fail(`prop attribute required not true or false: ${n.text}`, n) 703 | } 704 | break 705 | default: 706 | fail(`prop attribute not handled: ${key}`, n) 707 | } 708 | }, 709 | onMethod(meth: string, async: boolean, args: SyntaxNode, block: SyntaxNode) { 710 | assert(meth === "default", `prop attribute method not named default: ${meth}`, n) 711 | assert(args.text === "()", `prop attribute method default has unexpected args: ${args.text}`, args) 712 | state.propDefaultNodes[propName] = `() => ${reindent(block.text, 2)}` 713 | }, 714 | }) 715 | if (propType) { 716 | state.props[propName] = {type: propType, required: propRequired} 717 | } 718 | break 719 | default: 720 | fail(`prop value not identifier or object: ${n.children[2].type}`, n.children[2]) 721 | } 722 | }, 723 | onMethod(meth: string, async: boolean, args: SyntaxNode, block: SyntaxNode) { 724 | fail(`unexpected prop method: ${meth}`, s) // XXX wrong syntax node here 725 | }, 726 | }) 727 | } 728 | 729 | type OnNode = (n: SyntaxNode) => void | false 730 | function bfs(n: SyntaxNode, onNode: OnNode) { 731 | const q = [n] 732 | while (q.length) { 733 | const c = q.shift()! 734 | const ret = onNode(c) 735 | if (ret !== false) { 736 | q.push(...c.children) 737 | } 738 | } 739 | } 740 | // ... 741 | function treeSelect(n: SyntaxNode, selectors: string[], onNode: OnNode) { 742 | const [selector, ...rest] = selectors 743 | bfs(n, c => { 744 | if (c.type === selector) { 745 | if (rest.length) { 746 | treeSelect(n, rest, onNode) 747 | } else { 748 | onNode(c) 749 | } 750 | return false 751 | } 752 | }) 753 | } 754 | 755 | function transformToken(state: State, token: string): string { 756 | // XXX should warn about this usage and fix... 757 | if (token === "$el") { 758 | state.using.$el = true 759 | return "$el.value" 760 | } 761 | if (["$emit", "$slots", "$attrs", "$router", "$route"].includes(token)) { 762 | (state.using as any)[token] = true // XXX fix ts 763 | return token 764 | } 765 | // this.$refs.display -> display.value and add display to state.refs 766 | // TODO need to support this.$refs['a-not-safe-ident'] (use something like $this ??) 767 | if (token.match(/\$refs\./)) { 768 | const name = token.slice("$refs.".length) 769 | state.refs[name] = "" // default to undefined (), but TODO would like to type template refs better! 770 | return `${name}.value` 771 | } 772 | if (token.startsWith("$")) { 773 | // TODO need to supply a config of how to get prototype -- eg: 774 | // this.$sentry -> {$sentry: 'inject("$sentry")'}, etc. 775 | // XXX warn while transforming? 776 | const v = token.slice(1) 777 | state.using.injects[v] = {str: v, warn: true} 778 | return token 779 | } 780 | assert(!token.startsWith("$"), `config needed to determine how to replace global property: ${token}`) 781 | assert(token === identifier(token), `unsafe identifier not supported: ${token}`) 782 | if (state.props[token]) { 783 | state.using.props = true 784 | return `props.${token}` 785 | } 786 | if (state.computeds[token] || state.refs[token]) { 787 | return `${token}.value` 788 | } 789 | if (state.methods[token]) { 790 | return token 791 | } 792 | if (state.using.injects[token]) { 793 | return `$${token}` // convention 794 | } 795 | state.nonRefs.add(token) 796 | return `$this.${token}` 797 | } 798 | 799 | function getMember(c: SyntaxNode): string | undefined { 800 | if (c.type === "member_expression") { 801 | const c1 = c.children[1] 802 | const c2 = c.children[2] 803 | assert(c1.type === "." && c2.type === "property_identifier", "expected simple member expression", c) 804 | return c.children[2].text 805 | } 806 | if (c.type === "subscript_expression") { 807 | const c1 = c.children[1] 808 | const c2 = c.children[2] 809 | const c3 = c.children[3] 810 | assert(c1.type === "[" && c2.type === "string" && c3.type === "]", "expected simple subscript expression", c) 811 | return c2.text.slice(1, c2.text.length - 1) 812 | } 813 | } 814 | 815 | function transformNode(state: State, n: SyntaxNode) { 816 | const replacements: { 817 | startIndex: number, 818 | endIndex: number, 819 | value: string, 820 | }[] = [] 821 | const handleThisKey = (name: string, startIndex: number, endIndex: number) => { 822 | const pushReplacement = (value: string) => { 823 | replacements.push({ 824 | startIndex: startIndex - n.startIndex, 825 | endIndex: endIndex - n.startIndex, 826 | value, 827 | }) 828 | } 829 | if (name === "$nextTick") { 830 | state.using.nextTick = true 831 | pushReplacement("nextTick") 832 | return 833 | } 834 | // do not include nextTick in this (as it is used for watch key source transform) 835 | const rep = transformToken(state, name) 836 | pushReplacement(rep) 837 | } 838 | // want to preserve whitespace so i think strat should be start from text but navigate nodes and then replace 839 | bfs(n, (c: SyntaxNode) => { 840 | if (c.type === "this") { 841 | const p1 = c.parent 842 | const m1 = getMember(p1) 843 | if (m1 === "$refs") { 844 | const p2 = p1.parent 845 | assert(p2.type === "member_expression", "expected this.$refs parent to be member expression", c) 846 | const m2 = getMember(p2) 847 | // because next line does $refs. and don't have $this style solution for $refs yet 848 | assert(m2 === identifier(m2), "template ref does not have safe identifier", p2) 849 | handleThisKey(`$refs.${m2}`, p2.startIndex, p2.endIndex) 850 | } else if (m1) { 851 | handleThisKey(m1, p1.startIndex, p1.endIndex) 852 | } 853 | if (m1 === "$emit") { 854 | // collect first arg of this.$emit as well for using.emits 855 | let foundEmit = false 856 | let n: SyntaxNode | null = c 857 | // look up the tree until call_expression 858 | while (n) { 859 | if (n.type === "call_expression") { 860 | for (const c of n.children) { 861 | if (c.type === "arguments") { 862 | assert(c.children[0].type === "(", "expected parent after $emit", c) 863 | assert(c.children[1].type === "string", "expected string as first $emit arg", c) 864 | state.using.emits.add(c.children[1].text) 865 | foundEmit = true 866 | } 867 | } 868 | break 869 | } 870 | n = n.parent 871 | } 872 | assert(foundEmit, "could not find emit to define", c) 873 | } 874 | } 875 | }) 876 | const sortedReplacements = replacements.sort((a, b) => a.startIndex - b.startIndex) 877 | let ret = "" 878 | let idx = 0 879 | for (let i = 0; i < sortedReplacements.length; i++) { 880 | const r = sortedReplacements[i] 881 | ret += n.text.substring(idx, r.startIndex) + r.value 882 | idx = r.endIndex 883 | } 884 | ret += n.text.substring(idx) 885 | return ret 886 | } 887 | 888 | function handleComputeds(state: State, n: SyntaxNode, transformPass = true) { 889 | handleObject(n, { 890 | onKeyValue(key: string, n: SyntaxNode) { 891 | if (!transformPass) { 892 | state.computeds[key] = DISCOVERED 893 | return 894 | } 895 | switch (n.type) { 896 | case "object": 897 | // XXX check that just get/set? 898 | const computedString = transformNode(state, n) 899 | state.computeds[key] = reindent(computedString, 0) 900 | break 901 | default: 902 | fail(`computed object unexpected key: ${key}`, n) 903 | } 904 | }, 905 | onMethod(meth: string, async: boolean, args: SyntaxNode, block: SyntaxNode) { 906 | assert(!async, "computed async method unexpected", block) // XXX wrong syntax node 907 | if (transformPass) { 908 | const computedString = transformNode(state, block) 909 | assert(args.text === "()", `computed method has unexpected args: ${args.text}`, args) 910 | state.computeds[meth] = `() => ${reindent(computedString, 0)}` 911 | } else { 912 | state.computeds[meth] = DISCOVERED 913 | } 914 | }, 915 | }) 916 | } 917 | 918 | function handleMethodKeyValue(key: string, n: SyntaxNode) { 919 | // (async) formal_paramaters => statement_block 920 | let i = 0 921 | let async = false 922 | let args = "()" 923 | if (n.children[i].type === "async") { 924 | async = true 925 | i++ 926 | } 927 | if (n.children[i].type === "formal_parameters") { 928 | args = n.children[i].text 929 | i++ 930 | } 931 | assert(n.children[i].type === "=>", "expected =>", n.children[i]) 932 | i++ 933 | assert(n.children[i].type === "statement_block", "expected statement block", n.children[i]) 934 | const statement = n.children[i] 935 | return { 936 | async, 937 | args, 938 | statement, 939 | } 940 | } 941 | 942 | function handleMethods(state: State, n: SyntaxNode, transformPass = true) { 943 | handleObject(n, { 944 | onKeyValue(key, n) { 945 | if (!transformPass) { 946 | state.methods[key] = DISCOVERED 947 | } else { 948 | const { async, args, statement } = handleMethodKeyValue(key, n) 949 | state.methods[key] = `${async ? 'async ' : ''}function ${key}${args} ${reindent(transformNode(state, statement), 0)}` 950 | } 951 | }, 952 | onMethod(meth: string, async: boolean, args: SyntaxNode, block: SyntaxNode) { 953 | if (transformPass) { 954 | state.methods[meth] = `${async ? 'async ' : ''}function ${meth}${args.text} ${reindent(transformNode(state, block), 0)}` 955 | } else { 956 | state.methods[meth] = DISCOVERED 957 | } 958 | }, 959 | }) 960 | } 961 | 962 | function handleWatchers(state: State, n: SyntaxNode, transformPass = true) { 963 | if (!transformPass) { 964 | // cannot refer to watchers so need to discover them 965 | return 966 | } 967 | handleObject(n, { 968 | onKeyValue(key: string, n: SyntaxNode) { 969 | switch (n.type) { 970 | case "object": 971 | const watch: WatchConfig = {} 972 | handleObject(n, { 973 | onKeyValue(key: string, n: SyntaxNode) { 974 | switch (key) { 975 | case "deep": 976 | watch.deep = n.text 977 | break 978 | case "handler": 979 | watch.handler = n.text 980 | break 981 | case "immediate": 982 | watch.immediate = n.text 983 | break 984 | default: 985 | fail(`unexpected watch value attribute: ${key}`, n) 986 | } 987 | }, 988 | onMethod(meth: string, async: boolean, args: SyntaxNode, block: SyntaxNode) { 989 | watch.handler = `${async ? 'async ' : ''}${args.text} => ${reindent(transformNode(state, block), 0)}` 990 | }, 991 | }) 992 | const ds = key.split('.', 2) 993 | state.watchers[transformToken(state, ds[0]) + (ds[1] ? `.${ds[1]}` : '')] = watch 994 | break 995 | default: 996 | fail(`unexpected watch value type (not method or object): ${n.type}`, n) 997 | } 998 | }, 999 | onMethod(meth: string, async: boolean, args: SyntaxNode, block: SyntaxNode) { 1000 | const watch: WatchConfig = {} 1001 | watch.handler = `${async ? 'async ' : ''}${args.text} => ${reindent(transformNode(state, block), 0)}` 1002 | const ds = meth.split('.', 2) 1003 | state.watchers[transformToken(state, ds[0]) + (ds[1] ? `.${ds[1]}` : '')] = watch 1004 | }, 1005 | }) 1006 | } 1007 | 1008 | function handleDirectives(state: State, n: SyntaxNode, transformPass = true) { 1009 | if (!transformPass) { 1010 | // cannot refer to directives so need to discover them 1011 | return 1012 | } 1013 | // directives: { 1014 | // a: someDirective, // -> vA 1015 | // b: { created, ... } // -> vB 1016 | // } 1017 | handleObject(n, { 1018 | onKeyValue(key, n) { 1019 | state.directives[key] = reindent(transformNode(state, n), 0) 1020 | }, 1021 | onMethod(meth: string, async: boolean, args: SyntaxNode, block: SyntaxNode) { 1022 | assert(false, "expected only key-value directives object", n) 1023 | }, 1024 | }) 1025 | } 1026 | 1027 | function handleComponents(state: State, n: SyntaxNode, transformPass = true) { 1028 | if (transformPass) { 1029 | // nothing to transform 1030 | return 1031 | } 1032 | handleObject(n, { 1033 | onKeyValue(key, n) { 1034 | state.components[key] = reindent(n.text, 0) 1035 | }, 1036 | onMethod(meth: string, async: boolean, args: SyntaxNode, block: SyntaxNode) { 1037 | assert(false, "expected only key-value components object", n) 1038 | }, 1039 | }) 1040 | } 1041 | 1042 | function handleDefaultExportKeyValue(state: State, key: string, n: SyntaxNode, transformPass = true) { 1043 | switch (key) { 1044 | case "components": 1045 | // we don't need this now! but we should do better and remove/rewrite imports to use components.d.ts 1046 | handleComponents(state, n, transformPass) 1047 | break 1048 | case "computed": 1049 | handleComputeds(state, n, transformPass) 1050 | break 1051 | case "directives": 1052 | handleDirectives(state, n, transformPass) 1053 | break 1054 | case "emits": 1055 | assert(n.type === "array", "expected emits to be an array", n) 1056 | handleArray(n, c => { 1057 | assert(c.type === "string", "expected emits to be array of simple strings", c) 1058 | state.using.emits.add(c.text) 1059 | }) 1060 | break 1061 | case "filters": 1062 | assert(n.type === "object", "expected filters to be an object", n) 1063 | handleObject(n, { 1064 | onKeyValue(key, c) { 1065 | if (!transformPass) { 1066 | state.filters[key] = DISCOVERED 1067 | } else { 1068 | const { async, args, statement } = handleMethodKeyValue(key, c) 1069 | state.filters[key] = `${async ? 'async ' : ''}function ${key}${args} ${reindent(transformNode(state, statement), 0)}` 1070 | } 1071 | }, 1072 | onMethod(meth, async, args, block) { 1073 | if (!transformPass) { 1074 | state.filters[meth] = DISCOVERED 1075 | } else { 1076 | state.filters[meth] = `${async ? 'async ' : ''}function ${meth}${args.text} ${reindent(transformNode(state, block), 0)}` 1077 | } 1078 | }, 1079 | }) 1080 | break 1081 | case "inject": 1082 | if (n.type === "array") { 1083 | handleArray(n, c => { 1084 | assert(c.type === "string", "expected inject to be array of simple strings", c) 1085 | const v = c.text.slice(1, c.text.length - 1) 1086 | state.using.injects[v] = {str: v} 1087 | }) 1088 | } else if (n.type === "object") { 1089 | fail("inject object not supported yet") 1090 | } else { 1091 | fail("expected inject to be an array or object", n) 1092 | } 1093 | break 1094 | case "methods": 1095 | handleMethods(state, n, transformPass) 1096 | break 1097 | case "name": 1098 | // do nothing with this... 1099 | break 1100 | case "props": 1101 | assert(n.type === "object" || n.type === "array", `expected props to be an object or array: ${n.type}`, n) 1102 | handleProps(state, n, transformPass) 1103 | break 1104 | case "provide": 1105 | assert(n.type === "array", "expected provide to be an array", n) 1106 | handleArray(n, c => { 1107 | assert(c.type === "string", "expected provide to be array of simple strings", c) 1108 | state.using.provides.add(c.text.slice(1, c.text.length - 1)) 1109 | }) 1110 | break 1111 | case "watch": 1112 | handleWatchers(state, n, transformPass) 1113 | break 1114 | default: 1115 | fail(`export default key value not supported: ${key}`, n) 1116 | } 1117 | } 1118 | 1119 | function handleDataMethod(state: State, n: SyntaxNode, transformPass = true) { 1120 | for (const c of n.children) { 1121 | switch (c.type) { 1122 | case "{": 1123 | case "}": 1124 | case "comment": 1125 | break 1126 | case "return_statement": 1127 | if (c.children[1]?.type === "object") { 1128 | // simple version, we can just make naked refs 1129 | // input: 1130 | // data() { 1131 | // return { a: "hi" } 1132 | // } 1133 | // output: 1134 | // const a = ref("hi") 1135 | handleObject(c.children[1], { 1136 | onKeyValue(key: string, n: SyntaxNode) { 1137 | if (!transformPass) { 1138 | state.refs[key] = DISCOVERED 1139 | } else { 1140 | state.refs[key] = reindent(transformNode(state, n), 0) 1141 | } 1142 | }, 1143 | onMethod(meth: string, async: boolean, args: SyntaxNode, block: SyntaxNode) { 1144 | fail(`data() return object method key not supported: ${meth}`, block) // XXX wrong syntax node 1145 | }, 1146 | }) 1147 | break 1148 | } 1149 | /* fall-through */ 1150 | default: 1151 | // there might be a premable, we preserve entire function and then assign the returned object to refs in $data 1152 | // input: 1153 | // data() { 1154 | // console.log("hi") 1155 | // const ret = {}; ret.yo = "hi"; return ret 1156 | // } 1157 | // output: 1158 | // const $data = Object.entries((() => { 1159 | // console.log("hi") 1160 | // const ret = {}; ret.yo = "hi"; return ret 1161 | // })()).reduce((acc, [k, v]) => { 1162 | // acc[k] = ref(v) 1163 | // return acc 1164 | // }, {}) 1165 | // TODO we need to rewrite template in this case :/ 1166 | // we can try to find strings in the method block but it can be fully dynamic :/ 1167 | // state.using.$data = n.text 1168 | fail("complex data() not supported", c) 1169 | } 1170 | } 1171 | } 1172 | 1173 | function handleDefaultExportMethod(state: State, meth: string, async: boolean, args: SyntaxNode, block: SyntaxNode, transformPass = true) { 1174 | switch (meth) { 1175 | case "data": 1176 | handleDataMethod(state, block, transformPass) 1177 | break 1178 | case "beforeCreate": 1179 | case "created": 1180 | if (transformPass) { 1181 | assert(args.text === "()", `${meth} hook method has unexpected args: ${args.text}`, args) 1182 | assert(block.children[0]?.type === "{", "expected open brace in block", block) 1183 | assert(block.children[2]?.type === "}", "expected close brace in block", block) 1184 | state.hooks[meth] = reindent(transformNode(state, block.children[1]), 0) 1185 | } 1186 | break 1187 | case "beforeMount": 1188 | case "mounted": 1189 | case "beforeUpdate": // XXX arg? 1190 | case "updated": // XXX arg? 1191 | case "beforeDestroy": 1192 | case "destroyed": 1193 | if (transformPass) { 1194 | assert(args.text === "()", `${meth} hook method has unexpected args: ${args.text}`, args) 1195 | assert(block.children[0]?.type === "{", "expected open brace in block", block) 1196 | assert(block.children[block.children.length - 1]?.type === "}", "expected close brace in block", block) 1197 | state.hooks[meth] = `${async ? 'async ' : ''}() => ${reindent(transformNode(state, block), 0)}` 1198 | } 1199 | break 1200 | case "errorCaptured": 1201 | if (transformPass) { 1202 | assert(args.children[0].type === "(", "expected open paren in args", args) 1203 | assert(args.children[2].type === ")", "expected open paren in args", args) 1204 | assert(args.children[1].type === "identifier", "expected identifier in args", args) 1205 | assert(block.children[0]?.type === "{", "expected open brace in block", block) 1206 | assert(block.children[2]?.type === "}", "expected close brace in block", block) 1207 | state.hooks.errorCaptured = `${async ? 'async ' : ''}(${args.children[1].text}: Error) => ${reindent(transformNode(state, block), 0)}` 1208 | } 1209 | break 1210 | case "provide": 1211 | fail("provide() not supported yet") 1212 | default: 1213 | // TODO other hooks destroyed, etc. 1214 | fail(`export default key method not supported: ${meth}`, block) // XXX wrong syntax node 1215 | } 1216 | } 1217 | 1218 | type HandleObjectHooks = { 1219 | onKeyValue?: (key: string, n: SyntaxNode) => void 1220 | onMethod?: (meth: string, async: boolean, args: SyntaxNode, block: SyntaxNode) => void 1221 | } 1222 | 1223 | function handleObject(object: SyntaxNode, hooks: HandleObjectHooks) { // ObjectNode 1224 | for (const c of object.children) { 1225 | if (c.type === "pair") { 1226 | const n = c.children[0] 1227 | let key 1228 | if (n?.type === "property_identifier") { 1229 | key = n.text 1230 | } else if (n?.type === "string") { 1231 | key = n.text.slice(1, -1) // XXX might have to do some harder processing 1232 | } 1233 | assert(key, `pair[0] not supported: ${c.children[0].type}`, c.children[0]) 1234 | assert(c.children[1]?.type === ":", `pair[1] not ":": ${c.children[1].type}`, c.children[1]) 1235 | assert(c.children[2], "pair has no 3nd child", c) 1236 | hooks.onKeyValue?.(key, c.children[2]) 1237 | } else if (c.type === "method_definition") { 1238 | let meth: string | undefined 1239 | let async = false 1240 | let args: SyntaxNode | undefined 1241 | let block: SyntaxNode | undefined 1242 | for (const n of c.children) { 1243 | switch (n.type) { 1244 | case "async": 1245 | async = true 1246 | break 1247 | case "computed_property_name": 1248 | // eg. ["test"]() {} 1249 | if (n.children[0]?.type === "[" && n.children[2]?.type === "]" && n.children[1]?.type === "string") { 1250 | meth = n.text.slice(2, -2) 1251 | break 1252 | } 1253 | fail(`unhandled method_definition structure, found: ${n.type}`, n) 1254 | case "property_identifier": 1255 | meth = n.text 1256 | break 1257 | case "statement_block": 1258 | block = n 1259 | break 1260 | case "formal_parameters": 1261 | args = n 1262 | break 1263 | default: 1264 | fail(`unhandled method_definition structure, found: ${n.type}`, n) 1265 | } 1266 | } 1267 | assert(meth && args && block, "did not find required nodes for method_definition", c) 1268 | hooks.onMethod?.(meth!, async, args!, block!) // fix ts 1269 | } else if (c.type === "comment") { 1270 | // TODO preserve these -- onComment 1271 | } else if (c.type === "{" || c.type === "," || c.type === "}") { 1272 | // do nothing 1273 | } else { 1274 | fail(`unexpected node found while parsing object: ${c.type}`, c) 1275 | } 1276 | } 1277 | } 1278 | 1279 | function handleDefaultExport(state: State, n: SyntaxNode) { 1280 | let transformPass = false 1281 | handleObject(n, { 1282 | onKeyValue: (key: string, n: SyntaxNode) => handleDefaultExportKeyValue(state, key, n, transformPass), 1283 | onMethod: (meth: string, async: boolean, args: SyntaxNode, block: SyntaxNode) => 1284 | handleDefaultExportMethod(state, meth, async, args, block, transformPass), 1285 | }) 1286 | transformPass = true 1287 | handleObject(n, { 1288 | onKeyValue: (key: string, n: SyntaxNode) => handleDefaultExportKeyValue(state, key, n, transformPass), 1289 | onMethod: (meth: string, async: boolean, args: SyntaxNode, block: SyntaxNode) => 1290 | handleDefaultExportMethod(state, meth, async, args, block, transformPass), 1291 | }) 1292 | } 1293 | 1294 | function maybeHandleDefaultExport(state: State, n: SyntaxNode): boolean { 1295 | let defaultExport = false 1296 | for (const c1 of n.children) { 1297 | if (defaultExport) { 1298 | if (c1.type === "object") { 1299 | handleDefaultExport(state, c1) 1300 | return true 1301 | } 1302 | if (c1.type === "call_expression") { 1303 | const c2 = c1.children[0] 1304 | if (c2.type === "identifier" && c2.text === "defineComponent") { 1305 | const c3 = c1.children[1] 1306 | if (c3.type === "arguments") { 1307 | if (c3.children[0].type === "(" && c3.children[2].type === ")" && c3.children[1].type === "object") { 1308 | handleDefaultExport(state, c3.children[1]) 1309 | return true 1310 | } 1311 | } 1312 | } 1313 | } 1314 | fail("unexpected default export", c1) 1315 | } 1316 | if (c1.text === "default") { 1317 | defaultExport = true 1318 | } 1319 | } 1320 | return false 1321 | } --------------------------------------------------------------------------------