Update compiler, improve tests

This commit is contained in:
Drew Powers 2021-09-10 18:36:16 -06:00
parent c3e7b7f37d
commit 7296e0b0a2
9 changed files with 105 additions and 94 deletions

View file

@ -89,7 +89,7 @@
"srcset-parse": "^1.1.0",
"string-width": "^5.0.0",
"supports-esm": "^1.0.0",
"vite": "^2.5.2",
"vite": "^2.5.7",
"yargs-parser": "^20.2.9",
"zod": "^3.8.1"
},

View file

@ -42,7 +42,7 @@ async function _render(child: any) {
export class AstroComponent {
private htmlParts: string[];
private expressions: TemplateStringsArray;
constructor(htmlParts: string[], expressions: TemplateStringsArray) {
this.htmlParts = htmlParts;
this.expressions = expressions;
@ -74,7 +74,7 @@ export const createComponent = (cb: AstroComponentFactory) => {
// Add a flag to this callback to mark it as an Astro component
(cb as any).isAstroComponentFactory = true;
return cb;
}
};
function extractHydrationDirectives(inputProps: Record<string | number, any>): { hydrationDirective: [string, any] | null; props: Record<string | number, any> } {
let props: Record<string | number, any> = {};
@ -101,7 +101,7 @@ async function generateHydrateScript(scriptOptions: HydrateScriptOptions, metada
const { hydrate, componentUrl, componentExport } = metadata;
if (!componentExport) {
throw new Error(`Unable to resolve a componentExport for "${metadata.displayName}"! Please open an issue.`)
throw new Error(`Unable to resolve a componentExport for "${metadata.displayName}"! Please open an issue.`);
}
let hydrationSource = '';
@ -133,7 +133,7 @@ export const renderComponent = async (result: any, displayName: string, Componen
// children = await renderGenerator(children);
const { renderers } = result._metadata;
if (Component && (Component as any).isAstroComponentFactory) {
const output = await renderAstroComponent(await (Component as any)(result, Component, _props, children))
const output = await renderAstroComponent(await (Component as any)(result, Component, _props, children));
return output;
}

View file

@ -13,7 +13,7 @@ import type { AstroComponent, AstroComponentFactory } from '../internal';
interface SSROptions {
/** an instance of the AstroConfig */
astroConfig: AstroConfig,
astroConfig: AstroConfig;
/** location of file on disk */
filePath: URL;
/** logging options */
@ -51,14 +51,14 @@ export async function renderAstroComponent(component: InstanceType<typeof AstroC
export async function renderToString(result: any, componentFactory: AstroComponentFactory, props: any, children: any) {
const Component = await componentFactory(result, props, children);
let template = await renderAstroComponent(Component);
return template
return template;
}
async function renderPage(result: any, Component: AstroComponentFactory, props: any, children: any) {
const template = await renderToString(result, Component, props, children);
const styles = Array.from(result.styles).map(style => `<style>${style}</style>`);
const styles = Array.from(result.styles).map((style) => `<style>${style}</style>`);
const scripts = Array.from(result.scripts);
return template.replace("</head>", styles.join('\n') + scripts.join('\n') + "</head>");
return template.replace('</head>', styles.join('\n') + scripts.join('\n') + '</head>');
}
const cache = new Map();
@ -66,44 +66,46 @@ const cache = new Map();
// TODO: improve validation and error handling here.
async function resolveRenderers(viteServer: ViteDevServer, ids: string[]) {
const resolve = viteServer.config.createResolver();
const renderers = await Promise.all(ids.map(async renderer => {
if (cache.has(renderer)) return cache.get(renderer);
const resolvedRenderer: any = {};
const renderers = await Promise.all(
ids.map(async (renderer) => {
if (cache.has(renderer)) return cache.get(renderer);
const resolvedRenderer: any = {};
// We can dynamically import the renderer by itself because it shouldn't have
// any non-standard imports, the index is just meta info.
// The other entrypoints need to be loaded through Vite.
const { default: instance } = await import(renderer);
// This resolves the renderer's entrypoints to a final URL through Vite
const getPath = async (src: string) => {
const spec = path.posix.join(instance.name, src);
const resolved = await resolve(spec);
if (!resolved) {
throw new Error(`Unable to resolve "${spec}" to a package!`)
// We can dynamically import the renderer by itself because it shouldn't have
// any non-standard imports, the index is just meta info.
// The other entrypoints need to be loaded through Vite.
const { default: instance } = await import(renderer);
// This resolves the renderer's entrypoints to a final URL through Vite
const getPath = async (src: string) => {
const spec = path.posix.join(instance.name, src);
const resolved = await resolve(spec);
if (!resolved) {
throw new Error(`Unable to resolve "${spec}" to a package!`);
}
return resolved;
};
resolvedRenderer.name = instance.name;
if (instance.client) {
resolvedRenderer.source = await getPath(instance.client);
}
if (Array.isArray(instance.hydrationPolyfills)) {
resolvedRenderer.hydrationPolyfills = await Promise.all(instance.hydrationPolyfills.map((src: string) => getPath(src)));
}
if (Array.isArray(instance.polyfills)) {
resolvedRenderer.polyfills = await Promise.all(instance.polyfills.map((src: string) => getPath(src)));
}
return resolved;
}
resolvedRenderer.name = instance.name;
if (instance.client) {
resolvedRenderer.source = await getPath(instance.client);
}
if (Array.isArray(instance.hydrationPolyfills)) {
resolvedRenderer.hydrationPolyfills = await Promise.all(instance.hydrationPolyfills.map((src: string) => getPath(src)));
}
if (Array.isArray(instance.polyfills)) {
resolvedRenderer.polyfills = await Promise.all(instance.polyfills.map((src: string) => getPath(src)));
}
const { url } = await viteServer.moduleGraph.ensureEntryFromUrl(await getPath(instance.server));
const { default: server } = await viteServer.ssrLoadModule(url);
resolvedRenderer.ssr = server;
cache.set(renderer, resolvedRenderer);
return resolvedRenderer
}));
return resolvedRenderer;
})
);
return renderers;
}
@ -116,35 +118,39 @@ async function resolveImportedModules(viteServer: ViteDevServer, file: string) {
let importedModules: Record<string, any> = {};
const moduleNodes = Array.from(modulesByFile);
// Loop over the importedModules and grab the exports from each one.
// We'll pass these to the shared $$result so renderers can match
// components to their exported identifier and URL
// NOTE: Important that this is parallelized as much as possible!
await Promise.all(moduleNodes.map(moduleNode => {
const entries = Array.from(moduleNode.importedModules);
return Promise.all(entries.map(entry => {
// Skip our internal import that every module will have
if (entry.id?.endsWith('astro/dist/internal/index.js')) {
return;
}
return viteServer.moduleGraph.ensureEntryFromUrl(entry.url).then(mod => {
if (mod.ssrModule) {
importedModules[mod.url] = mod.ssrModule;
return;
} else {
return viteServer.ssrLoadModule(mod.url).then(result => {
importedModules[mod.url] = result.ssrModule;
return;
})
}
})
}))
}))
await Promise.all(
moduleNodes.map((moduleNode) => {
const entries = Array.from(moduleNode.importedModules);
return importedModules
return Promise.all(
entries.map((entry) => {
// Skip our internal import that every module will have
if (entry.id?.endsWith('astro/dist/internal/index.js')) {
return;
}
return viteServer.moduleGraph.ensureEntryFromUrl(entry.url).then((mod) => {
if (mod.ssrModule) {
importedModules[mod.url] = mod.ssrModule;
return;
} else {
return viteServer.ssrLoadModule(mod.url).then((result) => {
importedModules[mod.url] = result.ssrModule;
return;
});
}
});
})
);
})
);
return importedModules;
}
/** use Vite to SSR */
@ -154,10 +160,7 @@ export async function ssr({ astroConfig, filePath, logging, mode, origin, pathna
// 1.5. resolve renderers and imported modules.
// important that this happens _after_ ssrLoadModule, otherwise `importedModules` would be empty
const [renderers, importedModules] = await Promise.all([
resolveRenderers(viteServer, astroConfig.renderers),
resolveImportedModules(viteServer, fileURLToPath(filePath))
]);
const [renderers, importedModules] = await Promise.all([resolveRenderers(viteServer, astroConfig.renderers), resolveImportedModules(viteServer, fileURLToPath(filePath))]);
// 2. handle dynamic routes
let params: Params = {};
@ -192,22 +195,26 @@ export async function ssr({ astroConfig, filePath, logging, mode, origin, pathna
const fullURL = new URL(pathname, origin);
const Component = await mod.default;
if (!Component)
throw new Error(`Expected an exported Astro component but recieved typeof ${typeof Component}`);
if (!Component) throw new Error(`Expected an exported Astro component but recieved typeof ${typeof Component}`);
if (!Component.isAstroComponentFactory) throw new Error(`Unable to SSR non-Astro component (${route?.component})`);
let html = await renderPage({
styles: new Set(),
scripts: new Set(),
/** This function returns the `Astro` faux-global */
createAstro(props: any) {
const site = new URL(origin);
const url = new URL('.' + pathname, site);
const canonicalURL = getCanonicalURL(pathname, astroConfig.buildOptions.site || origin)
return { isPage: true, site, request: { url, canonicalURL }, props };
let html = await renderPage(
{
styles: new Set(),
scripts: new Set(),
/** This function returns the `Astro` faux-global */
createAstro(props: any) {
const site = new URL(origin);
const url = new URL('.' + pathname, site);
const canonicalURL = getCanonicalURL(pathname, astroConfig.buildOptions.site || origin);
return { isPage: true, site, request: { url, canonicalURL }, props };
},
_metadata: { importedModules, renderers },
},
_metadata: { importedModules, renderers },
}, Component, { }, null);
Component,
{},
null
);
// 4. modify response
if (mode === 'development') {

View file

@ -18,7 +18,10 @@ const require = createRequire(import.meta.url);
type ViteConfigWithSSR = InlineConfig & { ssr?: { external?: string[]; noExternal?: string[] } };
/** Return a common starting point for all Vite actions */
export async function loadViteConfig(viteConfig: ViteConfigWithSSR, { astroConfig, logging, devServer }: { astroConfig: AstroConfig; logging: LogOptions, devServer?: AstroDevServer }): Promise<ViteConfigWithSSR> {
export async function loadViteConfig(
viteConfig: ViteConfigWithSSR,
{ astroConfig, logging, devServer }: { astroConfig: AstroConfig; logging: LogOptions; devServer?: AstroDevServer }
): Promise<ViteConfigWithSSR> {
const optimizedDeps = new Set<string>(); // dependencies that must be bundled for the client (Vite may not detect all of these)
const dedupe = new Set<string>(); // dependencies that cant be duplicated (e.g. React & SolidJS)
const plugins: Plugin[] = []; // Vite plugins
@ -41,7 +44,8 @@ export async function loadViteConfig(viteConfig: ViteConfigWithSSR, { astroConfi
optimizedDeps.add(name + renderer.client.substr(1));
}
// 2. knownEntrypoints and polyfills need to be added to the client
for (const dep of [...(renderer.knownEntrypoints || []), ...(renderer.polyfills || [])]) {
for (let dep of [...(renderer.knownEntrypoints || []), ...(renderer.polyfills || [])]) {
if (dep[0] === '.') dep = name + dep.substr(1); // if local polyfill, use full path
optimizedDeps.add(dep);
dedupe.add(dep); // we can try and dedupe renderers by default
}

View file

@ -66,7 +66,7 @@ describe('Doctype', () => {
expect(html).toEqual(expect.stringMatching(/^<!doctype html>/));
// test 2: A link inside of the head
const $ = doc(html);
const $ = cheerio.load(html);
expect($('head link')).toHaveLength(1);
});

View file

@ -30,7 +30,7 @@ describe.skip('HMR tests', () => {
const result = await runtime.load('/static');
assert.ok(!result.error, `build error: ${result.error}`);
const html = result.contents;
const $ = doc(html);
const $ = cheerio.load(html);
assert.equal($('[src="/_snowpack/hmr-client.js"]').length, 1);
assert.ok(/window\.HMR_WEBSOCKET_PORT/.test(html), 'websocket port added');
});
@ -39,7 +39,7 @@ describe.skip('HMR tests', () => {
const result = await runtime.load('/no-elements');
assert.ok(!result.error, `build error: ${result.error}`);
const html = result.contents;
const $ = doc(html);
const $ = cheerio.load(html);
assert.equal($('[src="/_snowpack/hmr-client.js"]').length, 1);
assert.ok(/window\.HMR_WEBSOCKET_PORT/.test(html), 'websocket port added');
});

View file

@ -18,7 +18,7 @@ describe('Hoisted scripts', () => {
test('Moves external scripts up', async () => {
const html = await fixture.fetch('/external').then((res) => res.text());
const $ = doc(html);
const $ = cheerio.load(html);
expect($('head script[type="module"][data-astro="hoist"]')).toHaveLength(2);
expect($('body script')).toHaveLength(0);
@ -26,7 +26,7 @@ describe('Hoisted scripts', () => {
test('Moves inline scripts up', async () => {
const html = await fixture.fetch('/inline').then((res) => res.text());
const $ = doc(html);
const $ = cheerio.load(html);
expect($('head script[type="module"][data-astro="hoist"]')).toHaveLength(1);
expect($('body script')).toHaveLength(0);
@ -63,7 +63,7 @@ describe('Hoisted scripts', () => {
test('External page builds the scripts to a single bundle', async () => {
let external = await fixture.readFile('/external/index.html');
$ = doc(external);
$ = cheerio.load(external);
// test 1: there are two scripts
assert.equal($('script')).toHaveLength(2);

View file

@ -19,7 +19,7 @@ describe('LitElement test', () => {
return;
}
const html = await fixture.fetch('/').then((res) => res.text());
const $ = doc(html);
const $ = cheerio.load(html);
// test 1: attributes rendered
expect($('my-element').attr('foo')).toBe('bar');
@ -31,7 +31,7 @@ describe('LitElement test', () => {
// Skipped because not supported by Lit
test.skip('Renders a custom element by the constructor', async () => {
const html = await fixture.fetch('/ctr').then((res) => res.text());
const $ = doc(html);
const $ = cheerio.load(html);
// test 1: attributes rendered
expect($('my-element').attr('foo')).toBe('bar');

View file

@ -11679,10 +11679,10 @@ vfile@^5.0.0:
unist-util-stringify-position "^3.0.0"
vfile-message "^3.0.0"
vite@^2.5.2:
version "2.5.2"
resolved "https://registry.yarnpkg.com/vite/-/vite-2.5.2.tgz#3963a4ec1e6ecae49359eddfdd67f6cb1e6e07a1"
integrity sha512-JK5uhiVyMqHiAJbgBa8rCvpP8bEhAE9dKDv1gCmP+EUP2FSPmEeW3WXlCXauPB3MDa8behPW+ntyNXqnGaxslg==
vite@^2.5.7:
version "2.5.7"
resolved "https://registry.yarnpkg.com/vite/-/vite-2.5.7.tgz#e495be9d8bcbf9d30c7141efdccacde746ee0125"
integrity sha512-hyUoWmRPhjN1aI+ZSBqDINKdIq7aokHE2ZXiztOg4YlmtpeQtMwMeyxv6X9YxHZmvGzg/js/eATM9Z1nwyakxg==
dependencies:
esbuild "^0.12.17"
postcss "^8.3.6"