@jxsuite/site 2.7.0 → 2.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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **What a Jx project's page at a route consists of:** routing, layout, `<head>`, context and site style, as pure functions with their IO injected.
4
4
 
5
- Every one of these rules has to be answered identically by four things that cannot share code any other way: the compiler's static build, the Studio canvas, a Bun dev/desktop server, and a Cloudflare Worker serving a live preview. None of them can import `@jxsuite/compiler`, whose dependency graph carries `sharp` and `esbuild`, so neither a browser bundle nor a Worker can load it. And a route is exactly the kind of rule where a silent disagreement is a page that 404s in one surface and renders in another.
5
+ Every one of these rules has to be answered identically by four things that cannot share code any other way: the compiler's static build, the Studio canvas, a Bun dev/desktop server, and a Cloudflare Worker serving a live preview. None of them can import `@jxsuite/compiler`, whose dependency graph carries a native addon and `esbuild`, so neither a browser bundle nor a Worker can load it. And a route is exactly the kind of rule where a silent disagreement is a page that 404s in one surface and renders in another.
6
6
 
7
7
  So the rules live here, and this package has **no platform imports at all**: no `node:`, no DOM, no filesystem. Where a rule genuinely needs to read something, the reader is a parameter (`LayoutLoader`, `DocumentParser`) and is simply absent where there are no directories.
8
8
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/site",
3
- "version": "2.7.0",
3
+ "version": "2.8.0",
4
4
  "description": "What a Jx project's page at a route consists of — routing, layout, context, head and the document that boots it",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -34,7 +34,7 @@
34
34
  "test:coverage": "bun test --isolate --coverage"
35
35
  },
36
36
  "dependencies": {
37
- "@jxsuite/runtime": "^7.0.0",
38
- "@jxsuite/schema": "^6.0.0"
37
+ "@jxsuite/runtime": "^7.1.0",
38
+ "@jxsuite/schema": "^6.0.2"
39
39
  }
40
40
  }
package/src/routes.ts CHANGED
@@ -9,9 +9,9 @@
9
9
  * another.
10
10
  *
11
11
  * It lives HERE rather than in `@jxsuite/compiler` for the reason `resolveI18n` does (`i18n.ts`):
12
- * the compiler's dependency graph carries `sharp` and `esbuild`, so neither a browser bundle nor a
13
- * Cloudflare Worker can import it. This module is pure string math with no imports at all, so every
14
- * host that has to know what `/blog/:slug` means can share one answer.
12
+ * the compiler's dependency graph carries a native addon and `esbuild`, so neither a browser bundle
13
+ * nor a Cloudflare Worker can import it. This module is pure string math with no imports at all, so
14
+ * every host that has to know what `/blog/:slug` means can share one answer.
15
15
  */
16
16
 
17
17
  /** Bracket route segments: `[param]` (named) and `[...param]` (catch-all). */
package/src/site-style.ts CHANGED
@@ -29,6 +29,10 @@
29
29
  * to differ was a nested element key, on the belief that "the document's own style pass covers page
30
30
  * content": nothing else reads `project.json#/style`, so the canvas simply dropped a site's
31
31
  * typography and link rules while the build kept them (#296).
32
+ *
33
+ * A block's `$description` is the one thing only the build renders: a static build writes it as a
34
+ * comment above the rule (`{ comments: true }`, spec.md §9.2), and a host never does, because every
35
+ * consumer but a static build ignores the description.
32
36
  */
33
37
 
34
38
  import {
@@ -36,11 +40,12 @@ import {
36
40
  buildStyleRules,
37
41
  isDeclarationAtRule,
38
42
  isKeyframesAtRule,
43
+ commentedRuleText,
39
44
  isNestedSelectorKey,
40
45
  pureSchemeOf,
41
46
  } from "@jxsuite/runtime/css";
42
47
  import type { CssBuildOptions } from "@jxsuite/runtime/css";
43
- import { isNonNullObject } from "@jxsuite/schema/guards";
48
+ import { isNonNullObject, isString } from "@jxsuite/schema/guards";
44
49
  import type { JxRef, JxStyle } from "@jxsuite/schema/types";
45
50
 
46
51
  /** Id of the injected site-style tag (replace-in-place, never accumulate). */
@@ -76,9 +81,29 @@ export interface SiteStyleRoots {
76
81
  body: string;
77
82
  }
78
83
 
84
+ /** What a caller may ask of the sheet beyond its rules. */
85
+ export interface SiteStyleOptions {
86
+ /**
87
+ * Write each described rule's `$description` as a comment on the line above it. The static
88
+ * build's switch: the canvas and the live preview leave it off and emit the rule text alone.
89
+ */
90
+ comments?: boolean;
91
+ }
92
+
79
93
  /** The page's own root and body. */
80
94
  export const PAGE_ROOTS: SiteStyleRoots = { body: "body", root: ":root" };
81
95
 
96
+ /**
97
+ * Hand a block's own `$description` to the first rule its split writes: the root's when it holds a
98
+ * declaration, the body's otherwise. A block that writes neither rule carries no comment.
99
+ */
100
+ function describeFirstRule(block: JxStyle, root: JxStyle, body: JxStyle): void {
101
+ const description = block["$description"];
102
+ if (isString(description)) {
103
+ (Object.keys(root).length > 0 ? root : body)["$description"] = description;
104
+ }
105
+ }
106
+
82
107
  /**
83
108
  * Build the site-style sheet text: custom properties on `:root`, plain properties on `body`,
84
109
  * selector-keyed blocks (`"h1, h2"`, `a`, `.card`) as rules of their own, `&`-keyed blocks as
@@ -104,6 +129,7 @@ export const PAGE_ROOTS: SiteStyleRoots = { body: "body", root: ":root" };
104
129
  * is dropped, which is what every host does
105
130
  * @param {SiteStyleRoots} [roots] - The selectors that stand for the page's root and body; the
106
131
  * page's own `:root` and `body` unless the sheet styles a page drawn inside something else
132
+ * @param {SiteStyleOptions} [options] - `comments: true` is the static build's form
107
133
  * @returns {string}
108
134
  * @docs studio/interface/canvas
109
135
  */
@@ -113,19 +139,20 @@ export function buildSiteStyleCSS(
113
139
  transpose: (value: string) => string,
114
140
  resolveValue?: SiteStyleValueResolver,
115
141
  roots: SiteStyleRoots = PAGE_ROOTS,
142
+ options: SiteStyleOptions = {},
116
143
  ): string {
117
144
  const rules: string[] = [];
118
145
  const push = (style: JxStyle, selector: string | null) => {
119
- const options: CssBuildOptions = { mediaQueries, scope: selector, transposeValue: transpose };
146
+ const build: CssBuildOptions = { mediaQueries, scope: selector, transposeValue: transpose };
120
147
  /* The selector is closed over here rather than threaded through `buildStyleRules`, whose own
121
148
  hook sees a rule TARGET (self / descendant / unscoped) and not the selector it hangs off;
122
149
  the compiler's report names the selector, so this is where it is known. Set only when given:
123
150
  `exactOptionalPropertyTypes` refuses an explicit `undefined` for an optional hook. */
124
151
  if (resolveValue) {
125
- options.resolveValue = (property, value) => resolveValue(property, value, selector);
152
+ build.resolveValue = (property, value) => resolveValue(property, value, selector);
126
153
  }
127
- for (const rule of buildStyleRules(style, options)) {
128
- rules.push(rule.text);
154
+ for (const rule of buildStyleRules(style, build)) {
155
+ rules.push(options.comments === true ? commentedRuleText(rule) : rule.text);
129
156
  }
130
157
  };
131
158
 
@@ -157,7 +184,8 @@ export function buildSiteStyleCSS(
157
184
  }
158
185
  continue;
159
186
  }
160
- if (isNestedSelectorKey(key) || key.startsWith("@")) {
187
+ // Metadata is no declaration; the block's own `$description` is placed after the split.
188
+ if (isNestedSelectorKey(key) || key.startsWith("@") || key.startsWith("$")) {
161
189
  continue;
162
190
  }
163
191
  /* Custom properties go to `:root`; so does `color-scheme`, which is the one non-custom
@@ -172,6 +200,8 @@ export function buildSiteStyleCSS(
172
200
  }
173
201
  }
174
202
 
203
+ describeFirstRule(siteStyle, rootProps, bodyProps);
204
+
175
205
  // Base rules precede conditional blocks so equal-specificity overrides win by source order.
176
206
  push(rootProps, roots.root);
177
207
  push(bodyProps, roots.body);
@@ -206,12 +236,16 @@ export function buildSiteStyleCSS(
206
236
  }
207
237
  continue;
208
238
  }
239
+ if (k.startsWith("$")) {
240
+ continue;
241
+ }
209
242
  if (k.startsWith("--")) {
210
243
  condRoot[k] = v;
211
244
  } else {
212
245
  condBody[k] = v;
213
246
  }
214
247
  }
248
+ describeFirstRule(block, condRoot, condBody);
215
249
  push({ [atKey]: condRoot }, roots.root);
216
250
  push({ [atKey]: condBody }, roots.body);
217
251
  for (const [sel, sub] of condSubs) {