Inline hydration directive scripts (#3605)
* Inline hydration scripts * Adds a changeset * Update directiveAstroKeys type
This commit is contained in:
parent
816e963509
commit
4916b733c2
19 changed files with 203 additions and 174 deletions
5
.changeset/many-seas-notice.md
Normal file
5
.changeset/many-seas-notice.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'astro': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Inlines hydration scripts
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -16,6 +16,7 @@ package-lock.json
|
||||||
# do not commit .env files or any files that end with `.env`
|
# do not commit .env files or any files that end with `.env`
|
||||||
*.env
|
*.env
|
||||||
|
|
||||||
|
packages/astro/src/**/*.prebuilt.ts
|
||||||
!packages/astro/vendor/vite/dist
|
!packages/astro/vendor/vite/dist
|
||||||
packages/integrations/**/.netlify/
|
packages/integrations/**/.netlify/
|
||||||
|
|
||||||
|
|
|
@ -66,10 +66,10 @@
|
||||||
"vendor"
|
"vendor"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prebuild": "astro-scripts prebuild --to-string \"src/runtime/server/astro-island.ts\"",
|
"prebuild": "astro-scripts prebuild --to-string \"src/runtime/server/astro-island.ts\" \"src/runtime/client/{idle,load,media,only,visible}.ts\"",
|
||||||
"build": "astro-scripts build \"src/**/*.ts\" && tsc",
|
"build": "pnpm run prebuild && astro-scripts build \"src/**/*.ts\" && tsc",
|
||||||
"build:ci": "astro-scripts build \"src/**/*.ts\"",
|
"build:ci": "pnpm run prebuild && astro-scripts build \"src/**/*.ts\"",
|
||||||
"dev": "astro-scripts dev \"src/**/*.ts\"",
|
"dev": "astro-scripts dev --prebuild \"src/runtime/server/astro-island.ts\" --prebuild \"src/runtime/client/{idle,load,media,only,visible}.ts\" \"src/**/*.ts\"",
|
||||||
"postbuild": "astro-scripts copy \"src/**/*.astro\"",
|
"postbuild": "astro-scripts copy \"src/**/*.astro\"",
|
||||||
"benchmark": "node test/benchmark/dev.bench.js && node test/benchmark/build.bench.js",
|
"benchmark": "node test/benchmark/dev.bench.js && node test/benchmark/build.bench.js",
|
||||||
"test": "mocha --exit --timeout 20000 --ignore **/lit-element.test.js --ignore **/errors.test.js && mocha --timeout 20000 **/lit-element.test.js && mocha --timeout 20000 **/errors.test.js",
|
"test": "mocha --exit --timeout 20000 --ignore **/lit-element.test.js --ignore **/errors.test.js && mocha --timeout 20000 **/lit-element.test.js && mocha --timeout 20000 **/errors.test.js",
|
||||||
|
|
|
@ -68,8 +68,6 @@ export async function staticBuild(opts: StaticBuildOptions) {
|
||||||
...metadata.hydratedComponentPaths(),
|
...metadata.hydratedComponentPaths(),
|
||||||
// Client-only components
|
// Client-only components
|
||||||
...clientOnlys,
|
...clientOnlys,
|
||||||
// Any hydration directive like astro/client/idle.js
|
|
||||||
...metadata.hydrationDirectiveSpecifiers(),
|
|
||||||
// The client path for each renderer
|
// The client path for each renderer
|
||||||
...renderers
|
...renderers
|
||||||
.filter((renderer) => !!renderer.clientEntrypoint)
|
.filter((renderer) => !!renderer.clientEntrypoint)
|
||||||
|
|
17
packages/astro/src/runtime/client/hydration-directives.d.ts
vendored
Normal file
17
packages/astro/src/runtime/client/hydration-directives.d.ts
vendored
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro';
|
||||||
|
|
||||||
|
type DirectiveLoader = (get: GetHydrateCallback, opts: HydrateOptions, root: HTMLElement) => void;
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
Astro: {
|
||||||
|
idle: DirectiveLoader;
|
||||||
|
load: DirectiveLoader;
|
||||||
|
media: DirectiveLoader;
|
||||||
|
only: DirectiveLoader;
|
||||||
|
visible: DirectiveLoader;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
|
@ -1,27 +1,12 @@
|
||||||
import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro';
|
(self.Astro = self.Astro || {}).idle = (getHydrateCallback) => {
|
||||||
import { notify } from './events';
|
const cb = async () => {
|
||||||
|
let hydrate = await getHydrateCallback();
|
||||||
|
await hydrate();
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
if ('requestIdleCallback' in window) {
|
||||||
* Hydrate this component as soon as the main thread is free
|
(window as any).requestIdleCallback(cb);
|
||||||
* (or after a short delay, if `requestIdleCallback`) isn't supported
|
} else {
|
||||||
*/
|
setTimeout(cb, 200);
|
||||||
export default async function onIdle(
|
|
||||||
root: HTMLElement,
|
|
||||||
options: HydrateOptions,
|
|
||||||
getHydrateCallback: GetHydrateCallback
|
|
||||||
) {
|
|
||||||
async function idle() {
|
|
||||||
const cb = async () => {
|
|
||||||
let hydrate = await getHydrateCallback();
|
|
||||||
await hydrate();
|
|
||||||
notify();
|
|
||||||
};
|
|
||||||
|
|
||||||
if ('requestIdleCallback' in window) {
|
|
||||||
(window as any).requestIdleCallback(cb);
|
|
||||||
} else {
|
|
||||||
setTimeout(cb, 200);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
idle();
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,8 @@
|
||||||
import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro';
|
(self.Astro = self.Astro || {}).load = (getHydrateCallback) => {
|
||||||
import { notify } from './events';
|
(async () => {
|
||||||
|
|
||||||
/**
|
|
||||||
* Hydrate this component immediately
|
|
||||||
*/
|
|
||||||
export default async function onLoad(
|
|
||||||
root: HTMLElement,
|
|
||||||
options: HydrateOptions,
|
|
||||||
getHydrateCallback: GetHydrateCallback
|
|
||||||
) {
|
|
||||||
async function load() {
|
|
||||||
let hydrate = await getHydrateCallback();
|
let hydrate = await getHydrateCallback();
|
||||||
await hydrate();
|
await hydrate();
|
||||||
notify();
|
})();
|
||||||
}
|
};
|
||||||
load();
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,29 +1,22 @@
|
||||||
import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro';
|
|
||||||
import { notify } from './events';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hydrate this component when a matching media query is found
|
* Hydrate this component when a matching media query is found
|
||||||
*/
|
*/
|
||||||
export default async function onMedia(
|
(self.Astro = self.Astro || {}).media = (getHydrateCallback, options) => {
|
||||||
root: HTMLElement,
|
const cb = async () => {
|
||||||
options: HydrateOptions,
|
let hydrate = await getHydrateCallback();
|
||||||
getHydrateCallback: GetHydrateCallback
|
await hydrate();
|
||||||
) {
|
};
|
||||||
async function media() {
|
|
||||||
const cb = async () => {
|
|
||||||
let hydrate = await getHydrateCallback();
|
|
||||||
await hydrate();
|
|
||||||
notify();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (options.value) {
|
if (options.value) {
|
||||||
const mql = matchMedia(options.value);
|
const mql = matchMedia(options.value);
|
||||||
if (mql.matches) {
|
if (mql.matches) {
|
||||||
cb();
|
cb();
|
||||||
} else {
|
} else {
|
||||||
mql.addEventListener('change', cb, { once: true });
|
mql.addEventListener('change', cb, { once: true });
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
media();
|
};
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,12 @@
|
||||||
import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro';
|
|
||||||
import { notify } from './events';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hydrate this component only on the client
|
* Hydrate this component only on the client
|
||||||
*/
|
*/
|
||||||
export default async function onOnly(
|
(self.Astro = self.Astro || {}).only = (getHydrateCallback) => {
|
||||||
root: HTMLElement,
|
(async () => {
|
||||||
options: HydrateOptions,
|
|
||||||
getHydrateCallback: GetHydrateCallback
|
|
||||||
) {
|
|
||||||
async function only() {
|
|
||||||
let hydrate = await getHydrateCallback();
|
let hydrate = await getHydrateCallback();
|
||||||
await hydrate();
|
await hydrate();
|
||||||
notify();
|
})();
|
||||||
}
|
};
|
||||||
only();
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,30 +1,15 @@
|
||||||
import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro';
|
|
||||||
import { notify } from './events';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hydrate this component when one of it's children becomes visible
|
* Hydrate this component when one of it's children becomes visible
|
||||||
* We target the children because `astro-island` is set to `display: contents`
|
* We target the children because `astro-island` is set to `display: contents`
|
||||||
* which doesn't work with IntersectionObserver
|
* which doesn't work with IntersectionObserver
|
||||||
*/
|
*/
|
||||||
export default async function onVisible(
|
(self.Astro = self.Astro || {}).visible = (getHydrateCallback, _opts, root) => {
|
||||||
root: HTMLElement,
|
const cb = async () => {
|
||||||
options: HydrateOptions,
|
|
||||||
getHydrateCallback: GetHydrateCallback
|
|
||||||
) {
|
|
||||||
let io: IntersectionObserver;
|
|
||||||
|
|
||||||
async function visible() {
|
|
||||||
const cb = async () => {
|
|
||||||
let hydrate = await getHydrateCallback();
|
let hydrate = await getHydrateCallback();
|
||||||
await hydrate();
|
await hydrate();
|
||||||
notify();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (io) {
|
let io = new IntersectionObserver((entries) => {
|
||||||
io.disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
io = new IntersectionObserver((entries) => {
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (!entry.isIntersecting) continue;
|
if (!entry.isIntersecting) continue;
|
||||||
// As soon as we hydrate, disconnect this IntersectionObserver for every `astro-island`
|
// As soon as we hydrate, disconnect this IntersectionObserver for every `astro-island`
|
||||||
|
@ -38,7 +23,4 @@ export default async function onVisible(
|
||||||
const child = root.children[i];
|
const child = root.children[i];
|
||||||
io.observe(child);
|
io.observe(child);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
visible();
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
/**
|
|
||||||
* This file is prebuilt from packages/astro/src/runtime/server/astro-island.ts
|
|
||||||
* Do not edit this directly, but instead edit that file and rerun the prebuild
|
|
||||||
* to generate this file.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default `var a;{const o={0:t=>t,1:t=>JSON.parse(t,n),2:t=>new RegExp(t),3:t=>new Date(t),4:t=>new Map(JSON.parse(t,n)),5:t=>new Set(JSON.parse(t,n)),6:t=>BigInt(t),7:t=>new URL(t)},n=(t,e)=>{if(t===""||!Array.isArray(e))return e;const[r,s]=e;return r in o?o[r](s):void 0};customElements.get("astro-island")||customElements.define("astro-island",(a=class extends HTMLElement{constructor(){super(...arguments);this.hydrate=()=>{if(!this.hydrator||this.parentElement?.closest("astro-island[ssr]"))return;let e=null,r=this.querySelector("astro-fragment");if(r==null&&this.hasAttribute("tmpl")){let i=this.querySelector("template[data-astro-template]");i&&(e=i.innerHTML,i.remove())}else r&&(e=r.innerHTML);const s=this.hasAttribute("props")?JSON.parse(this.getAttribute("props"),n):{};this.hydrator(this)(this.Component,s,e,{client:this.getAttribute("client")}),this.removeAttribute("ssr"),window.removeEventListener("astro:hydrate",this.hydrate),window.dispatchEvent(new CustomEvent("astro:hydrate"))}}async connectedCallback(){const[{default:e}]=await Promise.all([import(this.getAttribute("directive-url")),import(this.getAttribute("before-hydration-url"))]);window.addEventListener("astro:hydrate",this.hydrate);const r=JSON.parse(this.getAttribute("opts"));e(this,r,async()=>{const s=this.getAttribute("renderer-url"),[i,{default:l}]=await Promise.all([import(this.getAttribute("component-url")),s?import(s):()=>()=>{}]);return this.Component=i[this.getAttribute("component-export")||"default"],this.hydrator=l,this.hydrate})}attributeChangedCallback(){this.hydrator&&this.hydrate()}},a.observedAttributes=["props"],a))}`;
|
|
|
@ -2,6 +2,16 @@
|
||||||
// Do not import this file directly, instead import the prebuilt one instead.
|
// Do not import this file directly, instead import the prebuilt one instead.
|
||||||
// pnpm --filter astro run prebuild
|
// pnpm --filter astro run prebuild
|
||||||
|
|
||||||
|
type directiveAstroKeys = 'load' | 'idle' | 'visible' | 'media' | 'only';
|
||||||
|
|
||||||
|
declare const Astro: {
|
||||||
|
[k in directiveAstroKeys]: (
|
||||||
|
fn: () => Promise<() => void>,
|
||||||
|
opts: Record<string, any>,
|
||||||
|
root: HTMLElement
|
||||||
|
) => void;
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
interface PropTypeSelector {
|
interface PropTypeSelector {
|
||||||
[k: string]: (value: any) => any;
|
[k: string]: (value: any) => any;
|
||||||
|
@ -32,14 +42,10 @@
|
||||||
public hydrator: any;
|
public hydrator: any;
|
||||||
static observedAttributes = ['props'];
|
static observedAttributes = ['props'];
|
||||||
async connectedCallback() {
|
async connectedCallback() {
|
||||||
const [{ default: setup }] = await Promise.all([
|
|
||||||
import(this.getAttribute('directive-url')!),
|
|
||||||
import(this.getAttribute('before-hydration-url')!),
|
|
||||||
]);
|
|
||||||
window.addEventListener('astro:hydrate', this.hydrate);
|
window.addEventListener('astro:hydrate', this.hydrate);
|
||||||
|
await import(this.getAttribute('before-hydration-url')!);
|
||||||
const opts = JSON.parse(this.getAttribute('opts')!);
|
const opts = JSON.parse(this.getAttribute('opts')!) as Record<string, any>;
|
||||||
setup(this, opts, async () => {
|
Astro[this.getAttribute('client') as directiveAstroKeys](async () => {
|
||||||
const rendererUrl = this.getAttribute('renderer-url');
|
const rendererUrl = this.getAttribute('renderer-url');
|
||||||
const [componentModule, { default: hydrator }] = await Promise.all([
|
const [componentModule, { default: hydrator }] = await Promise.all([
|
||||||
import(this.getAttribute('component-url')!),
|
import(this.getAttribute('component-url')!),
|
||||||
|
@ -48,7 +54,7 @@
|
||||||
this.Component = componentModule[this.getAttribute('component-export') || 'default'];
|
this.Component = componentModule[this.getAttribute('component-export') || 'default'];
|
||||||
this.hydrator = hydrator;
|
this.hydrator = hydrator;
|
||||||
return this.hydrate;
|
return this.hydrate;
|
||||||
});
|
}, opts, this);
|
||||||
}
|
}
|
||||||
hydrate = () => {
|
hydrate = () => {
|
||||||
if (!this.hydrator || this.parentElement?.closest('astro-island[ssr]')) {
|
if (!this.hydrator || this.parentElement?.closest('astro-island[ssr]')) {
|
||||||
|
|
|
@ -6,7 +6,7 @@ import type {
|
||||||
} from '../../@types/astro';
|
} from '../../@types/astro';
|
||||||
import { escapeHTML } from './escape.js';
|
import { escapeHTML } from './escape.js';
|
||||||
import { serializeProps } from './serialize.js';
|
import { serializeProps } from './serialize.js';
|
||||||
import { hydrationSpecifier, serializeListValue } from './util.js';
|
import { serializeListValue } from './util.js';
|
||||||
|
|
||||||
const HydrationDirectives = ['load', 'idle', 'media', 'visible', 'only'];
|
const HydrationDirectives = ['load', 'idle', 'media', 'visible', 'only'];
|
||||||
|
|
||||||
|
@ -129,7 +129,6 @@ export async function generateHydrateScript(
|
||||||
|
|
||||||
island.props['ssr'] = '';
|
island.props['ssr'] = '';
|
||||||
island.props['client'] = hydrate;
|
island.props['client'] = hydrate;
|
||||||
island.props['directive-url'] = await result.resolve(hydrationSpecifier(hydrate));
|
|
||||||
island.props['before-hydration-url'] = await result.resolve('astro:scripts/before-hydration.js');
|
island.props['before-hydration-url'] = await result.resolve('astro:scripts/before-hydration.js');
|
||||||
island.props['opts'] = escapeHTML(
|
island.props['opts'] = escapeHTML(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
|
|
|
@ -8,11 +8,17 @@ import type {
|
||||||
SSRLoadedRenderer,
|
SSRLoadedRenderer,
|
||||||
SSRResult,
|
SSRResult,
|
||||||
} from '../../@types/astro';
|
} from '../../@types/astro';
|
||||||
import islandScript from './astro-island.prebuilt.js';
|
|
||||||
import { escapeHTML, HTMLString, markHTMLString } from './escape.js';
|
import { escapeHTML, HTMLString, markHTMLString } from './escape.js';
|
||||||
import { extractDirectives, generateHydrateScript } from './hydration.js';
|
import { extractDirectives, generateHydrateScript } from './hydration.js';
|
||||||
import { serializeProps } from './serialize.js';
|
import { serializeProps } from './serialize.js';
|
||||||
import { shorthash } from './shorthash.js';
|
import { shorthash } from './shorthash.js';
|
||||||
|
import {
|
||||||
|
determineIfNeedsHydrationScript,
|
||||||
|
determinesIfNeedsDirectiveScript,
|
||||||
|
PrescriptType,
|
||||||
|
getPrescripts
|
||||||
|
} from './scripts.js';
|
||||||
import { serializeListValue } from './util.js';
|
import { serializeListValue } from './util.js';
|
||||||
|
|
||||||
export { markHTMLString, markHTMLString as unescapeHTML } from './escape.js';
|
export { markHTMLString, markHTMLString as unescapeHTML } from './escape.js';
|
||||||
|
@ -27,18 +33,6 @@ const htmlEnumAttributes = /^(contenteditable|draggable|spellcheck|value)$/i;
|
||||||
// Note: SVG is case-sensitive!
|
// Note: SVG is case-sensitive!
|
||||||
const svgEnumAttributes = /^(autoReverse|externalResourcesRequired|focusable|preserveAlpha)$/i;
|
const svgEnumAttributes = /^(autoReverse|externalResourcesRequired|focusable|preserveAlpha)$/i;
|
||||||
|
|
||||||
// This is used to keep track of which requests (pages) have had the hydration script
|
|
||||||
// appended. We only add the hydration script once per page, and since the SSRResult
|
|
||||||
// object corresponds to one page request, we are using it as a key to know.
|
|
||||||
const resultsWithHydrationScript = new WeakSet<SSRResult>();
|
|
||||||
|
|
||||||
function determineIfNeedsHydrationScript(result: SSRResult): boolean {
|
|
||||||
if (resultsWithHydrationScript.has(result)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
resultsWithHydrationScript.add(result);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// INVESTIGATE:
|
// INVESTIGATE:
|
||||||
// 2. Less anys when possible and make it well known when they are needed.
|
// 2. Less anys when possible and make it well known when they are needed.
|
||||||
|
@ -158,6 +152,7 @@ function formatList(values: string[]): string {
|
||||||
return `${values.slice(0, -1).join(', ')} or ${values[values.length - 1]}`;
|
return `${values.slice(0, -1).join(', ')} or ${values[values.length - 1]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function renderComponent(
|
export async function renderComponent(
|
||||||
result: SSRResult,
|
result: SSRResult,
|
||||||
displayName: string,
|
displayName: string,
|
||||||
|
@ -191,6 +186,7 @@ export async function renderComponent(
|
||||||
const { hydration, props } = extractDirectives(_props);
|
const { hydration, props } = extractDirectives(_props);
|
||||||
let html = '';
|
let html = '';
|
||||||
let needsHydrationScript = hydration && determineIfNeedsHydrationScript(result);
|
let needsHydrationScript = hydration && determineIfNeedsHydrationScript(result);
|
||||||
|
let needsDirectiveScript = hydration && determinesIfNeedsDirectiveScript(result, hydration.directive);
|
||||||
|
|
||||||
if (hydration) {
|
if (hydration) {
|
||||||
metadata.hydrate = hydration.directive as AstroComponentMetadata['hydrate'];
|
metadata.hydrate = hydration.directive as AstroComponentMetadata['hydrate'];
|
||||||
|
@ -348,19 +344,11 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr
|
||||||
|
|
||||||
island.children = `${html ?? ''}${template}`;
|
island.children = `${html ?? ''}${template}`;
|
||||||
|
|
||||||
// Add the astro-island definition only once. Since the SSRResult object
|
let prescriptType: PrescriptType = needsHydrationScript ? 'both' : needsDirectiveScript ?
|
||||||
// is scoped to a page renderer we can use it as a key to know if the script
|
'directive' : null;
|
||||||
// has been rendered or not.
|
let prescripts = getPrescripts(prescriptType, hydration.directive);
|
||||||
let script = '';
|
|
||||||
if (needsHydrationScript) {
|
return markHTMLString(prescripts + renderElement('astro-island', island, false));
|
||||||
// Note that this is a class script, not a module script.
|
|
||||||
// This is so that it executes immediate, and when the browser encounters
|
|
||||||
// an astro-island element the callbacks will fire immediately, causing the JS
|
|
||||||
// deps to be loaded immediately.
|
|
||||||
script = `<script>${islandScript}</script>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return markHTMLString(script + renderElement('astro-island', island, false));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Create the Astro.fetchContent() runtime function. */
|
/** Create the Astro.fetchContent() runtime function. */
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import { hydrationSpecifier } from './util.js';
|
|
||||||
|
|
||||||
interface ModuleInfo {
|
interface ModuleInfo {
|
||||||
module: Record<string, any>;
|
module: Record<string, any>;
|
||||||
specifier: string;
|
specifier: string;
|
||||||
|
@ -82,21 +80,6 @@ export class Metadata {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets all of the hydration specifiers used within this component.
|
|
||||||
*/
|
|
||||||
*hydrationDirectiveSpecifiers() {
|
|
||||||
const found = new Set<string>();
|
|
||||||
for (const metadata of this.deepMetadata()) {
|
|
||||||
for (const directive of metadata.hydrationDirectives) {
|
|
||||||
if (!found.has(directive)) {
|
|
||||||
found.add(directive);
|
|
||||||
yield hydrationSpecifier(directive);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
*hoistedScriptPaths() {
|
*hoistedScriptPaths() {
|
||||||
for (const metadata of this.deepMetadata()) {
|
for (const metadata of this.deepMetadata()) {
|
||||||
let i = 0,
|
let i = 0,
|
||||||
|
|
80
packages/astro/src/runtime/server/scripts.ts
Normal file
80
packages/astro/src/runtime/server/scripts.ts
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
import type {
|
||||||
|
APIContext,
|
||||||
|
AstroComponentMetadata,
|
||||||
|
AstroGlobalPartial,
|
||||||
|
EndpointHandler,
|
||||||
|
Params,
|
||||||
|
SSRElement,
|
||||||
|
SSRLoadedRenderer,
|
||||||
|
SSRResult,
|
||||||
|
} from '../../@types/astro';
|
||||||
|
|
||||||
|
import islandScript from './astro-island.prebuilt.js';
|
||||||
|
import idlePrebuilt from '../client/idle.prebuilt.js';
|
||||||
|
import loadPrebuilt from '../client/load.prebuilt.js';
|
||||||
|
import onlyPrebuilt from '../client/only.prebuilt.js';
|
||||||
|
import visiblePrebuilt from '../client/visible.prebuilt.js';
|
||||||
|
import mediaPrebuilt from '../client/media.prebuilt.js';
|
||||||
|
|
||||||
|
|
||||||
|
// This is used to keep track of which requests (pages) have had the hydration script
|
||||||
|
// appended. We only add the hydration script once per page, and since the SSRResult
|
||||||
|
// object corresponds to one page request, we are using it as a key to know.
|
||||||
|
const resultsWithHydrationScript = new WeakSet<SSRResult>();
|
||||||
|
|
||||||
|
export function determineIfNeedsHydrationScript(result: SSRResult): boolean {
|
||||||
|
if (resultsWithHydrationScript.has(result)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
resultsWithHydrationScript.add(result);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hydrationScripts: Record<string, string> = {
|
||||||
|
idle: idlePrebuilt,
|
||||||
|
load: loadPrebuilt,
|
||||||
|
only: onlyPrebuilt,
|
||||||
|
media: mediaPrebuilt,
|
||||||
|
visible: visiblePrebuilt
|
||||||
|
};
|
||||||
|
|
||||||
|
const resultsWithDirectiveScript = new Map<string, WeakSet<SSRResult>>();
|
||||||
|
|
||||||
|
export function determinesIfNeedsDirectiveScript(result: SSRResult, directive: string): boolean {
|
||||||
|
if(!resultsWithDirectiveScript.has(directive)) {
|
||||||
|
resultsWithDirectiveScript.set(directive, new WeakSet());
|
||||||
|
}
|
||||||
|
const set = resultsWithDirectiveScript.get(directive)!;
|
||||||
|
if(set.has(result)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
set.add(result);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export type PrescriptType = null | 'both' | 'directive';
|
||||||
|
|
||||||
|
function getDirectiveScriptText(directive: string): string {
|
||||||
|
if(!(directive in hydrationScripts)) {
|
||||||
|
throw new Error(`Unknown directive: ${directive}`);
|
||||||
|
}
|
||||||
|
const directiveScriptText = hydrationScripts[directive];
|
||||||
|
return directiveScriptText;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPrescripts(type: PrescriptType, directive: string): string {
|
||||||
|
// Note that this is a classic script, not a module script.
|
||||||
|
// This is so that it executes immediate, and when the browser encounters
|
||||||
|
// an astro-island element the callbacks will fire immediately, causing the JS
|
||||||
|
// deps to be loaded immediately.
|
||||||
|
switch(type) {
|
||||||
|
case 'both':
|
||||||
|
|
||||||
|
return `<script>${getDirectiveScriptText(directive) + islandScript}</script>`;
|
||||||
|
case 'directive':
|
||||||
|
return `<script>${getDirectiveScriptText(directive)}</script>`;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
|
@ -34,12 +34,3 @@ export function serializeListValue(value: any) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the import specifier for a given hydration directive.
|
|
||||||
* @param hydrate The hydration directive such as `idle` or `visible`
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export function hydrationSpecifier(hydrate: string) {
|
|
||||||
return `astro/client/${hydrate}.js`;
|
|
||||||
}
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import del from 'del';
|
||||||
import { promises as fs } from 'fs';
|
import { promises as fs } from 'fs';
|
||||||
import { dim, green, red, yellow } from 'kleur/colors';
|
import { dim, green, red, yellow } from 'kleur/colors';
|
||||||
import glob from 'tiny-glob';
|
import glob from 'tiny-glob';
|
||||||
|
import prebuild from './prebuild.js';
|
||||||
|
|
||||||
/** @type {import('esbuild').BuildOptions} */
|
/** @type {import('esbuild').BuildOptions} */
|
||||||
const defaultConfig = {
|
const defaultConfig = {
|
||||||
|
@ -20,9 +21,23 @@ const dt = new Intl.DateTimeFormat('en-us', {
|
||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function getPrebuilds(isDev, args) {
|
||||||
|
let prebuilds = [];
|
||||||
|
while(args.includes('--prebuild')) {
|
||||||
|
let idx = args.indexOf('--prebuild');
|
||||||
|
prebuilds.push(args[idx + 1]);
|
||||||
|
args.splice(idx, 2);
|
||||||
|
}
|
||||||
|
if(prebuilds.length && isDev) {
|
||||||
|
prebuilds.unshift('--no-minify');
|
||||||
|
}
|
||||||
|
return prebuilds;
|
||||||
|
}
|
||||||
|
|
||||||
export default async function build(...args) {
|
export default async function build(...args) {
|
||||||
const config = Object.assign({}, defaultConfig);
|
const config = Object.assign({}, defaultConfig);
|
||||||
const isDev = args.slice(-1)[0] === 'IS_DEV';
|
const isDev = args.slice(-1)[0] === 'IS_DEV';
|
||||||
|
const prebuilds = getPrebuilds(isDev, args);
|
||||||
const patterns = args
|
const patterns = args
|
||||||
.filter((f) => !!f) // remove empty args
|
.filter((f) => !!f) // remove empty args
|
||||||
.map((f) => f.replace(/^'/, '').replace(/'$/, '')); // Needed for Windows: glob strings contain surrounding string chars??? remove these
|
.map((f) => f.replace(/^'/, '').replace(/'$/, '')); // Needed for Windows: glob strings contain surrounding string chars??? remove these
|
||||||
|
@ -59,6 +74,9 @@ export default async function build(...args) {
|
||||||
...config,
|
...config,
|
||||||
watch: {
|
watch: {
|
||||||
onRebuild(error, result) {
|
onRebuild(error, result) {
|
||||||
|
if(prebuilds.length) {
|
||||||
|
prebuild(...prebuilds);
|
||||||
|
}
|
||||||
const date = dt.format(new Date());
|
const date = dt.format(new Date());
|
||||||
if (error || (result && result.errors.length)) {
|
if (error || (result && result.errors.length)) {
|
||||||
console.error(dim(`[${date}] `) + red(error || result.errors.join('\n')));
|
console.error(dim(`[${date}] `) + red(error || result.errors.join('\n')));
|
||||||
|
|
|
@ -11,6 +11,12 @@ export default async function prebuild(...args) {
|
||||||
args.splice(buildToString, 1);
|
args.splice(buildToString, 1);
|
||||||
buildToString = true;
|
buildToString = true;
|
||||||
}
|
}
|
||||||
|
let minify = true;
|
||||||
|
let minifyIdx = args.indexOf('--no-minify');
|
||||||
|
if(minifyIdx !== -1) {
|
||||||
|
minify = false;
|
||||||
|
args.splice(minifyIdx, 1);
|
||||||
|
}
|
||||||
|
|
||||||
let patterns = args;
|
let patterns = args;
|
||||||
let entryPoints = [].concat(
|
let entryPoints = [].concat(
|
||||||
|
@ -33,7 +39,7 @@ export default async function prebuild(...args) {
|
||||||
const tscode = await fs.promises.readFile(filepath, 'utf-8');
|
const tscode = await fs.promises.readFile(filepath, 'utf-8');
|
||||||
const esbuildresult = await esbuild.transform(tscode, {
|
const esbuildresult = await esbuild.transform(tscode, {
|
||||||
loader: 'ts',
|
loader: 'ts',
|
||||||
minify: true,
|
minify,
|
||||||
});
|
});
|
||||||
const rootURL = new URL('../../', import.meta.url);
|
const rootURL = new URL('../../', import.meta.url);
|
||||||
const rel = path.relative(fileURLToPath(rootURL), filepath);
|
const rel = path.relative(fileURLToPath(rootURL), filepath);
|
||||||
|
|
Loading…
Reference in a new issue