diff --git a/.changeset/odd-rivers-learn.md b/.changeset/odd-rivers-learn.md new file mode 100644 index 000000000000..3f11720b96d5 --- /dev/null +++ b/.changeset/odd-rivers-learn.md @@ -0,0 +1,5 @@ +--- +'@astrojs/vue': patch +--- + +Fixes a bug that caused styles referenced by `appEntrypoint` to be excluded from the build diff --git a/packages/integrations/vue/src/index.ts b/packages/integrations/vue/src/index.ts index 8cd1172a9b4a..3d34e084ac0d 100644 --- a/packages/integrations/vue/src/index.ts +++ b/packages/integrations/vue/src/index.ts @@ -4,6 +4,7 @@ import vue from '@vitejs/plugin-vue'; import type { Options as VueJsxOptions } from '@vitejs/plugin-vue-jsx'; import type { AstroIntegration, AstroRenderer } from 'astro'; import type { Plugin, UserConfig } from 'vite'; +import { MagicString } from '@vue/compiler-sfc'; interface Options extends VueOptions { jsx?: boolean | VueJsxOptions; @@ -39,6 +40,7 @@ function virtualAppEntrypoint(options?: Options): Plugin { let isBuild: boolean; let root: string; + let appEntrypoint: string | undefined; return { name: '@astrojs/vue/virtual-app', @@ -47,6 +49,11 @@ function virtualAppEntrypoint(options?: Options): Plugin { }, configResolved(config) { root = config.root; + if (options?.appEntrypoint) { + appEntrypoint = options.appEntrypoint.startsWith('.') + ? path.resolve(root, options.appEntrypoint) + : options.appEntrypoint; + } }, resolveId(id: string) { if (id == virtualModuleId) { @@ -55,11 +62,7 @@ function virtualAppEntrypoint(options?: Options): Plugin { }, load(id: string) { if (id === resolvedVirtualModuleId) { - if (options?.appEntrypoint) { - const appEntrypoint = options.appEntrypoint.startsWith('.') - ? path.resolve(root, options.appEntrypoint) - : options.appEntrypoint; - + if (appEntrypoint) { return `\ import * as mod from ${JSON.stringify(appEntrypoint)}; @@ -80,6 +83,20 @@ export const setup = async (app) => { return `export const setup = () => {};`; } }, + // Ensure that Vue components reference appEntrypoint directly + // This allows Astro to assosciate global styles imported in this file + // with the pages they should be injected to + transform(code, id) { + if (!appEntrypoint) return; + if (id.endsWith('.vue')) { + const s = new MagicString(code); + s.prepend(`import ${JSON.stringify(appEntrypoint)};\n`); + return { + code: s.toString(), + map: s.generateMap({ hires: 'boundary' }) + } + } + }, }; } diff --git a/packages/integrations/vue/test/app-entrypoint-css.test.js b/packages/integrations/vue/test/app-entrypoint-css.test.js new file mode 100644 index 000000000000..b629f1d25f58 --- /dev/null +++ b/packages/integrations/vue/test/app-entrypoint-css.test.js @@ -0,0 +1,67 @@ +import { loadFixture } from './test-utils.js'; +import { expect } from 'chai'; +import { load as cheerioLoad } from 'cheerio'; + +describe('App Entrypoint CSS', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/app-entrypoint-css/', + }); + }) + + describe('build', () => { + before(async () => { + await fixture.build(); + }) + + it('injects styles referenced in appEntrypoint', async () => { + const html = await fixture.readFile('/index.html'); + const $ = cheerioLoad(html); + + // test 1: basic component renders + expect($('#foo > #bar').text()).to.eq('works'); + + // test 2: injects the global style on the page + expect($('style').first().text().trim()).to.eq(':root{background-color:red}'); + }); + + it('does not inject styles to pages without a Vue component', async () => { + const html = await fixture.readFile('/unrelated/index.html'); + const $ = cheerioLoad(html); + + expect($('style').length).to.eq(0); + expect($('link[rel="stylesheet"]').length).to.eq(0); + }); + }) + + describe('dev', () => { + let devServer; + before(async () => { + devServer = await fixture.startDevServer(); + }) + after(async () => { + await devServer.stop(); + }) + + it('loads during SSR', async () => { + const html = await fixture.fetch('/').then((res) => res.text()); + const $ = cheerioLoad(html); + + // test 1: basic component renders + expect($('#foo > #bar').text()).to.eq('works'); + // test 2: injects the global style on the page + expect($('style').first().text().replace(/\s+/g, '')).to.eq(':root{background-color:red;}'); + }); + + it('does not inject styles to pages without a Vue component', async () => { + const html = await fixture.fetch('/unrelated').then((res) => res.text()); + const $ = cheerioLoad(html); + + expect($('style').length).to.eq(0); + expect($('link[rel="stylesheet"]').length).to.eq(0); + }); + }) +}); diff --git a/packages/integrations/vue/test/fixtures/app-entrypoint-css/astro.config.mjs b/packages/integrations/vue/test/fixtures/app-entrypoint-css/astro.config.mjs new file mode 100644 index 000000000000..c4a9f8f33f57 --- /dev/null +++ b/packages/integrations/vue/test/fixtures/app-entrypoint-css/astro.config.mjs @@ -0,0 +1,8 @@ +import { defineConfig } from 'astro/config'; +import vue from '@astrojs/vue'; + +export default defineConfig({ + integrations: [ + vue({ appEntrypoint: '/src/app.ts' }) + ], +}) diff --git a/packages/integrations/vue/test/fixtures/app-entrypoint-css/package.json b/packages/integrations/vue/test/fixtures/app-entrypoint-css/package.json new file mode 100644 index 000000000000..b34b4b99c50f --- /dev/null +++ b/packages/integrations/vue/test/fixtures/app-entrypoint-css/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/vue-app-entrypoint-css", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/vue": "workspace:*", + "astro": "workspace:*" + } +} diff --git a/packages/integrations/vue/test/fixtures/app-entrypoint-css/src/app.ts b/packages/integrations/vue/test/fixtures/app-entrypoint-css/src/app.ts new file mode 100644 index 000000000000..05742cb890fe --- /dev/null +++ b/packages/integrations/vue/test/fixtures/app-entrypoint-css/src/app.ts @@ -0,0 +1,9 @@ +import type { App } from 'vue' +import Bar from './components/Bar.vue' +// Important! Test that styles here are injected to the page +import '/src/main.css' + + +export default function setup(app: App) { + app.component('Bar', Bar); +} diff --git a/packages/integrations/vue/test/fixtures/app-entrypoint-css/src/components/Bar.vue b/packages/integrations/vue/test/fixtures/app-entrypoint-css/src/components/Bar.vue new file mode 100644 index 000000000000..9e690ea06adc --- /dev/null +++ b/packages/integrations/vue/test/fixtures/app-entrypoint-css/src/components/Bar.vue @@ -0,0 +1,3 @@ + diff --git a/packages/integrations/vue/test/fixtures/app-entrypoint-css/src/components/Foo.vue b/packages/integrations/vue/test/fixtures/app-entrypoint-css/src/components/Foo.vue new file mode 100644 index 000000000000..3e648808cb72 --- /dev/null +++ b/packages/integrations/vue/test/fixtures/app-entrypoint-css/src/components/Foo.vue @@ -0,0 +1,5 @@ + diff --git a/packages/integrations/vue/test/fixtures/app-entrypoint-css/src/main.css b/packages/integrations/vue/test/fixtures/app-entrypoint-css/src/main.css new file mode 100644 index 000000000000..5c197d2cfeda --- /dev/null +++ b/packages/integrations/vue/test/fixtures/app-entrypoint-css/src/main.css @@ -0,0 +1,3 @@ +:root { + background-color: red; +} diff --git a/packages/integrations/vue/test/fixtures/app-entrypoint-css/src/pages/index.astro b/packages/integrations/vue/test/fixtures/app-entrypoint-css/src/pages/index.astro new file mode 100644 index 000000000000..3240cbe0fd73 --- /dev/null +++ b/packages/integrations/vue/test/fixtures/app-entrypoint-css/src/pages/index.astro @@ -0,0 +1,12 @@ +--- +import Foo from '../components/Foo.vue'; +--- + + + + Vue App Entrypoint + + + + + diff --git a/packages/integrations/vue/test/fixtures/app-entrypoint-css/src/pages/unrelated.astro b/packages/integrations/vue/test/fixtures/app-entrypoint-css/src/pages/unrelated.astro new file mode 100644 index 000000000000..0952e25a76f1 --- /dev/null +++ b/packages/integrations/vue/test/fixtures/app-entrypoint-css/src/pages/unrelated.astro @@ -0,0 +1,8 @@ + + + Unrelated page + + +

I shouldn't have styles

+ + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e6ae07c7b947..80bedf0538bf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4915,6 +4915,15 @@ importers: specifier: 5.0.1 version: 5.0.1 + packages/integrations/vue/test/fixtures/app-entrypoint-css: + dependencies: + '@astrojs/vue': + specifier: workspace:* + version: link:../../.. + astro: + specifier: workspace:* + version: link:../../../../../astro + packages/integrations/vue/test/fixtures/app-entrypoint-no-export-default: dependencies: '@astrojs/vue':