Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
acbd709
feat(vue): transpile vue template and rollback the behavior of script…
Teages Apr 1, 2025
6264dd9
refactor(vue): rename internal vue utils
Teages Apr 1, 2025
0cfd28a
feat(vue): transplie script setup
Teages Apr 2, 2025
3c23763
chore(lint): apply lint fix
Teages Apr 2, 2025
f605a5a
test(vue): update test for vue loader
Teages Apr 2, 2025
0f05413
fix(vue): apply template offset to transpiling
Teages Apr 2, 2025
1270697
fix(vue): try to restore quotes in vue template
Teages Apr 3, 2025
c3c482c
chore(lint): apply lint fix
Teages Apr 3, 2025
98f2b11
feat(vue): transfrom defineModel in script setup
Teages Apr 3, 2025
3c68a63
test(vue): update defineModel test for vue loader
Teages Apr 3, 2025
8bf9103
feat(vue): using vue context utils to throw error
Teages Apr 4, 2025
8e7bda2
feat(vue): respect original block order
Teages Apr 4, 2025
5988e90
test(vue): move vue loader test to the correct place
Teages Apr 4, 2025
452c434
test(vue): unit test for script setup block
Teages Apr 4, 2025
5d10c3a
chore(deps): mark @babel/parser as peer dependency
Teages Apr 4, 2025
ad07207
chore: clean up import
Teages Apr 4, 2025
34e770d
test(vue): add test for v-bind:key
Teages Apr 4, 2025
9da6cda
chore: update import of babel/generator
Teages Apr 4, 2025
ae5fc33
fix(vue): allow `undefined` as the only argument for defineModel
Teages Apr 4, 2025
dcf54b5
refactor(vue): split vue transformer to package `vue-sfc-transformer`
Teages Apr 4, 2025
46a7b0d
feat: conditionally use `vue-sfc-transformer` if installed
danielroe Apr 4, 2025
a1ea9a0
chore: remove expect error
danielroe Apr 4, 2025
dcac2de
feat(vue): avoid to transfrom template and script block with out `vue…
Teages Apr 4, 2025
8bbb89d
style: apply lint fixes
autofix-ci[bot] Apr 4, 2025
9cab50c
chore: typo
danielroe Apr 4, 2025
b2e8af0
chore: bump version
danielroe Apr 4, 2025
d746bb6
test: update assertion
danielroe Apr 4, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"test": "pnpm lint && vitest run --coverage"
},
"dependencies": {
"@babel/generator": "^7.27.0",
"autoprefixer": "^10.4.21",
"citty": "^0.1.6",
"cssnano": "^7.0.6",
Expand All @@ -44,10 +45,14 @@
"tinyglobby": "^0.2.12"
},
"devDependencies": {
"@babel/parser": "^7.27.0",
"@babel/types": "^7.27.0",
"@types/babel__generator": "^7.6.8",
"@types/node": "^22.13.14",
"@types/semver": "^7.7.0",
"@vitest/coverage-v8": "^3.1.1",
"@volar/typescript": "^2.4.12",
"@vue/compiler-dom-types": "npm:@vue/compiler-dom@^3.5.13",
"@vue/language-core": "^2.2.8",
"@vue/language-core2.0": "npm:@vue/[email protected]",
"c8": "latest",
Expand All @@ -66,12 +71,16 @@
"vue-tsc2.0": "npm:[email protected]"
},
"peerDependencies": {
"@babel/parser": "^7.27.0",
"sass": "^1.85.0",
"typescript": ">=5.7.3",
"vue": "^3.5.13",
"vue-tsc": "^1.8.27 || ^2.0.21"
},
"peerDependenciesMeta": {
"@babel/parser": {
"optional": true
},
"sass": {
"optional": true
},
Expand Down
34 changes: 34 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

139 changes: 102 additions & 37 deletions src/loaders/vue.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,38 @@
import type { SFCBlock } from "vue/compiler-sfc";
import type { SFCBlock, SFCTemplateBlock } from "vue/compiler-sfc";
import type {
InputFile,
Loader,
LoaderContext,
LoaderResult,
OutputFile,
} from "../loader";
import { transpileVueTemplate } from "../utils/vue/template";
import { preTranspileScriptSetup } from "../utils/vue/script-setup";

export interface DefineVueLoaderOptions {
blockLoaders?: {
[blockType: string]: VueBlockLoader | undefined;
};
}

export type VueBlock = Pick<SFCBlock, "type" | "content" | "attrs">;
export type VueBlockOutput = Pick<SFCBlock, "type" | "content" | "attrs">;

export interface VueBlockLoaderContext extends LoaderContext {
requireTranspileTemplate: boolean;
rawInput: InputFile;
addOutput: (...files: OutputFile[]) => void;
}

export interface VueBlockLoader {
(
block: VueBlock,
context: LoaderContext & {
rawInput: InputFile;
addOutput: (...files: OutputFile[]) => void;
},
): Promise<VueBlock | undefined>;
block: SFCBlock,
context: VueBlockLoaderContext,
): Promise<VueBlockOutput | undefined>;
}

export interface DefaultBlockLoaderOptions {
type: "script" | "style" | "template";
outputLang: string;
defaultLang?: string;
defaultLang: string;
validExtensions?: string[];
}

Expand All @@ -40,7 +44,7 @@
return;
}

const { compileScript, parse } = await import("vue/compiler-sfc");
const { parse } = await import("vue/compiler-sfc");

let modified = false;
let fakeScriptBlock = false;
Expand All @@ -57,35 +61,52 @@
return;
}

// we need to remove typescript from template block if the block is typescript
const isTs = [sfc.descriptor.script, sfc.descriptor.scriptSetup].some(
(block) => block?.lang === "ts",
);

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 <script setup>
if (sfc.descriptor.scriptSetup && sfc.descriptor.scriptSetup.lang) {
const merged = compileScript(sfc.descriptor, { id: input.srcPath });
merged.setup = false;
merged.attrs = toOmit(merged.attrs, "setup");
blocks.unshift(merged);
} else {
const scriptBlocks = [
sfc.descriptor.script,
sfc.descriptor.scriptSetup,
].filter((item) => !!item);
blocks.unshift(...scriptBlocks);
}
} else {

if (sfc.descriptor.template) {
blocks.unshift(sfc.descriptor.template);
}

if (sfc.descriptor.script) {
blocks.unshift(sfc.descriptor.script);
}
if (sfc.descriptor.scriptSetup) {
blocks.unshift(
isTs
? await preTranspileScriptSetup(sfc.descriptor, input.srcPath)
: sfc.descriptor.scriptSetup,

Check warning on line 88 in src/loaders/vue.ts

View check run for this annotation

Codecov / codecov/patch

src/loaders/vue.ts#L88

Added line #L88 was not covered by tests
);
}
if (!sfc.descriptor.script && !sfc.descriptor.scriptSetup) {
// push a fake script block to generate dts
blocks.unshift({
type: "script",
content: "export default {}",
attrs: {},
loc: {
start: {
offset: 0,
line: 1,
column: 1,
},
end: {
offset: 0,
line: 1,
column: 1,
},
source: "",
},
});
fakeScriptBlock = true;
}
Expand All @@ -97,11 +118,12 @@
...context,
rawInput: input,
addOutput,
requireTranspileTemplate: isTs,
});
if (result) {
modified = true;
}
return result || data;
return { block: result || data, offset: data.loc.start.offset };
}),
);

Expand All @@ -110,7 +132,8 @@
}

const contents = results
.map((block) => {
.sort((a, b) => a.offset - b.offset)
.map(({ block }) => {
if (block.type === "script" && fakeScriptBlock) {
return undefined;
}
Expand Down Expand Up @@ -156,7 +179,7 @@
const lang =
typeof block.attrs.lang === "string"
? block.attrs.lang
: options.outputLang;
: options.defaultLang;
const extension = `.${lang}`;

const files =
Expand All @@ -169,10 +192,10 @@

const blockOutputFile = files.find(
(f) =>
f.extension === `.${options.outputLang}` ||
f.extension === `.${options.defaultLang}` ||
options.validExtensions?.includes(f.extension as string),
);
if (!blockOutputFile?.contents) {
if (!blockOutputFile) {
return;
}
addOutput(...files.filter((f) => f !== blockOutputFile));
Expand All @@ -185,21 +208,63 @@
};
}

const templateLoader: VueBlockLoader = async (
rawBlock,
{ requireTranspileTemplate, loadFile, rawInput },
) => {
if (rawBlock.type !== "template") {
return;
}

Check warning on line 217 in src/loaders/vue.ts

View check run for this annotation

Codecov / codecov/patch

src/loaders/vue.ts#L216-L217

Added lines #L216 - L217 were not covered by tests

if (!requireTranspileTemplate) {
return;
}

const block = rawBlock as SFCTemplateBlock;

const transformed = await transpileVueTemplate(
// for lower version of @vue/compiler-sfc, `ast.source` is the whole .vue file
block.content,
block.ast,
block.loc.start.offset,
async (code) => {
const res = await loadFile({
getContents: () => code,
path: `${rawInput.path}.ts`,
srcPath: `${rawInput.srcPath}.ts`,
extension: ".ts",
});

return (
res.find((f) => [".js", ".mjs", ".cjs"].includes(f.extension))
?.contents || code
);
},
);

return {
type: "template",
content: transformed,
attrs: block.attrs,
};
};

const styleLoader = defineDefaultBlockLoader({
outputLang: "css",
defaultLang: "css",
type: "style",
});

const scriptLoader = defineDefaultBlockLoader({
outputLang: "js",
defaultLang: "js",
type: "script",
validExtensions: [".js", ".mjs"],
});

export const vueLoader = defineVueLoader({
blockLoaders: {
style: styleLoader,
script: scriptLoader,
template: templateLoader,
style: styleLoader,
},
});

Expand Down
Loading
Loading