@homepages/template-kit 0.8.0-dev-20260717154644 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -1
- package/dist/cli/new/scaffold-assets.js +1 -1
- package/dist/eslint/rules/require-island-editor.js +74 -0
- package/dist/eslint.js +3 -0
- package/dist/index.d.ts +2 -2
- package/dist/island-runtime.d.ts +2 -2
- package/dist/islands/contract.d.ts +20 -4
- package/dist/package.js +1 -1
- package/guide/islands.md +26 -7
- package/guide/llms.txt +3 -0
- package/guide/rules/INDEX.md +2 -1
- package/guide/rules/require-island-editor.md +101 -0
- package/package.json +4 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @homepages/template-kit
|
|
2
2
|
|
|
3
|
-
## 0.8.0
|
|
3
|
+
## 0.8.0
|
|
4
4
|
|
|
5
5
|
### Minor Changes
|
|
6
6
|
|
|
@@ -11,6 +11,20 @@
|
|
|
11
11
|
asserting exact HTML from `@homepages/template-kit/ssr` — or from `template-kit
|
|
12
12
|
check` — needs its expectation updated.
|
|
13
13
|
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- 735cfae: Islands now declare their editor-canvas behavior explicitly. A "use client" component with
|
|
17
|
+
a default export must `export const editor` with a boolean `live`: `{ live: true }` hydrates
|
|
18
|
+
it in the editor canvas, `{ live: false }` leaves it as static server-rendered HTML there.
|
|
19
|
+
The new `require-island-editor` rule reports an island that declares neither. Previously an
|
|
20
|
+
undeclared island silently stayed static — the same code for "static, on purpose" and "not
|
|
21
|
+
thought about yet". Add `export const editor = { live: false };` to keep an existing
|
|
22
|
+
island's current behavior. The new `IslandEditor` type is exported for `satisfies`.
|
|
23
|
+
- dad1c9a: Repository layout only: both packages now live under `packages/*` behind a private
|
|
24
|
+
workspace root. No changes to the published API, exports, types, CLI, or the
|
|
25
|
+
`create-homepages-workspace` scaffolding experience — installing and using either
|
|
26
|
+
package is exactly as before.
|
|
27
|
+
|
|
14
28
|
## 0.7.0
|
|
15
29
|
|
|
16
30
|
### Minor Changes
|
|
@@ -7,7 +7,7 @@ const here = dirname(fileURLToPath(import.meta.url));
|
|
|
7
7
|
function assetsRoot() {
|
|
8
8
|
const dist = join(here, "scaffold");
|
|
9
9
|
if (existsSync(dist)) return dist;
|
|
10
|
-
return join(here, "..", "..", "..", "create-homepages-workspace", "scaffold");
|
|
10
|
+
return join(here, "..", "..", "..", "..", "create-homepages-workspace", "scaffold");
|
|
11
11
|
}
|
|
12
12
|
function templateAssetsDir() {
|
|
13
13
|
return join(assetsRoot(), "template");
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { docsPathForRule } from "../../rules/registry.js";
|
|
2
|
+
import { hasClientDirective } from "../is-client-file.js";
|
|
3
|
+
|
|
4
|
+
//#region src/eslint/rules/require-island-editor.ts
|
|
5
|
+
function unwrap(node) {
|
|
6
|
+
let current = node;
|
|
7
|
+
while (current && (current.type === "TSSatisfiesExpression" || current.type === "TSAsExpression" || current.type === "TSTypeAssertion")) current = current.expression;
|
|
8
|
+
return current;
|
|
9
|
+
}
|
|
10
|
+
function propNamed(object, want) {
|
|
11
|
+
for (const prop of object.properties ?? []) {
|
|
12
|
+
if (prop.type !== "Property" || prop.computed || prop.kind !== "init") continue;
|
|
13
|
+
if ((prop.key.type === "Identifier" ? prop.key.name : prop.key.type === "Literal" ? String(prop.key.value) : null) === want) return prop;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const requireIslandEditor = {
|
|
17
|
+
meta: {
|
|
18
|
+
type: "problem",
|
|
19
|
+
docs: {
|
|
20
|
+
description: "Every island must declare whether it is live in the editor canvas",
|
|
21
|
+
url: docsPathForRule("require-island-editor")
|
|
22
|
+
},
|
|
23
|
+
schema: [],
|
|
24
|
+
messages: {
|
|
25
|
+
missingEditor: "this island does not declare its editor-canvas behavior — add `export const editor`. Fix: `export const editor = { live: false };` if it should stay static SSR HTML in the editor, or `{ live: true }` to hydrate it there behind the selection gate.",
|
|
26
|
+
missingLive: "`editor` does not declare `live`. Fix: add `live: true` to hydrate this island in the editor canvas, or `live: false` to leave it as static SSR HTML there. There is no default — an omitted `live` used to mean `false` silently, which is the bug this rule exists to prevent.",
|
|
27
|
+
liveNotBoolean: "`editor.live` must be `true` or `false` written literally, not a computed value. Fix: write the literal. The editor host reads `editor` once when the module loads, so a value that varies at runtime cannot mean anything — and a declaration a reader cannot see is the very thing this rule exists to prevent."
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
create(context) {
|
|
31
|
+
return { "Program:exit"(program) {
|
|
32
|
+
if (!hasClientDirective(context.sourceCode.getText())) return;
|
|
33
|
+
let hasDefaultExport = false;
|
|
34
|
+
let editorInit;
|
|
35
|
+
let editorReportNode;
|
|
36
|
+
for (const stmt of program.body) {
|
|
37
|
+
if (stmt.type === "ExportDefaultDeclaration") {
|
|
38
|
+
hasDefaultExport = true;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (stmt.type !== "ExportNamedDeclaration") continue;
|
|
42
|
+
const declaration = stmt.declaration;
|
|
43
|
+
if (declaration?.type !== "VariableDeclaration") continue;
|
|
44
|
+
for (const decl of declaration.declarations) if (decl.id.type === "Identifier" && decl.id.name === "editor") {
|
|
45
|
+
editorInit = unwrap(decl.init);
|
|
46
|
+
editorReportNode = decl.init ?? decl.id;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (!hasDefaultExport) return;
|
|
50
|
+
if (!editorReportNode) {
|
|
51
|
+
context.report({
|
|
52
|
+
node: program,
|
|
53
|
+
messageId: "missingEditor"
|
|
54
|
+
});
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const live = editorInit?.type === "ObjectExpression" ? propNamed(editorInit, "live") : void 0;
|
|
58
|
+
if (!live) {
|
|
59
|
+
context.report({
|
|
60
|
+
node: editorReportNode,
|
|
61
|
+
messageId: "missingLive"
|
|
62
|
+
});
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (live.value.type !== "Literal" || typeof live.value.value !== "boolean") context.report({
|
|
66
|
+
node: live.value,
|
|
67
|
+
messageId: "liveNotBoolean"
|
|
68
|
+
});
|
|
69
|
+
} };
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
//#endregion
|
|
74
|
+
export { requireIslandEditor };
|
package/dist/eslint.js
CHANGED
|
@@ -7,6 +7,7 @@ import { noInlineStyle } from "./eslint/rules/no-inline-style.js";
|
|
|
7
7
|
import { noNondeterminism } from "./eslint/rules/no-nondeterminism.js";
|
|
8
8
|
import { noRawElement } from "./eslint/rules/no-raw-element.js";
|
|
9
9
|
import { propsFromSchema } from "./eslint/rules/props-from-schema.js";
|
|
10
|
+
import { requireIslandEditor } from "./eslint/rules/require-island-editor.js";
|
|
10
11
|
import { serializableIslandProps } from "./eslint/rules/serializable-island-props.js";
|
|
11
12
|
import { slotMarkerLiteral } from "./eslint/rules/slot-marker-literal.js";
|
|
12
13
|
import tsParser from "@typescript-eslint/parser";
|
|
@@ -17,6 +18,7 @@ const rules = {
|
|
|
17
18
|
"no-nondeterminism": noNondeterminism,
|
|
18
19
|
"no-client-runtime-in-server": noClientRuntimeInServer,
|
|
19
20
|
"serializable-island-props": serializableIslandProps,
|
|
21
|
+
"require-island-editor": requireIslandEditor,
|
|
20
22
|
"props-from-schema": propsFromSchema,
|
|
21
23
|
"no-inline-style": noInlineStyle,
|
|
22
24
|
"no-raw-element": noRawElement,
|
|
@@ -51,6 +53,7 @@ const templateKitConfig = [
|
|
|
51
53
|
"template-kit/no-nondeterminism": "error",
|
|
52
54
|
"template-kit/no-client-runtime-in-server": "error",
|
|
53
55
|
"template-kit/serializable-island-props": "error",
|
|
56
|
+
"template-kit/require-island-editor": "error",
|
|
54
57
|
"template-kit/no-inline-style": "error",
|
|
55
58
|
"template-kit/no-raw-element": "error",
|
|
56
59
|
"template-kit/image-bare-needs-reason": "error",
|
package/dist/index.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { Decision, FillSpec, FillSpecShape } from "./schema/fill-spec.js";
|
|
|
15
15
|
import { FixtureModule, FixtureModuleShape, FixtureOverride, ResolvedFixture, buildFixtureSet, buildInvariantFixtures } from "./schema/fixture-schema.js";
|
|
16
16
|
import { TemplateManifest, TemplateManifestShape } from "./schema/manifest.js";
|
|
17
17
|
import "./schema/index.js";
|
|
18
|
-
import { ATTR_ISLAND_ID, ATTR_ISLAND_KEY, ATTR_ISLAND_PROPS, ISLAND_ELEMENT, IslandComponent, IslandEditorOptions, IslandModule, IslandProps, IslandPropsError, ShieldMode, deserializeIslandProps, islandId, serializeIslandProps } from "./islands/contract.js";
|
|
18
|
+
import { ATTR_ISLAND_ID, ATTR_ISLAND_KEY, ATTR_ISLAND_PROPS, ISLAND_ELEMENT, IslandComponent, IslandEditor, IslandEditorOptions, IslandModule, IslandProps, IslandPropsError, ShieldMode, deserializeIslandProps, islandId, serializeIslandProps } from "./islands/contract.js";
|
|
19
19
|
import { IslandMarker, IslandMarkerProps } from "./islands/marker.js";
|
|
20
20
|
import { Image } from "./primitives/Image.js";
|
|
21
21
|
import { PicturePlan, PictureSourceSpec } from "./primitives/picture-sources.js";
|
|
@@ -39,4 +39,4 @@ import { TokenNameSchema, TokenTheme, TokenThemeSchema, compileThemeToCss } from
|
|
|
39
39
|
*/
|
|
40
40
|
declare const KIT_VERSION: string;
|
|
41
41
|
//#endregion
|
|
42
|
-
export { ATTR_ISLAND_ID, ATTR_ISLAND_KEY, ATTR_ISLAND_PROPS, ATTR_SECTION_INSTANCE_ID, ATTR_SLOT_GROUP, ATTR_SLOT_ID, ATTR_SLOT_ITEM, ATTR_SLOT_TEXT_LEAF, ATTR_TR_ENHANCE, type AddressParts, COLLECTIONS, CONTENT_SLOT_TYPES, type CollectionField, type CollectionIdKey, type CollectionOwnership, ContentSlot, type ContentSlotType, type Coordinates, DECISION_TYPES, DERIVED_INPUT_FIELDS, Decision, type DecisionType, type DerivedInputName, FORMATTER_NAMES, FillSpec, type FillSpecShape, type FixtureModule, FixtureModuleShape, type FixtureOverride, type FloorplanAssignment, type FormatterName, type FrameConfig, ISLAND_ELEMENT, Image, type ImageCollectionValue, type ImageValue, IslandComponent, IslandEditorOptions, IslandMarker, type IslandMarkerProps, IslandModule, IslandProps, IslandPropsError, KIT_VERSION, OptionDef, type OptionValue, type PicturePlan, type PictureSourceSpec, type PlaybackConfig, type PoiRow, type PropertyFacts, REFERRAL_FIELDS, REFERRAL_IMAGE_FIELDS, REF_SLOT_TYPES, type RefSlotType, type ReferralField, type ReferralImageField, type ResolvedFixture, type ResponsiveImage, type ResponsiveSource, SLOT_TYPES, SLOT_TYPE_DESCRIPTORS, SOURCE_FIELDS, Section, SectionMeta, type SectionMetaShape, type SectionNav, type SectionProps, SectionSchema, type SectionSchemaResolved, type SectionSchemaShape, ShieldMode, Slot, SlotDef, type Slot$1 as SlotDefinition, SlotGroup, SlotGroupDef, type SlotGroupDefShape, SlotItem, type SlotType, type SlotValue, type SourceFieldContainer, type SourceFieldDescriptor, type SourceFieldName, type SourceFieldValueType, TRANSFORMS, TRANSFORM_NAMES, TemplateManifest, type TemplateManifestShape, TokenNameSchema, type TokenTheme, TokenThemeSchema, type TransformDescriptor, type TransformFn, type TransformInput, type TransformName, type TransformUnit, type UnitRow, VERIFICATION_WIDTHS, type VariantSelectorFn, type VariantsMeta, type VerificationWidth, Video, type VideoCollectionValue, type VideoValue, type WritebackTarget, buildFixtureSet, buildInvariantFixtures, collectionForSlot, compileThemeToCss, defaultForSlotType, derived, deserializeIslandProps, direct, islandId, makeResolveVariants, serializeIslandProps, slotDefTypes };
|
|
42
|
+
export { ATTR_ISLAND_ID, ATTR_ISLAND_KEY, ATTR_ISLAND_PROPS, ATTR_SECTION_INSTANCE_ID, ATTR_SLOT_GROUP, ATTR_SLOT_ID, ATTR_SLOT_ITEM, ATTR_SLOT_TEXT_LEAF, ATTR_TR_ENHANCE, type AddressParts, COLLECTIONS, CONTENT_SLOT_TYPES, type CollectionField, type CollectionIdKey, type CollectionOwnership, ContentSlot, type ContentSlotType, type Coordinates, DECISION_TYPES, DERIVED_INPUT_FIELDS, Decision, type DecisionType, type DerivedInputName, FORMATTER_NAMES, FillSpec, type FillSpecShape, type FixtureModule, FixtureModuleShape, type FixtureOverride, type FloorplanAssignment, type FormatterName, type FrameConfig, ISLAND_ELEMENT, Image, type ImageCollectionValue, type ImageValue, IslandComponent, IslandEditor, IslandEditorOptions, IslandMarker, type IslandMarkerProps, IslandModule, IslandProps, IslandPropsError, KIT_VERSION, OptionDef, type OptionValue, type PicturePlan, type PictureSourceSpec, type PlaybackConfig, type PoiRow, type PropertyFacts, REFERRAL_FIELDS, REFERRAL_IMAGE_FIELDS, REF_SLOT_TYPES, type RefSlotType, type ReferralField, type ReferralImageField, type ResolvedFixture, type ResponsiveImage, type ResponsiveSource, SLOT_TYPES, SLOT_TYPE_DESCRIPTORS, SOURCE_FIELDS, Section, SectionMeta, type SectionMetaShape, type SectionNav, type SectionProps, SectionSchema, type SectionSchemaResolved, type SectionSchemaShape, ShieldMode, Slot, SlotDef, type Slot$1 as SlotDefinition, SlotGroup, SlotGroupDef, type SlotGroupDefShape, SlotItem, type SlotType, type SlotValue, type SourceFieldContainer, type SourceFieldDescriptor, type SourceFieldName, type SourceFieldValueType, TRANSFORMS, TRANSFORM_NAMES, TemplateManifest, type TemplateManifestShape, TokenNameSchema, type TokenTheme, TokenThemeSchema, type TransformDescriptor, type TransformFn, type TransformInput, type TransformName, type TransformUnit, type UnitRow, VERIFICATION_WIDTHS, type VariantSelectorFn, type VariantsMeta, type VerificationWidth, Video, type VideoCollectionValue, type VideoValue, type WritebackTarget, buildFixtureSet, buildInvariantFixtures, collectionForSlot, compileThemeToCss, defaultForSlotType, derived, deserializeIslandProps, direct, islandId, makeResolveVariants, serializeIslandProps, slotDefTypes };
|
package/dist/island-runtime.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ATTR_ISLAND_ID, ATTR_ISLAND_KEY, ATTR_ISLAND_PROPS, ISLAND_ELEMENT, IslandComponent, IslandEditorOptions, IslandModule, IslandProps, IslandPropsError, ShieldMode, deserializeIslandProps, islandId, serializeIslandProps } from "./islands/contract.js";
|
|
1
|
+
import { ATTR_ISLAND_ID, ATTR_ISLAND_KEY, ATTR_ISLAND_PROPS, ISLAND_ELEMENT, IslandComponent, IslandEditor, IslandEditorOptions, IslandModule, IslandProps, IslandPropsError, ShieldMode, deserializeIslandProps, islandId, serializeIslandProps } from "./islands/contract.js";
|
|
2
2
|
//#region src/islands/runtime.d.ts
|
|
3
3
|
/** What the loader knows about one island once its module has loaded. */
|
|
4
4
|
interface IslandContext {
|
|
@@ -27,4 +27,4 @@ declare function hydrateIslands(options: HydrateIslandsOptions): Promise<IslandH
|
|
|
27
27
|
*/
|
|
28
28
|
declare function unmountIslands(root?: ParentNode): void;
|
|
29
29
|
//#endregion
|
|
30
|
-
export { ATTR_ISLAND_ID, ATTR_ISLAND_KEY, ATTR_ISLAND_PROPS, HydrateIslandsOptions, ISLAND_ELEMENT, IslandComponent, IslandContext, IslandEditorOptions, IslandHandle, IslandModule, IslandProps, IslandPropsError, ShieldMode, deserializeIslandProps, hydrateIslands, islandId, serializeIslandProps, unmountIslands };
|
|
30
|
+
export { ATTR_ISLAND_ID, ATTR_ISLAND_KEY, ATTR_ISLAND_PROPS, HydrateIslandsOptions, ISLAND_ELEMENT, IslandComponent, IslandContext, IslandEditor, IslandEditorOptions, IslandHandle, IslandModule, IslandProps, IslandPropsError, ShieldMode, deserializeIslandProps, hydrateIslands, islandId, serializeIslandProps, unmountIslands };
|
|
@@ -12,10 +12,15 @@ declare const ATTR_ISLAND_PROPS = "data-tr-island-props";
|
|
|
12
12
|
type IslandProps = Record<string, unknown>;
|
|
13
13
|
/** How the editor sizes the transparent selection shield over a live island. */
|
|
14
14
|
type ShieldMode = "fill-self" | "fill-parent";
|
|
15
|
-
/**
|
|
16
|
-
|
|
15
|
+
/**
|
|
16
|
+
* What a `"use client"` island must declare about its editor-canvas behavior.
|
|
17
|
+
*
|
|
18
|
+
* An explicit `false` is a decision; a missing field was an accident wearing a
|
|
19
|
+
* decision's clothes.
|
|
20
|
+
*/
|
|
21
|
+
interface IslandEditor {
|
|
17
22
|
/** Hydrate this island in the editor canvas (behind the editor's selection gate). */
|
|
18
|
-
live
|
|
23
|
+
live: boolean;
|
|
19
24
|
/** Shield placement. Default "fill-parent". */
|
|
20
25
|
shieldMode?: ShieldMode;
|
|
21
26
|
/** Which element(s) inside the island the shield covers. Omit → the whole island. Runs in the browser. */
|
|
@@ -23,6 +28,17 @@ interface IslandEditorOptions {
|
|
|
23
28
|
/** Editor-only props, merged over the server-rendered props (e.g. an accordion's single-open canvas mirror). */
|
|
24
29
|
props?: IslandProps;
|
|
25
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* The RUNTIME's tolerant view of `IslandEditor` — every field optional, `live` included.
|
|
33
|
+
*
|
|
34
|
+
* Deliberately not the same type as the authoring contract. `hydrateIslands` reaches a
|
|
35
|
+
* module through a dynamic `import()` and a cast (runtime.ts): it cannot enforce what
|
|
36
|
+
* the module actually exports, and the published-page path never reads `editor` at all.
|
|
37
|
+
* Requiring `live` here would be an assumption the loader has no way to hold. The
|
|
38
|
+
* authoring requirement is enforced where it can be — the `require-island-editor` lint
|
|
39
|
+
* rule and the registry's ingest validator.
|
|
40
|
+
*/
|
|
41
|
+
type IslandEditorOptions = Partial<IslandEditor>;
|
|
26
42
|
/**
|
|
27
43
|
* A client component's own prop type is the author's business — island props are
|
|
28
44
|
* validated at runtime (serializeIslandProps), not through this alias, and a
|
|
@@ -53,4 +69,4 @@ declare function serializeIslandProps(props: IslandProps): string;
|
|
|
53
69
|
/** Read a props payload back. Throws IslandPropsError on anything that is not a JSON object. */
|
|
54
70
|
declare function deserializeIslandProps(payload: string): IslandProps;
|
|
55
71
|
//#endregion
|
|
56
|
-
export { ATTR_ISLAND_ID, ATTR_ISLAND_KEY, ATTR_ISLAND_PROPS, ISLAND_ELEMENT, IslandComponent, IslandEditorOptions, IslandModule, IslandProps, IslandPropsError, ShieldMode, deserializeIslandProps, islandId, serializeIslandProps };
|
|
72
|
+
export { ATTR_ISLAND_ID, ATTR_ISLAND_KEY, ATTR_ISLAND_PROPS, ISLAND_ELEMENT, IslandComponent, IslandEditor, IslandEditorOptions, IslandModule, IslandProps, IslandPropsError, ShieldMode, deserializeIslandProps, islandId, serializeIslandProps };
|
package/dist/package.js
CHANGED
package/guide/islands.md
CHANGED
|
@@ -83,20 +83,24 @@ runtime check's job.
|
|
|
83
83
|
## Editor options
|
|
84
84
|
|
|
85
85
|
Templates are edited in a canvas where the section is re-rendered on every keystroke.
|
|
86
|
-
|
|
86
|
+
Every island must declare how it behaves there:
|
|
87
|
+
|
|
88
|
+
- **`live`** — **required**. `true` hydrates the island in the editor canvas behind the
|
|
89
|
+
selection gate; `false` leaves it as static SSR HTML there.
|
|
90
|
+
- **`shieldMode`, `liveSurface`, `props`** — optional, and only meaningful when `live: true`.
|
|
87
91
|
|
|
88
92
|
```tsx
|
|
89
93
|
"use client";
|
|
90
|
-
import type {
|
|
94
|
+
import type { IslandEditor } from "@homepages/template-kit";
|
|
91
95
|
|
|
92
96
|
export default function Accordion({ items, singleOpen }: Props) { /* … */ }
|
|
93
97
|
|
|
94
|
-
export const editor
|
|
95
|
-
live: true, // hydrate in the editor canvas
|
|
98
|
+
export const editor = {
|
|
99
|
+
live: true, // required — hydrate in the editor canvas
|
|
96
100
|
shieldMode: "fill-parent", // how the editor sizes its selection shield
|
|
97
101
|
liveSurface: (root) => [...root.querySelectorAll(".accordion")], // shield only these
|
|
98
102
|
props: { singleOpen: true }, // editor-only props, merged over the real ones
|
|
99
|
-
};
|
|
103
|
+
} satisfies IslandEditor;
|
|
100
104
|
```
|
|
101
105
|
|
|
102
106
|
`props` merge in as an update immediately after the island hydrates, not as part of its
|
|
@@ -104,8 +108,23 @@ first mount — so an island must read them during render. A value read only at
|
|
|
104
108
|
(a `useState` initializer, an `<input defaultValue>`) sees the real prop, and the
|
|
105
109
|
editor-only override never reaches it.
|
|
106
110
|
|
|
107
|
-
|
|
108
|
-
which is
|
|
111
|
+
There is no default, deliberately. An island that declared nothing used to stay static in
|
|
112
|
+
the canvas, which made "I have not decided" and "static is what I want" the same code. The
|
|
113
|
+
[`require-island-editor`](rules/require-island-editor.md) rule now asks for the answer.
|
|
114
|
+
`live: false` is the safe one if you are unsure — it is what an undeclared island already
|
|
115
|
+
did, written down.
|
|
116
|
+
|
|
117
|
+
The minimal declaration carries just the required field:
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
"use client";
|
|
121
|
+
|
|
122
|
+
export const editor = { live: true };
|
|
123
|
+
|
|
124
|
+
export default function Lightbox({ headline }: { headline: string }) {
|
|
125
|
+
// …
|
|
126
|
+
}
|
|
127
|
+
```
|
|
109
128
|
|
|
110
129
|
## The DOM contract
|
|
111
130
|
|
package/guide/llms.txt
CHANGED
|
@@ -73,6 +73,9 @@ and [Pick a scenario](recipes/pick-a-scenario.md).
|
|
|
73
73
|
rendered inside the section.
|
|
74
74
|
- [README](../README.md): install, exports, and the release flow.
|
|
75
75
|
- [Islands](islands.md): interactivity — client components, the serializable-props contract, editor options, and the browser loader.
|
|
76
|
+
Every "use client" module with a default export MUST `export const editor` declaring a
|
|
77
|
+
boolean literal `live` — `{ live: true }` hydrates it in the editor canvas, `{ live: false }`
|
|
78
|
+
leaves it static there. There is no default; the require-island-editor rule enforces it.
|
|
76
79
|
- [Static assets](assets.md): shipping a logo, texture, icon, or brand font with a
|
|
77
80
|
template — where an asset file lives (template level, or a section folder if the
|
|
78
81
|
section references it), referencing it from a section's CSS with `url()`/`@font-face`
|
package/guide/rules/INDEX.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
purpose: Router for the rule corpus — every template-kit/<id> an author can hit, and the page that explains it.
|
|
3
3
|
status: living
|
|
4
4
|
related: [server-vs-client.md, ../eslint.md, ../check.md, ../INDEX.md]
|
|
5
|
-
updated: 2026-07-
|
|
5
|
+
updated: 2026-07-17
|
|
6
6
|
---
|
|
7
7
|
# Rules
|
|
8
8
|
|
|
@@ -25,6 +25,7 @@ and it is explained there rather than on each of them.
|
|
|
25
25
|
| [`no-nondeterminism`](no-nondeterminism.md) | eslint | No clock or randomness reads in server-rendered code. |
|
|
26
26
|
| [`no-client-runtime-in-server`](no-client-runtime-in-server.md) | eslint | No effects, state, event handlers, network, or browser globals in server-rendered code. |
|
|
27
27
|
| [`serializable-island-props`](serializable-island-props.md) | eslint | Props handed to an island must be JSON-serializable. |
|
|
28
|
+
| [`require-island-editor`](require-island-editor.md) | eslint | Every island must declare whether it is live in the editor canvas. |
|
|
28
29
|
| [`no-client-directive-in-contract`](no-client-directive-in-contract.md) | eslint | No "use client" in a section's four contract files. |
|
|
29
30
|
| [`props-from-schema`](props-from-schema.md) | eslint | A Renderer's Props type must be derived from schema.ts, not hand-written. |
|
|
30
31
|
| [`no-inline-style`](no-inline-style.md) | eslint | No inline style= in server-rendered JSX. Islands are exempt. |
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: The template-kit/require-island-editor authoring rule — what it enforces, why, and how to fix it.
|
|
3
|
+
status: living
|
|
4
|
+
related: [INDEX.md, server-vs-client.md, ../islands.md]
|
|
5
|
+
updated: 2026-07-17
|
|
6
|
+
---
|
|
7
|
+
# template-kit/require-island-editor
|
|
8
|
+
|
|
9
|
+
> Every island declares whether it hydrates in the editor canvas. There is no default.
|
|
10
|
+
|
|
11
|
+
## Rule
|
|
12
|
+
|
|
13
|
+
A file is an island when its directive prologue says `"use client"` **and** it has a
|
|
14
|
+
default export (see [server vs client](server-vs-client.md)). Every island must carry:
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
export const editor = { live: true };
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`live` must be written as `true` or `false` literally. A computed value reports.
|
|
21
|
+
|
|
22
|
+
Two files are exempt, and neither is a loophole:
|
|
23
|
+
|
|
24
|
+
- A module with no `"use client"` — it is server-rendered code, not an island.
|
|
25
|
+
- A `"use client"` module with **no default export** — the platform's codemod only turns
|
|
26
|
+
a default-exported client module into an island, so this file never becomes one.
|
|
27
|
+
|
|
28
|
+
## Reason
|
|
29
|
+
|
|
30
|
+
`live` controls one thing: whether the editor canvas hydrates your island, or leaves it
|
|
31
|
+
as the static HTML the server rendered. Both answers are legitimate. A gallery lightbox
|
|
32
|
+
wants `live: true` so an author can see it behave; a scroll-triggered animation wants
|
|
33
|
+
`live: false` so it does not fire while someone is editing copy.
|
|
34
|
+
|
|
35
|
+
The rule exists because the answer used to be expressed by **saying nothing**. An island
|
|
36
|
+
with no `editor` export simply never hydrated in the canvas — which meant "I have not
|
|
37
|
+
thought about this" and "I want static HTML here" produced identical code. They are not
|
|
38
|
+
the same statement, and the difference is invisible in review.
|
|
39
|
+
|
|
40
|
+
`live` is read **once**, when your module loads. That is why it must be a literal: a
|
|
41
|
+
value computed at runtime cannot change the answer, so a non-literal is never a working
|
|
42
|
+
declaration — only a misleading one.
|
|
43
|
+
|
|
44
|
+
## Fix
|
|
45
|
+
|
|
46
|
+
Declare it. If you are unsure, `live: false` is the safe answer: it is exactly what your
|
|
47
|
+
island does today, now written down.
|
|
48
|
+
|
|
49
|
+
### Before
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
// sections/gallery/Lightbox.tsx
|
|
53
|
+
"use client";
|
|
54
|
+
|
|
55
|
+
import { useState } from "react";
|
|
56
|
+
|
|
57
|
+
export default function Lightbox({ headline }: { headline: string }) {
|
|
58
|
+
const [open, setOpen] = useState(false);
|
|
59
|
+
return (
|
|
60
|
+
<div>
|
|
61
|
+
<button onClick={() => setOpen(true)}>Open gallery</button>
|
|
62
|
+
{open && <p>{headline}</p>}
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### After
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
// sections/gallery/Lightbox.tsx
|
|
72
|
+
"use client";
|
|
73
|
+
|
|
74
|
+
import { useState } from "react";
|
|
75
|
+
|
|
76
|
+
// Authors open the lightbox in the canvas to check it, so hydrate it there.
|
|
77
|
+
export const editor = { live: true };
|
|
78
|
+
|
|
79
|
+
export default function Lightbox({ headline }: { headline: string }) {
|
|
80
|
+
const [open, setOpen] = useState(false);
|
|
81
|
+
return (
|
|
82
|
+
<div>
|
|
83
|
+
<button onClick={() => setOpen(true)}>Open gallery</button>
|
|
84
|
+
{open && <p>{headline}</p>}
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Type it if you want the editor to check the shape:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
import type { IslandEditor } from "@homepages/template-kit";
|
|
94
|
+
|
|
95
|
+
export const editor = { live: true } satisfies IslandEditor;
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## See also
|
|
99
|
+
|
|
100
|
+
- [Islands](../islands.md) — the full `editor` contract: `shieldMode`, `liveSurface`, `props`.
|
|
101
|
+
- [Server vs client](server-vs-client.md) — the directive, the prologue, and what an island may do.
|
package/package.json
CHANGED
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homepages/template-kit",
|
|
3
|
-
"version": "0.8.0
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Authoring kit for HomePages marketing-section templates: schema system, contract primitives, theme tokens, and the template-kit CLI.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|
|
8
8
|
"node": ">=20.0.0"
|
|
9
9
|
},
|
|
10
|
-
"workspaces": [
|
|
11
|
-
".",
|
|
12
|
-
"create-homepages-workspace"
|
|
13
|
-
],
|
|
14
10
|
"repository": {
|
|
15
11
|
"type": "git",
|
|
16
|
-
"url": "git+https://github.com/falktravis/template-
|
|
12
|
+
"url": "git+https://github.com/falktravis/template-packages.git",
|
|
13
|
+
"directory": "packages/template-kit"
|
|
17
14
|
},
|
|
18
15
|
"publishConfig": {
|
|
19
16
|
"access": "public"
|
|
@@ -71,11 +68,7 @@
|
|
|
71
68
|
"lint:pkg": "publint --strict && attw --pack . --profile esm-only --exclude-entrypoints styles.css base.css",
|
|
72
69
|
"verify:consumer": "node scripts/verify-consumer.mjs",
|
|
73
70
|
"size:islands": "node scripts/check-island-runtime-size.mjs",
|
|
74
|
-
"check": "npm run typecheck && npm run lint && npm run build && npm run size:islands && npm run test && npm run lint:pkg && npm run verify:consumer
|
|
75
|
-
"check:scaffolder": "npm run typecheck -w create-homepages-workspace && npm run build -w create-homepages-workspace && npm run test -w create-homepages-workspace",
|
|
76
|
-
"changeset": "changeset",
|
|
77
|
-
"version": "changeset version && npm install --package-lock-only",
|
|
78
|
-
"release": "changeset publish"
|
|
71
|
+
"check": "npm run typecheck && npm run lint && npm run build && npm run size:islands && npm run test && npm run lint:pkg && npm run verify:consumer"
|
|
79
72
|
},
|
|
80
73
|
"peerDependencies": {
|
|
81
74
|
"react": "^19.0.0",
|
|
@@ -83,7 +76,6 @@
|
|
|
83
76
|
},
|
|
84
77
|
"devDependencies": {
|
|
85
78
|
"@arethetypeswrong/cli": "^0.18.5",
|
|
86
|
-
"@changesets/cli": "^2.29.8",
|
|
87
79
|
"@eslint/js": "^9.39.4",
|
|
88
80
|
"@playwright/test": "^1.60.0",
|
|
89
81
|
"@tailwindcss/cli": "^4.3.0",
|