├── .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 |
2 | {{concat(uppercase(hello), other)}}
3 |
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 |
2 | {{hello | uppercase | concat(other)}}
3 |
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 |
2 | div(ref="$el")
3 | p Wonderful
4 | RewrittenComponentName(prop="value")
5 |
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 |
2 | div
3 | p Wonderful
4 | RewrittenComponentName(prop="value")
5 |
6 |
7 |
74 |
75 |
--------------------------------------------------------------------------------
/tests/fixtures/issue-8/output.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
77 |
78 |
--------------------------------------------------------------------------------
/tests/fixtures/issue-8/input.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
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 |
60 | div
61 | p Wonderful
62 |
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 |
128 | div(ref="$el")
129 | p Wonderful
130 |
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 |
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 | }
--------------------------------------------------------------------------------