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 .'",
"build": "astro build"
},
"dependencies": {},
"devDependencies": {
"astro": "file:../../",
"nodemon": "^2.0.7"

View file

@ -3,7 +3,7 @@ const { readFile } = require('fs').promises;
// Snowpack plugins must be CommonJS :(
const transformPromise = import('./lib/compiler/index.js');
module.exports = function (snowpackConfig, { resolve, extensions, astroConfig } = {}) {
module.exports = function (snowpackConfig, { resolvePackageUrl, extensions, astroConfig } = {}) {
return {
name: 'snowpack-astro',
knownEntrypoints: [],
@ -17,7 +17,7 @@ module.exports = function (snowpackConfig, { resolve, extensions, astroConfig }
const contents = await readFile(filePath, 'utf-8');
const compileOptions = {
astroConfig,
resolve,
resolvePackageUrl,
extensions,
};
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 {
logging: LogOptions;
resolve: (p: string) => Promise<string>;
resolvePackageUrl: (p: string) => Promise<string>;
astroConfig: AstroConfig;
extensions?: Record<string, ValidExtensionPlugins>;
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 { runtimeConfig } = runtime;
const { backendSnowpack: snowpack } = runtimeConfig;
const resolve = (pkgName: string) => snowpack.getUrlForPackage(pkgName);
const resolvePackageUrl = (pkgName: string) => snowpack.getUrlForPackage(pkgName);
const imports = 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);

View file

@ -23,21 +23,21 @@ const { readFile } = fsPromises;
type DynamicImportMap = Map<'vue' | 'react' | 'react-dom' | 'preact', string>;
/** 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();
for (let plugin of plugins) {
switch (plugin) {
case 'vue': {
importMap.set('vue', await resolve('vue'));
importMap.set('vue', await resolvePackageUrl('vue'));
break;
}
case 'react': {
importMap.set('react', await resolve('react'));
importMap.set('react-dom', await resolve('react-dom'));
importMap.set('react', await resolvePackageUrl('react'));
importMap.set('react-dom', await resolvePackageUrl('react-dom'));
break;
}
case 'preact': {
importMap.set('preact', await resolve('preact'));
importMap.set('preact', await resolvePackageUrl('preact'));
break;
}
}
@ -64,13 +64,13 @@ const defaultExtensions: Readonly<Record<string, ValidExtensionPlugins>> = {
interface CollectDynamic {
astroConfig: AstroConfig;
resolve: (s: string) => Promise<string>;
resolvePackageUrl: (s: string) => Promise<string>;
logging: LogOptions;
mode: RuntimeMode;
}
/** 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>();
// Only astro files
@ -98,7 +98,7 @@ export async function collectDynamicImports(filename: URL, { astroConfig, loggin
fileID: '',
compileOptions: {
astroConfig,
resolve,
resolvePackageUrl,
logging,
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 */
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 */
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();
for (let plugin of plugins) {
switch (plugin) {
case 'vue': {
importMap.set('vue', await resolve('vue'));
importMap.set('vue', await resolvePackageUrl('vue'));
break;
}
case 'react': {
importMap.set('react', await resolve('react'));
importMap.set('react-dom', await resolve('react-dom'));
importMap.set('react', await resolvePackageUrl('react'));
importMap.set('react-dom', await resolvePackageUrl('react-dom'));
break;
}
case 'preact': {
importMap.set('preact', await resolve('preact'));
importMap.set('preact', await resolvePackageUrl('preact'));
break;
}
}
@ -643,7 +643,7 @@ export async function codegen(ast: Ast, { compileOptions, filename }: CodeGenOpt
};
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);

View file

@ -230,20 +230,27 @@ interface RuntimeOptions {
}
/** 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 { env, mode, resolvePackageUrl } = options;
const internalPath = new URL('./frontend/', import.meta.url);
let snowpack: SnowpackDevServer;
const astroPlugOptions: {
resolve?: (s: string) => Promise<string>;
resolvePackageUrl?: (s: string) => Promise<string>;
extensions?: Record<string, string>;
astroConfig: AstroConfig;
} = {
astroConfig,
extensions,
resolve: async (pkgName: string) => snowpack.getUrlForPackage(pkgName),
resolvePackageUrl,
};
const mountOptions = {
@ -258,7 +265,7 @@ async function createSnowpack(astroConfig: AstroConfig, env: Record<string, any>
const snowpackConfig = await loadConfiguration({
root: fileURLToPath(projectRoot),
mount: mountOptions,
mode: mode,
mode,
plugins: [
[fileURLToPath(new URL('../snowpack-plugin.cjs', import.meta.url)), astroPlugOptions],
require.resolve('@snowpack/plugin-sass'),
@ -293,20 +300,27 @@ async function createSnowpack(astroConfig: AstroConfig, env: Record<string, any>
/** Core Astro runtime */
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(
astroConfig,
{
astro: true,
},
mode
env: {
astro: true
},
mode,
resolvePackageUrl
}
);
const { snowpack: frontendSnowpack, snowpackRuntime: frontendSnowpackRuntime, snowpackConfig: frontendSnowpackConfig } = await createSnowpack(
astroConfig,
{
astro: false,
},
mode
env: {
astro: false
},
mode
}
);
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>