├── example
└── src
│ ├── views
│ ├── 404.vue
│ ├── 401.vue
│ └── Home.vue
│ ├── components
│ ├── Header.vue
│ └── Tab.vue
│ └── App.vue
├── .npmrc
├── playground
├── env.d.ts
├── .vscode
│ └── extensions.json
├── public
│ └── favicon.ico
├── src
│ ├── shims.d.ts
│ ├── constants
│ │ └── index.ts
│ ├── assets
│ │ ├── logo.svg
│ │ ├── main.css
│ │ └── base.css
│ ├── composables
│ │ ├── encode.ts
│ │ ├── store.ts
│ │ └── transform.ts
│ ├── components
│ │ ├── Share.vue
│ │ ├── Monaco.vue
│ │ ├── Edit.vue
│ │ ├── View.vue
│ │ └── Header.vue
│ ├── App.vue
│ └── main.ts
├── .prettierrc.json
├── tsconfig.node.json
├── index.html
├── .eslintrc.cjs
├── tsconfig.json
├── .gitignore
├── package.json
├── README.md
└── vite.config.ts
├── pnpm-workspace.yaml
├── bin
└── tosetup.mjs
├── src
├── transform
│ ├── index.ts
│ ├── sfc.ts
│ ├── components.ts
│ ├── expose.ts
│ ├── directives.ts
│ ├── attrsAndSlots.ts
│ ├── emits.ts
│ ├── utils.ts
│ ├── props.ts
│ └── script.ts
├── index.ts
├── writeFile.ts
├── constants.ts
├── utils.ts
└── setup.ts
├── .gitignore
├── test
├── fixtures
│ ├── tosetup.config.b.ts
│ └── tosetup.config.a.ts
├── index.test.ts
└── utils.ts
├── tsconfig.json
├── rome.json
├── .vscode
└── settings.json
├── .github
└── workflows
│ ├── npm-publish.yml
│ ├── deploy.yml
│ ├── release.yml
│ └── ci.yml
├── LICENSE
├── package.json
├── CHANGELOG.md
└── README.md
/example/src/views/404.vue:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/example/src/components/Header.vue:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/example/src/components/Tab.vue:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | ignore-workspace-root-check=true
2 |
--------------------------------------------------------------------------------
/playground/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - playground
3 | - examples/*
4 |
--------------------------------------------------------------------------------
/bin/tosetup.mjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | "use strict";
3 | import "../dist/setup.mjs";
4 |
--------------------------------------------------------------------------------
/playground/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
3 | }
4 |
--------------------------------------------------------------------------------
/playground/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/a145789/vue3-script-to-setup/HEAD/playground/public/favicon.ico
--------------------------------------------------------------------------------
/src/transform/index.ts:
--------------------------------------------------------------------------------
1 | import transformSfc from "./sfc";
2 | import transformScript from "./script";
3 |
4 | export { transformSfc, transformScript };
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .cache
2 | .DS_Store
3 | .idea
4 | *.log
5 | *.tgz
6 | coverage
7 | dist
8 | lib-cov
9 | logs
10 | node_modules
11 | temp
12 | **/*.new.vue
13 |
--------------------------------------------------------------------------------
/playground/src/shims.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.vue' {
2 | import { type DefineComponent } from 'vue'
3 | const component: DefineComponent<{}, {}, any>
4 | export default component
5 | }
6 |
--------------------------------------------------------------------------------
/playground/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/prettierrc",
3 | "semi": false,
4 | "tabWidth": 2,
5 | "singleQuote": true,
6 | "printWidth": 100,
7 | "trailingComma": "none"
8 | }
--------------------------------------------------------------------------------
/playground/src/constants/index.ts:
--------------------------------------------------------------------------------
1 | export const TEXT_COLOR = "text-#444 dark:text-#ddd";
2 |
3 | export enum CodeType {
4 | SFC = "sfc",
5 | SCRIPT = "script",
6 | }
7 |
8 | export const ICON_SIZE = 20 as const;
9 |
--------------------------------------------------------------------------------
/test/fixtures/tosetup.config.b.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "../../src";
2 |
3 | export default defineConfig({
4 | propsNotOnlyTs: true,
5 | path: {
6 | "example/src": {
7 | mode: "**",
8 | excludes: ["views/Home.vue"],
9 | },
10 | },
11 | });
12 |
--------------------------------------------------------------------------------
/playground/src/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/playground/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@vue/tsconfig/tsconfig.dom.json",
3 | "include": [
4 | "vite.config.*",
5 | "vitest.config.*",
6 | "cypress.config.*",
7 | "playwright.config.*"
8 | ],
9 | "compilerOptions": {
10 | "composite": true,
11 | "types": [
12 | "node"
13 | ]
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { DefaultOption } from "./constants";
2 |
3 | export { transformSfc, transformScript } from "./transform";
4 |
5 | export type { Output, SfcOptions, ScriptOptions } from "./constants";
6 |
7 | export { FileType } from "./constants";
8 |
9 | export function defineConfig(option: DefaultOption) {
10 | return option;
11 | }
12 |
--------------------------------------------------------------------------------
/playground/src/composables/encode.ts:
--------------------------------------------------------------------------------
1 | export function utoa(data: string): string {
2 | if (!data) {
3 | return "";
4 | }
5 | return btoa(unescape(encodeURIComponent(data)));
6 | }
7 |
8 | export function atou(base64: string): string {
9 | if (!base64) {
10 | return "";
11 | }
12 | return decodeURIComponent(escape(atob(base64)));
13 | }
14 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2018",
4 | "module": "esnext",
5 | "lib": ["esnext"],
6 | "moduleResolution": "node",
7 | "esModuleInterop": true,
8 | "strict": true,
9 | "strictNullChecks": true,
10 | "resolveJsonModule": true,
11 | "skipLibCheck": true,
12 | "skipDefaultLibCheck": true
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/test/fixtures/tosetup.config.a.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "../../src";
2 |
3 | export default defineConfig({
4 | path: {
5 | "example/src": {
6 | mode: "*",
7 | excludes: [],
8 | },
9 | "example/src/components": {
10 | mode: "*",
11 | excludes: "Header.vue",
12 | },
13 | "example/src/views": ["404.vue"],
14 | },
15 | });
16 |
--------------------------------------------------------------------------------
/playground/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | script to setup
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/playground/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | require('@rushstack/eslint-patch/modern-module-resolution')
3 |
4 | module.exports = {
5 | root: true,
6 | 'extends': [
7 | 'plugin:vue/vue3-essential',
8 | 'eslint:recommended',
9 | '@vue/eslint-config-typescript',
10 | '@vue/eslint-config-prettier/skip-formatting'
11 | ],
12 | parserOptions: {
13 | ecmaVersion: 'latest'
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/rome.json:
--------------------------------------------------------------------------------
1 | {
2 | "linter": {
3 | "ignore": ["./node_modules/**"],
4 | "enabled": true,
5 | "rules": {
6 | "recommended": true,
7 | "correctness": {
8 | "noUnusedVariables": "error"
9 | },
10 | "suspicious": {
11 | "noExplicitAny": "warn"
12 | }
13 | }
14 | },
15 | "formatter": {
16 | "indentStyle": "space",
17 | "indentSize": 2
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/writeFile.ts:
--------------------------------------------------------------------------------
1 | import { writeFileSync } from "fs";
2 | import { CommandsOption } from "./constants";
3 |
4 | function writeFile(
5 | code: string,
6 | path: string,
7 | { notUseNewFile }: CommandsOption,
8 | ) {
9 | const index = path.indexOf(".vue");
10 | const file = notUseNewFile ? path : `${path.slice(0, index)}.new.vue`;
11 |
12 | writeFileSync(file, code);
13 |
14 | return file;
15 | }
16 |
17 | export default writeFile;
18 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "prettier.enable": false,
3 | "eslint.enable": false,
4 | "editor.formatOnSave": true,
5 | "editor.codeActionsOnSave": {
6 | "source.fixAll.rome": true
7 | },
8 | "[typescript]": {
9 | "editor.defaultFormatter": "rome.rome"
10 | },
11 | "[javascript]": {
12 | "editor.defaultFormatter": "rome.rome"
13 | },
14 | "[vue]": {
15 | "editor.defaultFormatter": "Vue.volar"
16 | },
17 | "files.eol": "\n"
18 | }
19 |
--------------------------------------------------------------------------------
/playground/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@vue/tsconfig/tsconfig.dom.json",
3 | "include": [
4 | "env.d.ts",
5 | "src/**/*",
6 | "src/**/*.vue",
7 | "components.d.ts",
8 | "auto-imports.d.ts"
9 | ],
10 | "compilerOptions": {
11 | "baseUrl": ".",
12 | "paths": {
13 | "@/*": [
14 | "./src/*"
15 | ]
16 | }
17 | },
18 | "references": [
19 | {
20 | "path": "./tsconfig.node.json"
21 | }
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/playground/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | .DS_Store
12 | dist
13 | dist-ssr
14 | coverage
15 | *.local
16 |
17 | /cypress/videos/
18 | /cypress/screenshots/
19 |
20 | # Editor directories and files
21 | .vscode/*
22 | !.vscode/extensions.json
23 | .idea
24 | *.suo
25 | *.ntvs*
26 | *.njsproj
27 | *.sln
28 | *.sw?
29 |
30 | components.d.ts
31 | auto-imports.d.ts
32 |
--------------------------------------------------------------------------------
/example/src/App.vue:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 | App
21 |
22 |
--------------------------------------------------------------------------------
/playground/src/components/Share.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
16 |
--------------------------------------------------------------------------------
/playground/src/assets/main.css:
--------------------------------------------------------------------------------
1 | @import './base.css';
2 |
3 | #app {
4 | max-width: 1280px;
5 | margin: 0 auto;
6 | padding: 2rem;
7 |
8 | font-weight: normal;
9 | }
10 |
11 | a,
12 | .green {
13 | text-decoration: none;
14 | color: hsla(160, 100%, 37%, 1);
15 | transition: 0.4s;
16 | }
17 |
18 | @media (hover: hover) {
19 | a:hover {
20 | background-color: hsla(160, 100%, 37%, 0.2);
21 | }
22 | }
23 |
24 | @media (min-width: 1024px) {
25 | body {
26 | display: flex;
27 | place-items: center;
28 | }
29 |
30 | #app {
31 | display: grid;
32 | grid-template-columns: 1fr 1fr;
33 | padding: 0 2rem;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/playground/src/App.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
32 |
33 |
--------------------------------------------------------------------------------
/example/src/views/401.vue:
--------------------------------------------------------------------------------
1 |
30 |
31 |
32 | App
33 |
34 |
--------------------------------------------------------------------------------
/example/src/views/Home.vue:
--------------------------------------------------------------------------------
1 |
36 |
37 |
38 | App
39 |
40 |
--------------------------------------------------------------------------------
/playground/src/composables/store.ts:
--------------------------------------------------------------------------------
1 | import { CodeType } from "@/constants";
2 | import { useUrlSearchParams } from "@vueuse/core";
3 |
4 | export function createStore(cb: () => T) {
5 | const value = cb();
6 |
7 | return () => toRefs(value);
8 | }
9 |
10 | const initialValue: {
11 | code: string;
12 | codeType: CodeType;
13 | propsNotOnlyTs: "0" | "1";
14 | } = {
15 | code: "",
16 | codeType: CodeType.SFC,
17 | propsNotOnlyTs: "0",
18 | };
19 | export const useUrlKeepParams = createStore(() => {
20 | const params = useUrlSearchParams("hash-params", {
21 | initialValue,
22 | });
23 |
24 | const keys = Object.keys(initialValue) as (keyof typeof initialValue)[];
25 | for (const key of keys) {
26 | if (!(key in params)) {
27 | (params as any)[key] = initialValue[key];
28 | }
29 | }
30 |
31 | return params;
32 | });
33 |
--------------------------------------------------------------------------------
/.github/workflows/npm-publish.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: Publish
5 |
6 | on:
7 | push:
8 | tags:
9 | - 'v*'
10 |
11 | jobs:
12 | publish-npm:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v3
16 | with:
17 | fetch-depth: 0
18 | - uses: actions/setup-node@v3
19 | with:
20 | node-version: '16'
21 | registry-url: https://registry.npmjs.org/
22 | - run: npm i -g pnpm
23 | - run: pnpm install --no-frozen-lockfile
24 | - run: pnpm release
25 | env:
26 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
27 |
--------------------------------------------------------------------------------
/playground/src/main.ts:
--------------------------------------------------------------------------------
1 | import { createApp } from "vue";
2 | import App from "./App.vue";
3 |
4 | import * as monaco from "monaco-editor";
5 | import darkTheme from "theme-vitesse/themes/vitesse-dark.json";
6 | import lightTheme from "theme-vitesse/themes/vitesse-light.json";
7 |
8 | import EditorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
9 | import TsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker";
10 | import HtmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker";
11 |
12 | window.MonacoEnvironment = {
13 | getWorker(_: string, label: string) {
14 | if (label === "typescript") return new TsWorker();
15 | if (label === "html") return new HtmlWorker();
16 | return new EditorWorker();
17 | },
18 | };
19 |
20 | // @ts-expect-error
21 | monaco.editor.defineTheme("vitesse-dark", darkTheme);
22 | // @ts-expect-error
23 | monaco.editor.defineTheme("vitesse-light", lightTheme);
24 |
25 | import "uno.css";
26 | import "@unocss/reset/eric-meyer.css";
27 |
28 | import "@varlet/touch-emulator";
29 |
30 | createApp(App).mount("#app");
31 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 |
2 | name: Deploy GitHub Pages
3 |
4 | on:
5 | push:
6 | branches:
7 | - 'main'
8 |
9 | jobs:
10 | build-and-deploy:
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - uses: actions/checkout@v3
15 | with:
16 | fetch-depth: 0
17 | - uses: actions/setup-node@v3
18 | with:
19 | node-version: '16'
20 | registry-url: https://registry.npmjs.org/
21 |
22 | - name: Install and Build 🔧
23 | run: |
24 | # Install pnpm
25 | npm install -g pnpm
26 | # Update dependencies using pnpm
27 | pnpm install --no-frozen-lockfile
28 | # Enter playground directory and run build command
29 | cd playground && pnpm run build
30 |
31 | - name: Deploy 🚀
32 | uses: JamesIves/github-pages-deploy-action@v4.3.3
33 | with:
34 | branch: gh-pages # The branch the action should deploy to.
35 | folder: playground/dist # The folder the action should deploy.
36 | env:
37 | ACCESS_TOKEN: ${{secrets.GITHUB_TOKEN}}
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 clencat
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/playground/src/components/Monaco.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
44 |
--------------------------------------------------------------------------------
/playground/src/components/Edit.vue:
--------------------------------------------------------------------------------
1 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | SFC
31 |
32 |
33 | Script
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v*'
7 |
8 | jobs:
9 | release:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v3
13 | with:
14 | fetch-depth: 0
15 |
16 | - name: Install pnpm
17 | uses: pnpm/action-setup@v2
18 |
19 | - name: Set node
20 | uses: actions/setup-node@v3
21 | with:
22 | node-version: 16.x
23 | cache: pnpm
24 |
25 | - run: npx changelogithub
26 | env:
27 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
28 |
29 |
30 | deploy:
31 | runs-on: ubuntu-latest
32 | steps:
33 | - uses: actions/checkout@v3
34 | with:
35 | fetch-depth: 0
36 | - name: Set node
37 | uses: actions/setup-node@v3
38 | with:
39 | registry-url: https://registry.npmjs.org/
40 | node-version: 16.x
41 |
42 | - name: Setup
43 | run: npm i -g pnpm
44 |
45 | - name: Install
46 | run: pnpm install --no-frozen-lockfile
47 |
48 | - name: Build
49 | run: cd playground && pnpm build
50 |
51 | - name: Deploy 🚀
52 | uses: JamesIves/github-pages-deploy-action@v4.3.3
53 | with:
54 | branch: gh-pages
55 | folder: playground/dist
56 | env:
57 | ACCESS_TOKEN: ${{secrets.GITHUB_TOKEN}}
58 |
--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------
1 | import type { ArrowFunctionExpression, MethodProperty } from "@swc/core";
2 | import type { ParseOptions } from "@swc/core";
3 | import { parseSync } from "@swc/core";
4 |
5 | export type ParseSyncType = typeof parseSync;
6 |
7 | export enum FileType {
8 | js,
9 | ts,
10 | }
11 |
12 | export interface DefaultOption {
13 | propsNotOnlyTs?: boolean;
14 | notUseNewFile?: boolean;
15 | path: {
16 | [key: string]:
17 | | string
18 | | string[]
19 | | {
20 | mode: "*" | "**";
21 | excludes: string | string[];
22 | };
23 | };
24 | }
25 |
26 | export type CommandsOption = Omit;
27 |
28 | export interface Output {
29 | warn(message: string): void;
30 | error(message: string): void;
31 | log(message: string): void;
32 | success(message: string): void;
33 | }
34 |
35 | export interface Handlers {
36 | parseSync: ParseSyncType;
37 | output: Output;
38 | }
39 |
40 | export type SfcOptions = Pick & Handlers;
41 |
42 | export type ScriptOptions = {
43 | fileType: FileType;
44 | script: string;
45 | offset: number;
46 | propsNotOnlyTs?: boolean;
47 | } & Handlers;
48 |
49 | export const parseOption = {
50 | target: "es2022",
51 | syntax: "typescript",
52 | comments: true,
53 | } as ParseOptions;
54 |
55 | export type SetupAst = ArrowFunctionExpression | MethodProperty;
56 |
--------------------------------------------------------------------------------
/src/transform/sfc.ts:
--------------------------------------------------------------------------------
1 | import { parse } from "vue/compiler-sfc";
2 | import { SfcOptions, FileType } from "../constants";
3 | import transformScript from "./script";
4 | import MagicString from "magic-string";
5 |
6 | function transformSfc(sfc: string, options: SfcOptions) {
7 | const {
8 | descriptor: { script, scriptSetup },
9 | } = parse(sfc);
10 |
11 | const { output } = options;
12 |
13 | if (scriptSetup || !script) {
14 | output.log("Cannot find the code to be transform");
15 | return null;
16 | }
17 |
18 | let code: string | null = null;
19 | try {
20 | code = transformScript({
21 | ...options,
22 | fileType: script.lang === "ts" ? FileType.ts : FileType.js,
23 | script: script.content.trim(),
24 | offset: 0,
25 | });
26 | } catch (error) {
27 | output.error("transform script failed");
28 | console.log(error);
29 | } finally {
30 | if (code) {
31 | const ms = new MagicString(sfc);
32 | ms.update(script.loc.start.offset, script.loc.end.offset, `\n${code}`);
33 | ms.replaceAll(/\<\bscript\b.*\>/g, (str) => {
34 | const lastIdx = str.length - 1;
35 | return `${str.slice(0, lastIdx)} setup${str[lastIdx]}`;
36 | });
37 |
38 | return ms.toString();
39 | } else {
40 | output.error("transform failure");
41 | return null;
42 | }
43 | }
44 | }
45 |
46 | export default transformSfc;
47 |
--------------------------------------------------------------------------------
/playground/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "playground",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "vite",
7 | "build": "run-p build-only",
8 | "preview": "vite preview",
9 | "build-only": "vite build",
10 | "type-check": "vue-tsc --noEmit",
11 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
12 | "format": "prettier --write src/"
13 | },
14 | "dependencies": {
15 | "@swc/wasm-web": "^1.3.64",
16 | "@unocss/reset": "^0.53.1",
17 | "@varlet/touch-emulator": "^2.11.8",
18 | "@varlet/ui": "^2.11.8",
19 | "@vueuse/core": "^10.2.0",
20 | "monaco-editor": "^0.39.0",
21 | "theme-vitesse": "^0.7.2",
22 | "vue": "^3.3.4"
23 | },
24 | "devDependencies": {
25 | "@rushstack/eslint-patch": "^1.3.2",
26 | "@types/node": "^20.3.1",
27 | "@vitejs/plugin-vue": "^4.2.3",
28 | "@vitejs/plugin-vue-jsx": "^3.0.1",
29 | "@vue/eslint-config-prettier": "^7.1.0",
30 | "@vue/eslint-config-typescript": "^11.0.3",
31 | "@vue/tsconfig": "^0.4.0",
32 | "eslint": "^8.43.0",
33 | "eslint-plugin-vue": "^9.15.0",
34 | "npm-run-all": "^4.1.5",
35 | "prettier": "^2.8.8",
36 | "typescript": "~5.1.3",
37 | "unocss": "^0.53.1",
38 | "unplugin-auto-import": "^0.16.4",
39 | "unplugin-vue-components": "^0.25.1",
40 | "vite": "^4.3.9",
41 | "vite-plugin-static-copy": "^0.16.0",
42 | "vue-tsc": "^1.8.0"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/playground/src/composables/transform.ts:
--------------------------------------------------------------------------------
1 | import {
2 | transformScript,
3 | transformSfc,
4 | FileType,
5 | type Output,
6 | } from "../../../src/index";
7 | import initSwc, { parseSync } from "@swc/wasm-web";
8 | import type { CodeType } from "@/constants";
9 | import type { MaybeRefOrGetter } from "vue";
10 |
11 | export function useTransform(
12 | type: MaybeRefOrGetter,
13 | originCode: MaybeRefOrGetter,
14 | propsNotOnlyTs: MaybeRefOrGetter,
15 | output: Output,
16 | ) {
17 | const isReady = ref(false);
18 | async function init() {
19 | await initSwc();
20 | isReady.value = true;
21 | }
22 |
23 | const code = ref("");
24 | watchEffect(() => {
25 | if (!(isReady.value && toValue(originCode))) {
26 | code.value = "";
27 | return;
28 | }
29 |
30 | const text = toValue(originCode).trim();
31 | try {
32 | if (toValue(type) === "sfc") {
33 | code.value =
34 | transformSfc(text, {
35 | parseSync: parseSync as any,
36 | output,
37 | propsNotOnlyTs: toValue(propsNotOnlyTs),
38 | }) || "";
39 | } else {
40 | code.value =
41 | transformScript({
42 | fileType: FileType.ts,
43 | script: text,
44 | propsNotOnlyTs: toValue(propsNotOnlyTs),
45 | output,
46 | parseSync: parseSync as any,
47 | offset: 0,
48 | }) || "";
49 | }
50 | } catch (error) {
51 | output.error("Compilation error, please check the console.");
52 | console.log(error);
53 | }
54 | });
55 |
56 | init();
57 |
58 | return code;
59 | }
60 |
--------------------------------------------------------------------------------
/src/transform/components.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | ArrayExpression,
3 | ExportDefaultExpression,
4 | Identifier,
5 | ObjectExpression,
6 | } from "@swc/core";
7 | import { ScriptOptions, SetupAst } from "../constants";
8 | import { Visitor } from "@swc/core/Visitor.js";
9 | import type MagicString from "magic-string";
10 | import { getRealSpan } from "./utils";
11 |
12 | function transformComponents(
13 | componentsAst: ArrayExpression | Identifier | ObjectExpression,
14 | _setupAst: SetupAst,
15 | config: ScriptOptions,
16 | ) {
17 | const { script, offset } = config;
18 | if (
19 | componentsAst.type === "ArrayExpression" ||
20 | componentsAst.type === "Identifier"
21 | ) {
22 | return;
23 | }
24 |
25 | const { properties } = componentsAst;
26 |
27 | const str = properties.reduce((p, c) => {
28 | if (c.type === "KeyValueProperty" && c.key.type !== "Computed") {
29 | const key = c.key.value;
30 |
31 | const { span } = c.value as Identifier;
32 |
33 | const { start, end } = getRealSpan(span, offset);
34 | p += `const ${key} = ${script.slice(start, end)};\n`;
35 | }
36 |
37 | return p;
38 | }, "");
39 |
40 | if (!str) {
41 | return;
42 | } else {
43 | class MyVisitor extends Visitor {
44 | ms: MagicString;
45 | constructor(ms: MagicString) {
46 | super();
47 | this.ms = ms;
48 | }
49 | visitExportDefaultExpression(node: ExportDefaultExpression) {
50 | const { start } = getRealSpan(node.span, offset);
51 | this.ms.appendLeft(start, str);
52 |
53 | return node;
54 | }
55 | }
56 | return MyVisitor;
57 | }
58 | }
59 |
60 | export default transformComponents;
61 |
--------------------------------------------------------------------------------
/playground/src/components/View.vue:
--------------------------------------------------------------------------------
1 |
39 |
40 |
41 |
42 |
43 |
44 | PropsNotOnlyTs
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/playground/README.md:
--------------------------------------------------------------------------------
1 | # playground
2 |
3 | This template should help get you started developing with Vue 3 in Vite.
4 |
5 | ## Recommended IDE Setup
6 |
7 | [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
8 |
9 | ## Type Support for `.vue` Imports in TS
10 |
11 | TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
12 |
13 | If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
14 |
15 | 1. Disable the built-in TypeScript Extension
16 | 1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette
17 | 2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
18 | 2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
19 |
20 | ## Customize configuration
21 |
22 | See [Vite Configuration Reference](https://vitejs.dev/config/).
23 |
24 | ## Project Setup
25 |
26 | ```sh
27 | pnpm install
28 | ```
29 |
30 | ### Compile and Hot-Reload for Development
31 |
32 | ```sh
33 | pnpm dev
34 | ```
35 |
36 | ### Type-Check, Compile and Minify for Production
37 |
38 | ```sh
39 | pnpm build
40 | ```
41 |
42 | ### Lint with [ESLint](https://eslint.org/)
43 |
44 | ```sh
45 | pnpm lint
46 | ```
47 |
--------------------------------------------------------------------------------
/playground/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { fileURLToPath, URL } from "node:url";
2 |
3 | import { defineConfig, type PluginOption } from "vite";
4 | import vue from "@vitejs/plugin-vue";
5 | import vueJsx from "@vitejs/plugin-vue-jsx";
6 | import { viteStaticCopy } from "vite-plugin-static-copy";
7 | import Components from "unplugin-vue-components/vite";
8 | import AutoImport from "unplugin-auto-import/vite";
9 | import { VarletUIResolver } from "unplugin-vue-components/resolvers";
10 | import Unocss from "unocss/vite";
11 |
12 | // https://vitejs.dev/config/
13 | export default defineConfig(({ mode }) => {
14 | const plugins: PluginOption[] = [
15 | vue(),
16 | vueJsx(),
17 | Unocss({
18 | safelist: ["text-#444", "dark:text-#ddd"],
19 | theme: {
20 | breakpoints: {
21 | sm: "768px",
22 | },
23 | },
24 | }),
25 | AutoImport({
26 | imports: ["vue", "vue-router"],
27 | dirs: ["./src/composables", "./src/constants"],
28 | dts: true,
29 | vueTemplate: true,
30 | resolvers: [VarletUIResolver({ autoImport: true })],
31 | }),
32 | Components({
33 | dirs: ["src/components"],
34 | // allow auto load markdown components under `./src/components/`
35 | extensions: ["vue"],
36 | // allow auto import and register components used in markdown
37 | include: [/\.vue$/, /\.vue\?vue/],
38 | dts: true,
39 | resolvers: [VarletUIResolver({ autoImport: true })],
40 | }),
41 | ];
42 | if (mode !== "production") {
43 | plugins.push(
44 | viteStaticCopy({
45 | targets: [
46 | {
47 | src: "node_modules/@swc/wasm-web/wasm-web_bg.wasm",
48 | dest: "node_modules/.vite/deps/",
49 | },
50 | ],
51 | }),
52 | );
53 | }
54 |
55 | return {
56 | base: "./",
57 | plugins,
58 | resolve: {
59 | alias: {
60 | "@": fileURLToPath(new URL("./src", import.meta.url)),
61 | },
62 | },
63 | };
64 | });
65 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | import fs from "fs";
2 | import { resolve } from "path";
3 | import fg from "fast-glob";
4 | import { loadConfig } from "unconfig";
5 | import slash from "slash";
6 | import type { DefaultOption } from "./constants";
7 |
8 | export const cwd = process.cwd();
9 |
10 | export function pathResolve(...paths: string[]) {
11 | return slash(resolve(...paths));
12 | }
13 |
14 | export function getTheFileAbsolutePath(...pathNames: string[]) {
15 | const lastFile = pathNames[pathNames.length - 1] || "";
16 | if (!lastFile.endsWith(".vue")) {
17 | return;
18 | }
19 | const absolutePath = pathResolve(cwd, ...pathNames);
20 | try {
21 | fs.accessSync(absolutePath, fs.constants.F_OK);
22 | return absolutePath;
23 | } catch {
24 | console.warn(`File ${absolutePath} cannot be accessed`);
25 | return;
26 | }
27 | }
28 |
29 | export async function useConfigPath(files: string, beginDir = cwd) {
30 | const pathNames: string[] = [];
31 | const { config } = await loadConfig({
32 | sources: {
33 | files,
34 | extensions: ["ts", "js"],
35 | },
36 | cwd: beginDir,
37 | });
38 |
39 | const { path, ...option } = config;
40 | const keys = Object.keys(path);
41 |
42 | for (const key of keys) {
43 | const item = path[key];
44 | const files = Array.isArray(item)
45 | ? item
46 | : [typeof item === "string" ? item : item.mode];
47 | let vueFiles = getFgVueFile(files.map((p) => pathResolve(cwd, key, p)));
48 |
49 | if (typeof item === "object" && !Array.isArray(item)) {
50 | const excludes = Array.isArray(item.excludes)
51 | ? item.excludes
52 | : [item.excludes];
53 | const excludePaths = excludes.map((p) => pathResolve(cwd, key, p));
54 |
55 | vueFiles = vueFiles.filter((p) => !excludePaths.includes(p));
56 | }
57 | pathNames.push(...vueFiles);
58 | }
59 |
60 | return { pathNames, option };
61 | }
62 |
63 | function getFgVueFile(paths: string[]) {
64 | return fg.sync(paths).filter((p) => p.endsWith(".vue"));
65 | }
66 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue3-script-to-setup",
3 | "type": "module",
4 | "version": "0.2.2",
5 | "author": "clencat <2091927351@qq.com>",
6 | "packageManager": "pnpm@7.12.0",
7 | "description": "transform your vue3 script to setup mode",
8 | "license": "MIT",
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/a145789/vue3-script-to-setup"
12 | },
13 | "bugs": "https://github.com/a145789/vue3-script-to-setup/issues",
14 | "keywords": [
15 | "vue3"
16 | ],
17 | "sideEffects": false,
18 | "files": [
19 | "dist"
20 | ],
21 | "exports": {
22 | ".": {
23 | "types": "./dist/index.d.ts",
24 | "import": "./dist/index.mjs"
25 | }
26 | },
27 | "main": "./dist/index.mjs",
28 | "module": "./dist/index.mjs",
29 | "types": "./dist/index.d.ts",
30 | "bin": {
31 | "tosetup": "bin/tosetup.mjs"
32 | },
33 | "typesVersions": {
34 | "*": {
35 | "*": [
36 | "./dist/*",
37 | "./dist/index.d.ts"
38 | ]
39 | }
40 | },
41 | "engines": {
42 | "node": ">=14"
43 | },
44 | "scripts": {
45 | "build": "unbuild",
46 | "lint": "rome ci ./src",
47 | "prepublishOnly": "nr build",
48 | "dev": "esno src/setup.ts",
49 | "test": "vitest",
50 | "typecheck": "tsc --noEmit",
51 | "major": "standard-version -r major",
52 | "minor": "standard-version -r minor",
53 | "patch": "standard-version -r patch",
54 | "release": "pnpm run build && pnpm publish --access public --no-git-checks",
55 | "deploy": "cd playground && pnpm build && gh-pages -d playground/dist"
56 | },
57 | "devDependencies": {
58 | "@antfu/ni": "^0.21.4",
59 | "@types/node": "^20.3.1",
60 | "esno": "^0.16.3",
61 | "gh-pages": "^5.0.0",
62 | "rome": "^12.1.3",
63 | "standard-version": "^9.5.0",
64 | "typescript": "^5.1.3",
65 | "unbuild": "^1.2.1",
66 | "vite": "^4.3.9",
67 | "vitest": "^0.32.2"
68 | },
69 | "dependencies": {
70 | "@swc/core": "^1.3.64",
71 | "colorette": "^2.0.20",
72 | "fast-glob": "^3.2.12",
73 | "find-up": "^6.3.0",
74 | "magic-string": "^0.30.0",
75 | "slash": "^5.1.0",
76 | "unconfig": "^0.3.9",
77 | "vue": "^3.2.47"
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | pull_request:
9 | branches:
10 | - main
11 |
12 | jobs:
13 | lint:
14 | if: ${{ !contains(github.event.head_commit.message, '[skip check]') }}
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/checkout@v3
18 |
19 | - name: Install pnpm
20 | uses: pnpm/action-setup@v2
21 |
22 | - name: Set node
23 | uses: actions/setup-node@v3
24 | with:
25 | node-version: 16.x
26 | cache: pnpm
27 |
28 | - name: Ls
29 | run: ls -a
30 |
31 | - name: Setup
32 | run: npm i -g @antfu/ni
33 |
34 | - name: Install
35 | run: ni --frozen
36 |
37 | - name: Lint
38 | run: nr lint
39 |
40 | typecheck:
41 | if: ${{ !contains(github.event.head_commit.message, '[skip check]') }}
42 | runs-on: ubuntu-latest
43 | steps:
44 | - uses: actions/checkout@v3
45 |
46 | - name: Install pnpm
47 | uses: pnpm/action-setup@v2
48 |
49 | - name: Set node
50 | uses: actions/setup-node@v3
51 | with:
52 | node-version: 16.x
53 | cache: pnpm
54 |
55 | - name: Setup
56 | run: npm i -g @antfu/ni
57 |
58 | - name: Install
59 | run: ni --frozen
60 |
61 | - name: Typecheck
62 | run: nr typecheck
63 |
64 | test:
65 | if: ${{ !contains(github.event.head_commit.message, '[skip check]') }}
66 | runs-on: ${{ matrix.os }}
67 |
68 | strategy:
69 | matrix:
70 | node: [14.x, 16.x]
71 | os: [ubuntu-latest, windows-latest, macos-latest]
72 | fail-fast: false
73 |
74 | steps:
75 | - uses: actions/checkout@v3
76 |
77 | - name: Install pnpm
78 | uses: pnpm/action-setup@v2
79 |
80 | - name: Set node ${{ matrix.node }}
81 | uses: actions/setup-node@v3
82 | with:
83 | node-version: ${{ matrix.node }}
84 | cache: pnpm
85 |
86 | - name: Setup
87 | run: npm i -g @antfu/ni
88 |
89 | - name: Install
90 | run: ni --frozen
91 |
92 | - name: Build
93 | run: nr build
94 |
95 | - name: Test
96 | run: nr test
97 |
--------------------------------------------------------------------------------
/playground/src/components/Header.vue:
--------------------------------------------------------------------------------
1 |
39 |
40 |
41 |
42 |
43 |
44 | Vue3 script to script-setup
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/playground/src/assets/base.css:
--------------------------------------------------------------------------------
1 | /* color palette from */
2 | :root {
3 | --vt-c-white: #ffffff;
4 | --vt-c-white-soft: #f8f8f8;
5 | --vt-c-white-mute: #f2f2f2;
6 |
7 | --vt-c-black: #181818;
8 | --vt-c-black-soft: #222222;
9 | --vt-c-black-mute: #282828;
10 |
11 | --vt-c-indigo: #2c3e50;
12 |
13 | --vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
14 | --vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
15 | --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
16 | --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
17 |
18 | --vt-c-text-light-1: var(--vt-c-indigo);
19 | --vt-c-text-light-2: rgba(60, 60, 60, 0.66);
20 | --vt-c-text-dark-1: var(--vt-c-white);
21 | --vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
22 | }
23 |
24 | /* semantic color variables for this project */
25 | :root {
26 | --color-background: var(--vt-c-white);
27 | --color-background-soft: var(--vt-c-white-soft);
28 | --color-background-mute: var(--vt-c-white-mute);
29 |
30 | --color-border: var(--vt-c-divider-light-2);
31 | --color-border-hover: var(--vt-c-divider-light-1);
32 |
33 | --color-heading: var(--vt-c-text-light-1);
34 | --color-text: var(--vt-c-text-light-1);
35 |
36 | --section-gap: 160px;
37 | }
38 |
39 | @media (prefers-color-scheme: dark) {
40 | :root {
41 | --color-background: var(--vt-c-black);
42 | --color-background-soft: var(--vt-c-black-soft);
43 | --color-background-mute: var(--vt-c-black-mute);
44 |
45 | --color-border: var(--vt-c-divider-dark-2);
46 | --color-border-hover: var(--vt-c-divider-dark-1);
47 |
48 | --color-heading: var(--vt-c-text-dark-1);
49 | --color-text: var(--vt-c-text-dark-2);
50 | }
51 | }
52 |
53 | *,
54 | *::before,
55 | *::after {
56 | box-sizing: border-box;
57 | margin: 0;
58 | position: relative;
59 | font-weight: normal;
60 | }
61 |
62 | body {
63 | min-height: 100vh;
64 | color: var(--color-text);
65 | background: var(--color-background);
66 | transition: color 0.5s, background-color 0.5s;
67 | line-height: 1.6;
68 | font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
69 | Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
70 | font-size: 15px;
71 | text-rendering: optimizeLegibility;
72 | -webkit-font-smoothing: antialiased;
73 | -moz-osx-font-smoothing: grayscale;
74 | }
75 |
--------------------------------------------------------------------------------
/src/setup.ts:
--------------------------------------------------------------------------------
1 | import { findUpSync } from "find-up";
2 | import { getTheFileAbsolutePath, useConfigPath } from "./utils";
3 | import { transformSfc } from "./transform";
4 | import { CommandsOption } from "./constants";
5 | import writeFile from "./writeFile";
6 | import { readFileSync } from "fs";
7 | import { parseSync } from "@swc/core";
8 | import { blue, green, red, yellow } from "colorette";
9 |
10 | const CONFIG_FILE_NAME = "tosetup.config" as const;
11 |
12 | async function setup() {
13 | const start = Date.now();
14 | const argv = process.argv.slice(2).filter(Boolean);
15 |
16 | let { pathNames, commands } = argv.reduce(
17 | (p, c) => {
18 | if (c.startsWith("--")) {
19 | switch (c.split("--")[1] as keyof typeof p.commands) {
20 | case "propsNotOnlyTs":
21 | p.commands.propsNotOnlyTs = true;
22 | break;
23 | case "notUseNewFile":
24 | p.commands.notUseNewFile = true;
25 | break;
26 |
27 | default:
28 | break;
29 | }
30 | return p;
31 | }
32 |
33 | const absolutePath = getTheFileAbsolutePath(c);
34 | if (absolutePath) {
35 | p.pathNames.push(absolutePath);
36 | }
37 |
38 | return p;
39 | },
40 | { pathNames: [] as string[], commands: {} as CommandsOption },
41 | );
42 |
43 | if (!pathNames.length) {
44 | const configPath = findUpSync([
45 | `${CONFIG_FILE_NAME}.js`,
46 | `${CONFIG_FILE_NAME}.ts`,
47 | ]);
48 | if (!configPath) {
49 | console.error(
50 | red(`Please enter a file path or use a ${CONFIG_FILE_NAME} file.`),
51 | );
52 | process.exit(1);
53 | }
54 |
55 | const config = await useConfigPath(CONFIG_FILE_NAME);
56 |
57 | pathNames = config.pathNames;
58 |
59 | commands = { ...commands, ...config.option };
60 | }
61 |
62 | for (const path of pathNames) {
63 | const output = {
64 | warn: (message: string) =>
65 | console.log(`${yellow(message)} in the ${path}`),
66 | error: (message: string) => console.log(`${red(message)} in the ${path}`),
67 | log: (message: string) => console.log(`${blue(message)} in the ${path}`),
68 | success: (message: string) =>
69 | console.log(`${green(message)} in the ${path}`),
70 | };
71 | output.log(`File ${path} start of transform...`);
72 | const sfc = readFileSync(path).toString();
73 | const code = transformSfc(sfc, { ...commands, parseSync, output });
74 | if (code) {
75 | try {
76 | const file = writeFile(code, path, commands);
77 | output.success(`File ${file} transform success.\n`);
78 | } catch (error) {
79 | output.error(`write ${path} failure.\n`);
80 | console.log(error);
81 | }
82 | }
83 | }
84 |
85 | console.log(`Done in ${Math.floor(Date.now() - start)}ms.`);
86 | }
87 |
88 | setup();
89 |
--------------------------------------------------------------------------------
/src/transform/expose.ts:
--------------------------------------------------------------------------------
1 | import { ScriptOptions, SetupAst } from "../constants";
2 | import {
3 | GetCallExpressionFirstArg,
4 | getRealSpan,
5 | getSetupSecondParams,
6 | } from "./utils";
7 | import type {
8 | BlockStatement,
9 | ExportDefaultExpression,
10 | KeyValueProperty,
11 | MethodProperty,
12 | Statement,
13 | } from "@swc/core";
14 | import { Visitor } from "@swc/core/Visitor.js";
15 | import type MagicString from "magic-string";
16 |
17 | function transformExpose(setupAst: SetupAst, config: ScriptOptions) {
18 | const { script, offset, output } = config;
19 | const name = getSetupSecondParams("expose", setupAst, output);
20 | if (!name) {
21 | return;
22 | }
23 |
24 | let exposeArg: string[] = [];
25 | if ((setupAst.body as BlockStatement)?.stmts?.length) {
26 | const visitor = new GetCallExpressionFirstArg(name);
27 | visitor.visitFn(setupAst);
28 |
29 | exposeArg = visitor.firstArgAst
30 | .flatMap((ast) => {
31 | if (ast.type !== "ObjectExpression") {
32 | return "";
33 | }
34 |
35 | const { start, end } = getRealSpan(ast.span, offset);
36 |
37 | return script.slice(start, end).replace(/{|}/g, "").split(",");
38 | })
39 | .filter((s) => Boolean(s.trim()));
40 | }
41 | class MyVisitor extends Visitor {
42 | ms: MagicString;
43 | constructor(ms: MagicString) {
44 | super();
45 | this.ms = ms;
46 | }
47 | visitMethodProperty(n: MethodProperty) {
48 | if (n.key.type === "Identifier" && n.key.value === "setup") {
49 | if (n.body) {
50 | n.body.stmts = this.myVisitStatements(n.body.stmts);
51 | }
52 | }
53 |
54 | return n;
55 | }
56 | visitKeyValueProperty(n: KeyValueProperty) {
57 | if (
58 | n.key.type === "Identifier" &&
59 | n.key.value === "setup" &&
60 | n.value.type === "ArrowFunctionExpression"
61 | ) {
62 | if (n.value.body.type === "BlockStatement") {
63 | n.value.body.stmts = this.myVisitStatements(n.value.body.stmts);
64 | }
65 | }
66 | return n;
67 | }
68 | myVisitStatements(stmts: Statement[]): Statement[] {
69 | for (const stmt of stmts) {
70 | if (
71 | stmt.type === "ExpressionStatement" &&
72 | stmt.expression.type === "CallExpression" &&
73 | stmt.expression.callee.type === "Identifier" &&
74 | stmt.expression.callee.value === name
75 | ) {
76 | const { start, end } = getRealSpan(stmt.span, offset);
77 | this.ms.remove(start, end);
78 | }
79 | }
80 | return stmts;
81 | }
82 | visitExportDefaultExpression(node: ExportDefaultExpression) {
83 | const { end } = getRealSpan(node.span, offset);
84 | this.ms.appendRight(end, `defineExpose({${exposeArg.join(",")}});\n`);
85 |
86 | return node;
87 | }
88 | }
89 |
90 | return MyVisitor;
91 | }
92 |
93 | export default transformExpose;
94 |
--------------------------------------------------------------------------------
/src/transform/directives.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | ExportDefaultExpression,
3 | Identifier,
4 | ImportDefaultSpecifier,
5 | NamedImportSpecifier,
6 | ObjectExpression,
7 | } from "@swc/core";
8 | import { ScriptOptions, SetupAst } from "../constants";
9 | import { getRealSpan } from "./utils";
10 | import { Visitor } from "@swc/core/Visitor.js";
11 | import type MagicString from "magic-string";
12 |
13 | function transformDirectiveName(name: string) {
14 | return `v${name.slice(0, 1).toLocaleUpperCase() + name.slice(1)}`;
15 | }
16 |
17 | function transformDirectives(
18 | directivesAst: Identifier | ObjectExpression,
19 | _setupAst: SetupAst,
20 | config: ScriptOptions,
21 | ) {
22 | const { script, offset, output } = config;
23 | if (
24 | directivesAst.type === "Identifier" ||
25 | directivesAst.properties.some((ast) => ast.type === "SpreadElement")
26 | ) {
27 | output.warn("Please manually modify the custom directives");
28 | return;
29 | }
30 |
31 | const { properties } = directivesAst;
32 |
33 | if (!properties.length) {
34 | return;
35 | }
36 |
37 | const importDirective: string[] = [];
38 | const customDirective = properties.reduce((p, c) => {
39 | if (c.type === "Identifier") {
40 | // 设置一个转换回调
41 | importDirective.push(c.value);
42 | }
43 |
44 | if (c.type === "KeyValueProperty" && c.key.type !== "Computed") {
45 | const key = String(c.key.value);
46 |
47 | const { span } = c.value as Identifier;
48 |
49 | const { start, end } = getRealSpan(span, offset);
50 | p += `const v${
51 | key.slice(0, 1).toLocaleUpperCase() + key.slice(1)
52 | } = ${script.slice(start, end)};\n`;
53 | }
54 |
55 | return p;
56 | }, "");
57 | class MyVisitor extends Visitor {
58 | ms: MagicString;
59 | constructor(ms: MagicString) {
60 | super();
61 | this.ms = ms;
62 | }
63 | visitImportDefaultSpecifier(n: ImportDefaultSpecifier) {
64 | const { value, span } = n.local;
65 | const { start, end } = getRealSpan(span, offset);
66 | if (importDirective.includes(value)) {
67 | this.ms.update(start, end, transformDirectiveName(value));
68 | }
69 | return n;
70 | }
71 | visitNamedImportSpecifier(n: NamedImportSpecifier) {
72 | const {
73 | local: { value, span },
74 | imported,
75 | } = n;
76 | if (!imported) {
77 | if (importDirective.includes(value)) {
78 | const { end } = getRealSpan(span, offset);
79 | this.ms.appendRight(end, ` as ${transformDirectiveName(value)}`);
80 | }
81 | } else {
82 | if (importDirective.includes(value)) {
83 | const { start, end } = getRealSpan(span, offset);
84 | this.ms.update(start, end, transformDirectiveName(value));
85 | }
86 | }
87 | return n;
88 | }
89 | visitExportDefaultExpression(n: ExportDefaultExpression) {
90 | if (!customDirective) {
91 | return n;
92 | }
93 |
94 | const { start } = getRealSpan(n.span, offset);
95 | this.ms.appendLeft(start, `// custom directive \n${customDirective}`);
96 |
97 | return n;
98 | }
99 | }
100 |
101 | return MyVisitor;
102 | }
103 |
104 | export default transformDirectives;
105 |
--------------------------------------------------------------------------------
/src/transform/attrsAndSlots.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | ExportDefaultExpression,
3 | ImportDeclaration,
4 | ImportSpecifier,
5 | } from "@swc/core";
6 | import { ScriptOptions, SetupAst } from "../constants";
7 | import { getRealSpan, getSetupSecondParams } from "./utils";
8 | import { Visitor } from "@swc/core/Visitor.js";
9 | import type MagicString from "magic-string";
10 |
11 | function transformAttrsAndSlots(
12 | setupAst: SetupAst,
13 | { offset, output }: ScriptOptions,
14 | ) {
15 | const attrsName = getSetupSecondParams("attrs", setupAst, output);
16 | const slotsName = getSetupSecondParams("slots", setupAst, output);
17 | if (!(attrsName || slotsName)) {
18 | return;
19 | }
20 |
21 | class MyVisitor extends Visitor {
22 | ms: MagicString;
23 | constructor(ms: MagicString) {
24 | super();
25 | this.ms = ms;
26 | }
27 | visitImportDeclaration(n: ImportDeclaration) {
28 | if (n.source.value === "vue") {
29 | this.myVisitImportSpecifiers(n.specifiers);
30 | }
31 | return n;
32 | }
33 | myVisitImportSpecifiers(n: ImportSpecifier[]) {
34 | const { attrs, slots } = n.reduce(
35 | (p, ast) => {
36 | if (ast.type === "ImportSpecifier") {
37 | if (ast.local.value === "useAttrs") {
38 | p.attrs = true;
39 | }
40 |
41 | if (ast.local.value === "useSlots") {
42 | p.slots = true;
43 | }
44 | }
45 |
46 | return p;
47 | },
48 | {
49 | attrs: false,
50 | slots: false,
51 | },
52 | );
53 |
54 | const firstNode = n[0];
55 | if (firstNode) {
56 | const { start } = getRealSpan(firstNode.span, offset);
57 | this.ms.appendLeft(
58 | start,
59 | `${!attrs && attrsName ? "useAttrs, " : ""}${
60 | !slots && slotsName ? "useSlots, " : ""
61 | }`,
62 | );
63 | if (!attrs && attrsName) {
64 | n.unshift({
65 | type: "ImportSpecifier",
66 | span: {
67 | start: 41,
68 | end: 50,
69 | ctxt: 0,
70 | },
71 | local: {
72 | type: "Identifier",
73 | span: {
74 | start: 41,
75 | end: 50,
76 | ctxt: 1,
77 | },
78 | value: "useAttrs",
79 | optional: false,
80 | },
81 | isTypeOnly: false,
82 | });
83 | }
84 | if (!slots && slotsName) {
85 | n.unshift({
86 | type: "ImportSpecifier",
87 | span: {
88 | start: 41,
89 | end: 50,
90 | ctxt: 0,
91 | },
92 | local: {
93 | type: "Identifier",
94 | span: {
95 | start: 41,
96 | end: 50,
97 | ctxt: 1,
98 | },
99 | value: "useSlots",
100 | optional: false,
101 | },
102 | isTypeOnly: false,
103 | });
104 | }
105 | }
106 |
107 | return n;
108 | }
109 | visitExportDefaultExpression(node: ExportDefaultExpression) {
110 | const { start } = getRealSpan(node.span, offset);
111 | this.ms.appendLeft(
112 | start,
113 | `\n${attrsName ? `const ${attrsName} = useAttrs();\n` : ""}${
114 | slotsName ? `const ${slotsName} = useSlots();\n` : ""
115 | }\n`,
116 | );
117 |
118 | return node;
119 | }
120 | }
121 |
122 | return MyVisitor;
123 | }
124 |
125 | export default transformAttrsAndSlots;
126 |
--------------------------------------------------------------------------------
/test/index.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from "vitest";
2 | import {
3 | cwd,
4 | pathResolve,
5 | getTheFileAbsolutePath,
6 | useConfigPath,
7 | } from "../src/utils";
8 | import fg from "fast-glob";
9 | import { FileType, Output } from "../src/constants";
10 | import {
11 | testScript1,
12 | testScript2,
13 | testScript3,
14 | transformToSingeLine,
15 | } from "./utils";
16 | import transformScript from "../src/transform/script";
17 | import { parseSync } from "@swc/core";
18 | import { genScriptUnicodeMap, getRealSpan } from "../src/transform/utils";
19 |
20 | const output: Output = {
21 | warn() {},
22 | log() {},
23 | success() {},
24 | error() {},
25 | };
26 |
27 | describe("test utils", () => {
28 | it("test getTheFileAbsolutePath", () => {
29 | expect(getTheFileAbsolutePath()).toBeUndefined();
30 | expect(getTheFileAbsolutePath("foo.js")).toBeUndefined();
31 | expect(getTheFileAbsolutePath("./example/src/Foo.vue")).toBeUndefined();
32 | expect(getTheFileAbsolutePath("./example/src/App.vue")).toBe(
33 | pathResolve(cwd, "./example/src/App.vue"),
34 | );
35 | expect(getTheFileAbsolutePath(cwd, "./example", "src", "App.vue")).toBe(
36 | pathResolve(cwd, "./example/src/App.vue"),
37 | );
38 | });
39 |
40 | it("test useConfigPath", async () => {
41 | expect(
42 | (
43 | await useConfigPath(
44 | "tosetup.config.a",
45 | pathResolve(__dirname, "fixtures"),
46 | )
47 | ).pathNames,
48 | ).toEqual([
49 | pathResolve(cwd, "./example/src/App.vue"),
50 | pathResolve(cwd, "./example/src/components/Tab.vue"),
51 | pathResolve(cwd, "example/src/views/404.vue"),
52 | ]);
53 |
54 | const { pathNames, option } = await useConfigPath(
55 | "tosetup.config.b",
56 | pathResolve(__dirname, "fixtures"),
57 | );
58 | expect(pathNames.sort()).toEqual(
59 | fg
60 | .sync(pathResolve(cwd, "./example/src/**"))
61 | .filter((path) => !path.endsWith("views/Home.vue"))
62 | .sort(),
63 | );
64 |
65 | expect(option).toEqual({
66 | propsNotOnlyTs: true,
67 | });
68 | });
69 |
70 | it("test getRealSpan", async () => {
71 | const str = "Hello, may name is '真TM的好'。";
72 | const offset = 0;
73 |
74 | genScriptUnicodeMap(str, offset);
75 | expect(getRealSpan({ start: 0, end: 4 }, offset)).toEqual({
76 | start: 0,
77 | end: 4,
78 | });
79 |
80 | expect(getRealSpan({ start: 20, end: 26 }, offset)).toEqual({
81 | start: 20,
82 | end: 24,
83 | });
84 | });
85 | });
86 |
87 | describe("test transform script", () => {
88 | it("test", async () => {
89 | expect(
90 | transformToSingeLine(
91 | transformScript({
92 | fileType: FileType.ts,
93 | script: testScript1.code.trim(),
94 | offset: 0,
95 | parseSync,
96 | output,
97 | }),
98 | ),
99 | ).toBe(transformToSingeLine(testScript1.transform.trim()));
100 |
101 | expect(
102 | transformToSingeLine(
103 | transformScript({
104 | fileType: FileType.ts,
105 | script: testScript2.code.trim(),
106 | offset: 0,
107 | parseSync,
108 | output,
109 | }),
110 | ),
111 | ).toBe(transformToSingeLine(testScript2.transform.trim()));
112 |
113 | expect(
114 | transformToSingeLine(
115 | transformScript({
116 | fileType: FileType.ts,
117 | script: testScript3.code.trim(),
118 | offset: 0,
119 | parseSync,
120 | output,
121 | }),
122 | ),
123 | ).toBe(transformToSingeLine(testScript3.transform.trim()));
124 | });
125 | });
126 |
--------------------------------------------------------------------------------
/src/transform/emits.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | Identifier,
3 | ArrayExpression,
4 | ObjectExpression,
5 | ExportDefaultExpression,
6 | NamedImportSpecifier,
7 | ImportDefaultSpecifier,
8 | ImportSpecifier,
9 | BlockStatement,
10 | } from "@swc/core";
11 |
12 | import { ScriptOptions, SetupAst } from "../constants";
13 | import {
14 | GetCallExpressionFirstArg,
15 | getRealSpan,
16 | getSetupSecondParams,
17 | } from "./utils";
18 | import { Visitor } from "@swc/core/Visitor.js";
19 | import type MagicString from "magic-string";
20 |
21 | function transformEmits(
22 | emitsAst: ArrayExpression | ObjectExpression | Identifier | null,
23 | setupAst: SetupAst,
24 | config: ScriptOptions,
25 | ) {
26 | const { script, offset, output } = config;
27 | const name = getSetupSecondParams("emit", setupAst, output);
28 | if (!name) {
29 | return;
30 | }
31 |
32 | const preCode = `const ${name} = `;
33 | let str = "";
34 | let isSameEmitsName = false;
35 | class MyVisitor extends Visitor {
36 | ms: MagicString;
37 | constructor(ms: MagicString) {
38 | super();
39 | this.ms = ms;
40 | }
41 | visitExportDefaultExpression(node: ExportDefaultExpression) {
42 | const { start } = getRealSpan(node.span, offset);
43 | this.ms.appendLeft(start, str);
44 |
45 | return node;
46 | }
47 |
48 | visitImportDefaultSpecifier(n: ImportDefaultSpecifier): ImportSpecifier {
49 | if (!isSameEmitsName) {
50 | return n;
51 | }
52 | const { value, span } = n.local;
53 | const { start, end } = getRealSpan(span, offset);
54 | if (value === name) {
55 | this.ms.update(start, end, `$${name}`);
56 | }
57 | return n;
58 | }
59 | visitNamedImportSpecifier(n: NamedImportSpecifier) {
60 | if (!isSameEmitsName) {
61 | return n;
62 | }
63 | const {
64 | local: { value, span },
65 | imported,
66 | } = n;
67 | if (!imported) {
68 | if (value === name) {
69 | const { end } = getRealSpan(span, offset);
70 | this.ms.appendRight(end, ` as $${name}`);
71 | }
72 | } else {
73 | if (value === name) {
74 | const { start, end } = getRealSpan(span, offset);
75 | this.ms.update(start, end, `$${name}`);
76 | }
77 | }
78 | return n;
79 | }
80 | }
81 |
82 | let keys: string[] = [];
83 | if (emitsAst) {
84 | if (emitsAst.type === "ObjectExpression") {
85 | const { start, end } = getRealSpan(emitsAst.span, offset);
86 | str = `${preCode}defineEmits(${script.slice(start, end)});\n`;
87 |
88 | return MyVisitor;
89 | }
90 |
91 | if (emitsAst.type === "Identifier") {
92 | if (name !== emitsAst.value) {
93 | str = `${preCode}defineEmits(${emitsAst.value});\n`;
94 | } else {
95 | str = `${preCode}defineEmits($${emitsAst.value});\n`;
96 | isSameEmitsName = true;
97 | }
98 |
99 | return MyVisitor;
100 | }
101 |
102 | keys = emitsAst.elements.map((ast) => {
103 | const { span } = ast!.expression as Identifier;
104 | const { start, end } = getRealSpan(span, offset);
105 | return script.slice(start, end);
106 | });
107 | }
108 |
109 | let emitNames: string[] = [];
110 | if ((setupAst.body as BlockStatement)?.stmts?.length) {
111 | const visitor = new GetCallExpressionFirstArg(name);
112 | visitor.visitFn(setupAst);
113 |
114 | emitNames = (visitor.firstArgAst as Identifier[]).map((ast) => {
115 | const { start, end } = getRealSpan(ast.span, offset);
116 | return script.slice(start, end);
117 | });
118 | }
119 |
120 | str = `${preCode}defineEmits([${[...new Set([...keys, ...emitNames])].join(
121 | ", ",
122 | )}]);\n`;
123 | return MyVisitor;
124 | }
125 |
126 | export default transformEmits;
127 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4 |
5 | ### [0.2.2](https://github.com/a145789/vue3-script-to-setup/compare/v0.2.1...v0.2.2) (2023-06-19)
6 |
7 |
8 | ### Bug Fixes
9 |
10 | * **playground:** fix language change ([f6d48da](https://github.com/a145789/vue3-script-to-setup/commit/f6d48da98886f0a086cf94e15db257803b3d9f2f))
11 |
12 | ### [0.2.1](https://github.com/a145789/vue3-script-to-setup/compare/v0.2.0...v0.2.1) (2023-03-06)
13 |
14 | ## [0.2.0](https://github.com/a145789/vue3-script-to-setup/compare/v0.1.5...v0.2.0) (2023-03-06)
15 |
16 |
17 | ### Features
18 |
19 | * exposing transformSfc transformScript to the outside ([7a10cb2](https://github.com/a145789/vue3-script-to-setup/commit/7a10cb2f1613e83d0d39cce26637fd46218f086a))
20 | * Improvement api ([c20d095](https://github.com/a145789/vue3-script-to-setup/commit/c20d095a14e27974cf5f62ee2891dc24c742938d))
21 | * **playground:** playground ([7456e49](https://github.com/a145789/vue3-script-to-setup/commit/7456e493ed5ade07f781d30ddabbd6d3aba398c0))
22 | * **playground:** playground base ([a39c99c](https://github.com/a145789/vue3-script-to-setup/commit/a39c99ce8170b7141e146284d44e58c5f6ef892b))
23 |
24 | ### [0.1.5](https://github.com/a145789/vue3-script-to-setup/compare/v0.1.4...v0.1.5) (2023-01-04)
25 |
26 |
27 | ### Bug Fixes
28 |
29 | * span calculation error in multi-file mode ([6f39c09](https://github.com/a145789/vue3-script-to-setup/commit/6f39c09922067f5cf1d23494c67caee2ead7f553))
30 |
31 | ### [0.1.4](https://github.com/a145789/vue3-script-to-setup/compare/v0.1.3...v0.1.4) (2023-01-03)
32 |
33 |
34 | ### Bug Fixes
35 |
36 | * can't find tosetup.config ([a5183f0](https://github.com/a145789/vue3-script-to-setup/commit/a5183f0f81e577d497917a3cbeb292e061cb2372))
37 |
38 | ### [0.1.3](https://github.com/a145789/vue3-script-to-setup/compare/v0.1.2...v0.1.3) (2022-12-30)
39 |
40 |
41 | ### Features
42 |
43 | * optimize skipping the unwanted transform file prompt ([341649b](https://github.com/a145789/vue3-script-to-setup/commit/341649b4b5cc43156cf45c9c5705865e50cecda9))
44 |
45 | ### [0.1.2](https://github.com/a145789/vue3-script-to-setup/compare/v0.1.1...v0.1.2) (2022-12-26)
46 |
47 |
48 | ### Features
49 |
50 | * optimize the Only-Ts display when the props type is an array ([5281eb6](https://github.com/a145789/vue3-script-to-setup/commit/5281eb67ab984cd5c3f4cd0f24c20abe99f74f38))
51 |
52 |
53 | ### Bug Fixes
54 |
55 | * emit generates the name of the error ([a699991](https://github.com/a145789/vue3-script-to-setup/commit/a699991b8a457ee3a136a6b43589e0e1121b6c01))
56 |
57 | ### [0.1.1](https://github.com/a145789/vue3-script-to-setup/compare/v0.1.0...v0.1.1) (2022-12-23)
58 |
59 |
60 | ### Features
61 |
62 | * support for emit transform only in setup functions ([f273601](https://github.com/a145789/vue3-script-to-setup/commit/f273601ef8efcd925f296f1e33905196bdb3d0d2))
63 |
64 | ## [0.1.0](https://github.com/a145789/vue3-script-to-setup/compare/v0.0.4...v0.1.0) (2022-12-21)
65 |
66 |
67 | ### Features
68 |
69 | * processing for props emits of the same name ([e992f3e](https://github.com/a145789/vue3-script-to-setup/commit/e992f3e59100a1115d3234fe5bdd0b58110f8fa1))
70 |
71 |
72 | ### Bug Fixes
73 |
74 | * emits name de-duplication ([6b09c1d](https://github.com/a145789/vue3-script-to-setup/commit/6b09c1d3df53b1196a21d8485dac441d4acd6f9a))
75 | * import only when attrs or slots are required ([c3607c9](https://github.com/a145789/vue3-script-to-setup/commit/c3607c99b0f6b112a80a883615cda33e22786436))
76 | * offset calculation error ([92f0031](https://github.com/a145789/vue3-script-to-setup/commit/92f003192cc4884e36a24a274a80e21058271219))
77 |
78 | ### [0.0.4](https://github.com/a145789/vue3-script-to-setup/compare/v0.0.3...v0.0.4) (2022-12-20)
79 |
80 |
81 | ### Bug Fixes
82 |
83 | * rust & js unicode length inconsistency ([7864b63](https://github.com/a145789/vue3-script-to-setup/commit/7864b63cd9fdd9d5f3ca30f9cf8fa622b96c8ffd))
84 |
85 | ### [0.0.3](https://github.com/a145789/vue3-script-to-setup/compare/v0.0.2...v0.0.3) (2022-12-19)
86 |
87 | ### [0.0.2](https://github.com/a145789/vue3-script-to-setup/compare/v0.0.1...v0.0.2) (2022-12-19)
88 |
89 |
90 | ### Features
91 |
92 | * rewrite transform ([a5348af](https://github.com/a145789/vue3-script-to-setup/commit/a5348af30b03f6331172c676e728464b6665af19))
93 | * use magic string transform code ([b8e8eba](https://github.com/a145789/vue3-script-to-setup/commit/b8e8ebaf4a58ad82a720d4c3b18a15235743f643))
94 |
95 | ### 0.0.1 (2022-12-13)
96 |
97 |
98 | ### Features
99 |
100 | * base ([ca65549](https://github.com/a145789/vue3-script-to-setup/commit/ca65549dd61b9d48d57a6508fea73a4b50ab1d2c))
101 | * support node 14 ([a98171f](https://github.com/a145789/vue3-script-to-setup/commit/a98171fcc19cde58c32eca160c63f102d6215943))
102 | * support transform ([0237556](https://github.com/a145789/vue3-script-to-setup/commit/0237556f2a4f388548100f6766eab7ad2d47df2d))
103 | * support transform script ([55faed6](https://github.com/a145789/vue3-script-to-setup/commit/55faed62fa075da58a80b9a29412df84a089ad33))
104 | * support write file close [#1](https://github.com/a145789/vue3-script-to-setup/issues/1) ([867e481](https://github.com/a145789/vue3-script-to-setup/commit/867e481eed015e5d111a58b6bf3c95de5f1979f6))
105 |
106 |
107 | ### Bug Fixes
108 |
109 | * delete useless semicolon ([fd46dd2](https://github.com/a145789/vue3-script-to-setup/commit/fd46dd2055d9e35a933e9cd0080d0ca93989176e))
110 | * Optimize utils ([d213e8e](https://github.com/a145789/vue3-script-to-setup/commit/d213e8e5e12bb4c25080731b44f23587b0b72190))
111 | * wrong location ([c68e5e0](https://github.com/a145789/vue3-script-to-setup/commit/c68e5e01fd7a6fe146bdd513379d535c0591fb72))
112 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | vue3-script-to-setup
2 |
3 |
4 |
5 |
6 |
7 | Quickly transform your vue3 script to setup mode
8 | 快速将 vue3 script 转换为 setup 模式
9 |
10 |
11 | Playground 在线尝试
12 |
13 |
14 |
15 | **origin code**
16 | ```html
17 |
46 |
47 |
48 | App
49 |
50 | ```
51 |
52 | ```bash
53 | npx tosetup /src/App.vue
54 | ```
55 |
56 | **transform code**
57 |
58 | ```html
59 |
83 |
84 |
85 | App
86 |
87 | ```
88 |
89 | ## Installation
90 |
91 | ### npm
92 | ```bash
93 | npm install --save-dev vue3-script-to-setup
94 | ```
95 |
96 | ### yarn
97 | ```bash
98 | yarn add vue3-script-to-setup -D
99 | ```
100 | ### pnpm
101 | ```bash
102 | pnpm add vue3-script-to-setup -D
103 | ```
104 |
105 | ## Usage
106 |
107 | ### Using Command
108 |
109 | ```bash
110 | npx tosetup [filePath]
111 | ```
112 |
113 | example
114 | ```bash
115 | npx tosetup /src/App.vue --propsNotOnlyTs
116 | ```
117 |
118 | A new `App.new.vue` file will be created in the same directory
119 |
120 | 将会在同目录下创建一个 `App.new.vue` 的新文件
121 |
122 | ### command
123 |
124 | | options | english | chinese |
125 | | ------- | ------- | ------- |
126 | | --propsNotOnlyTs | `props` not using [TypeScript-only Features](https://vuejs.org/api/sfc-script-setup.html#typescript-only-features) | `props` 不使用 [TypeScript-only Features](https://vuejs.org/api/sfc-script-setup.html#typescript-only-features) |
127 | | --notUseNewFile | instead of creating a new file, replace the contents of the file directly with the `setup` mode | 不创建一个新文件,而是将文件中的内容直接替换为 `setup` 模式 |
128 |
129 | ### Using tosetup.config
130 |
131 | Create a `tosetup.config.ts/tosetup.config.js` file in the root directory
132 |
133 | 在根目录下创建一个 `tosetup.config.ts/tosetup.config.js` 文件
134 |
135 | ```ts
136 | import { defineConfig } from "vue3-script-to-setup";
137 |
138 | export default defineConfig({
139 | propsNotOnlyTs: true,
140 | notUseNewFile: true,
141 | path: {
142 | "example/src": {
143 | mode: "*",
144 | excludes: [],
145 | },
146 | "example/src/components": {
147 | mode: "*",
148 | excludes: "Header.vue",
149 | }, // Find the .vue file in the example/src directory, exclude Header.vue files
150 | "example/src/views": ["404.vue"], // transform only the 404.vue in the example/src/views directory
151 | },
152 | });
153 | ```
154 |
155 | ```bash
156 | npx tosetup
157 | ```
158 | defaultOption
159 |
160 | ```ts
161 | interface DefaultOption {
162 | propsNotOnlyTs?: boolean;
163 | notUseNewFile?: boolean;
164 | path: {
165 | [key: string]:
166 | | string
167 | | string[]
168 | | {
169 | mode: "*" | "**";
170 | excludes: string | string[];
171 | };
172 | };
173 | }
174 | ```
175 |
176 | ## Limitations/限制
177 |
178 | Unable to transform `TypeScript-only Features` of `defineEmits`, support only
179 |
180 | 无法将 `defineEmits` 转换为 `TypeScript-only Features` 模式,仅支持转换为数组
181 |
182 | ```ts
183 | const emit = defineEmits(['change', 'delete'])
184 | ```
185 |
186 | If `expose` is not specified, the reference may fail in the outer layer.
187 |
188 | 如果在 `script` 代码下子组件没有通过 `expose` 暴露内部状态,转换为 `setup` 代码后父组件将引用失败。
189 |
190 | ```ts
191 | // Child.vue
192 | export default {
193 | setup() {
194 | function foo() {}
195 | return { foo }
196 | }
197 | }
198 |
199 | // Parent.vue
200 | export default {
201 | mounted() {
202 | // Child.vue is script code, it`s work
203 | // Child.vue is setup code, foo is undefined, need `expose({ foo })`
204 | this.$refs.child.foo()
205 | }
206 | }
207 | ```
208 |
209 | ## Not supported/不支持
210 |
211 | ```ts
212 | export default defineComponent({
213 | name: 'App',
214 | ...optoons, // ❌
215 | directives: {
216 | ...directives, // ❌
217 | },
218 | emit: ["click"],
219 | // ...options ❌
220 | setup(props, { emit, ...options }) {
221 | const obj = reactive({ a, b, c })
222 | options.expose() // ❌
223 |
224 | const { ... } = toRefs(obj) // ✅
225 | function handle() {} // ✅
226 | return {
227 | ...toRefs(obj), // ❌
228 | handle() {}, // ❌
229 | }
230 | }
231 | })
232 | ```
233 |
234 | ## License
235 |
236 | [MIT](https://github.com/a145789/vue3-script-to-setup/blob/main/LICENSE)
237 |
--------------------------------------------------------------------------------
/src/transform/utils.ts:
--------------------------------------------------------------------------------
1 | import type MagicString from "magic-string";
2 | import type { TransformOption } from "./script";
3 | import type {
4 | AssignmentPatternProperty,
5 | CallExpression,
6 | Expression,
7 | Identifier,
8 | ImportDeclaration,
9 | KeyValuePatternProperty,
10 | ObjectPattern,
11 | Span,
12 | TsType,
13 | TsTypeReference,
14 | } from "@swc/core";
15 | import { Visitor } from "@swc/core/Visitor.js";
16 | import type { Output, SetupAst } from "../constants";
17 |
18 | function getPropsValueIdentifier(identifier: string) {
19 | let value = "";
20 | switch (identifier) {
21 | case "Function":
22 | case "Date": {
23 | value = identifier;
24 | break;
25 | }
26 | case "Array": {
27 | value = "any[]";
28 | break;
29 | }
30 | default: {
31 | value = identifier.toLocaleLowerCase();
32 | break;
33 | }
34 | }
35 | return value;
36 | }
37 | export function getPropsValue(
38 | ast: Expression,
39 | keyValue: string,
40 | script: string,
41 | offset: number,
42 | required = false,
43 | ) {
44 | if (ast.type === "Identifier") {
45 | return `${keyValue}${required ? "" : "?"}: ${getPropsValueIdentifier(
46 | ast.value,
47 | )}; `;
48 | }
49 |
50 | if (ast.type === "TsAsExpression") {
51 | const { span } = (ast.typeAnnotation as TsTypeReference).typeParams!
52 | .params[0];
53 | const { start, end } = getRealSpan(span, offset);
54 | return `${keyValue}${required ? "" : "?"}: ${script.slice(start, end)}; `;
55 | }
56 |
57 | if (ast.type === "ArrayExpression") {
58 | return `${keyValue}${required ? "" : "?"}: ${ast.elements
59 | .map((element) =>
60 | getPropsValueIdentifier((element!.expression as Identifier).value),
61 | )
62 | .join(" | ")}; `;
63 | }
64 | return "";
65 | }
66 |
67 | export function getSetupSecondParams(
68 | key: "attrs" | "slots" | "emit" | "expose",
69 | setupAst: SetupAst,
70 | output: Output,
71 | ) {
72 | if (!(setupAst.params.length || setupAst.params[1])) {
73 | return;
74 | }
75 |
76 | const [_, setupParamsAst] = setupAst.params;
77 |
78 | if (!setupParamsAst) {
79 | return;
80 | }
81 |
82 | if (
83 | setupParamsAst.type !== "ObjectPattern" &&
84 | setupParamsAst.type !== "Parameter"
85 | ) {
86 | output.warn(
87 | "The second argument to the setup function is not an object and cannot be resolved",
88 | );
89 | return;
90 | }
91 |
92 | const { properties } =
93 | setupParamsAst.type === "ObjectPattern"
94 | ? setupParamsAst
95 | : (setupParamsAst.pat as ObjectPattern);
96 |
97 | if (properties.some((ast) => ast.type === "RestElement")) {
98 | output.warn(
99 | "The second argument to the setup function has rest element(...rest) and cannot be resolved",
100 | );
101 | return;
102 | }
103 |
104 | const nameAst = (
105 | properties as (AssignmentPatternProperty | KeyValuePatternProperty)[]
106 | ).find((ast) => (ast.key as Identifier).value === key);
107 | if (!nameAst) {
108 | return;
109 | }
110 |
111 | return nameAst.type === "AssignmentPatternProperty"
112 | ? nameAst.key.value
113 | : (nameAst.value as Identifier).value;
114 | }
115 |
116 | Visitor.prototype.visitTsType = (n: TsType) => {
117 | return n;
118 | };
119 | export class MapVisitor extends Visitor {
120 | constructor(visitCb: TransformOption["props"][], ms: MagicString) {
121 | super();
122 | const visits = visitCb.map((V) => new V!(ms));
123 | const keys = [
124 | ...new Set(
125 | visitCb.flatMap((item) => Object.getOwnPropertyNames(item!.prototype)),
126 | ),
127 | ].filter((key) => key !== "constructor") as (keyof Visitor)[];
128 |
129 | for (const key of keys) {
130 | if (key in this && typeof this[key] === "function") {
131 | this[key] = (n: any) => {
132 | for (const visit of visits) {
133 | (visit[key] as any)?.(n);
134 | }
135 |
136 | (super[key] as any)(n);
137 | return n;
138 | };
139 | }
140 | }
141 | }
142 | }
143 |
144 | export class GetCallExpressionFirstArg extends Visitor {
145 | public firstArgAst: Expression[] = [];
146 | private callName = "";
147 | constructor(callName: string) {
148 | super();
149 | this.callName = callName;
150 | }
151 |
152 | visitCallExpression(n: CallExpression): Expression {
153 | const { callName } = this;
154 | if (
155 | n.callee.type === "Identifier" &&
156 | n.callee.value === callName &&
157 | n.arguments[0]
158 | ) {
159 | this.firstArgAst.push(n.arguments[0].expression);
160 | }
161 |
162 | super.visitCallExpression(n);
163 | return n;
164 | }
165 |
166 | visitFn(n: SetupAst): SetupAst {
167 | switch (n.type) {
168 | case "ArrowFunctionExpression":
169 | this.visitArrowFunctionExpression(n);
170 | break;
171 | case "MethodProperty":
172 | this.visitMethodProperty(n);
173 | break;
174 | }
175 |
176 | return n;
177 | }
178 |
179 | visitTsType(n: TsType) {
180 | return n;
181 | }
182 | }
183 |
184 | export function getSpecifierOffset(
185 | n: ImportDeclaration,
186 | index: number,
187 | script: string,
188 | offset: number,
189 | ) {
190 | const { specifiers } = n;
191 | const ast = specifiers[index];
192 | let { end } = getRealSpan({ start: 0, end: ast.span.end }, offset);
193 | const span = getRealSpan({ start: ast.span.start, end: n.span.end }, offset);
194 | if (index + 1 === specifiers.length) {
195 | const commaIdx = script.slice(span.start, span.end).indexOf(",");
196 | if (commaIdx !== -1) {
197 | end = span.start + 1;
198 | }
199 | } else {
200 | end = getRealSpan(specifiers[index + 1].span, offset).start;
201 | }
202 |
203 | return { start: span.start, end };
204 | }
205 |
206 | function isUniCode(str: string) {
207 | return str.charCodeAt(0) > 127;
208 | }
209 |
210 | function getUniCodeLen(str: string) {
211 | return new TextEncoder().encode(str).length;
212 | }
213 |
214 | let unicodeMap: Map = new Map();
215 | export function genScriptUnicodeMap(script: string, offset: number) {
216 | if (unicodeMap.size !== 0) {
217 | unicodeMap = new Map();
218 | }
219 | let keyOffset = 0;
220 | let valueOffset = 0;
221 | for (let i = 0, len = script.length; i < len; i++) {
222 | const str = script[i];
223 | if (isUniCode(str)) {
224 | const len = getUniCodeLen(str);
225 | keyOffset += len;
226 | valueOffset += len - 1;
227 | unicodeMap.set(i + keyOffset + offset, valueOffset);
228 | }
229 | }
230 | }
231 |
232 | export function getRealSpan(
233 | { start, end }: Omit,
234 | offset: number,
235 | ) {
236 | if (!unicodeMap.size) {
237 | return { start: start - offset, end: end - offset };
238 | } else {
239 | let realStart = start;
240 | let realEnd = end;
241 | unicodeMap.forEach((value, key) => {
242 | if (start > key) {
243 | realStart = start - value;
244 | }
245 | if (end > key) {
246 | realEnd = end - value;
247 | }
248 | });
249 |
250 | return { start: realStart - offset, end: realEnd - offset };
251 | }
252 | }
253 |
--------------------------------------------------------------------------------
/src/transform/props.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | ArrayExpression,
3 | BooleanLiteral,
4 | ExportDefaultExpression,
5 | Identifier,
6 | ImportDeclaration,
7 | ImportDefaultSpecifier,
8 | ImportSpecifier,
9 | KeyValueProperty,
10 | NamedImportSpecifier,
11 | ObjectExpression,
12 | } from "@swc/core";
13 | import { ScriptOptions, FileType, SetupAst } from "../constants";
14 | import { getPropsValue, getRealSpan, getSpecifierOffset } from "./utils";
15 | import { Visitor } from "@swc/core/Visitor.js";
16 | import type MagicString from "magic-string";
17 |
18 | function transformProps(
19 | propsAst: ArrayExpression | Identifier | ObjectExpression,
20 | setupAst: SetupAst,
21 | config: ScriptOptions,
22 | ) {
23 | const { script, offset, fileType, propsNotOnlyTs } = config;
24 |
25 | let preCode = "";
26 | let propsName = "";
27 | if (setupAst.params.length) {
28 | const propsNameAst =
29 | setupAst.type === "MethodProperty"
30 | ? setupAst.params[0].pat
31 | : setupAst.params[0];
32 |
33 | propsName = propsNameAst.type === "Identifier" ? propsNameAst.value : "";
34 | preCode = propsName ? `const ${propsName} = ` : "";
35 | }
36 |
37 | let isNormalProps = true;
38 | let isSamePropsName = false;
39 | let str = "";
40 | class MyVisitor extends Visitor {
41 | ms: MagicString;
42 | constructor(ms: MagicString) {
43 | super();
44 | this.ms = ms;
45 | }
46 | visitExportDefaultExpression(node: ExportDefaultExpression) {
47 | const { start } = getRealSpan(node.span, offset);
48 | this.ms.appendLeft(start, str);
49 |
50 | return node;
51 | }
52 | visitImportDeclaration(n: ImportDeclaration) {
53 | if (isNormalProps) {
54 | return n;
55 | }
56 | if (n.source.value === "vue") {
57 | const index = n.specifiers.findIndex(
58 | (ast) => ast.local.value === "PropType",
59 | );
60 | if (index !== -1) {
61 | const { start, end } = getSpecifierOffset(n, index, script, offset);
62 | this.ms.remove(start, end);
63 | n.specifiers.splice(index, 1);
64 | }
65 | }
66 |
67 | return n;
68 | }
69 | visitImportDefaultSpecifier(n: ImportDefaultSpecifier): ImportSpecifier {
70 | if (!isSamePropsName) {
71 | return n;
72 | }
73 | const { value, span } = n.local;
74 | const { start, end } = getRealSpan(span, offset);
75 | if (value === propsName) {
76 | this.ms.update(start, end, `$${propsName}`);
77 | }
78 | return n;
79 | }
80 | visitNamedImportSpecifier(n: NamedImportSpecifier) {
81 | if (!isSamePropsName) {
82 | return n;
83 | }
84 | const {
85 | local: { value, span },
86 | imported,
87 | } = n;
88 | if (!imported) {
89 | if (value === propsName) {
90 | const { end } = getRealSpan(span, offset);
91 | this.ms.appendRight(end, ` as $${propsName}`);
92 | }
93 | } else {
94 | if (value === propsName) {
95 | const { start, end } = getRealSpan(span, offset);
96 | this.ms.update(start, end, `$${propsName}`);
97 | }
98 | }
99 | return n;
100 | }
101 | }
102 | if (propsAst.type === "ArrayExpression") {
103 | const { start, end } = getRealSpan(propsAst.span, offset);
104 | str = `${preCode}defineProps(${script.slice(start, end)});\n`;
105 | return MyVisitor;
106 | }
107 |
108 | if (propsAst.type === "Identifier") {
109 | if (propsName !== propsAst.value) {
110 | str = `${preCode}defineProps(${propsAst.value});\n`;
111 | } else {
112 | str = `${preCode}defineProps($${propsAst.value});\n`;
113 | isSamePropsName = true;
114 | }
115 | return MyVisitor;
116 | }
117 |
118 | if (!propsAst.properties.length) {
119 | str = `${preCode}defineProps();\n`;
120 | return MyVisitor;
121 | }
122 |
123 | isNormalProps =
124 | propsNotOnlyTs ||
125 | fileType !== FileType.ts ||
126 | propsAst.properties.some(
127 | (ast) =>
128 | ast.type === "AssignmentProperty" ||
129 | ast.type === "GetterProperty" ||
130 | ast.type === "Identifier" ||
131 | ast.type === "MethodProperty" ||
132 | ast.type === "SetterProperty" ||
133 | ast.type === "SpreadElement" ||
134 | (ast.type === "KeyValueProperty" &&
135 | ast.value.type !== "Identifier" &&
136 | ast.value.type !== "TsAsExpression" &&
137 | ast.value.type !== "ArrayExpression" &&
138 | (ast.value.type !== "ObjectExpression" ||
139 | (ast.value.type === "ObjectExpression" &&
140 | ast.value.properties.some(
141 | (item) =>
142 | item.type === "KeyValueProperty" &&
143 | (((item.key.type === "StringLiteral" ||
144 | item.key.type === "Identifier") &&
145 | item.key.value === "validator") ||
146 | item.key.type === "Computed" ||
147 | item.value.type === "MemberExpression"),
148 | )))),
149 | );
150 |
151 | if (isNormalProps) {
152 | const { start, end } = getRealSpan(propsAst.span, offset);
153 | str = `${preCode}defineProps(${script.slice(start, end)});\n`;
154 | return MyVisitor;
155 | }
156 |
157 | let propsDefault = "";
158 | const propsType = (propsAst.properties as KeyValueProperty[]).map(
159 | ({ key, value }) => {
160 | const keyValue = (key as Identifier).value;
161 | const valueIdentifier = getPropsValue(value, keyValue, script, offset);
162 | if (valueIdentifier) {
163 | return valueIdentifier;
164 | }
165 |
166 | const { properties } = value as { properties: KeyValueProperty[] };
167 | if (!properties.length) {
168 | return "";
169 | }
170 |
171 | const requiredAstIndex = properties.findIndex(
172 | (ast) => (ast.key as Identifier).value === "required",
173 | );
174 | let required = false;
175 | if (requiredAstIndex !== -1) {
176 | const [requiredAst] = properties.splice(requiredAstIndex, 1);
177 | required = (requiredAst.value as BooleanLiteral).value;
178 | }
179 | const { propType, defaultProp } = properties.reduce<{
180 | propType?: string;
181 | defaultProp?: string;
182 | }>((p, c) => {
183 | const typeKeyValue = (c.key as Identifier).value;
184 | if (typeKeyValue === "type") {
185 | p.propType = getPropsValue(
186 | c.value,
187 | keyValue,
188 | script,
189 | offset,
190 | required,
191 | );
192 | }
193 |
194 | if (typeKeyValue === "default") {
195 | const { span } = c.value as Identifier;
196 | const { start, end } = getRealSpan(span, offset);
197 | p.defaultProp = script.slice(start, end);
198 | }
199 | return p;
200 | }, {});
201 |
202 | if (defaultProp) {
203 | propsDefault += `${keyValue}: ${defaultProp}, `;
204 | }
205 |
206 | return propType;
207 | },
208 | );
209 |
210 | const propsTypeTem = `defineProps<{ ${propsType.join("")} }>()`;
211 |
212 | str = `${preCode}${
213 | propsDefault
214 | ? `withDefaults(${propsTypeTem}, { ${propsDefault} });\n`
215 | : `${propsTypeTem};\n`
216 | }`;
217 | return MyVisitor;
218 | }
219 |
220 | export default transformProps;
221 |
--------------------------------------------------------------------------------
/src/transform/script.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | ArrayExpression,
3 | ArrowFunctionExpression,
4 | ExportDefaultExpression,
5 | Expression,
6 | Identifier,
7 | ImportDeclaration,
8 | KeyValueProperty,
9 | MethodProperty,
10 | ObjectExpression,
11 | PropertyName,
12 | Span,
13 | Statement,
14 | } from "@swc/core";
15 |
16 | import { ScriptOptions, parseOption } from "../constants";
17 | import {
18 | genScriptUnicodeMap,
19 | getRealSpan,
20 | getSpecifierOffset,
21 | MapVisitor,
22 | } from "./utils";
23 | import transformComponents from "./components";
24 | import transformDirectives from "./directives";
25 | import transformEmits from "./emits";
26 | import transformProps from "./props";
27 | import MagicString from "magic-string";
28 | import transformExpose from "./expose";
29 | import transformAttrsAndSlots from "./attrsAndSlots";
30 | import { Visitor } from "@swc/core/Visitor.js";
31 |
32 | const ILLEGAL_OPTIONS_API = [
33 | "data",
34 | "watch",
35 | "computed",
36 | "methods",
37 | "created",
38 | "beforeMount",
39 | "mounted",
40 | "beforeUnmount",
41 | "unmounted",
42 | "beforeUpdate",
43 | "updated",
44 | "activated",
45 | "deactivated",
46 | "render",
47 | "errorCaptured",
48 | "serverPrefetch",
49 | "mixins",
50 | "extends",
51 | ] as readonly string[];
52 |
53 | export interface TransformOption {
54 | props?: ReturnType;
55 | emits?: ReturnType;
56 | components?: ReturnType;
57 | directives?: ReturnType;
58 | attrsAndSlots?: ReturnType;
59 | expose?: ReturnType;
60 | setup?: ReturnType;
61 | }
62 |
63 | function transformScript(options: ScriptOptions) {
64 | const { parseSync, output } = options;
65 | const program = parseSync(options.script, parseOption);
66 |
67 | const {
68 | body,
69 | span: { start },
70 | } = program;
71 |
72 | options.offset = start;
73 | genScriptUnicodeMap(options.script, start);
74 |
75 | const exportDefaultAst = body.find(
76 | (item) => item.type === "ExportDefaultExpression",
77 | ) as { expression: Expression; span: Span };
78 |
79 | if (!exportDefaultAst) {
80 | output.warn("The export default cannot be found");
81 | return null;
82 | }
83 | let optionAst: ObjectExpression | null = null;
84 | switch (exportDefaultAst.expression.type) {
85 | case "CallExpression": {
86 | optionAst = exportDefaultAst.expression.arguments[0]
87 | .expression as ObjectExpression;
88 | break;
89 | }
90 | case "ObjectExpression": {
91 | optionAst = exportDefaultAst.expression;
92 | break;
93 | }
94 |
95 | default:
96 | break;
97 | }
98 |
99 | if (!optionAst) {
100 | output.warn("The options cannot be found");
101 | return null;
102 | }
103 |
104 | const setupAst = optionAst.properties.find((ast) => {
105 | if (
106 | (ast.type === "MethodProperty" || ast.type === "KeyValueProperty") &&
107 | (ast.key.type === "Identifier" || ast.key.type === "StringLiteral")
108 | ) {
109 | switch (ast.type) {
110 | case "MethodProperty":
111 | return ast.key.value === "setup";
112 | case "KeyValueProperty":
113 | return (
114 | ast.value.type === "ArrowFunctionExpression" &&
115 | ast.key.value === "setup"
116 | );
117 |
118 | default:
119 | return false;
120 | }
121 | }
122 | return false;
123 | }) as MethodProperty | KeyValueProperty;
124 | if (!setupAst) {
125 | output.warn("There is no setup method in options");
126 | return null;
127 | }
128 | const setupFnAst =
129 | setupAst.type === "MethodProperty"
130 | ? setupAst
131 | : (setupAst.value as ArrowFunctionExpression);
132 |
133 | const transformOption: TransformOption = {};
134 | for (const ast of optionAst.properties) {
135 | if (ast.type === "SpreadElement") {
136 | output.warn("The Spread syntax(...) cannot be resolved in options");
137 | return null;
138 | }
139 | if (
140 | ast.type === "GetterProperty" ||
141 | ast.type === "SetterProperty" ||
142 | ast.type === "AssignmentProperty" ||
143 | ast.type === "MethodProperty"
144 | ) {
145 | continue;
146 | }
147 |
148 | const key = ast.type === "Identifier" ? ast.value : getOptionKey(ast.key);
149 | if (!key || ILLEGAL_OPTIONS_API.includes(key)) {
150 | output.warn(`${ILLEGAL_OPTIONS_API.join()} cannot be parsed in option`);
151 | return null;
152 | }
153 | try {
154 | const value = ast.type === "Identifier" ? ast : ast.value;
155 | switch (key) {
156 | case "props": {
157 | transformOption.props = transformProps(
158 | value as ArrayExpression | Identifier | ObjectExpression,
159 | setupFnAst,
160 | options,
161 | );
162 | break;
163 | }
164 | case "emits": {
165 | transformOption.emits = transformEmits(
166 | value as ArrayExpression | Identifier | ObjectExpression,
167 | setupFnAst,
168 | options,
169 | );
170 | break;
171 | }
172 | case "components": {
173 | transformOption.components = transformComponents(
174 | value as ArrayExpression | Identifier | ObjectExpression,
175 | setupFnAst,
176 | options,
177 | );
178 | break;
179 | }
180 | case "directives": {
181 | transformOption.directives = transformDirectives(
182 | value as Identifier | ObjectExpression,
183 | setupFnAst,
184 | options,
185 | );
186 | break;
187 | }
188 | default:
189 | break;
190 | }
191 | } catch (error) {
192 | output.error("Error parsing option item");
193 | console.log(error);
194 | }
195 | }
196 |
197 | try {
198 | if (!transformOption.emits) {
199 | transformOption.emits = transformEmits(null, setupFnAst, options);
200 | }
201 | transformOption.expose = transformExpose(setupFnAst, options);
202 | transformOption.attrsAndSlots = transformAttrsAndSlots(setupFnAst, options);
203 | } catch (error) {
204 | output.error("Error parsing option item");
205 | console.log(error);
206 | }
207 |
208 | const { script, offset } = options;
209 | class SetupVisitor extends Visitor {
210 | ms: MagicString;
211 | exportDefaultExpressionSpan = {
212 | start: 0,
213 | end: 0,
214 | };
215 | constructor(ms: MagicString) {
216 | super();
217 | this.ms = ms;
218 | }
219 | visitExportDefaultExpression(node: ExportDefaultExpression) {
220 | this.exportDefaultExpressionSpan = node.span;
221 | return node;
222 | }
223 | myVisitStatements(items: Statement[]) {
224 | const { start, end } = this.exportDefaultExpressionSpan;
225 |
226 | for (const ast of items) {
227 | if (ast.type === "ReturnStatement") {
228 | const { start, end } = getRealSpan(ast.span, offset);
229 | this.ms.remove(start, end);
230 | break;
231 | }
232 | }
233 |
234 | const firstNode = items[0];
235 | const lastNode = items[items.length - 1];
236 | if (firstNode) {
237 | const frontSpan = getRealSpan(
238 | { start, end: firstNode.span.start },
239 | offset,
240 | );
241 | this.ms.remove(frontSpan.start, frontSpan.end);
242 | const rearSpan = getRealSpan({ start: lastNode.span.end, end }, offset);
243 | this.ms.remove(rearSpan.start, rearSpan.end);
244 | }
245 |
246 | return items;
247 | }
248 | visitMethodProperty(node: MethodProperty) {
249 | if (
250 | (node.key.type === "Identifier" || node.key.type === "StringLiteral") &&
251 | node.key.value === "setup" &&
252 | node.body?.stmts
253 | ) {
254 | this.myVisitStatements(node.body.stmts);
255 | }
256 | return node;
257 | }
258 | visitKeyValueProperty(node: KeyValueProperty) {
259 | if (
260 | (node.key.type === "Identifier" || node.key.type === "StringLiteral") &&
261 | node.key.value === "setup" &&
262 | node.value.type === "ArrowFunctionExpression"
263 | ) {
264 | const { body } = node.value;
265 | if (body.type === "BlockStatement") {
266 | this.myVisitStatements(body.stmts);
267 | }
268 | }
269 | return node;
270 | }
271 | visitImportDeclaration(n: ImportDeclaration) {
272 | if (n.source.value === "vue") {
273 | const index = n.specifiers.findIndex(
274 | (ast) => ast.local.value === "defineComponent",
275 | );
276 | if (index !== -1) {
277 | const { start, end } = getSpecifierOffset(n, index, script, offset);
278 | this.ms.remove(start, end);
279 | n.specifiers.splice(index, 1);
280 | }
281 | if (!n.specifiers.length) {
282 | const { start, end } = getRealSpan(n.span, offset);
283 | this.ms.remove(start, end);
284 | }
285 | }
286 |
287 | return n;
288 | }
289 | }
290 |
291 | transformOption.setup = SetupVisitor;
292 |
293 | const ms = new MagicString(script);
294 |
295 | const visitCbs = [
296 | transformOption?.components,
297 | transformOption?.props,
298 | transformOption?.emits,
299 | transformOption?.directives,
300 | transformOption?.attrsAndSlots,
301 | transformOption?.setup,
302 | transformOption?.expose,
303 | ].filter(Boolean) as TransformOption["props"][];
304 | if (visitCbs.length) {
305 | const visitor = new MapVisitor(visitCbs, ms);
306 | visitor.visitProgram(program);
307 | }
308 |
309 | return `${ms.toString()}\n`;
310 | }
311 |
312 | export default transformScript;
313 |
314 | function getOptionKey(key: PropertyName) {
315 | switch (key.type) {
316 | case "Identifier":
317 | case "StringLiteral":
318 | return key.value;
319 |
320 | default:
321 | return "";
322 | }
323 | }
324 |
--------------------------------------------------------------------------------
/test/utils.ts:
--------------------------------------------------------------------------------
1 | export const testScript1 = {
2 | code: `
3 | import type { PropType } from 'vue';
4 | import { defineComponent, onMounted, onUnmounted, ref } from 'vue';
5 | // @ts-ignore
6 | import BTween from 'b-tween';
7 | import { getPrefixCls } from '../_utils/global-config';
8 | import { on, off } from '../_utils/dom';
9 | import { throttleByRaf } from '../_utils/throttle-by-raf';
10 | import IconToTop from '../icon/icon-to-top';
11 | import { isString } from '../_utils/is';
12 | export default defineComponent({
13 | name: 'BackTop',
14 | components: {
15 | IconToTop,
16 | },
17 | props: {
18 | /**
19 | * @zh 显示回到顶部按钮的触发滚动高度
20 | * @en Display the trigger scroll height of the back to top button
21 | */
22 | visibleHeight: {
23 | type: Number as PropType,
24 | default: 200,
25 | },
26 | /**
27 | * @zh 滚动事件的监听容器
28 | * @en Scroll event listener container
29 | */
30 | targetContainer: {
31 | type: [String, Object] as PropType,
32 | },
33 | /**
34 | * @zh 滚动动画的缓动方式,可选值参考 [BTween](https://github.com/PengJiyuan/b-tween)
35 | * @en Easing mode of scrolling animation, refer to [BTween](https://github.com/PengJiyuan/b-tween) for optional values
36 | */
37 | easing: {
38 | type: String,
39 | default: 'quartOut',
40 | },
41 | /**
42 | * @zh 滚动动画的持续时间
43 | * @en Duration of scroll animation
44 | */
45 | duration: {
46 | type: Number,
47 | default: 200,
48 | },
49 | },
50 | setup(props) {
51 | const prefixCls = getPrefixCls('back-top');
52 | const visible = ref(false);
53 | const target = ref();
54 | const isWindow = !props.targetContainer;
55 | const scrollHandler = throttleByRaf(() => {
56 | if (target.value) {
57 | const { visibleHeight } = props;
58 | const { scrollTop } = target.value;
59 | visible.value = scrollTop >= visibleHeight;
60 | }
61 | });
62 | const getContainer = (container: string | HTMLElement) => {
63 | if (isString(container)) {
64 | return document.querySelector(container) as HTMLElement;
65 | }
66 | return container;
67 | };
68 | onMounted(() => {
69 | target.value = isWindow
70 | ? document?.documentElement
71 | : getContainer(props.targetContainer);
72 | if (target.value) {
73 | on(isWindow ? window : target.value, 'scroll', scrollHandler);
74 | scrollHandler();
75 | }
76 | });
77 | onUnmounted(() => {
78 | scrollHandler.cancel();
79 | if (target.value) {
80 | off(isWindow ? window : target.value, 'scroll', scrollHandler);
81 | }
82 | });
83 | const scrollToTop = () => {
84 | if (target.value) {
85 | const { scrollTop } = target.value;
86 | const tween = new BTween({
87 | from: { scrollTop },
88 | to: { scrollTop: 0 },
89 | easing: props.easing,
90 | duration: props.duration,
91 | onUpdate: (keys: any) => {
92 | if (target.value) {
93 | target.value.scrollTop = keys.scrollTop;
94 | }
95 | },
96 | });
97 | tween.start();
98 | // props.onClick && props.onClick();
99 | }
100 | };
101 | return {
102 | prefixCls,
103 | visible,
104 | scrollToTop,
105 | };
106 | },
107 | });
108 | `,
109 | transform: `
110 | import { onMounted, onUnmounted, ref } from 'vue';
111 | // @ts-ignore
112 | import BTween from 'b-tween';
113 | import { getPrefixCls } from '../_utils/global-config';
114 | import { on, off } from '../_utils/dom';
115 | import { throttleByRaf } from '../_utils/throttle-by-raf';
116 | import IconToTop from '../icon/icon-to-top';
117 | import { isString } from '../_utils/is';
118 |
119 | const props = withDefaults(defineProps<{visibleHeight?:number;targetContainer?:string | HTMLElement;easing?:string;duration?:number}>(),{visibleHeight:200,easing:'quartOut',duration:200,});
120 |
121 | const prefixCls = getPrefixCls('back-top');
122 | const visible = ref(false);
123 | const target = ref();
124 | const isWindow = !props.targetContainer;
125 | const scrollHandler = throttleByRaf(() => {
126 | if (target.value) {
127 | const { visibleHeight } = props;
128 | const { scrollTop } = target.value;
129 | visible.value = scrollTop >= visibleHeight;
130 | }
131 | });
132 | const getContainer = (container: string | HTMLElement) => {
133 | if (isString(container)) {
134 | return document.querySelector(container) as HTMLElement;
135 | }
136 | return container;
137 | };
138 | onMounted(() => {
139 | target.value = isWindow
140 | ? document?.documentElement
141 | : getContainer(props.targetContainer);
142 | if (target.value) {
143 | on(isWindow ? window : target.value, 'scroll', scrollHandler);
144 | scrollHandler();
145 | }
146 | });
147 | onUnmounted(() => {
148 | scrollHandler.cancel();
149 | if (target.value) {
150 | off(isWindow ? window : target.value, 'scroll', scrollHandler);
151 | }
152 | });
153 | const scrollToTop = () => {
154 | if (target.value) {
155 | const { scrollTop } = target.value;
156 | const tween = new BTween({
157 | from: { scrollTop },
158 | to: { scrollTop: 0 },
159 | easing: props.easing,
160 | duration: props.duration,
161 | onUpdate: (keys: any) => {
162 | if (target.value) {
163 | target.value.scrollTop = keys.scrollTop;
164 | }
165 | },
166 | });
167 | tween.start();
168 | // props.onClick && props.onClick();
169 | }
170 | };
171 | `,
172 | };
173 |
174 | export const testScript2 = {
175 | code: `
176 | import Ripple from '../ripple'
177 | import VarLoading from '../loading'
178 | import { defineComponent, ref } from 'vue'
179 | import { props } from './props'
180 | import emits from './emits'
181 | import { createNamespace } from '../utils/components'
182 | import type { Ref } from 'vue'
183 | const { n, classes } = createNamespace('button')
184 | export default defineComponent({
185 | name: 'VarButton',
186 | components: {
187 | VarLoading,
188 | },
189 | directives: { Ripple },
190 | props,
191 | emits,
192 | setup(props,{ emit:emits }) {
193 | const pending: Ref = ref(false)
194 | const attemptAutoLoading = (result: any) => {
195 | if (props.autoLoading) {
196 | pending.value = true
197 | Promise.resolve(result)
198 | .then(() => {
199 | pending.value = false
200 | })
201 | .catch(() => {
202 | pending.value = false
203 | })
204 | }
205 | }
206 | const handleClick = (e: Event) => {
207 | const { loading, disabled, onClick } = props
208 | if (!onClick || loading || disabled || pending.value) {
209 | return
210 | }
211 | attemptAutoLoading(onClick(e))
212 | }
213 | const handleTouchstart = (e: Event) => {
214 | const { loading, disabled, onTouchstart } = props
215 | if (!onTouchstart || loading || disabled || pending.value) {
216 | return
217 | }
218 | attemptAutoLoading(onTouchstart(e))
219 | }
220 | return {
221 | n,
222 | classes,
223 | pending,
224 | handleClick,
225 | handleTouchstart,
226 | }
227 | },
228 | })
229 | `,
230 | transform: `
231 | import vRipple from '../ripple'
232 | import VarLoading from '../loading'
233 | import { ref } from 'vue'
234 | import { props as $props } from './props'
235 | import $emits from './emits'
236 | import { createNamespace } from '../utils/components'
237 | import type { Ref } from 'vue'
238 | const { n, classes } = createNamespace('button')
239 |
240 | const props = defineProps($props);
241 | const emits = defineEmits($emits);
242 |
243 | const pending: Ref = ref(false)
244 |
245 | const attemptAutoLoading = (result: any) => {
246 | if (props.autoLoading) {
247 | pending.value = true
248 | Promise.resolve(result)
249 | .then(() => {
250 | pending.value = false
251 | })
252 | .catch(() => {
253 | pending.value = false
254 | })
255 | }
256 | }
257 | const handleClick = (e: Event) => {
258 | const { loading, disabled, onClick } = props
259 | if (!onClick || loading || disabled || pending.value) {
260 | return
261 | }
262 | attemptAutoLoading(onClick(e))
263 | }
264 | const handleTouchstart = (e: Event) => {
265 | const { loading, disabled, onTouchstart } = props
266 | if (!onTouchstart || loading || disabled || pending.value) {
267 | return
268 | }
269 | attemptAutoLoading(onTouchstart(e))
270 | }
271 | `,
272 | };
273 |
274 | export const testScript3 = {
275 | code: `
276 | import {
277 | defineComponent,
278 | type PropType,
279 | type CSSProperties,
280 | type ExtractPropTypes,
281 | } from 'vue';
282 |
283 | // Utils
284 | import {
285 | extend,
286 | numericProp,
287 | preventDefault,
288 | makeStringProp,
289 | createNamespace,
290 | BORDER_SURROUND,
291 | } from '../utils';
292 | import { useRoute, routeProps } from '../composables/use-route';
293 |
294 | // Components
295 | import { Icon } from '../icon';
296 | import { Loading, LoadingType } from '../loading';
297 |
298 | // Types
299 | import {
300 | ButtonSize,
301 | ButtonType,
302 | ButtonNativeType,
303 | ButtonIconPosition,
304 | } from './types';
305 |
306 | const [name, bem] = createNamespace('button');
307 |
308 | const buttonProps = extend({}, routeProps, {
309 | tag: makeStringProp('button'),
310 | text: String,
311 | icon: String,
312 | type: makeStringProp('default'),
313 | size: makeStringProp('normal'),
314 | color: String,
315 | block: Boolean,
316 | plain: Boolean,
317 | round: Boolean,
318 | square: Boolean,
319 | loading: Boolean,
320 | hairline: Boolean,
321 | disabled: Boolean,
322 | iconPrefix: String,
323 | nativeType: makeStringProp('button'),
324 | loadingSize: numericProp,
325 | loadingText: String,
326 | loadingType: String as PropType,
327 | iconPosition: makeStringProp('left'),
328 | });
329 |
330 | type ButtonProps = ExtractPropTypes;
331 |
332 | export default defineComponent({
333 | name,
334 |
335 | props: buttonProps,
336 |
337 | emits: ['click'],
338 |
339 | setup(props, { emit, slots }) {
340 | const route = useRoute();
341 |
342 | const renderLoadingIcon = () => {
343 | if (slots.loading) {
344 | return slots.loading();
345 | }
346 |
347 | return
348 | };
349 |
350 | const renderIcon = () => {
351 | if (props.loading) {
352 | return renderLoadingIcon();
353 | }
354 |
355 | if (slots.icon) {
356 | return
357 | }
358 |
359 | if (props.icon) {
360 | return
361 | }
362 | };
363 |
364 | const renderText = () => {
365 | let text;
366 | if (props.loading) {
367 | text = props.loadingText;
368 | } else {
369 | text = slots.default ? slots.default() : props.text;
370 | }
371 |
372 | if (text) {
373 | return
374 | }
375 | };
376 |
377 | const getStyle = () => {
378 | const { color, plain } = props;
379 | if (color) {
380 | const style: CSSProperties = {
381 | color: plain ? color : 'white',
382 | };
383 |
384 | if (!plain) {
385 | // Use background instead of backgroundColor to make linear-gradient work
386 | style.background = color;
387 | }
388 |
389 | // hide border when color is linear-gradient
390 | if (color.includes('gradient')) {
391 | style.border = 0;
392 | } else {
393 | style.borderColor = color;
394 | }
395 |
396 | return style;
397 | }
398 | };
399 |
400 | const onClick = (event: MouseEvent) => {
401 | if (props.loading) {
402 | preventDefault(event);
403 | } else if (!props.disabled) {
404 | emit('click', event);
405 | route();
406 | }
407 | };
408 |
409 | return {}
410 | },
411 | });`,
412 | transform: `
413 | import {
414 | useSlots,
415 | type PropType,
416 | type CSSProperties,
417 | type ExtractPropTypes,
418 | } from 'vue';
419 |
420 | // Utils
421 | import {
422 | extend,
423 | numericProp,
424 | preventDefault,
425 | makeStringProp,
426 | createNamespace,
427 | BORDER_SURROUND,
428 | } from '../utils';
429 | import { useRoute, routeProps } from '../composables/use-route';
430 | // Components
431 | import { Icon } from '../icon';
432 | import { Loading, LoadingType } from '../loading';
433 |
434 | // Types
435 | import {
436 | ButtonSize,
437 | ButtonType,
438 | ButtonNativeType,
439 | ButtonIconPosition,
440 | } from './types';
441 |
442 | const [name, bem] = createNamespace('button');
443 |
444 | const buttonProps = extend({}, routeProps, {
445 | tag: makeStringProp('button'),
446 | text: String,
447 | icon: String,
448 | type: makeStringProp('default'),
449 | size: makeStringProp('normal'),
450 | color: String,
451 | block: Boolean,
452 | plain: Boolean,
453 | round: Boolean,
454 | square: Boolean,
455 | loading: Boolean,
456 | hairline: Boolean,
457 | disabled: Boolean,
458 | iconPrefix: String,
459 | nativeType: makeStringProp('button'),
460 | loadingSize: numericProp,
461 | loadingText: String,
462 | loadingType: String as PropType,
463 | iconPosition: makeStringProp('left'),
464 | });
465 |
466 | type ButtonProps = ExtractPropTypes;
467 |
468 | const props = defineProps(buttonProps)
469 |
470 | const emit = defineEmits(['click']);
471 |
472 | const slots = useSlots()
473 |
474 | const route = useRoute();
475 |
476 | const renderLoadingIcon = () => {
477 | if (slots.loading) {
478 | return slots.loading();
479 | }
480 |
481 | return
482 | };
483 |
484 | const renderIcon = () => {
485 | if (props.loading) {
486 | return renderLoadingIcon();
487 | }
488 |
489 | if (slots.icon) {
490 | return
491 | }
492 |
493 | if (props.icon) {
494 | return
495 | }
496 | };
497 |
498 | const renderText = () => {
499 | let text;
500 | if (props.loading) {
501 | text = props.loadingText;
502 | } else {
503 | text = slots.default ? slots.default() : props.text;
504 | }
505 |
506 | if (text) {
507 | return
508 | }
509 | };
510 |
511 | const getStyle = () => {
512 | const { color, plain } = props;
513 | if (color) {
514 | const style: CSSProperties = {
515 | color: plain ? color : 'white',
516 | };
517 |
518 | if (!plain) {
519 | // Use background instead of backgroundColor to make linear-gradient work
520 | style.background = color;
521 | }
522 |
523 | // hide border when color is linear-gradient
524 | if (color.includes('gradient')) {
525 | style.border = 0;
526 | } else {
527 | style.borderColor = color;
528 | }
529 |
530 | return style;
531 | }
532 | };
533 |
534 | const onClick = (event: MouseEvent) => {
535 | if (props.loading) {
536 | preventDefault(event);
537 | } else if (!props.disabled) {
538 | emit('click', event);
539 | route();
540 | }
541 | };
542 | `,
543 | };
544 |
545 | export function transformToSingeLine(str: string | undefined | null) {
546 | if (!str) {
547 | return "";
548 | }
549 | return str.replace(/\s|,|;|"|'/g, "");
550 | }
551 |
--------------------------------------------------------------------------------