diff --git a/package.json b/package.json index 45a1aa8d..13e763ae 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "unbuild": "^3.5.0", "vitest": "^3.1.1", "vue": "^3.5.13", + "vue-sfc-transformer": "^0.1.2", "vue-tsc": "^2.2.8", "vue-tsc1": "npm:vue-tsc@^1.8.27", "vue-tsc2.0": "npm:vue-tsc@2.0.29" @@ -69,6 +70,7 @@ "sass": "^1.85.0", "typescript": ">=5.7.3", "vue": "^3.5.13", + "vue-sfc-transformer": "^0.1.1", "vue-tsc": "^1.8.27 || ^2.0.21" }, "peerDependenciesMeta": { @@ -81,6 +83,9 @@ "vue": { "optional": true }, + "vue-sfc-transformer": { + "optional": true + }, "vue-tsc": { "optional": true } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 03e3203d..45723dfb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -99,6 +99,9 @@ importers: vue: specifier: ^3.5.13 version: 3.5.13(typescript@5.8.2) + vue-sfc-transformer: + specifier: ^0.1.2 + version: 0.1.2(vue@3.5.13(typescript@5.8.2)) vue-tsc: specifier: ^2.2.8 version: 2.2.8(typescript@5.8.2) @@ -119,6 +122,10 @@ packages: resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} + '@babel/generator@7.27.0': + resolution: {integrity: sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==} + engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.25.9': resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} engines: {node: '>=6.9.0'} @@ -2441,6 +2448,12 @@ packages: vscode-uri@3.1.0: resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + vue-sfc-transformer@0.1.2: + resolution: {integrity: sha512-hPRoUyG9V/0rWRJ0rzYxfxhNyEa0EzOhhLy4lHMamP5urexBJZ6Ufod/SXlW5Vo96Au4ox/fDKNjQyKJE91Lwg==} + engines: {node: '>=6.9.0'} + peerDependencies: + vue: ^3.5.13 + vue-template-compiler@2.7.16: resolution: {integrity: sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==} @@ -2526,6 +2539,14 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 + '@babel/generator@7.27.0': + dependencies: + '@babel/parser': 7.27.0 + '@babel/types': 7.27.0 + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 3.1.0 + '@babel/helper-string-parser@7.25.9': {} '@babel/helper-validator-identifier@7.25.9': {} @@ -4804,6 +4825,12 @@ snapshots: vscode-uri@3.1.0: {} + vue-sfc-transformer@0.1.2(vue@3.5.13(typescript@5.8.2)): + dependencies: + '@babel/generator': 7.27.0 + '@babel/parser': 7.27.0 + vue: 3.5.13(typescript@5.8.2) + vue-template-compiler@2.7.16: dependencies: de-indent: 1.0.2 diff --git a/src/loaders/index.ts b/src/loaders/index.ts index 7ea06337..44af4b46 100644 --- a/src/loaders/index.ts +++ b/src/loaders/index.ts @@ -4,9 +4,18 @@ import { vueLoader } from "./vue"; import { sassLoader } from "./sass"; import { postcssLoader } from "./postcss"; +let cachedVueLoader: Loader | undefined; + export const loaders = { js: jsLoader, - vue: vueLoader, + vue: + cachedVueLoader || + (async (...args) => { + cachedVueLoader = await import("vue-sfc-transformer/mkdist") + .then((r) => r.vueLoader) + .catch(() => vueLoader); + return cachedVueLoader(...args); + }), sass: sassLoader, postcss: postcssLoader, }; diff --git a/src/loaders/vue.ts b/src/loaders/vue.ts index 0284347a..f382093d 100644 --- a/src/loaders/vue.ts +++ b/src/loaders/vue.ts @@ -28,11 +28,11 @@ export interface VueBlockLoader { export interface DefaultBlockLoaderOptions { type: "script" | "style" | "template"; outputLang: string; - defaultLang?: string; validExtensions?: string[]; } -export function defineVueLoader(options?: DefineVueLoaderOptions): Loader { +let warnedTypescript = false; +function defineVueLoader(options?: DefineVueLoaderOptions): Loader { const blockLoaders = options?.blockLoaders || {}; return async (input, context) => { @@ -40,10 +40,9 @@ export function defineVueLoader(options?: DefineVueLoaderOptions): Loader { return; } - const { compileScript, parse } = await import("vue/compiler-sfc"); + const { parse } = await import("vue/compiler-sfc"); let modified = false; - let fakeScriptBlock = false; const raw = await input.getContents(); const sfc = parse(raw, { @@ -57,38 +56,32 @@ export function defineVueLoader(options?: DefineVueLoaderOptions): Loader { return; } + const isTs = [ + sfc.descriptor.script?.lang, + sfc.descriptor.scriptSetup?.lang, + ].some((lang) => lang && lang.startsWith("ts")); + if (isTs && !warnedTypescript) { + console.warn( + "[mkdist] vue-sfc-transformer is not installed. mkdist will not transform typescript syntax in Vue SFCs.", + ); + warnedTypescript = true; + } + const output: LoaderResult = []; const addOutput = (...files: OutputFile[]) => output.push(...files); - const blocks: VueBlock[] = [ - sfc.descriptor.template, + const blocks: SFCBlock[] = [ ...sfc.descriptor.styles, ...sfc.descriptor.customBlocks, ].filter((item) => !!item); - // merge script blocks - if (sfc.descriptor.script || sfc.descriptor.scriptSetup) { - // need to compile script when using typescript with + + diff --git a/test/fixture/src/components/emit-and-with-default.vue b/test/fixture/src/components/emit-and-with-default.vue new file mode 100644 index 00000000..04954cd7 --- /dev/null +++ b/test/fixture/src/components/emit-and-with-default.vue @@ -0,0 +1,20 @@ + + + diff --git a/test/index.test.ts b/test/index.test.ts index cd6e7e17..c7e3a593 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -10,6 +10,7 @@ import { afterAll, } from "vitest"; import { createLoader } from "../src/loader"; +import { afterEach } from "vitest"; describe("mkdist", () => { let mkdist: typeof import("../src/make").mkdist; @@ -35,6 +36,8 @@ describe("mkdist", () => { "dist/star/other.mjs", "dist/components/index.mjs", "dist/components/blank.vue", + "dist/components/define-model.vue", + "dist/components/emit-and-with-default.vue", "dist/components/js.vue", "dist/components/script-multi-block.vue", "dist/components/script-setup-ts.vue", @@ -63,6 +66,8 @@ describe("mkdist", () => { [ "dist/components/index.mjs", "dist/components/blank.vue", + "dist/components/define-model.vue", + "dist/components/emit-and-with-default.vue", "dist/components/js.vue", "dist/components/script-multi-block.vue", "dist/components/script-setup-ts.vue", @@ -85,6 +90,8 @@ describe("mkdist", () => { [ "dist/components/index.mjs", "dist/components/blank.vue", + "dist/components/define-model.vue", + "dist/components/emit-and-with-default.vue", "dist/components/script-multi-block.vue", "dist/components/script-setup-ts.vue", "dist/components/ts.vue", @@ -124,6 +131,10 @@ describe("mkdist", () => { "dist/components/index.d.ts", "dist/components/blank.vue", "dist/components/blank.vue.d.ts", + "dist/components/define-model.vue", + "dist/components/define-model.vue.d.ts", + "dist/components/emit-and-with-default.vue", + "dist/components/emit-and-with-default.vue.d.ts", "dist/components/js.vue", "dist/components/js.vue.d.ts", "dist/components/script-multi-block.vue", @@ -291,6 +302,8 @@ describe("mkdist", () => { "dist/star/other.mjs", "dist/components/index.mjs", "dist/components/blank.vue", + "dist/components/define-model.vue", + "dist/components/emit-and-with-default.vue", "dist/components/js.vue", "dist/components/script-multi-block.vue", "dist/components/script-setup-ts.vue", @@ -307,6 +320,126 @@ describe("mkdist", () => { .map((f) => resolve(rootDir, f)) .sort(), ); + + expect( + await readFile( + resolve(rootDir, "dist/components/script-setup-ts.vue"), + "utf8", + ), + ).toMatchInlineSnapshot(` + " + + + " + `); + + expect( + await readFile( + resolve(rootDir, "dist/components/script-multi-block.vue"), + "utf8", + ), + ).toMatchInlineSnapshot(` + " + + + + + " + `); + + expect( + await readFile( + resolve(rootDir, "dist/components/emit-and-with-default.vue"), + "utf8", + ), + ).toMatchInlineSnapshot(` + " + + + " + `); + + expect( + await readFile( + resolve(rootDir, "dist/components/define-model.vue"), + "utf8", + ), + ).toMatchInlineSnapshot(` + " + + + " + `); }); describe("createLoader", () => { @@ -504,6 +637,100 @@ describe("mkdist", () => { }, 50_000); }); +describe("mkdist with fallback vue loader", () => { + const consoleWarnSpy = vi.spyOn(console, "warn"); + beforeAll(() => { + vi.resetModules(); + vi.doMock("vue-sfc-transformer/mkdist", async () => { + throw new Error("vue-sfc-transformer is not installed"); + }); + }); + + afterAll(() => { + vi.doUnmock("vue-sfc-transformer/mkdist"); + }); + + afterEach(() => { + consoleWarnSpy.mockReset(); + }); + + it("keep the template and script block", async () => { + expect(await fixture(``)) + .toMatchInlineSnapshot(` + "" + `); + + expect( + await fixture(``), + ).toMatchInlineSnapshot( + `""`, + ); + + expect( + await fixture( + [ + ``, + ``, + ].join("\n"), + ), + ).toMatchInlineSnapshot(` + " + " + `); + + expect(consoleWarnSpy).toHaveBeenCalledWith( + "[mkdist] vue-sfc-transformer is not installed. mkdist will not transform typescript syntax in Vue SFCs.", + ); + expect(consoleWarnSpy).toHaveBeenCalledTimes(1); + }); + + it("transform style block", async () => { + expect( + await fixture( + [ + ``, + ``, + ].join("\n"), + ), + ).toMatchInlineSnapshot(` + " + " + `); + + expect( + await fixture( + [ + ``, + ``, + ].join("\n"), + ), + ).toMatchInlineSnapshot(` + " + + + " + `); + }); + + async function fixture(input: string) { + const { loadFile } = createLoader({ + loaders: ["vue", "js", "sass"], + }); + const results = await loadFile({ + extension: ".vue", + getContents: () => input, + path: "test.vue", + }); + return results?.[0].contents || input; + } +}); + describe("mkdist with vue-tsc v1", () => { beforeAll(() => { vi.resetModules(); @@ -560,6 +787,10 @@ describe("mkdist with vue-tsc v1", () => { "dist/components/index.d.ts", "dist/components/blank.vue", "dist/components/blank.vue.d.ts", + "dist/components/define-model.vue", + "dist/components/define-model.vue.d.ts", + "dist/components/emit-and-with-default.vue", + "dist/components/emit-and-with-default.vue.d.ts", "dist/components/js.vue", "dist/components/js.vue.d.ts", "dist/components/script-multi-block.vue", @@ -637,66 +868,6 @@ describe("mkdist with vue-tsc v1", () => { " `); - expect( - await readFile( - resolve(rootDir, "dist/components/script-setup-ts.vue"), - "utf8", - ), - ).toMatchInlineSnapshot(` - " - - - " - `); - - expect( - await readFile( - resolve(rootDir, "dist/components/script-multi-block.vue"), - "utf8", - ), - ).toMatchInlineSnapshot(` - " - - - " - `); - expect( await readFile( resolve(rootDir, "dist/components/script-multi-block.vue.d.ts"), @@ -818,6 +989,10 @@ describe("mkdist with vue-tsc ~v2.0.21", () => { "dist/components/index.d.ts", "dist/components/blank.vue", "dist/components/blank.vue.d.ts", + "dist/components/define-model.vue", + "dist/components/define-model.vue.d.ts", + "dist/components/emit-and-with-default.vue", + "dist/components/emit-and-with-default.vue.d.ts", "dist/components/js.vue", "dist/components/js.vue.d.ts", "dist/components/script-multi-block.vue", @@ -895,34 +1070,6 @@ describe("mkdist with vue-tsc ~v2.0.21", () => { " `); - expect( - await readFile( - resolve(rootDir, "dist/components/script-multi-block.vue"), - "utf8", - ), - ).toMatchInlineSnapshot(` - " - - - " - `); - expect( await readFile( resolve(rootDir, "dist/components/script-multi-block.vue.d.ts"),