More progress here

This commit is contained in:
Matthew Phillips 2021-12-06 15:31:01 -05:00
parent 6b3d4ed075
commit abdde962a9
13 changed files with 295 additions and 97 deletions

View file

@ -1,4 +1,11 @@
import { imagetools } from 'vite-imagetools';
// @ts-check
export default /** @type {import('astro').AstroUserConfig} */ ({
renderers: [],
renderers: [
"@astrojs/renderer-vue"
],
vite: {
plugins: [imagetools()]
}
});

View file

@ -9,6 +9,8 @@
"preview": "astro preview"
},
"devDependencies": {
"astro": "^0.21.6"
"astro": "^0.21.6",
"unocss": "^0.15.5",
"vite-imagetools": "^4.0.1"
}
}

View file

@ -0,0 +1,20 @@
<script>
export default {
data() {
return {
greeting: 'Hello World!'
}
}
}
</script>
<template>
<p class="greeting">{{ greeting }}</p>
</template>
<style>
.greeting {
color: red;
font-weight: bold;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -1,16 +1,32 @@
---
import imgUrl from '../images/penguin.jpg';
import styleUrl from '../styles/global.css?url';
debugger;
import grayscaleUrl from '../images/random.jpg?grayscale=true';
import Greeting from '../components/Greeting.vue';
---
<html>
<head>
<title>Demo app</title>
<link rel="stylesheet" href={styleUrl}>
<style>
h1 { color: salmon; }
</style>
</head>
<body>
<h1>Penguins are cool</h1>
<img src={imgUrl} />
<section>
<h1>Images</h1>
<h2>Imported in JS</h2>
<img src={imgUrl} />
</section>
<section>
<h1>Component CSS</h1>
<Greeting />
</section>
<section>
<h1>ImageTools</h1>
<img src={grayscaleUrl} />
</section>
</body>
</html>

View file

@ -368,6 +368,7 @@ export interface SSRMetadata {
export interface SSRResult {
styles: Set<SSRElement>;
scripts: Set<SSRElement>;
links: Set<SSRElement>;
createAstro(Astro: AstroGlobalPartial, props: Record<string, any>, slots: Record<string, any> | null): AstroGlobal;
_metadata: SSRMetadata;
}

View file

@ -165,6 +165,7 @@ class AstroBuilder {
// Blah
const facadeIdToPageDataMap = new Map<string, PageBuildData>();
const facadeIdToAssetsMap = new Map<string, string[]>();
// Bundle the assets in your final build: This currently takes the HTML output
// of every page (stored in memory) and bundles the assets pointed to on those pages.
@ -207,6 +208,7 @@ class AstroBuilder {
astroStyleMap,
chunkToReferenceIdMap,
pureCSSChunks,
facadeIdToAssetsMap,
}),
...(viteConfig.plugins || []),
],
@ -222,7 +224,7 @@ class AstroBuilder {
console.log('End build step, now generating');
for(let out of (result as any).output) {
if(out.facadeModuleId)
await this.doTheRest(out, facadeIdToPageDataMap);
await this.renderPages(out, facadeIdToPageDataMap, facadeIdToAssetsMap);
}
// Write any additionally generated assets to disk.
@ -253,9 +255,11 @@ class AstroBuilder {
}
}
private async doTheRest(out: any, facadeIdToPageDataMap: Map<string, PageBuildData>) {
private async renderPages(out: any, facadeIdToPageDataMap: Map<string, PageBuildData>, facadeIdToAssetsMap: Map<string, string[]>) {
let url = new URL('./' + out.fileName, this.config.dist);
let pageData = facadeIdToPageDataMap.get(out.facadeModuleId)!;
const facadeId: string = out.facadeModuleId;
let pageData = facadeIdToPageDataMap.get(facadeId)!;
let linkIds = facadeIdToAssetsMap.get(facadeId) || [];
let compiledModule = await import(url.toString());
let Component = compiledModule.default;
@ -271,7 +275,7 @@ class AstroBuilder {
mod
})
console.log(`Generating: ${path}`);
let html = await renderComponent(renderers, Component, this.config, path, this.origin, params, pageProps);
let html = await renderComponent(renderers, Component, this.config, path, this.origin, params, pageProps, linkIds);
let outFolder = new URL('.' + path + '/', this.config.dist);
let outFile = new URL('./index.html', outFolder);
await fs.promises.mkdir(outFolder, { recursive: true });

View file

@ -139,10 +139,18 @@ export async function preload({ astroConfig, filePath, viteServer }: SSROptions)
return [renderers, mod];
}
export async function renderComponent(renderers: Renderer[], Component: AstroComponentFactory, astroConfig: AstroConfig, pathname: string, origin: string, params: Params, pageProps: Props): Promise<string> {
export async function renderComponent(renderers: Renderer[], Component: AstroComponentFactory, astroConfig: AstroConfig, pathname: string, origin: string, params: Params, pageProps: Props, links: string[] = []): Promise<string> {
const _links = new Set<SSRElement>(links.map(href => ({
props: {
rel: 'stylesheet',
href
},
children: ''
})));
const result: SSRResult = {
styles: new Set<SSRElement>(),
scripts: new Set<SSRElement>(),
links: _links,
/** This function returns the `Astro` faux-global */
createAstro(astroGlobal: AstroGlobalPartial, props: Record<string, any>, slots: Record<string, any> | null) {
const site = new URL(origin);
@ -267,6 +275,7 @@ export async function render(renderers: Renderer[], mod: ComponentInstance, ssrO
const result: SSRResult = {
styles: new Set<SSRElement>(),
scripts: new Set<SSRElement>(),
links: new Set<SSRElement>(),
/** This function returns the `Astro` faux-global */
createAstro(astroGlobal: AstroGlobalPartial, props: Record<string, any>, slots: Record<string, any> | null) {
const site = new URL(origin);

View file

@ -396,12 +396,16 @@ export async function renderPage(result: SSRResult, Component: AstroComponentFac
styles.push(renderElement('style', { props: { 'astro-style': true }, children: 'astro-root, astro-fragment { display: contents; }' }));
}
const links = Array.from(result.links)
.filter(uniqueElements)
.map(link => renderElement('link', link))
// inject styles & scripts at end of <head>
let headPos = template.indexOf('</head>');
if (headPos === -1) {
return styles.join('\n') + scripts.join('\n') + template; // if no </head>, prepend styles & scripts
return links.join('\n') + styles.join('\n') + scripts.join('\n') + template; // if no </head>, prepend styles & scripts
}
return template.substring(0, headPos) + styles.join('\n') + scripts.join('\n') + template.substring(headPos);
return template.substring(0, headPos) + links.join('\n') + styles.join('\n') + scripts.join('\n') + template.substring(headPos);
}
export async function renderAstroComponent(component: InstanceType<typeof AstroComponent>) {

View file

@ -0,0 +1,108 @@
import type { AstroConfig } from '../@types/astro';
import type { TransformResult } from '@astrojs/compiler';
import type { SourceMapInput } from 'rollup';
import type { TransformHook } from './styles';
import esbuild from 'esbuild';
import fs from 'fs';
import { fileURLToPath } from 'url';
import { transform } from '@astrojs/compiler';
import { AstroDevServer } from '../core/dev/index.js';
import { getViteTransform, transformWithVite } from './styles.js';
type CompilationCache = Map<string, TransformResult>;
const configCache = new WeakMap<AstroConfig, CompilationCache>();
// https://github.com/vitejs/vite/discussions/5109#discussioncomment-1450726
function isSSR(options: undefined | boolean | { ssr: boolean }): boolean {
if (options === undefined) {
return false;
}
if (typeof options === 'boolean') {
return options;
}
if (typeof options == 'object') {
return !!options.ssr;
}
return false;
}
async function compile(config: AstroConfig, filename: string, source: string, viteTransform: TransformHook, opts: boolean | undefined) {
// pages and layouts should be transformed as full documents (implicit <head> <body> etc)
// everything else is treated as a fragment
const normalizedID = fileURLToPath(new URL(`file://${filename}`));
const isPage = normalizedID.startsWith(fileURLToPath(config.pages)) ||
normalizedID.startsWith(fileURLToPath(config.layouts));
//let source = await fs.promises.readFile(id, 'utf8');
let cssTransformError: Error | undefined;
// Transform from `.astro` to valid `.ts`
// use `sourcemap: "both"` so that sourcemap is included in the code
// result passed to esbuild, but also available in the catch handler.
const transformResult = await transform(source, {
as: isPage ? 'document' : 'fragment',
projectRoot: config.projectRoot.toString(),
site: config.buildOptions.site,
sourcefile: filename,
sourcemap: 'both',
internalURL: 'astro/internal',
preprocessStyle: async (value: string, attrs: Record<string, string>) => {
const lang = `.${attrs?.lang || 'css'}`.toLowerCase();
try {
const result = await transformWithVite({
value, lang,
id: filename,
transformHook: viteTransform,
ssr: isSSR(opts)
});
let map: SourceMapInput | undefined;
if (!result) return null as any; // TODO: add type in compiler to fix "any"
if (result.map) {
if (typeof result.map === 'string') {
map = result.map;
} else if (result.map.mappings) {
map = result.map.toString();
}
}
return { code: result.code, map };
} catch (err) {
// save error to throw in plugin context
cssTransformError = err as any;
return null;
}
},
});
// throw CSS transform errors here if encountered
if (cssTransformError) throw cssTransformError;
return transformResult;
}
export function invalidateCompilation(config: AstroConfig, filename: string) {
if(configCache.has(config)) {
const cache = configCache.get(config)!;
cache.delete(filename);
}
}
export async function cachedCompilation(config: AstroConfig, filename: string, source: string | null, viteTransform: TransformHook, opts: boolean | undefined) {
let cache: CompilationCache;
if(!configCache.has(config)) {
cache = new Map();
configCache.set(config, cache);
} else {
cache = configCache.get(config)!;
}
if(cache.has(filename)) {
return cache.get(filename)!;
}
if(source === null) {
throw new Error(`Oh no, this should have been cached.`);
}
const transformResult = await compile(config, filename, source, viteTransform, opts);
cache.set(filename, transformResult);
return transformResult;
}

View file

@ -3,12 +3,15 @@ import type { SourceMapInput } from 'rollup';
import type vite from '../core/vite';
import type { AstroConfig } from '../@types/astro';
import esbuild from 'esbuild';
import fs from 'fs';
import { fileURLToPath } from 'url';
import { transform } from '@astrojs/compiler';
import { AstroDevServer } from '../core/dev/index.js';
import { getViteTransform, TransformHook, transformWithVite } from './styles.js';
import { parseAstroRequest } from './query.js';
import { cachedCompilation } from './compile.js';
const FRONTMATTER_PARSE_REGEXP = /^\-\-\-(.*)^\-\-\-/ms;
interface AstroPluginOptions {
@ -16,20 +19,6 @@ interface AstroPluginOptions {
devServer?: AstroDevServer;
}
// https://github.com/vitejs/vite/discussions/5109#discussioncomment-1450726
function isSSR(options: undefined | boolean | { ssr: boolean }): boolean {
if (options === undefined) {
return false;
}
if (typeof options === 'boolean') {
return options;
}
if (typeof options == 'object') {
return !!options.ssr;
}
return false;
}
/** Transform .astro files for Vite */
export default function astro({ config, devServer }: AstroPluginOptions): vite.Plugin {
let viteTransform: TransformHook;
@ -39,58 +28,54 @@ export default function astro({ config, devServer }: AstroPluginOptions): vite.P
configResolved(resolvedConfig) {
viteTransform = getViteTransform(resolvedConfig);
},
async resolveId(id) {
// serve sub-part requests (*?astro) as virtual modules
if (parseAstroRequest(id).query.astro) {
return id;
}
},
// note: dont claim .astro files with resolveId() — it prevents Vite from transpiling the final JS (import.meta.globEager, etc.)
async load(id, opts) {
if (!id.endsWith('.astro')) {
return null;
let { filename, query } = parseAstroRequest(id);
if(query.astro) {
if(query.type === 'style') {
if(filename.startsWith('/') && !filename.startsWith(config.projectRoot.pathname)) {
filename = new URL('.' + filename, config.projectRoot).pathname;
}
const transformResult = await cachedCompilation(config, filename, null,
viteTransform, opts);
if(typeof query.index === 'undefined') {
throw new Error(`Requests for Astro CSS must include an index.`);
}
const csses = transformResult.css;
const code = csses[query.index];
return {
code
};
}
}
return null;
},
async transform(source, id, opts) {
if (!id.endsWith('.astro')) {
return;
}
// pages and layouts should be transformed as full documents (implicit <head> <body> etc)
// everything else is treated as a fragment
const normalizedID = fileURLToPath(new URL(`file://${id}`));
const isPage = normalizedID.startsWith(fileURLToPath(config.pages)) || normalizedID.startsWith(fileURLToPath(config.layouts));
let source = await fs.promises.readFile(id, 'utf8');
let tsResult: TransformResult | undefined;
let cssTransformError: Error | undefined;
try {
// Transform from `.astro` to valid `.ts`
// use `sourcemap: "both"` so that sourcemap is included in the code
// result passed to esbuild, but also available in the catch handler.
tsResult = await transform(source, {
as: isPage ? 'document' : 'fragment',
projectRoot: config.projectRoot.toString(),
site: config.buildOptions.site,
sourcefile: id,
sourcemap: 'both',
internalURL: 'astro/internal',
preprocessStyle: async (value: string, attrs: Record<string, string>) => {
const lang = `.${attrs?.lang || 'css'}`.toLowerCase();
try {
const result = await transformWithVite({ value, lang, id, transformHook: viteTransform, ssr: isSSR(opts) });
let map: SourceMapInput | undefined;
if (!result) return null as any; // TODO: add type in compiler to fix "any"
if (result.map) {
if (typeof result.map === 'string') {
map = result.map;
} else if (result.map.mappings) {
map = result.map.toString();
}
}
return { code: result.code, map };
} catch (err) {
// save error to throw in plugin context
cssTransformError = err as any;
return null;
}
},
});
// throw CSS transform errors here if encountered
if (cssTransformError) throw cssTransformError;
const transformResult = await cachedCompilation(config, id, source,
viteTransform, opts);
// Compile all TypeScript to JavaScript.
// Also, catches invalid JS/TS in the compiled output before returning.
const { code, map } = await esbuild.transform(tsResult.code, { loader: 'ts', sourcemap: 'external', sourcefile: id });
const { code, map } = await esbuild.transform(transformResult.code, {
loader: 'ts',
sourcemap: 'external',
sourcefile: id
});
return {
code,
@ -125,27 +110,28 @@ export default function astro({ config, devServer }: AstroPluginOptions): vite.P
labels: 'compiler',
title: '🐛 BUG: `@astrojs/compiler` panic',
body: `### Describe the Bug
\`@astrojs/compiler\` encountered an unrecoverable error when compiling the following file.
**${id.replace(fileURLToPath(config.projectRoot), '')}**
\`\`\`astro
${source}
\`\`\`
`,
\`@astrojs/compiler\` encountered an unrecoverable error when compiling the following file.
**${id.replace(fileURLToPath(config.projectRoot), '')}**
\`\`\`astro
${source}
\`\`\`
`,
});
err.url = `https://github.com/withastro/astro/issues/new?${search.toString()}`;
err.message = `Error: Uh oh, the Astro compiler encountered an unrecoverable error!
Please open
a GitHub issue using the link below:
${err.url}`;
Please open
a GitHub issue using the link below:
${err.url}`;
// TODO: remove stack replacement when compiler throws better errors
err.stack = ` at ${id}`;
}
throw err;
}
},
// async handleHotUpdate(context) {
// if (devServer) {

View file

@ -0,0 +1,32 @@
export interface AstroQuery {
astro?: boolean
src?: boolean
type?: 'script' | 'template' | 'style' | 'custom'
index?: number
lang?: string
raw?: boolean
}
export function parseAstroRequest(id: string): {
filename: string
query: AstroQuery
} {
const [filename, rawQuery] = id.split(`?`, 2)
const query = Object.fromEntries(new URLSearchParams(rawQuery).entries()) as AstroQuery;
if (query.astro != null) {
query.astro = true
}
if (query.src != null) {
query.src = true
}
if (query.index != null) {
query.index = Number(query.index)
}
if (query.raw != null) {
query.raw = true
}
return {
filename,
query
}
}

View file

@ -49,10 +49,11 @@ interface PluginOptions {
astroPageStyleMap: Map<string, string>;
chunkToReferenceIdMap: Map<string, string>;
pureCSSChunks: Set<RenderedChunk>;
facadeIdToAssetsMap: Map<string, string[]>;
}
export function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin {
const { astroPageStyleMap, astroStyleMap, chunkToReferenceIdMap, pureCSSChunks } = options;
const { astroPageStyleMap, astroStyleMap, chunkToReferenceIdMap, pureCSSChunks, facadeIdToAssetsMap } = options;
const styleSourceMap = new Map<string, string>();
return {
@ -127,17 +128,26 @@ export function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin {
// if (!chunkCSS) return null; // dont output empty .css files
if (isPureCSS) {
const { code: minifiedCSS } = await esbuild.transform(chunkCSS, {
loader: 'css',
minify: true,
});
const referenceId = this.emitFile({
name: chunk.name + '.css',
type: 'asset',
source: minifiedCSS,
});
pureCSSChunks.add(chunk);
chunkToReferenceIdMap.set(chunk.fileName, referenceId);
}
const { code: minifiedCSS } = await esbuild.transform(chunkCSS, {
loader: 'css',
minify: true,
});
const referenceId = this.emitFile({
name: chunk.name + '.css',
type: 'asset',
source: minifiedCSS,
});
chunkToReferenceIdMap.set(chunk.fileName, referenceId);
if(chunk.type === 'chunk') {
const facadeId = chunk.facadeModuleId!;
if(!facadeIdToAssetsMap.has(facadeId)) {
facadeIdToAssetsMap.set(facadeId, []);
}
facadeIdToAssetsMap.get(facadeId)!.push(this.getFileName(referenceId));
}
return null;
@ -145,7 +155,6 @@ export function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin {
// Delete CSS chunks so JS is not produced for them.
generateBundle(opts, bundle) {
debugger;
if (pureCSSChunks.size) {
const pureChunkFilenames = new Set([...pureCSSChunks].map((chunk) => chunk.fileName));
const emptyChunkFiles = [...pureChunkFilenames]