Skip to content
Open
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
100 changes: 100 additions & 0 deletions packages/realm-server/handlers/handle-publish-realm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type Koa from 'koa';
import {
createResponse,
fetchUserPermissions,
isResolvedCodeRef,
query,
SupportedMimeType,
logger,
Expand Down Expand Up @@ -45,6 +46,20 @@ import { withRealmWriteLock } from '../lib/realm-advisory-locks';

const log = logger('handle-publish');

// The CardsGrid CardDef can be referenced two equivalent ways in a
// `meta.adoptsFrom.module` field — the absolute base-realm URL, or
// the registered `@cardstack/base/` prefix form. `@cardstack/base/`
// isn't currently wired as a virtual-network mapping in production
// (only test fixtures register it), so today the canonical form on
// disk is the absolute URL. Listing both forms keeps the detection
// future-proof: once `@cardstack/base/` becomes a live prefix
// mapping, `index.json` files that use it are still caught.
const CARDS_GRID_MODULE_FORMS = new Set<string>([
'https://cardstack.com/base/cards-grid',
'@cardstack/base/cards-grid',
]);
const CARDS_GRID_NAME = 'CardsGrid';

const PUBLISHED_REALM_DOMAIN_OVERRIDES = getPublishedRealmDomainOverrides(
process.env.PUBLISHED_REALM_DOMAIN_OVERRIDES,
);
Expand Down Expand Up @@ -136,6 +151,77 @@ function rewriteHostHomeForPublishedRealm(
: undefined;
}

// If the published realm's index card is the default CardsGrid, write
// `includePrerenderedDefaultRealmIndex: true` into the realm's
// RealmConfig card on disk so the indexer (which the publish handler
// kicks off below) produces a real isolated HTML for the index card
// instead of the boilerplate placeholder. Anonymous visitors of a
// published realm's homepage hit the SSR injection path, which
// expects real prerendered content; without this opt-in they'd see
// the placeholder until JS boots. The check is a structural read of
// the index.json's adoptsFrom — a published realm that has customised
// its index to a different CardDef is left alone (its isolated render
// is presumably the bespoke landing page the publisher wanted).
function ensureRealmIndexBoilerplateOptIn(publishedRealmPath: string): void {
let indexJsonPath = join(publishedRealmPath, 'index.json');
let realmJsonPath = join(publishedRealmPath, 'realm.json');
if (!existsSync(indexJsonPath) || !existsSync(realmJsonPath)) {
return;
}
let indexDoc: unknown;
try {
indexDoc = readJsonSync(indexJsonPath);
Comment thread
habdelra marked this conversation as resolved.
} catch (e) {
log.warn(
`could not parse published index.json at ${indexJsonPath}: ${
e instanceof Error ? e.message : String(e)
}`,
);
return;
}
let adoptsFrom = (indexDoc as { data?: { meta?: { adoptsFrom?: unknown } } })
?.data?.meta?.adoptsFrom as
| Parameters<typeof isResolvedCodeRef>[0]
| undefined;
if (!isResolvedCodeRef(adoptsFrom)) {
return;
}
if (
!CARDS_GRID_MODULE_FORMS.has(adoptsFrom.module) ||
adoptsFrom.name !== CARDS_GRID_NAME
) {
return;
}
let realmConfigDoc: Record<string, unknown>;
try {
realmConfigDoc = readJsonSync(realmJsonPath) as Record<string, unknown>;
} catch (e) {
log.warn(
`could not parse published realm.json at ${realmJsonPath}: ${
e instanceof Error ? e.message : String(e)
}`,
);
return;
}
let data = (realmConfigDoc.data ?? {}) as Record<string, unknown>;
let attributes = (data.attributes ?? {}) as Record<string, unknown>;
if (attributes.includePrerenderedDefaultRealmIndex === true) {
return;
}
attributes.includePrerenderedDefaultRealmIndex = true;
data.attributes = attributes;
realmConfigDoc.data = data;
try {
writeJsonSync(realmJsonPath, realmConfigDoc, { spaces: 2 });
} catch (e) {
log.warn(
`could not write includePrerenderedDefaultRealmIndex into ${realmJsonPath}: ${
e instanceof Error ? e.message : String(e)
}`,
);
}
Comment thread
habdelra marked this conversation as resolved.
}

export default function handlePublishRealm({
dbAdapter,
matrixClient,
Expand Down Expand Up @@ -428,6 +514,20 @@ export default function handlePublishRealm({
writeJsonSync(publishedRealmConfigPath, newlyPublishedRealmConfig);
}

// For published realms whose homepage is the default
// CardsGrid, opt them in to keeping the full prerendered
// isolated HTML on the realm index card. Anonymous visitors
// of the published homepage hit the SSR injection path
// (server.ts → retrieveIsolatedHTML → injectIsolatedHTML);
// without the opt-in the host returns a boilerplate
// placeholder for that card and the SSR shell would inject
// an empty grid. Unpublished realms with the same CardsGrid
// index never need this; only the published snapshot does.
// The flag is written to the published realm's RealmConfig
// card (/realm.json) on disk before the reindex below picks
// it up.
ensureRealmIndexBoilerplateOptIn(publishedRealmPath);

// Clear stale modules cache for the published realm so that
// error entries from a previous publish don't persist
await query(dbAdapter, [
Expand Down
116 changes: 116 additions & 0 deletions packages/realm-server/tests/publish-unpublish-realm-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,122 @@ module(basename(__filename), function () {
);
});

test('publishing a realm with the default CardsGrid index writes includePrerenderedDefaultRealmIndex into the published realm.json', async function (assert) {
let response = await request
.post('/_publish-realm')
.set('Accept', 'application/vnd.api+json')
.set('Content-Type', 'application/json')
.set(
'Authorization',
`Bearer ${createRealmServerJWT(
{ user: ownerUserId, sessionRoom: 'session-room-test' },
realmSecretSeed,
)}`,
)
.send(
JSON.stringify({
sourceRealmURL: sourceRealmUrlString,
publishedRealmURL:
'http://testuser.localhost:4445/cards-grid-default/',
}),
);
assert.strictEqual(response.status, 202, 'HTTP 202 status');

let publishedRealmId = response.body.data.id;
let publishedDir = join(dir.name, 'realm_server_3', '_published');
let publishedRealmConfigPath = join(
publishedDir,
publishedRealmId,
'realm.json',
);
assert.ok(
existsSync(publishedRealmConfigPath),
'published realm.json exists on disk',
);
let publishedRealmConfig = readJsonSync(publishedRealmConfigPath) as {
data?: {
attributes?: { includePrerenderedDefaultRealmIndex?: boolean };
};
};
assert.true(
publishedRealmConfig?.data?.attributes
?.includePrerenderedDefaultRealmIndex,
'published realm.json carries includePrerenderedDefaultRealmIndex: true after publish',
);
});

test('publishing a realm whose index.json is not a CardsGrid leaves the published realm.json untouched', async function (assert) {
let sourceRealmPath = new URL(sourceRealmUrlString).pathname;

// Replace the source realm's default CardsGrid index with a
// bespoke CardDef-adopting index so the publish handler should
// NOT set the opt-in flag.
let customIndexResponse = await request
.post(`${sourceRealmPath}index.json`)
.set('Accept', 'application/vnd.card+source')
.send(
JSON.stringify({
data: {
type: 'card',
attributes: {},
meta: {
adoptsFrom: {
module: 'https://cardstack.com/base/card-api',
name: 'CardDef',
},
},
},
}),
);
assert.strictEqual(
customIndexResponse.status,
204,
'custom non-CardsGrid index.json can be written',
);

let response = await request
.post('/_publish-realm')
.set('Accept', 'application/vnd.api+json')
.set('Content-Type', 'application/json')
.set(
'Authorization',
`Bearer ${createRealmServerJWT(
{ user: ownerUserId, sessionRoom: 'session-room-test' },
realmSecretSeed,
)}`,
)
.send(
JSON.stringify({
sourceRealmURL: sourceRealmUrlString,
publishedRealmURL: 'http://testuser.localhost:4445/custom-index/',
}),
);
assert.strictEqual(response.status, 202, 'HTTP 202 status');

let publishedRealmId = response.body.data.id;
let publishedDir = join(dir.name, 'realm_server_3', '_published');
let publishedRealmConfigPath = join(
publishedDir,
publishedRealmId,
'realm.json',
);
assert.ok(
existsSync(publishedRealmConfigPath),
'published realm.json exists on disk',
);
let publishedRealmConfig = readJsonSync(publishedRealmConfigPath) as {
data?: {
attributes?: { includePrerenderedDefaultRealmIndex?: boolean };
};
};
assert.notStrictEqual(
publishedRealmConfig?.data?.attributes
?.includePrerenderedDefaultRealmIndex,
true,
'published realm.json does NOT carry includePrerenderedDefaultRealmIndex when the source index is a non-CardsGrid card',
);
});

test('POST /_publish-realm serves cached module entries for published realm URLs', async function (assert) {
let requestedPublishedRealmURL = 'http://localhost:4445/test-realm/';
let sourceRealmPath = new URL(sourceRealmUrlString).pathname;
Expand Down
Loading