mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-30 19:58:50 +00:00
Let the panel reach the card by its hashed name, not by the one address without a version
The sidebar page could serve a previous card for hours. It imported the card through the stable facade, `./houseplan-card.js` — a relative specifier, and relative resolution does not inherit a query. A dashboard reaches the same file as `houseplan-card.js?v=1.74.0`, so an upgrade changes its URL and the browser must refetch. The panel always asked for the same address, and entries are served with no Cache-Control at all — only ETag and Last-Modified — so the browser applies heuristic freshness and may answer from cache without asking. A stale 1164-byte loader names a stale chunk, chunks are immutable for a year, and the panel then ran a previous card against the current backend without a single error. The version banner was telling the truth; reloading could not help, because the address never changed. Rollup already emits the right edge: the panel's side-effect import points straight at the shared implementation. The rewrite in entryFallbackPlugin replaced it with the facade for a fallback that the hashed name gives anyway — and better: a chunk the manifest no longer serves now raises the panel's own "House Plan was updated" notice instead of silently working on old code. Two #486 assertions change meaning and are rewritten, not adjusted: the panel no longer routes through the facade, and its initial graph no longer contains it. The invariant they defended — the panel reuses the exact card graph, never a second copy — is now stated over the implementation, and a new test pins that no built entry reaches the card by an address without a version. Issue: #535 User-Visible: yes
This commit is contained in:
@@ -358,10 +358,18 @@ export function entryFallbackPlugin() {
|
||||
);
|
||||
|
||||
// Rollup folds the small panel shell into its stable entry and points its
|
||||
// side-effect import directly at the shared card implementation. Route it
|
||||
// through the stable card facade instead: the real startup closure then
|
||||
// contains the exact card graph (not a second copy), and a stale card
|
||||
// implementation renders the existing card fallback inside the panel.
|
||||
// side-effect import directly at the shared card implementation. Keep that
|
||||
// edge (#535). Routing it through the stable card facade instead used to
|
||||
// look harmless — the chunk is the same URL either way, so no second copy
|
||||
// arises — but the facade is the ONE address in the distribution with no
|
||||
// version in it: a dashboard reaches the same file through the Lovelace
|
||||
// resource's `?v=`, while a relative specifier cannot inherit that query.
|
||||
// Entries are served without Cache-Control (only ETag/Last-Modified), so
|
||||
// a browser may keep its copy for hours; a stale panel entry then pulled
|
||||
// a stale chunk — both cached — and silently ran a previous card against
|
||||
// the current backend, with nothing but the version banner to show for it.
|
||||
// The hashed name changes with the content, so now either the matching
|
||||
// implementation arrives or the panel's own fallback says so out loud.
|
||||
const panelContract = ENTRY_CONTRACTS[1];
|
||||
const panelEntry = exactEntryChunk(bundle, panelContract);
|
||||
const panelPattern = new RegExp(
|
||||
@@ -376,11 +384,12 @@ export function entryFallbackPlugin() {
|
||||
}
|
||||
panelEntry.code = panelEntry.code.replace(
|
||||
panelPattern,
|
||||
`try{await import("./${CARD_ENTRY_FILE}")}`
|
||||
`try{await import("${cardAsset}")}`
|
||||
+ `catch(e){${fallbackDefinition(panelContract)}`
|
||||
+ 'console.error("[houseplan] stale houseplan-panel.js: the card entry is unavailable",e)}',
|
||||
+ 'console.error("[houseplan] stale houseplan-panel.js: the card implementation '
|
||||
+ 'chunk is unavailable",e)}',
|
||||
);
|
||||
panelEntry.imports = [CARD_ENTRY_FILE];
|
||||
panelEntry.imports = [cardAsset.replace(/^\.\//, '')];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -63,11 +63,16 @@ export function assertBundleManifest(manifest, label = BUNDLE_MANIFEST) {
|
||||
if (!initialView.includes(CARD_ENTRY) || initialView.includes(PANEL_ENTRY)) {
|
||||
throw new Error(`${label}: initial View graph must contain only the card stable entry`);
|
||||
}
|
||||
if (!initialPanel.includes(CARD_ENTRY) || !initialPanel.includes(PANEL_ENTRY)) {
|
||||
throw new Error(`${label}: initial panel graph must contain both stable entries`);
|
||||
// #535: the panel reaches the implementation by its content-hashed name, so
|
||||
// the card's stable facade is no longer part of what the panel loads. That is
|
||||
// the point: the facade is the one address with no version in it, and the
|
||||
// panel must not depend on a URL a browser may keep for hours. What still has
|
||||
// to hold is that the panel reuses the exact card IMPLEMENTATION graph.
|
||||
if (initialPanel.includes(CARD_ENTRY) || !initialPanel.includes(PANEL_ENTRY)) {
|
||||
throw new Error(`${label}: initial panel graph must contain its own stable entry only`);
|
||||
}
|
||||
if (initialView.some((path) => !initialPanel.includes(path))) {
|
||||
throw new Error(`${label}: initial View graph is not a subset of initial panel graph`);
|
||||
if (initialView.some((path) => path !== CARD_ENTRY && !initialPanel.includes(path))) {
|
||||
throw new Error(`${label}: initial View implementation is not a subset of initial panel graph`);
|
||||
}
|
||||
const expectedPanelOnly = initialPanel
|
||||
.filter((path) => !initialView.includes(path))
|
||||
|
||||
@@ -3100,23 +3100,44 @@ const MUTANT_DEFINITIONS = [
|
||||
file: 'scripts/bundle-manifest.mjs',
|
||||
find: ' panelEntry.code = panelEntry.code.replace(\n'
|
||||
+ ' panelPattern,\n'
|
||||
+ ' `try{await import("./${CARD_ENTRY_FILE}")}`\n'
|
||||
+ ' `try{await import("${cardAsset}")}`\n'
|
||||
+ ' + `catch(e){${fallbackDefinition(panelContract)}`\n'
|
||||
+ " + 'console.error(\"[houseplan] stale houseplan-panel.js: the card entry is unavailable\",e)}',\n"
|
||||
+ " + 'console.error(\"[houseplan] stale houseplan-panel.js: the card implementation '\n"
|
||||
+ " + 'chunk is unavailable\",e)}',\n"
|
||||
+ ' );',
|
||||
replace: ' void panelEntry; // mutant: cached panel keeps its static shared-chunk import',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'panel-imports-unversioned-card-entry',
|
||||
guard: 'node --test --test-name-pattern="#486 both stable entries" '
|
||||
+ 'test/bundle-assets.test.mjs',
|
||||
because: 'the stable card facade is the one address in the distribution without a version '
|
||||
+ 'in it, and entries are served without Cache-Control: routing the panel through it let a '
|
||||
+ 'browser keep a previous card for hours and run it against the current backend in silence '
|
||||
+ '(#535 AC1/AC2)',
|
||||
patches: [{
|
||||
file: 'scripts/bundle-manifest.mjs',
|
||||
find: ' `try{await import("${cardAsset}")}`\n'
|
||||
+ ' + `catch(e){${fallbackDefinition(panelContract)}`\n'
|
||||
+ " + 'console.error(\"[houseplan] stale houseplan-panel.js: the card implementation '",
|
||||
replace: ' `try{await import("./${CARD_ENTRY_FILE}")}`\n'
|
||||
+ ' + `catch(e){${fallbackDefinition(panelContract)}`\n'
|
||||
+ " + 'console.error(\"[houseplan] stale houseplan-panel.js: the card implementation '",
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'panel-entry-bypasses-card-graph',
|
||||
guard: 'node --test --test-name-pattern="#486 both stable entries" '
|
||||
+ 'test/bundle-assets.test.mjs',
|
||||
because: 'routing panel startup straight to a hashed implementation bypasses the stable '
|
||||
+ 'card root and breaks the required exact card-graph subset (#486 AC5)',
|
||||
because: 'the manifest edge must name what the panel actually imports. Since #535 that is '
|
||||
+ 'the hashed implementation, not the stable facade; an edge naming the facade would make '
|
||||
+ 'initialPanelFiles describe a graph the panel never loads, and the "panel reuses the '
|
||||
+ 'exact card graph" check would then pass on a fiction (#486 AC5, #535 AC4)',
|
||||
patches: [{
|
||||
file: 'scripts/bundle-manifest.mjs',
|
||||
find: ' panelEntry.imports = [CARD_ENTRY_FILE];',
|
||||
replace: ' panelEntry.imports = [cardAsset.slice(2)]; // mutant: direct hashed edge',
|
||||
find: " panelEntry.imports = [cardAsset.replace(/^\\.\\//, '')];",
|
||||
replace: ' panelEntry.imports = [CARD_ENTRY_FILE]; // mutant: edge names the facade',
|
||||
}],
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user