Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(vue): vue regular script block exports not being recognized inside editor #8998

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/two-oranges-knock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/vue': patch
---

Adds editor support for Vue non setup script blocks and Vue 3.3 generics.
1 change: 1 addition & 0 deletions packages/integrations/vue/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"astro-scripts": "workspace:*",
"chai": "^4.3.7",
"linkedom": "^0.15.1",
"cheerio": "1.0.0-rc.12",
"mocha": "^10.2.0",
"vite": "^4.4.9",
"vue": "^3.3.4"
Expand Down
28 changes: 19 additions & 9 deletions packages/integrations/vue/src/editor.cts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export function toTSX(code: string, className: string): string {
// NOTE: As you can expect, using regexes for this is not exactly the most reliable way of doing things
// However, I couldn't figure out a way to do it using Vue's compiler, I tried looking at how Volar does it, but I
// didn't really understand everything happening there and it seemed to be pretty Volar-specific. I do believe
// someone more knowledgable on Vue's internals could figure it out, but since this solution is good enough for most
// someone more knowledgeable on Vue's internals could figure it out, but since this solution is good enough for most
// Vue components (and it's an improvement over, well, nothing), it's alright, I think
try {
const parsedResult = parse(code);
Expand All @@ -18,29 +18,39 @@ export function toTSX(code: string, className: string): string {
`;
}

if (parsedResult.descriptor.scriptSetup) {
const definePropsType =
parsedResult.descriptor.scriptSetup.content.match(/defineProps<([\s\S]+)>/m);
// Vue supports 2 type of script blocks: setup and non-setup
const regularScriptBlockContent = parsedResult.descriptor.script?.content ?? '';
const { scriptSetup } = parsedResult.descriptor;

if (scriptSetup) {
const definePropsType = scriptSetup.content.match(/defineProps<([\S\s]+?)>\s?\(\)/m);
const propsGeneric = scriptSetup.attrs.generic;
const propsGenericType = propsGeneric ? `<${propsGeneric}>` : '';

if (definePropsType) {
result = `
${parsedResult.descriptor.scriptSetup.content}
${regularScriptBlockContent}
${scriptSetup.content}

export default function ${className}__AstroComponent_(_props: ${definePropsType[1]}): any {
export default function ${className}__AstroComponent_${propsGenericType}(_props: ${definePropsType[1]}): any {
<div></div>
}
`;
} else {
const defineProps =
parsedResult.descriptor.scriptSetup.content.match(/defineProps\([\s\S]+\)/m);
// TODO. Find a way to support generics when using defineProps without passing explicit types.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ultimately, this module shouldn't use a regex to get the types, it should use something like https://github.com/vuejs/language-tools/tree/master/packages/component-meta and https://github.com/vuejs/language-tools/tree/master/packages/component-type-helpers

which should support everything (or at least, if it doesn't, would be a bug upstream that's not in our hands)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, I have looked briefly into these packages after I opened this PR and its not so far I haven't found an easy way.

component-meta gives narrows prop types and output loses generic information, so T extends string becomes just string.

component-type-helpers needs a component instance types, to import which requires vue language server I think and still it loses all generic information and narrows types. I am not sure how would that work with virtual file.

Current approach is not sustainable and prone to fail, right now astro doesn't account for https://vuejs.org/guide/typescript/composition-api.html#props-default-values or experimental defineModel and so on and yet I am not sure how to get something better in a short term.

// Right now something like this `defineProps({ prop: { type: Array as PropType<T[]> } })`
// won't be correctly typed in Astro.
const defineProps = scriptSetup.content.match(/defineProps\([\s\S]+\)/m);

if (defineProps) {
result = `
import { defineProps } from '@vue/runtime-core';

${regularScriptBlockContent}

const Props = ${defineProps[0]}

export default function ${className}__AstroComponent_(_props: typeof Props): any {
export default function ${className}__AstroComponent_${propsGenericType}(_props: typeof Props): any {
<div></div>
}
`;
Expand Down
22 changes: 17 additions & 5 deletions packages/integrations/vue/test/app-entrypoint.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { loadFixture } from './test-utils.js';
import { expect } from 'chai';
import { load as cheerioLoad } from 'cheerio';
import { parseHTML } from 'linkedom';

describe('App Entrypoint', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;
Expand All @@ -13,11 +15,21 @@ describe('App Entrypoint', () => {
});

it('loads during SSR', async () => {
const data = await fixture.readFile('/index.html');
const { document } = parseHTML(data);
const bar = document.querySelector('#foo > #bar');
expect(bar).not.to.be.undefined;
expect(bar.textContent).to.eq('works');
const html = await fixture.readFile('/index.html');
const $ = cheerioLoad(html);

// test 1: basic component renders
expect($('#foo > #bar').text()).to.eq('works');

// test 2: component with multiple script blocks renders and exports
// values from non setup block correctly
expect($('#multiple-script-blocks').text()).to.equal('2 4');

// test 3: component using generics renders
expect($('#generics').text()).to.equal('generic');

// test 4: component using generics and multiple script blocks renders
expect($('#generics-and-blocks').text()).to.equal('1 3!!!');
});

it('setup included in renderer bundle', async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<script lang="ts" setup generic="T extends 'generic' | 'not-generic'">
interface GenericComponentProps {
value: T
}

defineProps<GenericComponentProps>()
</script>

<template>
<div id="generics">
{{ value }}
</div>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<script lang="ts">
export const customFormatter = (num: number) => `${num * 3}!!!`

export type FormatNumber<T> = (num: T) => string;
</script>

<script lang="ts" setup generic="T extends number, Formatter extends FormatNumber<T>">
const props = defineProps<{
value: T,
formatter: Formatter
}>()
</script>

<template>
<div id="generics-and-blocks">
{{ value }} {{ props.formatter(props.value) }}
</div>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<script lang="ts">
export const doubleNumber = (num: number) => num * 2
</script>

<script lang="ts" setup>
defineProps({
value: {
type: Number,
required: true
}
})
</script>

<template>
<div id="multiple-script-blocks">
{{ doubleNumber(value) }} <slot />
</div>
</template>
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
---
import Foo from '../components/Foo.vue';
import MultipleScriptBlocks, { doubleNumber } from '../components/MultipleScriptBlocks.vue';
import GenericComponent from '../components/Generics.vue';
import GenericsAndBlocks, { customFormatter } from '../components/GenericsAndBlocks.vue';
---

<html>
Expand All @@ -8,5 +11,13 @@ import Foo from '../components/Foo.vue';
</head>
<body>
<Foo client:load />

<MultipleScriptBlocks value={1}>
{doubleNumber(2)}
</MultipleScriptBlocks>

<GenericComponent value={'generic'} />

<GenericsAndBlocks value={1} formatter={customFormatter} />
</body>
</html>
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

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

Loading