Skip to content
Draft
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
4 changes: 2 additions & 2 deletions packages/host/app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
</style>
</head>
<body>
<script type="x/boundary" id="boxel-isolated-start"></script>
<script type="x/boundary" id="fastboot-body-start"></script>
<div id="host-loading">
<div class="loading-container">
<div class="loading-indicator">
Expand All @@ -97,7 +97,7 @@
<div class="loading-text">Loading…</div>
</div>
</div>
<script type="x/boundary" id="boxel-isolated-end"></script>
<script type="x/boundary" id="fastboot-body-end"></script>

<!-- in case embercli's hooks insn't run,
we embed the following div manually -->
Expand Down
102 changes: 102 additions & 0 deletions packages/host/app/initializers/experimental-rehydrate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import ApplicationInstance from '@ember/application/instance';
import type { BootOptions } from '@ember/engine/instance';

import Ember from 'ember';

declare const FastBoot: unknown;

let hasPatchedBootSync = false;

export function initialize(): void {
let log = (message: string) => console.log(`[rehydrate:init] ${message}`);

// let fastbootBodyStart = document?.getElementById('fastboot-body-start');

// if (fastbootBodyStart) {
// log('Found body start, removing');
// fastbootBodyStart.parentNode?.removeChild(fastbootBodyStart);
// }

// let fastbootBodyEnd = document?.getElementById('fastboot-body-end');

// if (fastbootBodyEnd) {
// log('Found body end, removing');
// fastbootBodyEnd.parentNode?.removeChild(fastbootBodyEnd);
// }

return;

log('start');

if (hasPatchedBootSync) {
log('already patched');
return;
}

if (typeof FastBoot !== 'undefined') {
log('FastBoot detected, skipping');
return;
}

if (typeof document === 'undefined') {
log('no document, skipping');
return;
}

let current = document.getElementById('fastboot-body-start');

if (!current) {
log('fastboot-body-start not found');
return;
}

let isSerializationFirstNode = Ember.ViewUtils?.isSerializationFirstNode;

if (typeof isSerializationFirstNode !== 'function') {
console.error(
"Experimental render mode rehydrate isn't working because it couldn't find Ember.ViewUtils.isSerializationFirstNode.",
);
log('isSerializationFirstNode missing');
return;
}

// debugger;

// let nextSibling = current.nextSibling;

// if (!nextSibling || !isSerializationFirstNode(nextSibling)) {
// log('serialization marker not found');
// return;
// }

log('patching ApplicationInstance._bootSync');
hasPatchedBootSync = true;
let originalBootSync = ApplicationInstance.prototype._bootSync;

ApplicationInstance.reopen({
_bootSync(this: ApplicationInstance, options?: BootOptions) {
console.log('bootSync', this, options);
if (options === undefined) {
options = {
_renderMode: 'rehydrate',
};
}

return originalBootSync.call(this, options);
},
});

log('removing fastboot markers');
current.parentNode?.removeChild(current);
let end = document.getElementById('fastboot-body-end');

if (end?.parentNode) {
end.parentNode.removeChild(end);
}

log('done');
}

export default {
initialize,
};
6 changes: 3 additions & 3 deletions packages/host/app/templates/index.gts
Original file line number Diff line number Diff line change
Expand Up @@ -204,8 +204,8 @@ export class IndexComponent extends Component<IndexComponentComponentSignature>
if (typeof document === 'undefined') {
return;
}
let start = document.getElementById('boxel-isolated-start');
let end = document.getElementById('boxel-isolated-end');
let start = document.getElementById('fastboot-body-start');
let end = document.getElementById('fastboot-body-end');
if (!start || !end) {
return;
}
Expand Down Expand Up @@ -233,7 +233,7 @@ export class IndexComponent extends Component<IndexComponentComponentSignature>
@removeCardFromStack={{this.removeCardFromStack}}
@viewCard={{this.viewCard}}
class='host-mode-content'
{{this.removeIsolatedMarkup}}
{{!-- {{this.removeIsolatedMarkup}} --}}
/>
{{/if}}
{{else}}
Expand Down
29 changes: 26 additions & 3 deletions packages/matrix/tests/host-mode.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,29 @@ test.describe('Host mode', () => {
'host-mode-isolated-card.gts',
`
import { CardDef, Component } from 'https://cardstack.com/base/card-api';
import { on } from '@ember/modifier';
import { tracked } from '@glimmer/tracking';

export class HostModeIsolatedCard extends CardDef {
static isolated = class Isolated extends Component<typeof this> {
@tracked showExtra = false;

addExtra = () => {
this.showExtra = true;
};

<template>
<p data-test-host-mode-isolated>Host mode isolated</p>
<button
type="button"
data-test-host-mode-button
{{on 'click' this.addExtra}}
>
Add extra
</button>
{{#if this.showExtra}}
<div data-test-host-mode-extra>Extra content</div>
{{/if}}
</template>
};
}
Expand Down Expand Up @@ -133,9 +151,14 @@ test.describe('Host mode', () => {
expect(html).toContain('data-test-host-mode-isolated');

await page.goto(publishedCardURL);
await expect(
page.locator('[data-test-host-mode-isolated]'),
).toBeVisible();
await expect(page.locator('[data-test-host-mode-isolated]')).toBeVisible();
let button = page.locator('[data-test-host-mode-button]');
await expect(button).toBeVisible();
await waitUntil(async () => {
await button.click();
return await page.locator('[data-test-host-mode-extra]').isVisible();
});
await expect(page.locator('[data-test-host-mode-extra]')).toBeVisible();
});

test.skip('card in a published realm renders in host mode with a connect button', async ({
Expand Down
30 changes: 30 additions & 0 deletions packages/realm-server/prerender/page-pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,8 +349,38 @@ export class PagePool {
let browser = await this.#browserManager.getBrowser();
context = await browser.createBrowserContext();
let page = await context.newPage();

page.on('pageerror', (err) => log.error(`pageerror ${pageId}:`, err));
page.on('error', (err) => log.error(`error ${pageId}:`, err));
page.on('requestfailed', (req) =>
log.warn(
`requestfailed ${pageId}: ${req.url()} ${req.failure()?.errorText}`,
),
);
page.on('response', (res) => {
if (res.status() >= 400) {
log.warn(`response ${pageId}: ${res.status()} ${res.url()}`);
}
});

let pageId = uuidv4();
this.#attachPageConsole(page, 'standby', pageId);
log.debug(`Created standby page ${pageId}`);
log.debug('About to add script for new document');

// await page.evaluateOnNewDocument(
// `globalThis.__boxelRenderMode = 'serialize';`,
// );
await page.evaluateOnNewDocument(`console.log('hello from whatever');`);
await page.evaluateOnNewDocument(`console.error('hey an error');`);
console.log('sending globalThis thing');
await page.evaluateOnNewDocument(`console.log(globalThis);`);
// FIXME can this be globalThis indeed?
await page.evaluateOnNewDocument(
'window.__boxelRenderMode = "serialize";',
);
console.log('done');

await this.#loadStandbyPage(page, pageId);
let entry: StandbyEntry = {
type: 'standby',
Expand Down
2 changes: 1 addition & 1 deletion packages/realm-server/prerender/remote-prerenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ export function createRemotePrerenderer(
if (e?.name === 'AbortError') {
// AbortError from request timeout—consider this a hard timeout, not a retryable deployment blip.
throw new Error(
`Prerender request to ${endpoint.href} aborted after ${requestTimeoutMs}ms`,
`${new Date()} Prerender request to ${endpoint.href} aborted after ${requestTimeoutMs}ms`,
);
}
let retryable =
Expand Down
44 changes: 44 additions & 0 deletions packages/realm-server/prerender/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,36 @@ export async function renderHTML(
opts?: CaptureOptions,
): Promise<string | RenderError> {
await transitionTo(page, 'render.html', format, String(ancestorLevel));

let markerInfo = await page.evaluate(() => {
let el = document.querySelector('[data-prerender]') as HTMLElement | null;
if (!el) return { hasContainer: false };
let comments = Array.from(el.childNodes)
.filter((n) => n.nodeType === 8)
.map((n) => (n as Comment).nodeValue);
return {
hasContainer: true,
hasMarkerInContainer: el.innerHTML.includes('%+b:'),
commentSamples: comments.slice(0, 5),
};
});
log.info('container markers', markerInfo);

let around = await page.evaluate(() => {
let el = document.querySelector('[data-prerender]');
if (!el || !el.parentElement) return { hasParent: false };
let parent = el.parentElement;
let prev = el.previousSibling;
let next = el.nextSibling;
return {
hasParent: true,
parentHasMarkers: parent.innerHTML.includes('%+b:'),
prevComment: prev?.nodeType === 8 ? prev.nodeValue : null,
nextComment: next?.nodeType === 8 ? next.nodeValue : null,
};
});
log.info('marker siblings', around);

let result = await captureResult(
page,
['isolated', 'atom', 'head'].includes(format) ? 'innerHTML' : 'outerHTML',
Expand Down Expand Up @@ -710,6 +740,10 @@ export async function captureResult(
undefined,
} as RenderCapture;
} else {
// Serialize mode emits rehydrate markers as siblings of the container.
let useParentCapture =
capture !== 'textContent' &&
(globalThis as any).__boxelRenderMode === 'serialize';
const firstChild = resolvedElement.children[0] as
| (HTMLElement & {
textContent: string;
Expand All @@ -736,6 +770,16 @@ export async function captureResult(
nonce: resolvedElement.dataset.prerenderNonce ?? undefined,
} as RenderCapture;
}
if (useParentCapture) {
let parent = resolvedElement.parentElement;
return {
status: finalStatus,
value: parent ? parent.innerHTML : resolvedElement.innerHTML,
alive,
id: resolvedElement.dataset.prerenderId ?? undefined,
nonce: resolvedElement.dataset.prerenderNonce ?? undefined,
} as RenderCapture;
}
return {
status: finalStatus,
value: (firstChild as any)[capture]!,
Expand Down
15 changes: 14 additions & 1 deletion packages/realm-server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,7 @@ export class RealmServer {

if (isolatedHTML != null) {
responseHTML = this.injectIsolatedHTML(responseHTML, isolatedHTML);
responseHTML = this.injectRenderModeScript(responseHTML);
}

ctxt.body = responseHTML;
Expand Down Expand Up @@ -576,11 +577,23 @@ export class RealmServer {

private injectIsolatedHTML(indexHTML: string, isolatedHTML: string): string {
return indexHTML.replace(
/(<script[^>]+id="boxel-isolated-start"[^>]*>\s*<\/script>)([\s\S]*?)(<script[^>]+id="boxel-isolated-end"[^>]*>\s*<\/script>)/,
/(<script[^>]+id="fastboot-body-start"[^>]*>\s*<\/script>)([\s\S]*?)(<script[^>]+id="fastboot-body-end"[^>]*>\s*<\/script>)/,
`$1\n${isolatedHTML}\n$3`,
);
}

private injectRenderModeScript(indexHTML: string): string {
let script = `<script>globalThis.__boxelRenderMode = 'rehydrate';</script>`;
let updated = indexHTML.replace(
/(<meta[^>]+data-boxel-head-end[^>]*>)/,
`$1\n${script}`,
);
if (updated === indexHTML) {
return indexHTML.replace(/<\/head>/i, `${script}\n</head>`);
}
return updated;
}

private serveFromRealm = async (ctxt: Koa.Context, _next: Koa.Next) => {
if (ctxt.request.path === '/_boom') {
throw new Error('boom');
Expand Down
21 changes: 21 additions & 0 deletions patches/ember-source@5.4.1.patch
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,24 @@ index 485c2db34f559e1c42ba1b515e8ac6959e2de783..245ebe458fbe69cf494c7ec5b03570b4
}
}
return new SourceSlice({
diff --git a/dist/packages/@ember/-internals/glimmer/index.js b/dist/packages/@ember/-internals/glimmer/index.js
index 110720eee3522722af6e77732ef1ae10292b06af..514254c6a20af851d8e5b6fb6a55e259183fc3cd 100644
--- a/dist/packages/@ember/-internals/glimmer/index.js
+++ b/dist/packages/@ember/-internals/glimmer/index.js
@@ -4955,6 +4955,16 @@ function setupApplicationRegistry(registry) {
let owner = getOwner(props);
assert('DomBuilderService is unexpectedly missing an owner', owner);
let env = owner.lookup('-environment:main');
+
+ console.error('checking for render mode');
+ if (!env._renderMode && typeof globalThis !== 'undefined') {
+ env._renderMode = globalThis.__boxelRenderMode;
+ console.error('render mode changed');
+ }
+
+ console.error('render mode is:');
+ console.error(env._renderMode);
+
switch (env._renderMode) {
case 'serialize':
return serializeBuilder.bind(null);
Loading
Loading