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(`
+ "
+ {{ str }}
+
+
+
+ "
+ `);
+
+ expect(
+ await readFile(
+ resolve(rootDir, "dist/components/script-multi-block.vue"),
+ "utf8",
+ ),
+ ).toMatchInlineSnapshot(`
+ "
+ {{ msg }}
+
+
+
+
+
+ "
+ `);
+
+ 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(
+ [
+ ``,
+ `{{ a!.toFixed(2) }}
`,
+ ].join("\n"),
+ ),
+ ).toMatchInlineSnapshot(`
+ "
+ {{ a!.toFixed(2) }}
"
+ `);
+
+ 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(`
- "
-
-
- {{ str }}
-
- "
- `);
-
- expect(
- await readFile(
- resolve(rootDir, "dist/components/script-multi-block.vue"),
- "utf8",
- ),
- ).toMatchInlineSnapshot(`
- "
-
-
- {{ msg }}
-
- "
- `);
-
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(`
- "
-
-
- {{ msg }}
-
- "
- `);
-
expect(
await readFile(
resolve(rootDir, "dist/components/script-multi-block.vue.d.ts"),