Fix dynamic React components (#111)

Another change in snowpack@3 caused this bug. It's not actually a bug in snowpack. Previously snowpack was keeping its list of installed packages in a global cache. In 3.3 it stopped doing so. We were accidentally relying on that global cache to be able to resolve dynamic components.

This fixes it so that we use the frontend snowpack instance to resolve dynamic components. Doing so means they are available when we try to load them.
This commit is contained in:
Matthew Phillips 2021-04-19 14:41:06 -04:00 committed by GitHub
parent 188541260a
commit eb984559a8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 1343 additions and 783 deletions

File diff suppressed because it is too large Load diff

View file

@ -6,7 +6,6 @@
"start": "nodemon -w ../../lib -x 'astro dev .'", "start": "nodemon -w ../../lib -x 'astro dev .'",
"build": "astro build" "build": "astro build"
}, },
"dependencies": {},
"devDependencies": { "devDependencies": {
"astro": "file:../../", "astro": "file:../../",
"nodemon": "^2.0.7" "nodemon": "^2.0.7"

View file

@ -3,7 +3,7 @@ const { readFile } = require('fs').promises;
// Snowpack plugins must be CommonJS :( // Snowpack plugins must be CommonJS :(
const transformPromise = import('./lib/compiler/index.js'); const transformPromise = import('./lib/compiler/index.js');
module.exports = function (snowpackConfig, { resolve, extensions, astroConfig } = {}) { module.exports = function (snowpackConfig, { resolvePackageUrl, extensions, astroConfig } = {}) {
return { return {
name: 'snowpack-astro', name: 'snowpack-astro',
knownEntrypoints: [], knownEntrypoints: [],
@ -17,7 +17,7 @@ module.exports = function (snowpackConfig, { resolve, extensions, astroConfig }
const contents = await readFile(filePath, 'utf-8'); const contents = await readFile(filePath, 'utf-8');
const compileOptions = { const compileOptions = {
astroConfig, astroConfig,
resolve, resolvePackageUrl,
extensions, extensions,
}; };
const result = await compileComponent(contents, { compileOptions, filename: filePath, projectRoot }); const result = await compileComponent(contents, { compileOptions, filename: filePath, projectRoot });

View file

@ -3,7 +3,7 @@ import type { AstroConfig, RuntimeMode, ValidExtensionPlugins } from './astro';
export interface CompileOptions { export interface CompileOptions {
logging: LogOptions; logging: LogOptions;
resolve: (p: string) => Promise<string>; resolvePackageUrl: (p: string) => Promise<string>;
astroConfig: AstroConfig; astroConfig: AstroConfig;
extensions?: Record<string, ValidExtensionPlugins>; extensions?: Record<string, ValidExtensionPlugins>;
mode: RuntimeMode; mode: RuntimeMode;

View file

@ -155,11 +155,11 @@ export async function build(astroConfig: AstroConfig): Promise<0 | 1> {
const runtime = await createRuntime(astroConfig, { mode, logging: runtimeLogging }); const runtime = await createRuntime(astroConfig, { mode, logging: runtimeLogging });
const { runtimeConfig } = runtime; const { runtimeConfig } = runtime;
const { backendSnowpack: snowpack } = runtimeConfig; const { backendSnowpack: snowpack } = runtimeConfig;
const resolve = (pkgName: string) => snowpack.getUrlForPackage(pkgName); const resolvePackageUrl = (pkgName: string) => snowpack.getUrlForPackage(pkgName);
const imports = new Set<string>(); const imports = new Set<string>();
const statics = new Set<string>(); const statics = new Set<string>();
const collectImportsOptions = { astroConfig, logging, resolve, mode }; const collectImportsOptions = { astroConfig, logging, resolvePackageUrl, mode };
const pages = await allPages(pageRoot); const pages = await allPages(pageRoot);

View file

@ -23,21 +23,21 @@ const { readFile } = fsPromises;
type DynamicImportMap = Map<'vue' | 'react' | 'react-dom' | 'preact', string>; type DynamicImportMap = Map<'vue' | 'react' | 'react-dom' | 'preact', string>;
/** Add framework runtimes when needed */ /** Add framework runtimes when needed */
async function acquireDynamicComponentImports(plugins: Set<ValidExtensionPlugins>, resolve: (s: string) => Promise<string>): Promise<DynamicImportMap> { async function acquireDynamicComponentImports(plugins: Set<ValidExtensionPlugins>, resolvePackageUrl: (s: string) => Promise<string>): Promise<DynamicImportMap> {
const importMap: DynamicImportMap = new Map(); const importMap: DynamicImportMap = new Map();
for (let plugin of plugins) { for (let plugin of plugins) {
switch (plugin) { switch (plugin) {
case 'vue': { case 'vue': {
importMap.set('vue', await resolve('vue')); importMap.set('vue', await resolvePackageUrl('vue'));
break; break;
} }
case 'react': { case 'react': {
importMap.set('react', await resolve('react')); importMap.set('react', await resolvePackageUrl('react'));
importMap.set('react-dom', await resolve('react-dom')); importMap.set('react-dom', await resolvePackageUrl('react-dom'));
break; break;
} }
case 'preact': { case 'preact': {
importMap.set('preact', await resolve('preact')); importMap.set('preact', await resolvePackageUrl('preact'));
break; break;
} }
} }
@ -64,13 +64,13 @@ const defaultExtensions: Readonly<Record<string, ValidExtensionPlugins>> = {
interface CollectDynamic { interface CollectDynamic {
astroConfig: AstroConfig; astroConfig: AstroConfig;
resolve: (s: string) => Promise<string>; resolvePackageUrl: (s: string) => Promise<string>;
logging: LogOptions; logging: LogOptions;
mode: RuntimeMode; mode: RuntimeMode;
} }
/** Gather necessary framework runtimes for dynamic components */ /** Gather necessary framework runtimes for dynamic components */
export async function collectDynamicImports(filename: URL, { astroConfig, logging, resolve, mode }: CollectDynamic) { export async function collectDynamicImports(filename: URL, { astroConfig, logging, resolvePackageUrl, mode }: CollectDynamic) {
const imports = new Set<string>(); const imports = new Set<string>();
// Only astro files // Only astro files
@ -98,7 +98,7 @@ export async function collectDynamicImports(filename: URL, { astroConfig, loggin
fileID: '', fileID: '',
compileOptions: { compileOptions: {
astroConfig, astroConfig,
resolve, resolvePackageUrl,
logging, logging,
mode, mode,
}, },
@ -135,7 +135,7 @@ export async function collectDynamicImports(filename: URL, { astroConfig, loggin
}; };
} }
const dynamic = await acquireDynamicComponentImports(plugins, resolve); const dynamic = await acquireDynamicComponentImports(plugins, resolvePackageUrl);
/** Add dynamic component runtimes to imports */ /** Add dynamic component runtimes to imports */
function appendImports(rawName: string, importUrl: URL) { function appendImports(rawName: string, importUrl: URL) {

View file

@ -258,21 +258,21 @@ function compileExpressionSafe(raw: string): string {
} }
/** Build dependency map of dynamic component runtime frameworks */ /** Build dependency map of dynamic component runtime frameworks */
async function acquireDynamicComponentImports(plugins: Set<ValidExtensionPlugins>, resolve: (s: string) => Promise<string>): Promise<DynamicImportMap> { async function acquireDynamicComponentImports(plugins: Set<ValidExtensionPlugins>, resolvePackageUrl: (s: string) => Promise<string>): Promise<DynamicImportMap> {
const importMap: DynamicImportMap = new Map(); const importMap: DynamicImportMap = new Map();
for (let plugin of plugins) { for (let plugin of plugins) {
switch (plugin) { switch (plugin) {
case 'vue': { case 'vue': {
importMap.set('vue', await resolve('vue')); importMap.set('vue', await resolvePackageUrl('vue'));
break; break;
} }
case 'react': { case 'react': {
importMap.set('react', await resolve('react')); importMap.set('react', await resolvePackageUrl('react'));
importMap.set('react-dom', await resolve('react-dom')); importMap.set('react-dom', await resolvePackageUrl('react-dom'));
break; break;
} }
case 'preact': { case 'preact': {
importMap.set('preact', await resolve('preact')); importMap.set('preact', await resolvePackageUrl('preact'));
break; break;
} }
} }
@ -643,7 +643,7 @@ export async function codegen(ast: Ast, { compileOptions, filename }: CodeGenOpt
}; };
const { script, componentPlugins, createCollection } = compileModule(ast.module, state, compileOptions); const { script, componentPlugins, createCollection } = compileModule(ast.module, state, compileOptions);
state.dynamicImports = await acquireDynamicComponentImports(componentPlugins, compileOptions.resolve); state.dynamicImports = await acquireDynamicComponentImports(componentPlugins, compileOptions.resolvePackageUrl);
compileCss(ast.css, state); compileCss(ast.css, state);

View file

@ -230,20 +230,27 @@ interface RuntimeOptions {
} }
/** Create a new Snowpack instance to power Astro */ /** Create a new Snowpack instance to power Astro */
async function createSnowpack(astroConfig: AstroConfig, env: Record<string, any>, mode: RuntimeMode) { interface CreateSnowpackOptions {
env: Record<string, any>;
mode: RuntimeMode;
resolvePackageUrl?: (pkgName: string) => Promise<string>;
}
async function createSnowpack(astroConfig: AstroConfig, options: CreateSnowpackOptions) {
const { projectRoot, astroRoot, extensions } = astroConfig; const { projectRoot, astroRoot, extensions } = astroConfig;
const { env, mode, resolvePackageUrl } = options;
const internalPath = new URL('./frontend/', import.meta.url); const internalPath = new URL('./frontend/', import.meta.url);
let snowpack: SnowpackDevServer; let snowpack: SnowpackDevServer;
const astroPlugOptions: { const astroPlugOptions: {
resolve?: (s: string) => Promise<string>; resolvePackageUrl?: (s: string) => Promise<string>;
extensions?: Record<string, string>; extensions?: Record<string, string>;
astroConfig: AstroConfig; astroConfig: AstroConfig;
} = { } = {
astroConfig, astroConfig,
extensions, extensions,
resolve: async (pkgName: string) => snowpack.getUrlForPackage(pkgName), resolvePackageUrl,
}; };
const mountOptions = { const mountOptions = {
@ -258,7 +265,7 @@ async function createSnowpack(astroConfig: AstroConfig, env: Record<string, any>
const snowpackConfig = await loadConfiguration({ const snowpackConfig = await loadConfiguration({
root: fileURLToPath(projectRoot), root: fileURLToPath(projectRoot),
mount: mountOptions, mount: mountOptions,
mode: mode, mode,
plugins: [ plugins: [
[fileURLToPath(new URL('../snowpack-plugin.cjs', import.meta.url)), astroPlugOptions], [fileURLToPath(new URL('../snowpack-plugin.cjs', import.meta.url)), astroPlugOptions],
require.resolve('@snowpack/plugin-sass'), require.resolve('@snowpack/plugin-sass'),
@ -293,20 +300,27 @@ async function createSnowpack(astroConfig: AstroConfig, env: Record<string, any>
/** Core Astro runtime */ /** Core Astro runtime */
export async function createRuntime(astroConfig: AstroConfig, { mode, logging }: RuntimeOptions): Promise<AstroRuntime> { export async function createRuntime(astroConfig: AstroConfig, { mode, logging }: RuntimeOptions): Promise<AstroRuntime> {
const resolvePackageUrl = async (pkgName: string) => frontendSnowpack.getUrlForPackage(pkgName);
const { snowpack: backendSnowpack, snowpackRuntime: backendSnowpackRuntime, snowpackConfig: backendSnowpackConfig } = await createSnowpack( const { snowpack: backendSnowpack, snowpackRuntime: backendSnowpackRuntime, snowpackConfig: backendSnowpackConfig } = await createSnowpack(
astroConfig, astroConfig,
{ {
astro: true, env: {
}, astro: true
mode },
mode,
resolvePackageUrl
}
); );
const { snowpack: frontendSnowpack, snowpackRuntime: frontendSnowpackRuntime, snowpackConfig: frontendSnowpackConfig } = await createSnowpack( const { snowpack: frontendSnowpack, snowpackRuntime: frontendSnowpackRuntime, snowpackConfig: frontendSnowpackConfig } = await createSnowpack(
astroConfig, astroConfig,
{ {
astro: false, env: {
}, astro: false
mode },
mode
}
); );
const runtimeConfig: RuntimeConfig = { const runtimeConfig: RuntimeConfig = {

View file

@ -0,0 +1,30 @@
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import { doc } from './test-utils.js';
import { setup } from './helpers.js';
const DynamicComponents = suite('Dynamic components tests');
setup(DynamicComponents, './fixtures/astro-dynamic');
DynamicComponents('Loads client-only packages', async ({ runtime }) => {
let result = await runtime.load('/');
assert.equal(result.statusCode, 200);
// Grab the react-dom import
const exp = /import\("(.+?)"\)/g;
let match, reactDomURL;
while(match = exp.exec(result.contents)) {
if(match[1].includes('react-dom')) {
reactDomURL = match[1];
}
}
assert.ok(reactDomURL, 'React dom is on the page');
result = await runtime.load(reactDomURL);
assert.equal(result.statusCode, 200, 'Can load react-dom');
});
DynamicComponents.run();

View file

@ -0,0 +1,9 @@
import React from 'react';
export default function() {
return (
<div>
<button type="button">Increment -</button>
</div>
)
}

View file

@ -0,0 +1,9 @@
---
import Counter from '../components/Counter.jsx';
---
<html>
<head><title>Dynamic pages</title></head>
<body>
<Counter:load />
</body>
</html>