Enable HMR (#260)

* feat: enable HMR in `createSnowpack`

* feat: enable Snowpack's HMR

* chore: add changeset

* chore: remove unused file

* chore: add changeset
This commit is contained in:
Nate Moore 2021-05-27 09:55:23 -05:00 committed by GitHub
parent a5782a37c8
commit 8ff79981db
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 113 additions and 68 deletions

View file

@ -0,0 +1,5 @@
---
'astro': minor
---
Enable Snowpack's [built-in HMR support](https://www.snowpack.dev/concepts/hot-module-replacement) to enable seamless live updates while editing.

View file

@ -0,0 +1,5 @@
---
'astro': minor
---
Enabled Snowpack's built-in HMR engine for Astro pages

View file

@ -3,7 +3,7 @@ const { readFile } = require('fs').promises;
// Snowpack plugins must be CommonJS :( // Snowpack plugins must be CommonJS :(
const transformPromise = import('./dist/compiler/index.js'); const transformPromise = import('./dist/compiler/index.js');
module.exports = function (snowpackConfig, { resolvePackageUrl, renderers, astroConfig } = {}) { module.exports = (snowpackConfig, { resolvePackageUrl, hmrPort, renderers, astroConfig } = {}) => {
return { return {
name: 'snowpack-astro', name: 'snowpack-astro',
resolve: { resolve: {
@ -48,6 +48,7 @@ ${contents}`;
const contents = await readFile(filePath, 'utf-8'); const contents = await readFile(filePath, 'utf-8');
const compileOptions = { const compileOptions = {
astroConfig, astroConfig,
hmrPort,
resolvePackageUrl, resolvePackageUrl,
renderers, renderers,
}; };

View file

@ -5,6 +5,7 @@ export interface CompileOptions {
logging: LogOptions; logging: LogOptions;
resolvePackageUrl: (p: string) => Promise<string>; resolvePackageUrl: (p: string) => Promise<string>;
astroConfig: AstroConfig; astroConfig: AstroConfig;
hmrPort?: number;
mode: RuntimeMode; mode: RuntimeMode;
tailwindConfig?: string; tailwindConfig?: string;
} }

View file

@ -0,0 +1,86 @@
import type { Transformer, TransformOptions } from '../../@types/transformer';
import type { TemplateNode } from 'astro-parser';
/** If there are hydrated components, inject styles for [data-astro-root] and [data-astro-children] */
export default function (opts: TransformOptions): Transformer {
let head: TemplateNode;
let hasComponents = false;
let isHmrEnabled = typeof opts.compileOptions.hmrPort !== 'undefined';
return {
visitors: {
html: {
InlineComponent: {
enter(node, parent) {
const [name, kind] = node.name.split(':');
if (kind && !hasComponents) {
hasComponents = true;
}
},
},
Element: {
enter(node) {
if (!hasComponents) return;
switch (node.name) {
case 'head': {
head = node;
return;
}
default:
return;
}
},
},
},
},
async finalize() {
if (!head) return;
const children = [];
if (hasComponents) {
children.push({
type: 'Element',
name: 'style',
attributes: [{ name: 'type', type: 'Attribute', value: [{ type: 'Text', raw: 'text/css', data: 'text/css' }] }],
start: 0,
end: 0,
children: [
{
start: 0,
end: 0,
type: 'Text',
data: 'astro-root, astro-fragment { display: contents; }',
raw: 'astro-root, astro-fragment { display: contents; }',
},
],
});
}
if (isHmrEnabled) {
const { hmrPort } = opts.compileOptions;
children.push({
type: 'Element',
name: 'script',
attributes: [],
children: [
{ type: 'Text', data: `window.HMR_WEBSOCKET_URL = 'ws://localhost:${hmrPort}'`, start: 0, end: 0 }
],
start: 0,
end: 0
}, {
type: 'Element',
name: 'script',
attributes: [
{ type: 'Attribute', name: 'type', value: [{ type: 'Text', data: 'module', start: 0, end: 0 }], start: 0, end: 0 },
{ type: 'Attribute', name: 'src', value: [{ type: 'Text', data: '/_snowpack/hmr-client.js', start: 0, end: 0 }], start: 0, end: 0 },
],
children: [],
start: 0,
end: 0
})
}
head.children = head.children ?? [];
head.children.push(...children);
},
};
}

View file

@ -1,62 +0,0 @@
import { Transformer } from '../../@types/transformer';
import type { TemplateNode } from 'astro-parser';
/** If there are hydrated components, inject styles for [data-astro-root] and [data-astro-children] */
export default function (): Transformer {
let head: TemplateNode;
let body: TemplateNode;
let hasComponents = false;
return {
visitors: {
html: {
InlineComponent: {
enter(node, parent) {
const [name, kind] = node.name.split(':');
if (kind && !hasComponents) {
hasComponents = true;
}
},
},
Element: {
enter(node) {
if (!hasComponents) return;
switch (node.name) {
case 'head': {
head = node;
return;
}
case 'body': {
body = node;
return;
}
default:
return;
}
},
},
},
},
async finalize() {
if (!(head && hasComponents)) return;
const style: TemplateNode = {
type: 'Element',
name: 'style',
attributes: [{ name: 'type', type: 'Attribute', value: [{ type: 'Text', raw: 'text/css', data: 'text/css' }] }],
start: 0,
end: 0,
children: [
{
start: 0,
end: 0,
type: 'Text',
data: 'astro-root, astro-fragment { display: contents; }',
raw: 'astro-root, astro-fragment { display: contents; }',
},
],
};
head.children = [...(head.children ?? []), style];
},
};
}

View file

@ -8,7 +8,7 @@ import transformStyles from './styles.js';
import transformDoctype from './doctype.js'; import transformDoctype from './doctype.js';
import transformModuleScripts from './module-scripts.js'; import transformModuleScripts from './module-scripts.js';
import transformCodeBlocks from './prism.js'; import transformCodeBlocks from './prism.js';
import transformHydration from './hydration.js'; import transformHead from './head.js';
interface VisitorCollection { interface VisitorCollection {
enter: Map<string, VisitorFn[]>; enter: Map<string, VisitorFn[]>;
@ -85,7 +85,7 @@ export async function transform(ast: Ast, opts: TransformOptions) {
const cssVisitors = createVisitorCollection(); const cssVisitors = createVisitorCollection();
const finalizers: Array<() => Promise<void>> = []; const finalizers: Array<() => Promise<void>> = [];
const optimizers = [transformHydration(), transformStyles(opts), transformDoctype(opts), transformModuleScripts(opts), transformCodeBlocks(ast.module)]; const optimizers = [transformHead(opts), transformStyles(opts), transformDoctype(opts), transformModuleScripts(opts), transformCodeBlocks(ast.module)];
for (const optimizer of optimizers) { for (const optimizer of optimizers) {
collectVisitors(optimizer, htmlVisitors, cssVisitors, finalizers); collectVisitors(optimizer, htmlVisitors, cssVisitors, finalizers);

View file

@ -264,26 +264,31 @@ interface CreateSnowpackOptions {
env: Record<string, any>; env: Record<string, any>;
mode: RuntimeMode; mode: RuntimeMode;
resolvePackageUrl?: (pkgName: string) => Promise<string>; resolvePackageUrl?: (pkgName: string) => Promise<string>;
target: 'frontend'|'backend';
} }
const defaultRenderers = ['@astro-renderer/vue', '@astro-renderer/svelte', '@astro-renderer/react', '@astro-renderer/preact']; const DEFAULT_HMR_PORT = 12321;
const DEFAULT_RENDERERS = ['@astro-renderer/vue', '@astro-renderer/svelte', '@astro-renderer/react', '@astro-renderer/preact'];
/** Create a new Snowpack instance to power Astro */ /** Create a new Snowpack instance to power Astro */
async function createSnowpack(astroConfig: AstroConfig, options: CreateSnowpackOptions) { async function createSnowpack(astroConfig: AstroConfig, options: CreateSnowpackOptions) {
const { projectRoot, astroRoot, renderers = defaultRenderers } = astroConfig; const { projectRoot, astroRoot, renderers = DEFAULT_RENDERERS } = astroConfig;
const { env, mode, resolvePackageUrl } = options; const { env, mode, resolvePackageUrl, target } = options;
const internalPath = new URL('./frontend/', import.meta.url); const internalPath = new URL('./frontend/', import.meta.url);
const resolveDependency = (dep: string) => resolve.sync(dep, { basedir: fileURLToPath(projectRoot) }); const resolveDependency = (dep: string) => resolve.sync(dep, { basedir: fileURLToPath(projectRoot) });
const isHmrEnabled = mode === 'development' && target === 'backend';
let snowpack: SnowpackDevServer; let snowpack: SnowpackDevServer;
let astroPluginOptions: { let astroPluginOptions: {
resolvePackageUrl?: (s: string) => Promise<string>; resolvePackageUrl?: (s: string) => Promise<string>;
renderers?: { name: string; client: string; server: string }[]; renderers?: { name: string; client: string; server: string }[];
astroConfig: AstroConfig; astroConfig: AstroConfig;
hmrPort?: number
} = { } = {
astroConfig, astroConfig,
resolvePackageUrl, resolvePackageUrl,
hmrPort: isHmrEnabled ? DEFAULT_HMR_PORT : undefined
}; };
const mountOptions = { const mountOptions = {
@ -360,6 +365,8 @@ async function createSnowpack(astroConfig: AstroConfig, options: CreateSnowpackO
open: 'none', open: 'none',
output: 'stream', output: 'stream',
port: 0, port: 0,
hmr: isHmrEnabled,
hmrPort: isHmrEnabled ? DEFAULT_HMR_PORT : undefined,
tailwindConfig: astroConfig.devOptions.tailwindConfig, tailwindConfig: astroConfig.devOptions.tailwindConfig,
}, },
buildOptions: { buildOptions: {
@ -394,6 +401,7 @@ export async function createRuntime(astroConfig: AstroConfig, { mode, logging }:
snowpackRuntime: backendSnowpackRuntime, snowpackRuntime: backendSnowpackRuntime,
snowpackConfig: backendSnowpackConfig, snowpackConfig: backendSnowpackConfig,
} = await createSnowpack(astroConfig, { } = await createSnowpack(astroConfig, {
target: 'backend',
env: { env: {
astro: true, astro: true,
}, },
@ -408,6 +416,7 @@ export async function createRuntime(astroConfig: AstroConfig, { mode, logging }:
snowpackRuntime: frontendSnowpackRuntime, snowpackRuntime: frontendSnowpackRuntime,
snowpackConfig: frontendSnowpackConfig, snowpackConfig: frontendSnowpackConfig,
} = await createSnowpack(astroConfig, { } = await createSnowpack(astroConfig, {
target: 'frontend',
env: { env: {
astro: false, astro: false,
}, },