Implements top-level Astro + Astro.resolve (#1556)

* Implements top-level Astro + Astro.resolve

* Fix linting
This commit is contained in:
Matthew Phillips 2021-10-14 14:02:00 -04:00 committed by Drew Powers
parent 72b66ddb75
commit c269f7b687
11 changed files with 80 additions and 73 deletions

View file

@ -40,7 +40,7 @@
"test": "mocha --parallel --timeout 15000" "test": "mocha --parallel --timeout 15000"
}, },
"dependencies": { "dependencies": {
"@astrojs/compiler": "^0.1.13", "@astrojs/compiler": "^0.1.14",
"@astrojs/language-server": "^0.7.16", "@astrojs/language-server": "^0.7.16",
"@astrojs/markdown-remark": "^0.3.1", "@astrojs/markdown-remark": "^0.3.1",
"@astrojs/markdown-support": "0.3.1", "@astrojs/markdown-support": "0.3.1",

View file

@ -26,12 +26,15 @@ export interface AstroBuiltinProps {
'client:visible'?: boolean; 'client:visible'?: boolean;
} }
export interface Astro { export interface TopLevelAstro {
isPage: boolean; isPage: boolean;
fetchContent<T = any>(globStr: string): Promise<FetchContentResult<T>[]>; fetchContent<T = any>(globStr: string): Promise<FetchContentResult<T>[]>;
props: Record<string, number | string | any>;
request: AstroPageRequest;
resolve: (path: string) => string; resolve: (path: string) => string;
site: URL; site: URL;
}
export interface Astro extends TopLevelAstro {
props: Record<string, number | string | any>;
request: AstroPageRequest;
slots: Record<string, true | undefined>; slots: Record<string, true | undefined>;
} }

View file

@ -1,4 +1,4 @@
import { Astro as AstroGlobal } from './astro-file'; import { Astro as AstroGlobal, TopLevelAstro } from './astro-file';
import { Renderer } from './astro'; import { Renderer } from './astro';
export interface SSRMetadata { export interface SSRMetadata {
@ -8,6 +8,6 @@ export interface SSRMetadata {
export interface SSRResult { export interface SSRResult {
styles: Set<string>; styles: Set<string>;
scripts: Set<string>; scripts: Set<string>;
createAstro(props: Record<string, any>, slots: Record<string, any> | null): AstroGlobal; createAstro(Astro: TopLevelAstro, props: Record<string, any>, slots: Record<string, any> | null): AstroGlobal;
_metadata: SSRMetadata; _metadata: SSRMetadata;
} }

View file

@ -30,6 +30,7 @@ class HydrationMap {
private getComponentMetadata(Component: any): ComponentMetadata | null { private getComponentMetadata(Component: any): ComponentMetadata | null {
if(this.metadataCache.has(Component)) { if(this.metadataCache.has(Component)) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return this.metadataCache.get(Component)!; return this.metadataCache.get(Component)!;
} }
const metadata = this.findComponentMetadata(Component); const metadata = this.findComponentMetadata(Component);

View file

@ -1,6 +1,8 @@
import type { AstroComponentMetadata } from '../@types/astro'; import type { AstroComponentMetadata } from '../@types/astro';
import type { SSRResult } from '../@types/ssr'; import type { SSRResult } from '../@types/ssr';
import type { TopLevelAstro } from '../@types/astro-file';
import { pathToFileURL } from 'url';
import { valueToEstree } from 'estree-util-value-to-estree'; import { valueToEstree } from 'estree-util-value-to-estree';
import * as astring from 'astring'; import * as astring from 'astring';
import shorthash from 'shorthash'; import shorthash from 'shorthash';
@ -236,6 +238,47 @@ export async function renderComponent(result: SSRResult, displayName: string, Co
return `<astro-root uid="${astroId}">${html}</astro-root>`; return `<astro-root uid="${astroId}">${html}</astro-root>`;
} }
/** Create the Astro.fetchContent() runtime function. */
function createFetchContentFn(url: URL) {
const fetchContent = (importMetaGlobResult: Record<string, any>) => {
let allEntries = [...Object.entries(importMetaGlobResult)];
if (allEntries.length === 0) {
throw new Error(`[${url.pathname}] Astro.fetchContent() no matches found.`);
}
return allEntries
.map(([spec, mod]) => {
// Only return Markdown files for now.
if (!mod.frontmatter) {
return;
}
return {
content: mod.metadata,
metadata: mod.frontmatter,
file: new URL(spec, url),
};
})
.filter(Boolean);
};
return fetchContent;
}
export function createAstro(fileURLStr: string, site: string): TopLevelAstro {
const url = pathToFileURL(fileURLStr);
const fetchContent = createFetchContentFn(url) as unknown as TopLevelAstro['fetchContent'];
return {
// TODO I think this is no longer needed.
isPage: false,
site: new URL(site),
fetchContent,
resolve(...segments) {
return segments.reduce(
(u, segment) => new URL(segment, u),
url
).pathname
}
};
}
export function addAttribute(value: any, key: string) { export function addAttribute(value: any, key: string) {
if (value == null || value === false) { if (value == null || value === false) {
return ''; return '';

View file

@ -2,7 +2,7 @@ import type { BuildResult } from 'esbuild';
import type { ViteDevServer } from 'vite'; import type { ViteDevServer } from 'vite';
import type { AstroConfig, ComponentInstance, GetStaticPathsResult, Params, Props, Renderer, RouteCache, RouteData, RuntimeMode, SSRError } from '../@types/astro'; import type { AstroConfig, ComponentInstance, GetStaticPathsResult, Params, Props, Renderer, RouteCache, RouteData, RuntimeMode, SSRError } from '../@types/astro';
import type { SSRResult } from '../@types/ssr'; import type { SSRResult } from '../@types/ssr';
import type { Astro } from '../@types/astro-file'; import type { Astro, TopLevelAstro } from '../@types/astro-file';
import type { LogOptions } from '../logger'; import type { LogOptions } from '../logger';
import cheerio from 'cheerio'; import cheerio from 'cheerio';
@ -80,30 +80,6 @@ async function resolveRenderers(viteServer: ViteDevServer, ids: string[]): Promi
return renderers; return renderers;
} }
/** Create the Astro.fetchContent() runtime function. */
function createFetchContentFn(url: URL) {
const fetchContent = (importMetaGlobResult: Record<string, any>) => {
let allEntries = [...Object.entries(importMetaGlobResult)];
if (allEntries.length === 0) {
throw new Error(`[${url.pathname}] Astro.fetchContent() no matches found.`);
}
return allEntries
.map(([spec, mod]) => {
// Only return Markdown files for now.
if (!mod.frontmatter) {
return;
}
return {
content: mod.metadata,
metadata: mod.frontmatter,
file: new URL(spec, url),
};
})
.filter(Boolean);
};
return fetchContent;
}
/** use Vite to SSR */ /** use Vite to SSR */
export async function ssr({ astroConfig, filePath, logging, mode, origin, pathname, route, routeCache, viteServer }: SSROptions): Promise<string> { export async function ssr({ astroConfig, filePath, logging, mode, origin, pathname, route, routeCache, viteServer }: SSROptions): Promise<string> {
try { try {
@ -128,6 +104,7 @@ export async function ssr({ astroConfig, filePath, logging, mode, origin, pathna
routeCache[route.component] = routeCache[route.component] =
routeCache[route.component] || routeCache[route.component] ||
( (
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await mod.getStaticPaths!({ await mod.getStaticPaths!({
paginate: generatePaginateFunction(route), paginate: generatePaginateFunction(route),
rss: () => { rss: () => {
@ -158,35 +135,28 @@ export async function ssr({ astroConfig, filePath, logging, mode, origin, pathna
styles: new Set(), styles: new Set(),
scripts: new Set(), scripts: new Set(),
/** This function returns the `Astro` faux-global */ /** This function returns the `Astro` faux-global */
createAstro: (props: Record<string, any>, slots: Record<string, any> | null) => { createAstro(AstroGlobal: TopLevelAstro, props: Record<string, any>, slots: Record<string, any> | null) {
const site = new URL(origin); const site = new URL(origin);
const url = new URL('.' + pathname, site); const url = new URL('.' + pathname, site);
const canonicalURL = getCanonicalURL(pathname, astroConfig.buildOptions.site || origin); const canonicalURL = getCanonicalURL('.' + pathname, astroConfig.buildOptions.site || origin);
// Cast this type because the actual fetchContent implementation relies on import.meta.globEager
const fetchContent = createFetchContentFn(filePath) as unknown as Astro['fetchContent'];
return { return {
isPage: true, __proto__: AstroGlobal,
site, props,
request: { request: {
canonicalURL, canonicalURL,
params: {}, params: {},
url url
}, },
props,
fetchContent,
slots: Object.fromEntries( slots: Object.fromEntries(
Object.entries(slots || {}).map(([slotName]) => [slotName, true]) Object.entries(slots || {}).map(([slotName]) => [slotName, true])
), )
// Only temporary to get types working. } as unknown as Astro;
resolve(_s: string) {
throw new Error('Astro.resolve() is not currently supported in next.');
}
};
}, },
_metadata: { renderers }, _metadata: { renderers },
}; };
let html = await renderPage(result, Component, {}, null); let html = await renderPage(result, Component, pageProps, null);
// 4. modify response // 4. modify response
if (mode === 'development') { if (mode === 'development') {

View file

@ -29,6 +29,7 @@ export default function astro({ config, devServer }: AstroPluginOptions): Plugin
// 1. Transform from `.astro` to valid `.ts` // 1. Transform from `.astro` to valid `.ts`
// use `sourcemap: "inline"` so that the sourcemap is included in the "code" result that we pass to esbuild. // use `sourcemap: "inline"` so that the sourcemap is included in the "code" result that we pass to esbuild.
tsResult = await transform(source, { tsResult = await transform(source, {
site: config.buildOptions.site,
sourcefile: id, sourcefile: id,
sourcemap: 'both', sourcemap: 'both',
internalURL: 'astro/internal' internalURL: 'astro/internal'

View file

@ -1,6 +1,3 @@
/**
* UNCOMMENT: add Astro.* global
import { expect } from 'chai'; import { expect } from 'chai';
import cheerio from 'cheerio'; import cheerio from 'cheerio';
import { loadFixture } from './test-utils.js'; import { loadFixture } from './test-utils.js';
@ -32,15 +29,16 @@ describe('Astro.*', () => {
it('Astro.request.canonicalURL', async () => { it('Astro.request.canonicalURL', async () => {
// given a URL, expect the following canonical URL // given a URL, expect the following canonical URL
const canonicalURLs = { const canonicalURLs = {
'/': 'https://mysite.dev/blog/index.html', '/index.html': 'https://mysite.dev/blog/',
'/post/post': 'https://mysite.dev/blog/post/post/index.html', '/post/post/index.html': 'https://mysite.dev/blog/post/post/',
'/posts/1': 'https://mysite.dev/blog/posts/index.html', '/posts/1/index.html': 'https://mysite.dev/blog/posts/',
'/posts/2': 'https://mysite.dev/blog/posts/2/index.html', '/posts/2/index.html': 'https://mysite.dev/blog/posts/2/',
}; };
for (const [url, canonicalURL] of Object.entries(canonicalURLs)) { for (const [url, canonicalURL] of Object.entries(canonicalURLs)) {
const result = await fixture.readFile(url); const html = await fixture.readFile(url);
const $ = cheerio.load(result.contents);
const $ = cheerio.load(html);
expect($('link[rel="canonical"]').attr('href')).to.equal(canonicalURL); expect($('link[rel="canonical"]').attr('href')).to.equal(canonicalURL);
} }
}); });
@ -54,16 +52,7 @@ describe('Astro.*', () => {
it('Astro.resolve in development', async () => { it('Astro.resolve in development', async () => {
const html = await fixture.readFile('/resolve/index.html'); const html = await fixture.readFile('/resolve/index.html');
const $ = cheerio.load(html); const $ = cheerio.load(html);
expect($('img').attr('src')).to.equal('/_astro/src/images/penguin.png'); expect($('img').attr('src')).to.equal('/src/images/penguin.png');
expect($('#inner-child img').attr('src')).to.equal('/_astro/src/components/nested/images/penguin.png'); expect($('#inner-child img').attr('src')).to.equal('/src/components/nested/images/penguin.png');
});
it('Astro.resolve in the build', async () => {
const html = await fixture.readFile('/resolve/index.html');
const $ = cheerio.load(html);
expect($('img').attr('src')).to.equal('/blog/_astro/src/images/penguin.png');
}); });
}); });
*/
it.skip('is skipped', () => {});

View file

@ -1,6 +1,6 @@
--- ---
const title = 'My App';
import '../components/my-element.js'; import '../components/my-element.js';
const title = 'My App';
--- ---
<html> <html>

View file

@ -1,6 +1,6 @@
--- ---
const title = 'My App';
import '../components/my-element.js'; import '../components/my-element.js';
const title = 'My App';
--- ---
<html> <html>

View file

@ -106,10 +106,10 @@
"@algolia/logger-common" "4.10.5" "@algolia/logger-common" "4.10.5"
"@algolia/requester-common" "4.10.5" "@algolia/requester-common" "4.10.5"
"@astrojs/compiler@^0.1.13": "@astrojs/compiler@^0.1.14":
version "0.1.13" version "0.1.14"
resolved "https://registry.yarnpkg.com/@astrojs/compiler/-/compiler-0.1.13.tgz#06c810d71c7bcce1603ea77f9381940523544e69" resolved "https://registry.yarnpkg.com/@astrojs/compiler/-/compiler-0.1.14.tgz#15bb8211312c0ef2cacb9483fdb31c5dff1013ea"
integrity sha512-t/soXPJ34AQ67goh8EInCNVWd2HYOpgL5hE2xcZGwG74cIuczT4c+Guiqt50KYF5a7eNTdolPdy/dd7yipW8nQ== integrity sha512-iqc8upge2o9AAL87lB5ptTDZWmrrWpiFX7jGTU3Cuo4X9+KXmJL4oKSMy8sGt7JoKd2j5yZsFfLq9R5V/nwfbQ==
dependencies: dependencies:
typescript "^4.3.5" typescript "^4.3.5"