Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix dynamic React components #111

Merged
merged 1 commit into from
Apr 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,003 changes: 1,251 additions & 752 deletions examples/kitchen-sink/package-lock.json

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion examples/kitchen-sink/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
"start": "nodemon -w ../../lib -x 'astro dev .'",
"build": "astro build"
},
"dependencies": {},
"devDependencies": {
"astro": "file:../../",
"nodemon": "^2.0.7"
Expand Down
4 changes: 2 additions & 2 deletions snowpack-plugin.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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: [],
Expand All @@ -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 });
Expand Down
2 changes: 1 addition & 1 deletion src/@types/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
18 changes: 9 additions & 9 deletions src/build/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand All @@ -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
Expand Down Expand Up @@ -98,7 +98,7 @@ export async function collectDynamicImports(filename: URL, { astroConfig, loggin
fileID: '',
compileOptions: {
astroConfig,
resolve,
resolvePackageUrl,
logging,
mode,
},
Expand Down Expand Up @@ -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) {
Expand Down
12 changes: 6 additions & 6 deletions src/compiler/codegen/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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);

Expand Down
34 changes: 24 additions & 10 deletions src/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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'),
Expand Down Expand Up @@ -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 = {
Expand Down
30 changes: 30 additions & 0 deletions test/astro-dynamic.test.js
Original file line number Diff line number Diff line change
@@ -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 changes: 9 additions & 0 deletions test/fixtures/astro-dynamic/astro/components/Counter.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from 'react';

export default function() {
return (
<div>
<button type="button">Increment -</button>
</div>
)
}
9 changes: 9 additions & 0 deletions test/fixtures/astro-dynamic/astro/pages/index.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
import Counter from '../components/Counter.jsx';
---
<html>
<head><title>Dynamic pages</title></head>
<body>
<Counter:load />
</body>
</html>