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:
parent
188541260a
commit
eb984559a8
11 changed files with 1343 additions and 783 deletions
2003
examples/kitchen-sink/package-lock.json
generated
2003
examples/kitchen-sink/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -6,7 +6,6 @@
|
|||
"start": "nodemon -w ../../lib -x 'astro dev .'",
|
||||
"build": "astro build"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"astro": "file:../../",
|
||||
"nodemon": "^2.0.7"
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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 = {
|
||||
|
|
30
test/astro-dynamic.test.js
Normal file
30
test/astro-dynamic.test.js
Normal 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();
|
9
test/fixtures/astro-dynamic/astro/components/Counter.jsx
vendored
Normal file
9
test/fixtures/astro-dynamic/astro/components/Counter.jsx
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
import React from 'react';
|
||||
|
||||
export default function() {
|
||||
return (
|
||||
<div>
|
||||
<button type="button">Increment -</button>
|
||||
</div>
|
||||
)
|
||||
}
|
9
test/fixtures/astro-dynamic/astro/pages/index.astro
vendored
Normal file
9
test/fixtures/astro-dynamic/astro/pages/index.astro
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
import Counter from '../components/Counter.jsx';
|
||||
---
|
||||
<html>
|
||||
<head><title>Dynamic pages</title></head>
|
||||
<body>
|
||||
<Counter:load />
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in a new issue