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

Allow defining Astro components in Vite plugins #3889

Merged
merged 7 commits into from
Jul 11, 2022
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/hungry-cougars-yell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Allow defining Astro components in Vite plugins
29 changes: 25 additions & 4 deletions packages/astro/src/vite-plugin-astro/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ import { viteID } from '../core/util.js';
import { transformWithVite } from './styles.js';

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
Expand Down Expand Up @@ -44,6 +47,16 @@ export interface CompileProps {
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({
config,
filename,
Expand All @@ -53,9 +66,7 @@ async function compile({
viteTransform,
pluginContext,
}: CompileProps): Promise<CompileResult> {
const filenameURL = new URL(`file://${filename}`);
const normalizedID = fileURLToPath(filenameURL);

const normalizedID = getNormalizedID(filename);
let rawCSSDeps = new Set<string>();
let cssTransformError: Error | undefined;

Expand Down Expand Up @@ -141,6 +152,9 @@ async function compile({
rawCSSDeps: {
value: rawCSSDeps,
},
source: {
value: source,
},
});

return compileResult;
Expand All @@ -150,6 +164,13 @@ export function isCached(config: AstroConfig, filename: string) {
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) {
if (configCache.has(config)) {
const cache = configCache.get(config)!;
Expand Down
77 changes: 56 additions & 21 deletions packages/astro/src/vite-plugin-astro/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { isRelativePath, startsWithForwardSlash } from '../core/path.js';
import { resolvePages } from '../core/util.js';
import { PAGE_SCRIPT_ID, PAGE_SSR_SCRIPT_ID } from '../vite-plugin-scripts/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 { parseAstroRequest, ParsedRequestResult } from './query.js';
import { getViteTransform, TransformHook } from './styles.js';
Expand Down Expand Up @@ -96,24 +96,25 @@ export default function astro({ config, logging }: AstroPluginOptions): vite.Plu
return id;
}
},
async load(this: PluginContext, id, opts) {
async load(id, opts) {
const parsedId = parseAstroRequest(id);
const query = parsedId.query;
if (!id.endsWith('.astro') && !query.astro) {
if (!query.astro) {
return null;
}
// if we still get a relative path here, vite couldn't resolve the import
if (isRelativePath(parsedId.filename)) {
let filename = 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;
}

const filename = normalizeFilename(parsedId.filename);
const fileUrl = new URL(`file://${filename}`);
let source = await fs.promises.readFile(fileUrl, 'utf-8');
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}" />`;
let source = getCachedSource(config, raw.id);
if(!source) {
return null;
}

const compileProps: CompileProps = {
config,
filename,
Expand All @@ -123,14 +124,15 @@ export default function astro({ config, logging }: AstroPluginOptions): vite.Plu
viteTransform,
pluginContext: this,
};
if (query.astro) {
if (query.type === 'style') {

switch(query.type) {
case 'style': {
if (typeof query.index === 'undefined') {
throw new Error(`Requests for Astro CSS must include an index.`);
}

const transformResult = await cachedCompilation(compileProps);

// Track any CSS dependencies so that HMR is triggered when they change.
await trackCSSDependencies.call(this, {
viteDevServer,
Expand All @@ -140,11 +142,12 @@ export default function astro({ config, logging }: AstroPluginOptions): vite.Plu
});
const csses = transformResult.css;
const code = csses[query.index];

return {
code,
};
} else if (query.type === 'script') {
}
case 'script': {
if (typeof query.index === 'undefined') {
throw new Error(`Requests for hoisted scripts must include an index`);
}
Expand All @@ -154,15 +157,15 @@ export default function astro({ config, logging }: AstroPluginOptions): vite.Plu
code: `/* client hoisted script, empty in SSR: ${id} */`,
};
}

const transformResult = await cachedCompilation(compileProps);
const scripts = transformResult.scripts;
const hoistedScript = scripts[query.index];

if (!hoistedScript) {
throw new Error(`No hoisted script at index ${query.index}`);
}

if (hoistedScript.type === 'external') {
const src = hoistedScript.src!;
if (src.startsWith('/') && !isBrowserPath(src)) {
Expand All @@ -172,7 +175,7 @@ export default function astro({ config, logging }: AstroPluginOptions): vite.Plu
);
}
}

return {
code:
hoistedScript.type === 'inline'
Expand All @@ -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 {
const transformResult = await cachedCompilation(compileProps);
Expand Down
33 changes: 18 additions & 15 deletions packages/astro/src/vite-plugin-markdown/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { collectErrorMetadata } from '../core/errors.js';
import { prependForwardSlash } from '../core/path.js';
import { resolvePages, viteID } from '../core/util.js';
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 { getFileInfo } from '../vite-plugin-utils/index.js';

Expand Down Expand Up @@ -61,9 +63,14 @@ export default function markdown({ config }: AstroPluginOptions): Plugin {
return false;
}

let viteTransform: TransformHook;

return {
name: 'astro:markdown',
enforce: 'pre',
configResolved(_resolvedConfig) {
viteTransform = getViteTransform(_resolvedConfig);
},
async resolveId(id, importer, options) {
// 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.
Expand All @@ -85,7 +92,7 @@ export default function markdown({ config }: AstroPluginOptions): Plugin {
// In all other cases, we do nothing and rely on normal Vite resolution.
return undefined;
},
async load(id) {
async load(id, opts) {
// A markdown file has been imported via ESM!
// Return the file's JS representation, including all Markdown
// frontmatter and a deferred `import() of the compiled markdown content.
Expand Down Expand Up @@ -174,21 +181,17 @@ ${setup}`.trim();
}

// Transform from `.astro` to valid `.ts`
let transformResult = await transform(astroResult, {
pathname: '/@fs' + prependForwardSlash(fileUrl.pathname),
projectRoot: config.root.toString(),
site: config.site
? new URL(config.base, config.site).toString()
: `http://localhost:${config.server.port}/`,
sourcefile: id,
sourcemap: 'inline',
// TODO: baseline flag
experimentalStaticExtraction: true,
internalURL: `/@fs${prependForwardSlash(
viteID(new URL('../runtime/server/index.js', import.meta.url))
)}`,
});
const compileProps: CompileProps = {
config,
filename,
moduleId: id,
source: astroResult,
ssr: Boolean(opts?.ssr),
viteTransform,
pluginContext: this,
};

let transformResult = await cachedCompilation(compileProps)
let { code: tsResult } = transformResult;

tsResult = `\nexport const metadata = ${JSON.stringify(metadata)};
Expand Down
Original file line number Diff line number Diff line change
@@ -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 changes: 8 additions & 0 deletions packages/astro/test/fixtures/virtual-astro-file/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "@test/virtual-astro-file",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
import Something from '@my-plugin/virtual.astro';
---
<html>
<head><title>Testing</title></head>
<body>
<Something />
</body>
</html>

Original file line number Diff line number Diff line change
@@ -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>
`;
}
},
};
}
3 changes: 3 additions & 0 deletions packages/astro/test/hmr-css.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ describe('HMR - CSS', () => {
});

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(
'/src/pages/index.astro?astro=&type=style&index=0&lang.css=&t=1653657441095'
);
Expand Down
27 changes: 27 additions & 0 deletions packages/astro/test/virtual-astro-file.js
Original file line number Diff line number Diff line change
@@ -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');
});
});
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

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