├── .prettierignore
├── pnpm-workspace.yaml
├── packages
├── examples
│ ├── env.d.ts
│ ├── public
│ │ └── favicon.ico
│ ├── src
│ │ ├── main.ts
│ │ ├── App.vue
│ │ ├── App.vue.original
│ │ └── App.vue.magrated
│ ├── tsconfig.json
│ ├── tsconfig.app.json
│ ├── vite.config.ts
│ ├── index.html
│ ├── tsconfig.node.json
│ ├── package.json
│ └── migration
│ │ ├── index.js
│ │ ├── 001_RegexpExamples.js
│ │ ├── 002_AstManipulateExamples.js
│ │ └── 003_MagicStringExamples.js
└── vue-service-bay
│ ├── tsconfig.json
│ ├── src
│ ├── helper.ts
│ ├── index.ts
│ ├── languages
│ │ ├── style.spec.ts
│ │ ├── template.spec.ts
│ │ ├── script.ts
│ │ ├── script.spec.ts
│ │ ├── style.ts
│ │ └── template.ts
│ ├── save.ts
│ └── parse.ts
│ ├── CHANGELOG.md
│ ├── package.json
│ └── README.md
├── .changeset
└── config.json
├── package.json
├── renovate.json
├── .github
└── workflows
│ ├── ci.yaml
│ └── release.yml
├── LICENSE
├── .gitignore
├── README.md
└── pnpm-lock.yaml
/.prettierignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - "packages/*"
3 |
--------------------------------------------------------------------------------
/packages/examples/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/packages/examples/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flyle-io/vue-service-bay/HEAD/packages/examples/public/favicon.ico
--------------------------------------------------------------------------------
/packages/examples/src/main.ts:
--------------------------------------------------------------------------------
1 | import "./assets/main.css";
2 |
3 | import { createApp } from "vue";
4 | import App from "./App.vue";
5 |
6 | createApp(App).mount("#app");
7 |
--------------------------------------------------------------------------------
/packages/vue-service-bay/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@tsconfig/strictest/tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "dist",
5 | "module": "Node16",
6 | },
7 | "exclude": ["node_modules", "dist", "**/*.spec.ts"],
8 | }
9 |
--------------------------------------------------------------------------------
/packages/examples/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | {
5 | "path": "./tsconfig.node.json"
6 | },
7 | {
8 | "path": "./tsconfig.app.json"
9 | }
10 | ],
11 | "compilerOptions": {
12 | "allowJs": true,
13 | "checkJs": true,
14 | "outDir": "dist"
15 | },
16 | "include": ["migration/*"]
17 | }
18 |
--------------------------------------------------------------------------------
/packages/vue-service-bay/src/helper.ts:
--------------------------------------------------------------------------------
1 | import { globby } from "globby";
2 | import { resolve } from "path";
3 |
4 | export const getAllVueFiles = async (dir: string): Promise => {
5 | const files = await globby("**/*.vue", {
6 | cwd: dir,
7 | ignore: ["node_modules"],
8 | });
9 | return files.map((file) => resolve(dir, file));
10 | };
11 |
--------------------------------------------------------------------------------
/packages/examples/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@vue/tsconfig/tsconfig.dom.json",
3 | "include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
4 | "exclude": ["src/**/__tests__/*"],
5 | "compilerOptions": {
6 | "composite": true,
7 | "noEmit": false,
8 | "baseUrl": ".",
9 | "paths": {
10 | "@/*": ["./src/*"]
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/packages/examples/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { fileURLToPath, URL } from 'node:url'
2 |
3 | import { defineConfig } from 'vite'
4 | import vue from '@vitejs/plugin-vue'
5 |
6 | // https://vitejs.dev/config/
7 | export default defineConfig({
8 | plugins: [
9 | vue(),
10 | ],
11 | resolve: {
12 | alias: {
13 | '@': fileURLToPath(new URL('./src', import.meta.url))
14 | }
15 | }
16 | })
17 |
--------------------------------------------------------------------------------
/packages/examples/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite App
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/packages/examples/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@tsconfig/node18/tsconfig.json",
3 | "include": [
4 | "vite.config.*",
5 | "vitest.config.*",
6 | "cypress.config.*",
7 | "nightwatch.conf.*",
8 | "playwright.config.*"
9 | ],
10 | "compilerOptions": {
11 | "composite": true,
12 | "module": "ESNext",
13 | "moduleResolution": "Bundler",
14 | "types": ["node"]
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/.changeset/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json",
3 | "changelog": [
4 | "@changesets/changelog-github",
5 | {
6 | "repo": "flyle-io/vue-service-bay"
7 | }
8 | ],
9 | "commit": false,
10 | "fixed": [],
11 | "linked": [],
12 | "access": "public",
13 | "baseBranch": "main",
14 | "updateInternalDependencies": "patch",
15 | "ignore": []
16 | }
17 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "author": "baseballyama",
3 | "license": "MIT",
4 | "scripts": {
5 | "update:version": "changeset version",
6 | "release": "changeset publish"
7 | },
8 | "devDependencies": {
9 | "@changesets/changelog-github": "0.5.2",
10 | "@changesets/cli": "2.28.1",
11 | "@tsconfig/strictest": "2.0.8",
12 | "@types/node": "22.13.17",
13 | "prettier": "3.5.3",
14 | "typescript": "5.8.3"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "config:base",
5 | ":automergePatch",
6 | ":timezone(Asia/Tokyo)",
7 | "npm:unpublishSafe"
8 | ],
9 | "dependencyDashboard": false,
10 | "schedule": ["after 4am and before 7am every weekday"],
11 | "prConcurrentLimit": 10,
12 | "prHourlyLimit": 0,
13 | "minimumReleaseAge": "3 days",
14 | "major": {
15 | "stabilityDays": 30
16 | },
17 | "minor": {
18 | "stabilityDays": 7
19 | },
20 | "patch": {
21 | "stabilityDays": 3
22 | },
23 | "ignoreTests": false
24 | }
25 |
--------------------------------------------------------------------------------
/packages/vue-service-bay/src/index.ts:
--------------------------------------------------------------------------------
1 | import MagicString from "magic-string";
2 |
3 | export { MagicString };
4 | export { is as tsNodeIs } from "@babel/types";
5 |
6 | export { getAllVueFiles } from "./helper.js";
7 | export type { VueFile } from "./parse.js";
8 | export { parseVueFile } from "./parse.js";
9 | export { save, saveAsString } from "./save.js";
10 | export {
11 | walk as walkTemplate,
12 | manipulate as manipulateHtml,
13 | } from "./languages/template.js";
14 | export {
15 | walk as walkScript,
16 | manipulate as manipulateScript,
17 | } from "./languages/script.js";
18 | export {
19 | walk as walkStyle,
20 | manipulate as manipulateStyle,
21 | cssNodeIs,
22 | } from "./languages/style.js";
23 |
--------------------------------------------------------------------------------
/packages/vue-service-bay/src/languages/style.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, test, expect } from "vitest";
2 | import * as scss from "./style.js";
3 |
4 | describe("scss", () => {
5 | test("basic", async () => {
6 | const vueBlock = {
7 | type: "style",
8 | openTag: '",
11 | } as const;
12 |
13 | await scss.manipulate(vueBlock, (node) => {
14 | if (scss.cssNodeIs("rule", node)) {
15 | if (node.selector === ".foo") {
16 | node.selector = ".baz";
17 | }
18 | }
19 | });
20 | expect(vueBlock.body).toBe(".baz { &.bar: { color: red; } }");
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/packages/vue-service-bay/src/save.ts:
--------------------------------------------------------------------------------
1 | import type { VueFile } from "./parse.js";
2 | import { writeFileSync } from "node:fs";
3 |
4 | export const saveAsString = (vueFile: VueFile): string => {
5 | let updated = "";
6 | for (const block of vueFile.blocks) {
7 | const prefix = updated ? "\n\n" : "";
8 | updated += `${prefix}${block.openTag}${block.body}${block.closeTag}`;
9 | }
10 | return updated;
11 | };
12 |
13 | export const save = async (
14 | vueFile: VueFile,
15 | format?: (vueFileAsString: string) => Promise,
16 | ) => {
17 | let updated = saveAsString(vueFile);
18 | if (format) {
19 | updated = await format(updated);
20 | }
21 | writeFileSync(vueFile.filePath, updated);
22 | };
23 |
--------------------------------------------------------------------------------
/packages/examples/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "examples",
3 | "version": "0.0.0",
4 | "private": true,
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "run-p type-check \"build-only {@}\" --",
9 | "preview": "vite preview",
10 | "build-only": "vite build",
11 | "type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false"
12 | },
13 | "dependencies": {
14 | "vue": "3.5.13"
15 | },
16 | "devDependencies": {
17 | "vue-service-bay": "*",
18 | "@tsconfig/node18": "18.2.4",
19 | "@types/node": "22.13.14",
20 | "@vitejs/plugin-vue": "5.2.3",
21 | "@vue/tsconfig": "0.7.0",
22 | "npm-run-all2": "7.0.2",
23 | "typescript": "5.8.2",
24 | "vite": "6.2.3",
25 | "vue-tsc": "2.2.8"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [main, develop]
6 | pull_request:
7 | workflow_dispatch:
8 |
9 | concurrency:
10 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
11 | cancel-in-progress: true
12 |
13 | jobs:
14 | test:
15 | timeout-minutes: 15
16 | runs-on: ubuntu-latest
17 | steps:
18 | - uses: actions/checkout@v4
19 | - uses: pnpm/action-setup@v4
20 | with:
21 | version: 10
22 | - uses: actions/setup-node@v4
23 | with:
24 | node-version: 22
25 | cache: "pnpm"
26 |
27 | - name: prebuild
28 | run: pnpm install --frozen-lockfile
29 |
30 | - name: check project
31 | run: cd packages/vue-service-bay && pnpm build && pnpm check:all
32 |
--------------------------------------------------------------------------------
/packages/examples/migration/index.js:
--------------------------------------------------------------------------------
1 | import { getAllVueFiles, parseVueFile, save } from "vue-service-bay";
2 | import regexpExamples from "./001_RegexpExamples.js";
3 | import astManipulateExamples from "./002_AstManipulateExamples.js";
4 | import magicStringExamples from "./003_MagicStringExamples.js";
5 | import { format } from "prettier";
6 |
7 | /**
8 | * @param {import ('vue-service-bay').VueFile} vueFile
9 | */
10 | const migrate = (vueFile) => {
11 | regexpExamples(vueFile);
12 | astManipulateExamples(vueFile);
13 | magicStringExamples(vueFile);
14 | };
15 |
16 | const main = async () => {
17 | const files = await getAllVueFiles("./src");
18 | for (const file of files) {
19 | const vueFile = await parseVueFile(file);
20 | migrate(vueFile);
21 | save(vueFile, (vueFileAsString) =>
22 | format(vueFileAsString, { parser: "vue" })
23 | );
24 | }
25 | };
26 |
27 | void main();
28 |
--------------------------------------------------------------------------------
/packages/vue-service-bay/src/languages/template.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, test, expect } from "vitest";
2 | import * as html from "./template.js";
3 |
4 | describe("html", () => {
5 | test("basic", async () => {
6 | const vueBlock = {
7 | type: "template",
8 | openTag: "",
9 | body: 'hello
',
10 | closeTag: "",
11 | } as const;
12 |
13 | html.manipulate(vueBlock, (node) => {
14 | if (node.type === "tag" && node.name === "div") {
15 | node.name = "span";
16 | node.attribs = { ...node.attribs, class: "foo" };
17 | }
18 | if (node.type === "text") {
19 | node.data = node.data.toUpperCase();
20 | }
21 | });
22 | expect(vueBlock.body).toBe(
23 | 'HELLO',
24 | );
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/packages/vue-service-bay/src/languages/script.ts:
--------------------------------------------------------------------------------
1 | import { parse, print } from "recast";
2 | import * as tsParser from "recast/parsers/typescript.js";
3 | import type { Node } from "@babel/types";
4 | import { walk as zimmerframe } from "zimmerframe";
5 | import { VueBlock } from "../parse.js";
6 |
7 | const throwIfNotScript = (block: VueBlock) => {
8 | if (!block.openTag.startsWith("
5 |
6 |
7 |
8 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
32 |
33 |
61 |
--------------------------------------------------------------------------------
/packages/examples/src/App.vue.original:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
32 |
33 |
61 |
--------------------------------------------------------------------------------
/packages/vue-service-bay/src/languages/script.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, test, expect } from "vitest";
2 | import * as typescript from "./script";
3 |
4 | describe("typescript", () => {
5 | test("javascript", async () => {
6 | const vueBlock = {
7 | type: "script",
8 | openTag: '",
11 | } as const;
12 |
13 | typescript.manipulate(
14 | {
15 | type: "script",
16 | openTag: '",
19 | },
20 | (node) => {
21 | return node;
22 | },
23 | );
24 | expect(vueBlock.body).toBe('console.log("hello");');
25 | });
26 | test("typescript", async () => {
27 | const vueBlock = {
28 | type: "script",
29 | openTag: '",
32 | } as const;
33 |
34 | typescript.manipulate(vueBlock, (node) => {
35 | if (node.type === "Identifier" && node.name === "foo") {
36 | node.name = "bar";
37 | }
38 | return node;
39 | });
40 | expect(vueBlock.body).toBe("const bar = (str: string) => str;");
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/packages/vue-service-bay/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # vue-service-bay
2 |
3 | ## 0.2.3
4 |
5 | ### Patch Changes
6 |
7 | - [#192](https://github.com/flyle-io/vue-service-bay/pull/192) [`ed0d3e9`](https://github.com/flyle-io/vue-service-bay/commit/ed0d3e98bc173523a546dfad4b0eb84635b6d5f2) Thanks [@baseballyama](https://github.com/baseballyama)! - chore: update deps
8 |
9 | ## 0.2.2
10 |
11 | ### Patch Changes
12 |
13 | - [`722588b`](https://github.com/flyle-io/vue-service-bay/commit/722588b084685ac05cf5cfefd04b63af1510e484) Thanks [@baseballyama](https://github.com/baseballyama)! - chore: remove unnecessary files from npm
14 |
15 | ## 0.2.1
16 |
17 | ### Patch Changes
18 |
19 | - [`2e2863d`](https://github.com/flyle-io/vue-service-bay/commit/2e2863da4fef865ba36a122952a78d53b1453cda) Thanks [@baseballyama](https://github.com/baseballyama)! - chore: fix README
20 |
21 | ## 0.2.0
22 |
23 | ### Minor Changes
24 |
25 | - [`c1b48d9`](https://github.com/flyle-io/vue-service-bay/commit/c1b48d9c8a167b0f3131ad37b10ee7d1e36a5160) Thanks [@baseballyama](https://github.com/baseballyama)! - feat: Added API to receive migration results as strings
26 |
27 | ## 0.1.0
28 |
29 | ### Minor Changes
30 |
31 | - [`2c1a784`](https://github.com/flyle-io/vue-service-bay/commit/2c1a784407e2fbd10380053ceccf8ff66e34f151) Thanks [@baseballyama](https://github.com/baseballyama)! - feat: Initial version of vue-service-bay
32 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | permissions:
9 | contents: write
10 | issues: write
11 | pull-requests: write
12 |
13 | jobs:
14 | release:
15 | name: Release
16 | runs-on: ubuntu-latest
17 | steps:
18 | - name: Checkout Repo
19 | uses: actions/checkout@v4
20 | with:
21 | fetch-depth: 0
22 | - uses: pnpm/action-setup@v4
23 | with:
24 | version: 10
25 | - uses: actions/setup-node@v4
26 | with:
27 | node-version: 22
28 | cache: "pnpm"
29 |
30 | - name: prebuild
31 | run: pnpm install --frozen-lockfile
32 |
33 | - name: build
34 | run: cd packages/vue-service-bay && pnpm build
35 |
36 | - name: Remove src directory
37 | run: rm -rf packages/vue-service-bay/src
38 |
39 | - name: Copy README.md
40 | run: cp README.md packages/vue-service-bay/README.md
41 |
42 | - name: Create Release Pull Request or Publish to npm
43 | id: changesets
44 | uses: changesets/action@v1
45 | with:
46 | version: pnpm update:version
47 | publish: pnpm release
48 | commit: "chore: release vue-service-bay"
49 | title: "chore: release vue-service-bay"
50 | env:
51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
52 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
53 |
--------------------------------------------------------------------------------
/packages/vue-service-bay/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-service-bay",
3 | "version": "0.2.3",
4 | "description": "Your One-Stop Solution for Vue.js Refactoring",
5 | "main": "./dist/index.js",
6 | "type": "module",
7 | "types": "./dist/index.d.ts",
8 | "exports": {
9 | "./package.json": "./package.json",
10 | ".": {
11 | "types": "./dist/index.d.ts",
12 | "import": "./dist/index.js"
13 | }
14 | },
15 | "scripts": {
16 | "dev": "tsc -d -w",
17 | "build": "tsc -d",
18 | "type:check": "tsc --noEmit",
19 | "format:check": "prettier --cache --check \"./src/**/*.{js,ts,vue,json}\"",
20 | "format:fix": "prettier --cache --write \"./src/**/*.{js,ts,vue,json}\"",
21 | "publish:check": "publint",
22 | "test:watch": "vitest",
23 | "check:all": "pnpm type:check && pnpm format:check && pnpm publish:check"
24 | },
25 | "keywords": [
26 | "vue",
27 | "vue3",
28 | "ast",
29 | "static analysis",
30 | "code generation",
31 | "code refactor"
32 | ],
33 | "author": "baseballyama",
34 | "license": "MIT",
35 | "devDependencies": {
36 | "publint": "0.3.15",
37 | "vitest": "3.0.9"
38 | },
39 | "dependencies": {
40 | "@babel/types": "7.27.7",
41 | "dom-serializer": "2.0.0",
42 | "domhandler": "5.0.3",
43 | "globby": "14.1.0",
44 | "htmlparser2": "10.0.0",
45 | "magic-string": "0.30.21",
46 | "postcss": "8.5.6",
47 | "postcss-scss": "4.0.9",
48 | "recast": "0.23.11",
49 | "zimmerframe": "1.1.4"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/packages/examples/src/App.vue.magrated:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
34 |
35 |
68 |
--------------------------------------------------------------------------------
/packages/vue-service-bay/src/languages/style.ts:
--------------------------------------------------------------------------------
1 | import postcss, {
2 | Node,
3 | AtRule,
4 | Rule,
5 | Declaration,
6 | Comment,
7 | ChildNode,
8 | } from "postcss";
9 | import * as postcssScss from "postcss-scss";
10 | import { VueBlock } from "../parse.js";
11 |
12 | type InferNode = T extends "atrule"
13 | ? AtRule
14 | : T extends "rule"
15 | ? Rule
16 | : T extends "decl"
17 | ? Declaration
18 | : T extends "comment"
19 | ? Comment
20 | : never;
21 |
22 | export const cssNodeIs = (
23 | type: T,
24 | node: Node,
25 | ): node is InferNode => {
26 | return node.type === type;
27 | };
28 |
29 | const throwIfNotStyle = (block: VueBlock) => {
30 | if (!block.openTag.startsWith("