Allow defining Astro components in Vite plugins (#3889)
* Allow defining Astro components in Vite plugins * Adds a changeset * Move non-main compilation into load * Use the cachedCompilation in the markdown plugin * Fix HMR test * Simplify getNormalizedID * Use a windows-friendly virtual module id for the test
This commit is contained in:
parent
1896931931
commit
5f4ecbad1b
11 changed files with 194 additions and 40 deletions
5
.changeset/hungry-cougars-yell.md
Normal file
5
.changeset/hungry-cougars-yell.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'astro': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Allow defining Astro components in Vite plugins
|
|
@ -11,7 +11,10 @@ import { viteID } from '../core/util.js';
|
||||||
import { transformWithVite } from './styles.js';
|
import { transformWithVite } from './styles.js';
|
||||||
|
|
||||||
type CompilationCache = Map<string, CompileResult>;
|
type CompilationCache = Map<string, CompileResult>;
|
||||||
type CompileResult = TransformResult & { rawCSSDeps: Set<string> };
|
type CompileResult = TransformResult & {
|
||||||
|
rawCSSDeps: Set<string>;
|
||||||
|
source: string;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Note: this is currently needed because Astro is directly using a Vite internal CSS transform. This gives us
|
* Note: this is currently needed because Astro is directly using a Vite internal CSS transform. This gives us
|
||||||
|
@ -44,6 +47,16 @@ export interface CompileProps {
|
||||||
pluginContext: PluginContext;
|
pluginContext: PluginContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getNormalizedID(filename: string): string {
|
||||||
|
try {
|
||||||
|
const filenameURL = new URL(`file://${filename}`);
|
||||||
|
return fileURLToPath(filenameURL);
|
||||||
|
} catch(err) {
|
||||||
|
// Not a real file, so just use the provided filename as the normalized id
|
||||||
|
return filename;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function compile({
|
async function compile({
|
||||||
config,
|
config,
|
||||||
filename,
|
filename,
|
||||||
|
@ -53,9 +66,7 @@ async function compile({
|
||||||
viteTransform,
|
viteTransform,
|
||||||
pluginContext,
|
pluginContext,
|
||||||
}: CompileProps): Promise<CompileResult> {
|
}: CompileProps): Promise<CompileResult> {
|
||||||
const filenameURL = new URL(`file://${filename}`);
|
const normalizedID = getNormalizedID(filename);
|
||||||
const normalizedID = fileURLToPath(filenameURL);
|
|
||||||
|
|
||||||
let rawCSSDeps = new Set<string>();
|
let rawCSSDeps = new Set<string>();
|
||||||
let cssTransformError: Error | undefined;
|
let cssTransformError: Error | undefined;
|
||||||
|
|
||||||
|
@ -141,6 +152,9 @@ async function compile({
|
||||||
rawCSSDeps: {
|
rawCSSDeps: {
|
||||||
value: rawCSSDeps,
|
value: rawCSSDeps,
|
||||||
},
|
},
|
||||||
|
source: {
|
||||||
|
value: source,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return compileResult;
|
return compileResult;
|
||||||
|
@ -150,6 +164,13 @@ export function isCached(config: AstroConfig, filename: string) {
|
||||||
return configCache.has(config) && configCache.get(config)!.has(filename);
|
return configCache.has(config) && configCache.get(config)!.has(filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getCachedSource(config: AstroConfig, filename: string): string | null {
|
||||||
|
if(!isCached(config, filename)) return null;
|
||||||
|
let src = configCache.get(config)!.get(filename);
|
||||||
|
if(!src) return null;
|
||||||
|
return src.source;
|
||||||
|
}
|
||||||
|
|
||||||
export function invalidateCompilation(config: AstroConfig, filename: string) {
|
export function invalidateCompilation(config: AstroConfig, filename: string) {
|
||||||
if (configCache.has(config)) {
|
if (configCache.has(config)) {
|
||||||
const cache = configCache.get(config)!;
|
const cache = configCache.get(config)!;
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { isRelativePath, startsWithForwardSlash } from '../core/path.js';
|
||||||
import { resolvePages } from '../core/util.js';
|
import { resolvePages } from '../core/util.js';
|
||||||
import { PAGE_SCRIPT_ID, PAGE_SSR_SCRIPT_ID } from '../vite-plugin-scripts/index.js';
|
import { PAGE_SCRIPT_ID, PAGE_SSR_SCRIPT_ID } from '../vite-plugin-scripts/index.js';
|
||||||
import { getFileInfo } from '../vite-plugin-utils/index.js';
|
import { getFileInfo } from '../vite-plugin-utils/index.js';
|
||||||
import { cachedCompilation, CompileProps } from './compile.js';
|
import { cachedCompilation, CompileProps, getCachedSource } from './compile.js';
|
||||||
import { handleHotUpdate, trackCSSDependencies } from './hmr.js';
|
import { handleHotUpdate, trackCSSDependencies } from './hmr.js';
|
||||||
import { parseAstroRequest, ParsedRequestResult } from './query.js';
|
import { parseAstroRequest, ParsedRequestResult } from './query.js';
|
||||||
import { getViteTransform, TransformHook } from './styles.js';
|
import { getViteTransform, TransformHook } from './styles.js';
|
||||||
|
@ -96,24 +96,25 @@ export default function astro({ config, logging }: AstroPluginOptions): vite.Plu
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async load(this: PluginContext, id, opts) {
|
async load(id, opts) {
|
||||||
const parsedId = parseAstroRequest(id);
|
const parsedId = parseAstroRequest(id);
|
||||||
const query = parsedId.query;
|
const query = parsedId.query;
|
||||||
if (!id.endsWith('.astro') && !query.astro) {
|
if (!query.astro) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// if we still get a relative path here, vite couldn't resolve the import
|
let filename = parsedId.filename;
|
||||||
if (isRelativePath(parsedId.filename)) {
|
// For CSS / hoisted scripts we need to load the source ourselves.
|
||||||
|
// It should be in the compilation cache at this point.
|
||||||
|
let raw = await this.resolve(filename, undefined);
|
||||||
|
if(!raw) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const filename = normalizeFilename(parsedId.filename);
|
let source = getCachedSource(config, raw.id);
|
||||||
const fileUrl = new URL(`file://${filename}`);
|
if(!source) {
|
||||||
let source = await fs.promises.readFile(fileUrl, 'utf-8');
|
return null;
|
||||||
const isPage = fileUrl.pathname.startsWith(resolvePages(config).pathname);
|
|
||||||
if (isPage && config._ctx.scripts.some((s) => s.stage === 'page')) {
|
|
||||||
source += `\n<script src="${PAGE_SCRIPT_ID}" />`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const compileProps: CompileProps = {
|
const compileProps: CompileProps = {
|
||||||
config,
|
config,
|
||||||
filename,
|
filename,
|
||||||
|
@ -123,8 +124,9 @@ export default function astro({ config, logging }: AstroPluginOptions): vite.Plu
|
||||||
viteTransform,
|
viteTransform,
|
||||||
pluginContext: this,
|
pluginContext: this,
|
||||||
};
|
};
|
||||||
if (query.astro) {
|
|
||||||
if (query.type === 'style') {
|
switch(query.type) {
|
||||||
|
case 'style': {
|
||||||
if (typeof query.index === 'undefined') {
|
if (typeof query.index === 'undefined') {
|
||||||
throw new Error(`Requests for Astro CSS must include an index.`);
|
throw new Error(`Requests for Astro CSS must include an index.`);
|
||||||
}
|
}
|
||||||
|
@ -144,7 +146,8 @@ export default function astro({ config, logging }: AstroPluginOptions): vite.Plu
|
||||||
return {
|
return {
|
||||||
code,
|
code,
|
||||||
};
|
};
|
||||||
} else if (query.type === 'script') {
|
}
|
||||||
|
case 'script': {
|
||||||
if (typeof query.index === 'undefined') {
|
if (typeof query.index === 'undefined') {
|
||||||
throw new Error(`Requests for hoisted scripts must include an index`);
|
throw new Error(`Requests for hoisted scripts must include an index`);
|
||||||
}
|
}
|
||||||
|
@ -185,7 +188,39 @@ export default function astro({ config, logging }: AstroPluginOptions): vite.Plu
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
default: return null;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
async transform(this: PluginContext, source, id, opts) {
|
||||||
|
const parsedId = parseAstroRequest(id);
|
||||||
|
const query = parsedId.query;
|
||||||
|
if (!id.endsWith('.astro') || query.astro) {
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
// if we still get a relative path here, vite couldn't resolve the import
|
||||||
|
if (isRelativePath(parsedId.filename)) {
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filename = normalizeFilename(parsedId.filename);
|
||||||
|
let isPage = false;
|
||||||
|
try {
|
||||||
|
const fileUrl = new URL(`file://${filename}`);
|
||||||
|
isPage = fileUrl.pathname.startsWith(resolvePages(config).pathname);
|
||||||
|
} catch {}
|
||||||
|
if (isPage && config._ctx.scripts.some((s) => s.stage === 'page')) {
|
||||||
|
source += `\n<script src="${PAGE_SCRIPT_ID}" />`;
|
||||||
|
}
|
||||||
|
const compileProps: CompileProps = {
|
||||||
|
config,
|
||||||
|
filename,
|
||||||
|
moduleId: id,
|
||||||
|
source,
|
||||||
|
ssr: Boolean(opts?.ssr),
|
||||||
|
viteTransform,
|
||||||
|
pluginContext: this,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const transformResult = await cachedCompilation(compileProps);
|
const transformResult = await cachedCompilation(compileProps);
|
||||||
|
|
|
@ -12,6 +12,8 @@ import { collectErrorMetadata } from '../core/errors.js';
|
||||||
import { prependForwardSlash } from '../core/path.js';
|
import { prependForwardSlash } from '../core/path.js';
|
||||||
import { resolvePages, viteID } from '../core/util.js';
|
import { resolvePages, viteID } from '../core/util.js';
|
||||||
import type { PluginMetadata as AstroPluginMetadata } from '../vite-plugin-astro/types';
|
import type { PluginMetadata as AstroPluginMetadata } from '../vite-plugin-astro/types';
|
||||||
|
import { cachedCompilation, CompileProps } from '../vite-plugin-astro/compile.js';
|
||||||
|
import { getViteTransform, TransformHook } from '../vite-plugin-astro/styles.js';
|
||||||
import { PAGE_SSR_SCRIPT_ID } from '../vite-plugin-scripts/index.js';
|
import { PAGE_SSR_SCRIPT_ID } from '../vite-plugin-scripts/index.js';
|
||||||
import { getFileInfo } from '../vite-plugin-utils/index.js';
|
import { getFileInfo } from '../vite-plugin-utils/index.js';
|
||||||
|
|
||||||
|
@ -61,9 +63,14 @@ export default function markdown({ config }: AstroPluginOptions): Plugin {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let viteTransform: TransformHook;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: 'astro:markdown',
|
name: 'astro:markdown',
|
||||||
enforce: 'pre',
|
enforce: 'pre',
|
||||||
|
configResolved(_resolvedConfig) {
|
||||||
|
viteTransform = getViteTransform(_resolvedConfig);
|
||||||
|
},
|
||||||
async resolveId(id, importer, options) {
|
async resolveId(id, importer, options) {
|
||||||
// Resolve any .md files with the `?content` cache buster. This should only come from
|
// Resolve any .md files with the `?content` cache buster. This should only come from
|
||||||
// an already-resolved JS module wrapper. Needed to prevent infinite loops in Vite.
|
// an already-resolved JS module wrapper. Needed to prevent infinite loops in Vite.
|
||||||
|
@ -85,7 +92,7 @@ export default function markdown({ config }: AstroPluginOptions): Plugin {
|
||||||
// In all other cases, we do nothing and rely on normal Vite resolution.
|
// In all other cases, we do nothing and rely on normal Vite resolution.
|
||||||
return undefined;
|
return undefined;
|
||||||
},
|
},
|
||||||
async load(id) {
|
async load(id, opts) {
|
||||||
// A markdown file has been imported via ESM!
|
// A markdown file has been imported via ESM!
|
||||||
// Return the file's JS representation, including all Markdown
|
// Return the file's JS representation, including all Markdown
|
||||||
// frontmatter and a deferred `import() of the compiled markdown content.
|
// frontmatter and a deferred `import() of the compiled markdown content.
|
||||||
|
@ -174,21 +181,17 @@ ${setup}`.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transform from `.astro` to valid `.ts`
|
// Transform from `.astro` to valid `.ts`
|
||||||
let transformResult = await transform(astroResult, {
|
const compileProps: CompileProps = {
|
||||||
pathname: '/@fs' + prependForwardSlash(fileUrl.pathname),
|
config,
|
||||||
projectRoot: config.root.toString(),
|
filename,
|
||||||
site: config.site
|
moduleId: id,
|
||||||
? new URL(config.base, config.site).toString()
|
source: astroResult,
|
||||||
: `http://localhost:${config.server.port}/`,
|
ssr: Boolean(opts?.ssr),
|
||||||
sourcefile: id,
|
viteTransform,
|
||||||
sourcemap: 'inline',
|
pluginContext: this,
|
||||||
// TODO: baseline flag
|
};
|
||||||
experimentalStaticExtraction: true,
|
|
||||||
internalURL: `/@fs${prependForwardSlash(
|
|
||||||
viteID(new URL('../runtime/server/index.js', import.meta.url))
|
|
||||||
)}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
let transformResult = await cachedCompilation(compileProps)
|
||||||
let { code: tsResult } = transformResult;
|
let { code: tsResult } = transformResult;
|
||||||
|
|
||||||
tsResult = `\nexport const metadata = ${JSON.stringify(metadata)};
|
tsResult = `\nexport const metadata = ${JSON.stringify(metadata)};
|
||||||
|
|
9
packages/astro/test/fixtures/virtual-astro-file/astro.config.mjs
vendored
Normal file
9
packages/astro/test/fixtures/virtual-astro-file/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
import myPlugin from './src/plugin/my-plugin.mjs';
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
|
export default defineConfig({
|
||||||
|
vite: {
|
||||||
|
plugins: [myPlugin()]
|
||||||
|
}
|
||||||
|
});
|
8
packages/astro/test/fixtures/virtual-astro-file/package.json
vendored
Normal file
8
packages/astro/test/fixtures/virtual-astro-file/package.json
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"name": "@test/virtual-astro-file",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"astro": "workspace:*"
|
||||||
|
}
|
||||||
|
}
|
10
packages/astro/test/fixtures/virtual-astro-file/src/pages/index.astro
vendored
Normal file
10
packages/astro/test/fixtures/virtual-astro-file/src/pages/index.astro
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
---
|
||||||
|
import Something from '@my-plugin/virtual.astro';
|
||||||
|
---
|
||||||
|
<html>
|
||||||
|
<head><title>Testing</title></head>
|
||||||
|
<body>
|
||||||
|
<Something />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
27
packages/astro/test/fixtures/virtual-astro-file/src/plugin/my-plugin.mjs
vendored
Normal file
27
packages/astro/test/fixtures/virtual-astro-file/src/plugin/my-plugin.mjs
vendored
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
|
||||||
|
|
||||||
|
export default function myPlugin() {
|
||||||
|
const pluginId = `@my-plugin/virtual.astro`;
|
||||||
|
return {
|
||||||
|
enforce: 'pre',
|
||||||
|
name: 'virtual-astro-plugin',
|
||||||
|
resolveId(id) {
|
||||||
|
if (id === pluginId) return id;
|
||||||
|
},
|
||||||
|
load(id) {
|
||||||
|
if (id === pluginId) {
|
||||||
|
return `---
|
||||||
|
const works = true;
|
||||||
|
---
|
||||||
|
<h1 id="something">This is a virtual module id</h1>
|
||||||
|
<h2 id="works">{works}</h2>
|
||||||
|
<style>
|
||||||
|
h1 {
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
|
@ -22,6 +22,9 @@ describe('HMR - CSS', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Timestamp URL used by Vite gets the right mime type', async () => {
|
it('Timestamp URL used by Vite gets the right mime type', async () => {
|
||||||
|
// Index page is always loaded first by the browser
|
||||||
|
await fixture.fetch('/');
|
||||||
|
// Now we can simulate what happens in the browser
|
||||||
let res = await fixture.fetch(
|
let res = await fixture.fetch(
|
||||||
'/src/pages/index.astro?astro=&type=style&index=0&lang.css=&t=1653657441095'
|
'/src/pages/index.astro?astro=&type=style&index=0&lang.css=&t=1653657441095'
|
||||||
);
|
);
|
||||||
|
|
27
packages/astro/test/virtual-astro-file.js
Normal file
27
packages/astro/test/virtual-astro-file.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import * as cheerio from 'cheerio';
|
||||||
|
import { loadFixture } from './test-utils.js';
|
||||||
|
|
||||||
|
describe('Loading virtual Astro files', () => {
|
||||||
|
let fixture;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
fixture = await loadFixture({ root: './fixtures/virtual-astro-file/' });
|
||||||
|
await fixture.build();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the component', async () => {
|
||||||
|
const html = await fixture.readFile('/index.html');
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
expect($('#something')).to.have.a.lengthOf(1);
|
||||||
|
expect($('#works').text()).to.equal('true');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('builds component CSS', async () => {
|
||||||
|
const html = await fixture.readFile('/index.html');
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
const href = $('link').attr('href');
|
||||||
|
const css = await fixture.readFile(href);
|
||||||
|
expect(css).to.match(/green/, 'css bundled from virtual astro module');
|
||||||
|
});
|
||||||
|
});
|
|
@ -1802,6 +1802,12 @@ importers:
|
||||||
postcss: 8.4.14
|
postcss: 8.4.14
|
||||||
tailwindcss: 3.1.5
|
tailwindcss: 3.1.5
|
||||||
|
|
||||||
|
packages/astro/test/fixtures/virtual-astro-file:
|
||||||
|
specifiers:
|
||||||
|
astro: workspace:*
|
||||||
|
dependencies:
|
||||||
|
astro: link:../../..
|
||||||
|
|
||||||
packages/astro/test/fixtures/vue-component:
|
packages/astro/test/fixtures/vue-component:
|
||||||
specifiers:
|
specifiers:
|
||||||
'@astrojs/vue': workspace:*
|
'@astrojs/vue': workspace:*
|
||||||
|
|
Loading…
Reference in a new issue