@plitzi/sdk-server 0.38.6 → 0.38.8

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.
Files changed (88) hide show
  1. package/CHANGELOG.md +209 -0
  2. package/README.md +102 -16
  3. package/dist/adapters/jsonAdapters.d.ts +8 -4
  4. package/dist/adapters/jsonAdapters.js +5 -9
  5. package/dist/core/auth/api.d.ts +4 -1
  6. package/dist/core/auth/api.js +14 -5
  7. package/dist/core/auth/createAuth.d.ts +7 -1
  8. package/dist/core/auth/createAuth.js +14 -0
  9. package/dist/core/auth/credentials.d.ts +2 -0
  10. package/dist/core/auth/credentials.js +4 -2
  11. package/dist/core/auth/session.d.ts +2 -0
  12. package/dist/core/auth/session.js +6 -5
  13. package/dist/core/createServer.js +25 -4
  14. package/dist/core/http/stages/devReload.d.ts +2 -2
  15. package/dist/core/http/stages/devReload.js +2 -2
  16. package/dist/env.d.ts +1 -0
  17. package/dist/env.js +19 -0
  18. package/dist/env.test.d.ts +1 -0
  19. package/dist/functions-api.d.ts +5 -0
  20. package/dist/helpers/buildServerInfo.js +6 -0
  21. package/dist/index.d.ts +1 -1
  22. package/dist/modules/actions/runtime/projectData.d.ts +18 -0
  23. package/dist/modules/actions/runtime/projectData.js +69 -0
  24. package/dist/modules/actions/runtime/taskContext.d.ts +1 -1
  25. package/dist/modules/actions/runtime/taskContext.js +6 -3
  26. package/dist/modules/actions/types.d.ts +7 -1
  27. package/dist/modules/functions/build.js +10 -0
  28. package/dist/modules/functions/capabilities.js +5 -0
  29. package/dist/modules/functions/context.js +10 -2
  30. package/dist/modules/functions/contract.d.ts +5 -0
  31. package/dist/modules/functions/driver.js +4 -0
  32. package/dist/modules/functions/local.d.ts +3 -1
  33. package/dist/modules/functions/local.js +4 -2
  34. package/dist/modules/functions/protocol.d.ts +3 -0
  35. package/dist/modules/functions/scope.d.ts +1 -1
  36. package/dist/modules/images/imageProxy.js +1 -1
  37. package/dist/modules/mysql/accounts.js +1 -5
  38. package/dist/modules/project/plugins.d.ts +31 -0
  39. package/dist/modules/project/plugins.js +162 -0
  40. package/dist/modules/project/serveProject.d.ts +78 -0
  41. package/dist/modules/project/serveProject.js +200 -0
  42. package/dist/modules/project/serveProject.test.d.ts +1 -0
  43. package/dist/modules/project/watchSpace.d.ts +14 -0
  44. package/dist/modules/project/watchSpace.js +77 -0
  45. package/dist/modules/realtime/failure.d.ts +6 -0
  46. package/dist/modules/realtime/failure.js +11 -0
  47. package/dist/modules/realtime/handlers.js +13 -3
  48. package/dist/modules/realtime/hub.js +4 -1
  49. package/dist/modules/realtime/socket.js +3 -2
  50. package/dist/modules/rsc/connectorRscData.d.ts +7 -3
  51. package/dist/modules/rsc/connectorRscData.js +3 -4
  52. package/dist/modules/rsc/handler.d.ts +1 -0
  53. package/dist/modules/rsc/handler.js +11 -1
  54. package/dist/modules/rsc/publicFileResolver.d.ts +4 -8
  55. package/dist/modules/rsc/publicFileResolver.js +6 -10
  56. package/dist/modules/runtime/bundle.d.ts +1 -1
  57. package/dist/modules/runtime/bundle.js +2 -2
  58. package/dist/modules/ssr/prepareRender.js +10 -2
  59. package/dist/modules/ssr/registerExternalPlugins.js +1 -1
  60. package/dist/modules/ssr/vendor.test.d.ts +1 -0
  61. package/dist/modules/ssr/views/template.ejs +7 -3
  62. package/dist/plugins/compile.js +1 -1
  63. package/dist/plugins/copy.js +1 -1
  64. package/dist/plugins/manager.js +1 -1
  65. package/dist/project.d.ts +9 -0
  66. package/dist/project.js +2 -0
  67. package/package.json +16 -4
  68. package/skills/plitzi-authoring/CHEATSHEET.md +2 -2
  69. package/skills/plitzi-authoring/SKILL.md +34 -148
  70. package/skills/plitzi-authoring/recipes/plugin.ts +1 -1
  71. package/skills/plitzi-authoring/reference/accessibility.md +3 -4
  72. package/skills/plitzi-authoring/reference/auth.md +75 -0
  73. package/skills/plitzi-authoring/reference/authoring-errors.md +19 -4
  74. package/skills/plitzi-authoring/reference/colours-and-motion.md +3 -2
  75. package/skills/plitzi-authoring/reference/drawing.md +1 -1
  76. package/skills/plitzi-authoring/reference/efficiency.md +5 -2
  77. package/skills/plitzi-authoring/reference/elements-and-styles.md +23 -23
  78. package/skills/plitzi-authoring/reference/flows.md +15 -8
  79. package/skills/plitzi-authoring/reference/index.md +33 -0
  80. package/skills/plitzi-authoring/reference/layouts.md +15 -18
  81. package/skills/plitzi-authoring/reference/plugins.md +3 -3
  82. package/skills/plitzi-authoring/reference/recipes.md +25 -0
  83. package/skills/plitzi-authoring/reference/review-checklist.md +2 -1
  84. package/skills/plitzi-authoring/reference/rules.md +61 -0
  85. package/skills/plitzi-authoring/reference/snippets-and-export.md +2 -2
  86. package/skills/plitzi-authoring/reference/structure.md +4 -3
  87. package/skills/plitzi-authoring/reference/testing.md +1 -1
  88. package/skills/plitzi-authoring/reference/validation.md +1 -1
@@ -1,6 +1,6 @@
1
1
  import { createActionResolver } from "../actions/runtime/renderResolver.js";
2
2
  import { createConnectorResolver } from "../connectors/resolver.js";
3
- import { dataFileResolver, dataLookupResolver, publicFileResolver } from "./publicFileResolver.js";
3
+ import { dataLookupResolver, publicFileResolver } from "./publicFileResolver.js";
4
4
  import { resolveRscData } from "./resolveRscData.js";
5
5
  //#region src/modules/rsc/connectorRscData.ts
6
6
  /**
@@ -18,12 +18,11 @@ import { resolveRscData } from "./resolveRscData.js";
18
18
  *
19
19
  * A deployment that resolves server elements some other way still supplies its own `getRscData`, and that one wins.
20
20
  */
21
- var connectorRscData = ({ connectors, actions, publicDir, dataDir, elementTimeoutMs }) => {
21
+ var connectorRscData = ({ connectors, actions, publicDir, data, elementTimeoutMs }) => {
22
22
  const resolveConnector = connectors ? createConnectorResolver(connectors) : void 0;
23
23
  const resolveAction = actions ? createActionResolver(actions.lookups, actions.module) : void 0;
24
24
  const resolvePublicFile = publicDir ? publicFileResolver(publicDir) : void 0;
25
- const dataLookup = actions?.lookups.getData;
26
- const resolveDataFile = dataDir ? dataFileResolver(dataDir) : dataLookup ? dataLookupResolver((spaceId, at) => dataLookup(spaceId, at)) : void 0;
25
+ const resolveDataFile = data ? dataLookupResolver(data) : void 0;
27
26
  /**
28
27
  * An element names ONE producer, and which one decides how its data is fetched.
29
28
  *
@@ -14,6 +14,7 @@ export declare const withPageLocation: (req: SSRRequest) => SSRRequest;
14
14
  * - main environment: no-store (development, always fresh)
15
15
  * - Authenticated requests: Cache-Control: private, max-age=<ttl>
16
16
  * - Unauthenticated requests: Cache-Control: public, max-age=<ttl>
17
+ * - A request carrying `Cache-Control: no-cache` is resolved again, and its answer replaces the one cached.
17
18
  */
18
19
  export declare const handleRsc: (req: SSRRequest, res: SSRResponseHelpers, config: SSRPageServerConfig, _pluginManager: PluginManager, cache?: TtlCache<string>,
19
20
  /** The request's own: a browser that stops waiting stops the elements it asked about. */
@@ -22,6 +22,15 @@ var meterRsc = async (req, config, spaceId, environment, revision, cached) => {
22
22
  });
23
23
  } catch {}
24
24
  };
25
+ /**
26
+ * Whether the browser asked for the server's answer as of now — a refresh that exists because something changed (a
27
+ * write, an explicit reload), which the answer kept for the last `cacheTtlMs` would undo.
28
+ *
29
+ * It skips reading the cache, never writing it: the answer it gets is the newest there is, and the next visitor is
30
+ * served it. Nobody gains anything they could not already have by changing the query string, which misses the cache
31
+ * as well.
32
+ */
33
+ var asksFresh = (req) => (req.headers["cache-control"] ?? "").split(",").some((directive) => directive.trim().toLowerCase() === "no-cache");
25
34
  /** The endpoint's own parameters: what it is asked about, never input to what it resolves. */
26
35
  var ENDPOINT_PARAMS = /* @__PURE__ */ new Set(["location", "ids"]);
27
36
  /**
@@ -70,6 +79,7 @@ var withPageLocation = (req) => {
70
79
  * - main environment: no-store (development, always fresh)
71
80
  * - Authenticated requests: Cache-Control: private, max-age=<ttl>
72
81
  * - Unauthenticated requests: Cache-Control: public, max-age=<ttl>
82
+ * - A request carrying `Cache-Control: no-cache` is resolved again, and its answer replaces the one cached.
73
83
  */
74
84
  var handleRsc = async (req, res, config, _pluginManager, cache, signal) => {
75
85
  if (!config.adapters.getRscData) {
@@ -110,7 +120,7 @@ var handleRsc = async (req, res, config, _pluginManager, cache, signal) => {
110
120
  const isAuthenticated = !!req.ctx.user;
111
121
  const cacheControl = environment === "main" || uncached ? "no-store" : isAuthenticated ? `private, max-age=${Math.floor(ttlMs / 1e3)}` : `public, max-age=${Math.floor(ttlMs / 1e3)}`;
112
122
  const cacheKey = environment !== "main" && !uncached ? buildRscCacheKey(spaceId, environment, revision, req.ctx.user?.id, idsParam, req) : void 0;
113
- const cached = cacheKey ? cache?.get(cacheKey) : void 0;
123
+ const cached = cacheKey && !asksFresh(req) ? cache?.get(cacheKey) : void 0;
114
124
  if (!previewing) await meterRsc(req, config, spaceId, environment, revision, !!cached);
115
125
  if (cached) {
116
126
  res.setHeader("Content-Type", "application/json; charset=utf-8");
@@ -7,13 +7,9 @@ import { SpaceRevision } from '@plitzi/sdk-shared';
7
7
  */
8
8
  export declare const publicFileResolver: (publicDir: string) => RscElementResolver;
9
9
  /**
10
- * A server provider whose `query` is `/data/<file>`, read from `dataDir` — the project's own data, which is never
11
- * served as a file: nobody downloads the folder. What the provider reads is in the page it renders.
12
- */
13
- export declare const dataFileResolver: (dataDir: string) => RscElementResolver;
14
- /**
15
- * A server provider whose `query` is `/data/<file>`, answered from the space's own data as Plitzi keeps it — of the
16
- * version being rendered, read off the same deployment record an action's run is (`req.ctx.spaceDeployment`), so a
17
- * published page reads the data it was published with. What `dataFileResolver` is for a folder, for a platform.
10
+ * A server provider whose `query` is `/data/<file>`, answered from the space's own data — of the version being
11
+ * rendered, read off the same deployment record an action's run is (`req.ctx.spaceDeployment`), so a published page
12
+ * reads the data it was published with. The lookup is the platform's, or a self-hosted server's `dataDir`
13
+ * (`dataDirLookup`): the data is never served as a file, and what the provider reads is in the page it renders.
18
14
  */
19
15
  export declare const dataLookupResolver: (getData: (spaceId: number, at: SpaceRevision) => Promise<Record<string, string> | undefined>) => RscElementResolver;
@@ -1,5 +1,5 @@
1
- import path from "node:path";
2
1
  import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
3
  import { PROJECT_DATA_PREFIX } from "@plitzi/sdk-shared/server/rsc/projectData";
4
4
  //#region src/modules/rsc/publicFileResolver.ts
5
5
  /** Where a provider's `query` is a path: a URL, a protocol-relative one or one with `{{tokens}}` is not. */
@@ -44,14 +44,10 @@ var fileResolver = (folder, prefix) => {
44
44
  */
45
45
  var publicFileResolver = (publicDir) => fileResolver(publicDir, "/");
46
46
  /**
47
- * A server provider whose `query` is `/data/<file>`, read from `dataDir` — the project's own data, which is never
48
- * served as a file: nobody downloads the folder. What the provider reads is in the page it renders.
49
- */
50
- var dataFileResolver = (dataDir) => fileResolver(dataDir, PROJECT_DATA_PREFIX);
51
- /**
52
- * A server provider whose `query` is `/data/<file>`, answered from the space's own data as Plitzi keeps it — of the
53
- * version being rendered, read off the same deployment record an action's run is (`req.ctx.spaceDeployment`), so a
54
- * published page reads the data it was published with. What `dataFileResolver` is for a folder, for a platform.
47
+ * A server provider whose `query` is `/data/<file>`, answered from the space's own data — of the version being
48
+ * rendered, read off the same deployment record an action's run is (`req.ctx.spaceDeployment`), so a published page
49
+ * reads the data it was published with. The lookup is the platform's, or a self-hosted server's `dataDir`
50
+ * (`dataDirLookup`): the data is never served as a file, and what the provider reads is in the page it renders.
55
51
  */
56
52
  var dataLookupResolver = (getData) => {
57
53
  return async ({ element, spaceId, environment, req }) => {
@@ -74,4 +70,4 @@ var dataLookupResolver = (getData) => {
74
70
  };
75
71
  };
76
72
  //#endregion
77
- export { dataFileResolver, dataLookupResolver, publicFileResolver };
73
+ export { dataLookupResolver, publicFileResolver };
@@ -19,7 +19,7 @@ export declare const inspectRuntime: (bytes: Uint8Array) => {
19
19
  };
20
20
  /**
21
21
  * A project's runtime module, as a server of its own runs it — `src/runtime/index.ts`, or what `build` compiled it to —
22
- * or nothing when the project has none: a project starts one by writing it (`plitzi add runtime`), with nothing to wire.
22
+ * or nothing when the project has none: a project starts one by writing it (`plitzi runtime add`), with nothing to wire.
23
23
  */
24
24
  export declare const loadRuntimeModule: (file: string) => Promise<SpaceRuntime | undefined>;
25
25
  /**
@@ -1,8 +1,8 @@
1
1
  import { isRecord } from "@plitzi/sdk-shared/helpers/isRecord";
2
2
  import { createHash } from "node:crypto";
3
+ import fs from "node:fs/promises";
3
4
  import path from "node:path";
4
5
  import esbuild from "esbuild";
5
- import fs from "node:fs/promises";
6
6
  import { pathToFileURL } from "node:url";
7
7
  import { gunzipSync, gzipSync } from "node:zlib";
8
8
  //#region src/modules/runtime/bundle.ts
@@ -93,7 +93,7 @@ var runtimeOf = (loaded) => {
93
93
  };
94
94
  /**
95
95
  * A project's runtime module, as a server of its own runs it — `src/runtime/index.ts`, or what `build` compiled it to —
96
- * or nothing when the project has none: a project starts one by writing it (`plitzi add runtime`), with nothing to wire.
96
+ * or nothing when the project has none: a project starts one by writing it (`plitzi runtime add`), with nothing to wire.
97
97
  */
98
98
  var loadRuntimeModule = async (file) => {
99
99
  try {
@@ -14,6 +14,7 @@ import { publishSpaceDocument } from "./spaceDocument.js";
14
14
  import { registerExternalPlugins } from "./registerExternalPlugins.js";
15
15
  import { reportMissingPlugins } from "./reportMissingPlugins.js";
16
16
  import { resolvePageSeo } from "./resolvePageSeo.js";
17
+ import { SPACE_TOKEN_PARAM } from "../../core/auth/credentials.js";
17
18
  import { imagesPathOf } from "../../core/http/stages/images.js";
18
19
  import { sdkAssetVersion } from "../../core/sdkAssets.js";
19
20
  import { buildServerInfo } from "../../helpers/buildServerInfo.js";
@@ -165,7 +166,14 @@ var prepareRender = async (req, config, spaceId, environment, revision, pluginMa
165
166
  * Not for a draft, which no other process could produce when the fetch reaches it, and not for a deployment's own
166
167
  * template, which was written for the space inline; a page with no script needs no space at all.
167
168
  */
168
- const spaceDocumentPath = offlineData !== void 0 && offlineDataOverride === void 0 && config.templateFn === void 0 && config.ssrOnly !== true ? publishSpaceDocument(spaceId, environment, spaceDocument(offlineData)) : void 0;
169
+ const publishedDocument = offlineData !== void 0 && offlineDataOverride === void 0 && config.templateFn === void 0 && config.ssrOnly !== true ? publishSpaceDocument(spaceId, environment, spaceDocument(offlineData)) : void 0;
170
+ /**
171
+ * A page that named its space with a token in its address (`?access-token=`, how a host serving many spaces is asked
172
+ * for one) has its document asked for the same way: fetched bare, the host's own space answered it — another space's
173
+ * document, hydrated over this one's page.
174
+ */
175
+ const spaceToken = req.query[SPACE_TOKEN_PARAM];
176
+ const spaceDocumentPath = publishedDocument && spaceToken ? `${publishedDocument}?${SPACE_TOKEN_PARAM}=${encodeURIComponent(spaceToken)}` : publishedDocument;
169
177
  const offlineDataStr = hydrationPayload(offlineData, {
170
178
  offlineMode: true,
171
179
  environment,
@@ -221,7 +229,7 @@ var prepareRender = async (req, config, spaceId, environment, revision, pluginMa
221
229
  ...deferred ? { deferred } : {}
222
230
  };
223
231
  });
224
- const vendorJs = (debugAuthorized ? "/sdk-assets/plitzi-sdk-dev-vendor.js" : "/sdk-assets/plitzi-sdk-vendor.js") + v;
232
+ const vendorJs = (debugRendered ? "/sdk-assets/plitzi-sdk-dev-vendor.js" : "/sdk-assets/plitzi-sdk-vendor.js") + v;
225
233
  return {
226
234
  componentProps: {
227
235
  plugins: Object.keys(pluginComponents).length > 0 ? pluginComponents : void 0,
@@ -2,8 +2,8 @@ import { serverLog } from "../../helpers/serverLog.js";
2
2
  import { fetchOutbound } from "../../helpers/outboundGuard.js";
3
3
  import { writeFileAtomic } from "../../helpers/atomicFile.js";
4
4
  import crypto from "node:crypto";
5
- import path from "node:path";
6
5
  import fs from "node:fs/promises";
6
+ import path from "node:path";
7
7
  //#region src/modules/ssr/registerExternalPlugins.ts
8
8
  var MANIFEST_TTL_MS = 6e5;
9
9
  var manifestCache = /* @__PURE__ */ new Map();
@@ -0,0 +1 @@
1
+ export {};
@@ -77,9 +77,10 @@
77
77
  <% } %>
78
78
 
79
79
  <% if (!locals.ssrOnly) { %>
80
- <link rel="modulepreload" href="<%= locals.react || '' %>" crossorigin />
81
- <link rel="modulepreload" href="<%= locals.reactDom || '' %>" crossorigin />
82
-
80
+ <%#
81
+ The import map before anything that loads a module: a browser that fetched one already (a modulepreload counts)
82
+ ignores a map that comes after it, and the SDK's `import "react"` then resolves to nothing.
83
+ %>
83
84
  <script type="importmap">
84
85
  {
85
86
  "imports": {
@@ -92,6 +93,9 @@
92
93
  }
93
94
  }
94
95
  </script>
96
+
97
+ <link rel="modulepreload" href="<%= locals.react || '' %>" crossorigin />
98
+ <link rel="modulepreload" href="<%= locals.reactDom || '' %>" crossorigin />
95
99
  <% } %>
96
100
 
97
101
  <%# The space, fetched beside the page while the scripts load, rather than carried in it. %>
@@ -1,7 +1,7 @@
1
1
  import { writeFileAtomic } from "../helpers/atomicFile.js";
2
+ import fs from "node:fs/promises";
2
3
  import path from "node:path";
3
4
  import esbuild from "esbuild";
4
- import fs from "node:fs/promises";
5
5
  import { pluginAssetLoaders, pluginImportQueries } from "@plitzi/sdk-shared/plugins/bundle";
6
6
  import { inPluginLayer } from "@plitzi/sdk-shared/style/cssLayers";
7
7
  //#region src/plugins/compile.ts
@@ -1,6 +1,6 @@
1
1
  import { copyFileAtomic, writeFileAtomic } from "../helpers/atomicFile.js";
2
- import path from "node:path";
3
2
  import fs from "node:fs/promises";
3
+ import path from "node:path";
4
4
  import { inPluginLayer } from "@plitzi/sdk-shared/style/cssLayers";
5
5
  //#region src/plugins/copy.ts
6
6
  var copyPlugin = async (src, destDir, filename) => {
@@ -5,8 +5,8 @@ import { copyPlugin, copyPluginStylesheet } from "./copy.js";
5
5
  import { detectAction, isComponentSource } from "./detect.js";
6
6
  import { assertPluginSources } from "./validate.js";
7
7
  import { createHash } from "node:crypto";
8
- import path from "node:path";
9
8
  import fs from "node:fs/promises";
9
+ import path from "node:path";
10
10
  //#region src/plugins/manager.ts
11
11
  var META_FILE = "meta.json";
12
12
  var DEFAULT_TTL_MS = 6048e5;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The server of a project `@plitzi/cli` writes: its `src/main.ts` hands `serveProject` the space, its actions and its
3
+ * options, and the rest — the port, the plugins, the functions, the runtime, the files, the reloads while developing —
4
+ * comes from where the project keeps each part (`@plitzi/sdk-shared/project/paths`). A fix to it arrives with the
5
+ * package, never as a file to upgrade.
6
+ */
7
+ export { serveProject } from './modules/project/serveProject';
8
+ export type { ProjectServerOptions, ProjectSpace, ServedProject, ServeProjectOptions } from './modules/project/serveProject';
9
+ export type { AuthoredDocuments } from './modules/project/watchSpace';
@@ -0,0 +1,2 @@
1
+ import { serveProject } from "./modules/project/serveProject.js";
2
+ export { serveProject };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plitzi/sdk-server",
3
- "version": "0.38.6",
3
+ "version": "0.38.8",
4
4
  "license": "AGPL-3.0",
5
5
  "files": [
6
6
  "dist",
@@ -75,6 +75,16 @@
75
75
  "import": "./dist/runtime.js",
76
76
  "default": "./dist/runtime.js"
77
77
  },
78
+ "./project": {
79
+ "types": "./dist/project.d.ts",
80
+ "import": "./dist/project.js",
81
+ "default": "./dist/project.js"
82
+ },
83
+ "./env": {
84
+ "types": "./dist/env.d.ts",
85
+ "import": "./dist/env.js",
86
+ "default": "./dist/env.js"
87
+ },
78
88
  "./functions-api.d.ts": "./dist/functions-api.d.ts"
79
89
  },
80
90
  "repository": {
@@ -82,10 +92,12 @@
82
92
  "url": "https://github.com/plitzi/plitzi-workspace.git"
83
93
  },
84
94
  "type": "module",
85
- "sideEffects": false,
95
+ "sideEffects": [
96
+ "./dist/env.js"
97
+ ],
86
98
  "dependencies": {
87
- "@plitzi/plitzi-sdk": "0.38.6",
88
- "@plitzi/sdk-shared": "0.38.6",
99
+ "@plitzi/plitzi-sdk": "0.38.8",
100
+ "@plitzi/sdk-shared": "0.38.8",
89
101
  "ejs": "^6.0.1",
90
102
  "esbuild": "^0.28.2",
91
103
  "jsonwebtoken": "^9.0.3",
@@ -13,7 +13,7 @@ const space: SpaceSpec = {
13
13
  classes: { card }, // styles() declarations, by their own name; styles('card', tw('p-4 rounded-xl'))
14
14
  computed: { total: '{{ … }}' }, // values read anywhere as computed.total
15
15
  flags: { beta: { description, value: false, rules: [] } },
16
- customCss: '@keyframes …', // keyframes and what no property says
16
+ keyframes: { blink: { '50%': { opacity: 0 } } }, // a class plays them: animation: 'blink 1s infinite'
17
17
  layouts: [{ id: 'shell', body }], // chrome shared by pages
18
18
  components: [{ id, props, slots, root }], // a block placed many times with different content
19
19
  pages: [{ id, name, slug: '', layout: { id: 'shell', slot: 'main' }, body: [] }] // slot: where the body goes
@@ -81,7 +81,7 @@ bind: { alt: 'p.data.title' } // other attribut
81
81
  bind: [bindTemplate('content', 'p.data.items', '{{ source|length }} items')]
82
82
  bind: [bindTemplate('items', 'p.data.rows', '{{ source|filter(r => r.on) }}', { returns: 'value' })]
83
83
  bind: [variantFrom(pill, 'rows.item.status')] // a variant chosen by the data
84
- bind: [activeOn(navLink, 'about')] // the current page's link
84
+ current: 'section' // a link lit on its page and the pages under it
85
85
  bind: [activeWhen(dot, '{{ list_dots.index == state.slide }}')] // a variant while a condition holds
86
86
  visible: 'p.data.signedIn' // shown while true; '!p.data.signedIn' inverts
87
87
  visible: { source: 'p.data.rows', template: '{{ source is defined and source|length == 0 }}' }
@@ -9,173 +9,59 @@ description: >-
9
9
 
10
10
  # Authoring Plitzi spaces
11
11
 
12
- A space is two JSON documents — a schema and a style — full of cross-referenced ids. **Never write them by hand.**
13
- `@plitzi/sdk-authoring` derives every id, class name, parent link, breakpoint map and flow chain from a small
14
- declaration, and refuses a declaration that would not render.
12
+ A space is two JSON documents full of cross-referenced ids — **never write them by hand**. `@plitzi/sdk-authoring`
13
+ derives them from a small declaration, and refuses one that would not render.
15
14
 
16
15
  ```ts
17
- import { authorSpace, container, heading, styles, text } from '@plitzi/sdk-authoring';
16
+ import { heading, styles, text } from '@plitzi/sdk-authoring';
18
17
 
19
- const page = styles('page', { display: 'flex', 'flex-direction': 'column', padding: '96px 24px', gap: '16px' });
18
+ const page = styles('page', { display: 'flex', 'flex-direction': 'column', gap: '16px' });
20
19
 
21
20
  export const space = {
22
21
  name: 'My space',
23
22
  permanentUrl: 'my-space',
24
23
  pages: [{ name: 'Home', slug: '', class: page, body: [heading('Hello', { subType: 'h1' }), text('A paragraph.')] }]
25
24
  };
26
-
27
- const { schema, style, warnings } = authorSpace(space);
28
25
  ```
29
26
 
30
- **Start with [the cheatsheet](CHEATSHEET.md)** — the factories, fields, steps and the problems met most, on one page —
31
- and open a reference below only when the task names its subject. What an element takes, a step's params, what a
32
- trigger hands its flow: `npx @plitzi/cli explain <name>` answers in a few lines (over MCP, `plitzi://explain/<name>`).
33
- The published `.d.ts` documents the rest — search it by name, never read it whole.
34
-
35
- This skill writes a space that lives in code. One that lives on Plitzi — edited in the builder, published from there —
36
- is edited through the Plitzi MCP server instead, never both on one space; `plitzi create --from <space>` turns the
37
- second into the first. If the MCP asks for a sign-in nobody can give, a project in code needs no account.
27
+ A space on Plitzi — edited in the builder — is edited through the Plitzi MCP server instead, never both on one space.
38
28
 
39
29
  ## How to work
40
30
 
41
- 1. **Read before you write.** Open `src/space/index.ts` (and whatever it imports) and find the layout, the classes and the
42
- helpers already there. Extend them; do not add a second way of doing something the space already does.
43
- 2. **Change the declaration, then author it.** `npm run author` (or a restart) runs `authorSpace` — not `doctor`, which
44
- is the project's. It checks every field, value, template, name, param and page link, and a refusal says what to
45
- write instead: do exactly that. Never work around a check, cast past it, or move the logic into a plugin to escape
46
- it. The linter's problems come as ONE list — fix every line before running again. How it checks, and what it
47
- cannot see: [validation](reference/validation.md).
48
- 3. **Read every warning, then every suggestion.** A warning names something written that will not do what it says —
49
- zero warnings is the bar. A suggestion (`[suggest]`) names a shorter way to the same page — a layout, a component,
50
- a `list`, a button's own `content` — or what nothing uses. Take them, the ones that save the most first
51
- ([efficiency](reference/efficiency.md)). Then `npm run lint:space`: how the source is written — long files, rows of
52
- data inline, colours that are not tokens — at file:line. Leave both clean.
53
- 4. **Look at it.** `npm run check -- /about --width 1440,390` says in text whether the page is whole — every element on
54
- screen, nothing overflowing, a clean console; `npm run shot -- /about --width 390 --scheme dark` is the picture
55
- (`--frames 4` shows what moves); `npm run visual` runs the checks. Look at desktop, tablet and mobile, light and dark, and the page while its data is still loading.
56
- 5. **Go through the [review checklist](reference/review-checklist.md) before saying it is done.** It is the feedback a
57
- reviewer gives on every change, written down so you do not need to hear it.
31
+ 1. **Find, then write.** `npx plitzi element where <id|class|words>` answers the file, the line and the call: edit there, and
32
+ extend what the space already has. People move files — never keep a note of where something is.
33
+ 2. **Author.** `npm run author`. A refusal says what to write instead: do exactly that. Every problem comes at once —
34
+ fix them all before running again. Zero warnings; take the suggestions, the biggest first.
35
+ 3. **Look.** `npm run check -- /path --width 1440,390` says in text whether the page is whole; `npm run shot` only
36
+ when the text is not enough.
37
+ 4. **Before saying done:** `npm run lint:space` clean, and the [review checklist](reference/review-checklist.md).
58
38
 
59
- ## The rules that matter most
39
+ What a name means — an element, a step, a trigger, a code, any export (`pageFamily`, `SpaceSpec`): `npx plitzi explain
40
+ <name>`, in a few lines. Never read the `.d.ts`.
60
41
 
61
- 1. **Name what is referred to.** Give an `id` to every element a binding, a flow or a test addresses. Ids are ONE
62
- namespace for the whole space — layouts and every page share it — so a helper that runs more than once builds inside
63
- `scope('promos', ref => …)`: every id in it is prefixed (`promos-panel`), and `ref('slides')` names one in full. An `id` is the element's name in the
64
- space (`data-plitzi-el` in the page); the DOM id a URL's `#fragment` lands on is its `anchor`.
65
- 2. **Share with classes, layouts and components, never with copies.** A look used twice is a `styles()` class.
66
- Chrome shown on several pages — a header, a sidebar, a footer — is a **layout** the pages name, written once. A block
67
- placed many times with different content — a product card, a testimonial — is a **component** placed with
68
- `component(id, { props })`. A menu, a card grid, a list of steps is DATA, never a block pasted per item: a short
69
- menu mapped in code is fine; rows — cards, dishes, tiles — are one `list` (`items: [ … ]`) whose row is written
70
- once. A link or a button says its words as
71
- its own `content`, not through a `text` inside it. Pages of one shape are one `pageFamily` over data. See
72
- [layouts](reference/layouts.md), [components](reference/components.md), [structure](reference/structure.md) and
73
- [efficiency](reference/efficiency.md).
74
- 3. **One element, one base selector.** An element takes a shared `class` OR its own `css`, never both — authoring
75
- refuses the pair. "This class plus one thing" is the class with rules on top, last in the list:
76
- `class: [cover, { opacity: '0.25' }]` (needs the element's `id`; it becomes the class `<id>--own`). A look that
77
- never changes is its own class.
78
- 4. **Visible by default; decide which way the logic flips.** An element is on screen unless something hides it. When
79
- the logic REVEALS it (hidden → shown: an empty state, a "get started" card, an admin-only panel), it starts hidden —
80
- `visible: 'source'`, or `visible: false` plus a binding for a computed condition — so it never flashes while data
81
- loads (`condition-starts-visible` warns). When a flag HIDES it (shown → hidden: a sidebar label until the sidebar
82
- is folded), it keeps the default and an absent flag must leave it shown. See
83
- [data and visibility](reference/data-and-visibility.md). What a PERSON switches on — a feature in beta, a rollout —
84
- is a feature flag, not a visibility: gated off, it is not rendered at all. See [feature flags](reference/feature-flags.md).
85
- 5. **Know where a template is evaluated.** A binding's template (`bindTemplate`) and a step's params evaluate full
86
- Twig. An ATTRIBUTE resolves a name with filters (`{{ list_games.item.slug }}`, `{{ post.slug|url_encode }}`) against
87
- the sources around the element; a condition there is used as written. Inside any template a source is spelled in
88
- full — `apiContainer_stats`, `list_rows` — and a query param is `navigation.queryParams.x`. A template that feeds a
89
- list its `items` hands over its value (`returns: 'value'`). `authorSpace` refuses a template it cannot read and a
90
- name nothing answers to. See [templates](reference/templates.md).
91
- 6. **Tokens, not colours.** Every colour is a variable of the space with a `light` and a `dark` value; both themes are
92
- checked from the first commit.
93
- 7. **Times in UTC, and say so.** Format with an explicit zone (`|date('j M · H:i', 'UTC')`) and print "UTC" beside it:
94
- a page rendered on a server and hydrated in a browser in another zone must agree on the hour.
95
- 8. **Breakpoints are ranges.** `tablet` (48–64rem) and `mobile` (up to 48rem) each inherit only from `desktop`; a rule
96
- meant for both is written under `compact`.
97
- 9. **Never invent data.** Numbers, names and states on screen come from a source. A panel with nothing true to say is
98
- an empty state, not a placeholder figure. With no backend, the data is JSON read by an `apiContainer`: a server
99
- project's `src/data/*.json`, read on the server and never served; with no server, `public/data/*.json`, which is
100
- public — see [data and visibility](reference/data-and-visibility.md).
101
- 10. **Never hand-write** `flat`, derived ids, `styleSelectors`, `beforeNode`/`afterNode`/`flowId`, or a
102
- `styleVariant` binding's key — use the factories, `variantFrom` and `activeOn`.
103
- 11. **A switch is named for how it leaves its default.** `toggleState` turns a key nobody has set yet ON, so a key
104
- named for the default (`showPlates`, on by default) takes a first click to do nothing: name it `platesOff`, and read
105
- every switch through `computed` (`plates: '{{ state.platesOff ? false : true }}'`) so its default shows before
106
- anybody touches it.
107
- 12. **Hand plugins over as their declarations**: `authorSpace(space, { plugins: [declaration] })` holds a plugin —
108
- its own type, or a `custom({ renderType })` host — to the events, actions and attributes it declares. See
109
- [plugins](reference/plugins.md).
110
- 13. **Usable without sight.** Screen readers and browser agents (Claude in Chrome) find a page's controls in its
111
- accessibility tree: every button and link has words (an icon-only button a `title`), every field a `label`
112
- (`hideLabel: true` hides it and keeps the name), every image an `alt` or `decorative: true`. Clicks go on a
113
- `button` — it holds children, so a whole card can be one — or a `link`, never on a container. See
114
- [accessibility](reference/accessibility.md).
115
- 14. **Motion that stays smooth.** Animate `opacity` and `transform` only; no animated blur, shadow or size. See
116
- [motion](reference/colours-and-motion.md).
42
+ ## Five rules authoring cannot check for you
117
43
 
118
- ## Recipes
44
+ 1. **Name what is referred to.** An `id` for every element a binding, a flow or a test addresses; ids are one namespace,
45
+ so a helper that runs twice builds inside `scope('promos', ref => …)`.
46
+ 2. **Share, never copy.** A look used twice is a class; chrome on several pages a layout; a block placed again with
47
+ other content a component; rows of data one `list`.
48
+ 3. **What is revealed starts hidden** (`visible: 'source'`), so it never flashes while its data loads.
49
+ 4. **Know where a template runs.** A binding and a step's params are full Twig; an attribute resolves a name with
50
+ filters. A source is spelled in full: `apiContainer_stats`.
51
+ 5. **Tokens, not colours** — a variable with a `light` and a `dark` value.
119
52
 
120
- Each is a whole file that authors with no warning — CI holds it to that. Copy the one that fits, then change the names.
53
+ The rest, and why: [rules](reference/rules.md).
121
54
 
122
- | To… | Open |
123
- | --- | --- |
124
- | Show data with no backend: a JSON file, a card per row, a count, an empty state, a computed value | [recipes/show-data.ts](recipes/show-data.ts) |
125
- | Data typed by a sample of it: completed and checked paths, typed rows | [recipes/typed-data.ts](recipes/typed-data.ts) |
126
- | Data read by the page server (`--mode server`) | [recipes/server-data.ts](recipes/server-data.ts) |
127
- | An accordion, one answer open at a time | [recipes/accordion.ts](recipes/accordion.ts) |
128
- | Filter and sort a list from a select | [recipes/filter-a-list.ts](recipes/filter-a-list.ts) |
129
- | A page per record (`/products/:slug`), and "not found" | [recipes/detail-page.ts](recipes/detail-page.ts) |
130
- | Link to a section of a page (`/#plans`) | [recipes/link-to-a-section.ts](recipes/link-to-a-section.ts) |
131
- | A carousel: slides with arrows, dots and autoplay; a marquee; a row that swipes | [recipes/carousel.ts](recipes/carousel.ts) |
132
- | Do something every few seconds — a ticker, a poll, a slideshow by hand | [recipes/every-few-seconds.ts](recipes/every-few-seconds.ts) |
133
- | A marquee that scrolls for ever and stops under the pointer | [recipes/marquee.ts](recipes/marquee.ts) |
134
- | A row that scrolls sideways, with arrows that hide at its ends | [recipes/scroll-a-row.ts](recipes/scroll-a-row.ts) |
135
- | A link built from a row: WhatsApp with the product's name, `mailto:` | [recipes/link-with-data.ts](recipes/link-with-data.ts) |
136
- | React to a click or a submit; a modal; a switch | [recipes/forms-and-modals.ts](recipes/forms-and-modals.ts) |
137
- | A feature behind a flag | [recipes/feature-flag.ts](recipes/feature-flag.ts) |
138
- | An element of your own (a plugin), checked | [recipes/plugin.ts](recipes/plugin.ts) |
139
- | Controls a screen reader and a browser agent can use | [recipes/usable-without-sight.ts](recipes/usable-without-sight.ts) |
140
- | Tokens, a shared class, "the class plus one thing", tablet and phone at once | [recipes/style-a-page.ts](recipes/style-a-page.ts) |
141
- | A design in Tailwind classes, kept as classes the builder edits | [recipes/from-tailwind.ts](recipes/from-tailwind.ts) |
142
- | A page from elsewhere in a frame (`embed`: a map, a player) and a drawing of your own (`svg`) | [recipes/embed-and-svg.ts](recipes/embed-and-svg.ts) |
55
+ ## What to read
143
56
 
144
- ## References
57
+ Start with [the cheatsheet](CHEATSHEET.md), then only what the task names:
145
58
 
146
- | Read | When |
59
+ | The task | Read |
147
60
  | --- | --- |
148
- | [elements-and-styles.md](reference/elements-and-styles.md) | Any element or CSS: factories, fields, classes, states, variants, tokens, fonts, lists, links |
149
- | [colours-and-motion.md](reference/colours-and-motion.md) | Colours for both themes, tokens, keyframes, and motion that stays smooth (good practices) |
150
- | [tailwind.md](reference/tailwind.md) | A design written in Tailwind classes: `tw()`, its breakpoints and states, what it refuses |
151
- | [layouts.md](reference/layouts.md) | Anything shown on more than one page; menus; reducing duplication of elements and styles |
152
- | [efficiency.md](reference/efficiency.md) | The same page with fewer elements: the suggestions `authorSpace` makes, and the short way for each long one |
153
- | [components.md](reference/components.md) | One block placed many times — a card, a tier, a testimonial: props, slots, binding a row into one, why it is closed |
154
- | [data-and-visibility.md](reference/data-and-visibility.md) | Bindings, providers, offline data, loading/empty/error states, live data, caching, showing and hiding, kept state |
155
- | [kept-state.md](reference/kept-state.md) | State that outlives a reload: `keepState`, transient and painted keys |
156
- | [feature-flags.md](reference/feature-flags.md) | Switching a part of the space on or off — a beta, a rollout, the old version kept until the new one ships |
157
- | [lists.md](reference/lists.md) | Rendering rows, filtering and sorting them, a detail page for one record, carousels |
158
- | [typed-sources.md](reference/typed-sources.md) | Data typed by a sample of it: `source()`, typed rows, `twig` for templates |
159
- | [validation.md](reference/validation.md) | How `authorSpace` checks, the loop that wastes no attempts, and what it cannot see |
160
- | [authoring-errors.md](reference/authoring-errors.md) | What `authorSpace` refuses or warns about, and what to write instead |
161
- | [templates.md](reference/templates.md) | Any `{{ … }}` or `{% … %}`: where it runs, naming sources, filters, tests, dates |
162
- | [flows.md](reference/flows.md) | Clicks, submits, page loads, every few seconds, server actions, modals, state |
163
- | [realtime.md](reference/realtime.md) | Pages that see each other: channels, presence, cursors, a shared board, who may hear a topic |
164
- | [plugins.md](reference/plugins.md) | A component of your own: props, binding them, writing state, channels, registering, behaving in the builder |
165
- | [drawing.md](reference/drawing.md) | A plugin that draws or animates: a canvas sized to the device, WebGL shaders that say why they failed, a loop that stops when nobody sees it |
166
- | [structure.md](reference/structure.md) | A space bigger than one screen: files, helpers, naming, keeping it short |
167
- | [testing.md](reference/testing.md) | Any test: `inspectPage` (one call, every problem), handles, fixtures, catching a flash from the first frame, shortcuts, counting renders |
168
- | [performance.md](reference/performance.md) | A page with many elements, a busy flow, something that feels slow: what renders, what it costs, how to measure it |
169
- | [snippets-and-export.md](reference/snippets-and-export.md) | Publishing a snippet; turning an exported JSON — or a server action — into code |
170
- | [accessibility.md](reference/accessibility.md) | Icon buttons, fields, images, clickable cards, toggles, headings, landmarks, a canvas — anything a screen reader or a browser agent has to use |
171
- | [review-checklist.md](reference/review-checklist.md) | Before you say it is done |
172
-
173
- **What to read for a kind of task** — the cheatsheet, then only these:
174
-
175
- - A site — landing, catalogue, blog: elements-and-styles, layouts, components, lists.
176
- - An app with state and actions: data-and-visibility, templates, flows — realtime when pages see each other.
177
- - A component of your own: plugins.
178
- - A refusal or a warning: search authoring-errors.md for its code — never read it whole.
179
- - Tests: testing.
180
-
181
- Nothing else needs reading up front.
61
+ | A site — landing, catalogue, blog | [elements-and-styles](reference/elements-and-styles.md), [layouts](reference/layouts.md), [components](reference/components.md), [lists](reference/lists.md) |
62
+ | An app with state and actions | [data-and-visibility](reference/data-and-visibility.md), [templates](reference/templates.md), [flows](reference/flows.md) |
63
+ | A component of your own | [plugins](reference/plugins.md) |
64
+ | A whole working file to copy | [recipes](reference/recipes.md) |
65
+ | A refusal or a warning | `npx plitzi explain <code>` |
66
+ | Tests | [testing](reference/testing.md) |
67
+ | Anything else | [the index](reference/index.md): every reference, and when |
@@ -1,5 +1,5 @@
1
1
  /**
2
- * An element of your own (a plugin): authored from its declaration — the one `plitzi add plugin` writes — so a flow on
2
+ * An element of your own (a plugin): authored from its declaration — the one `plitzi plugin add` writes — so a flow on
3
3
  * its events, a step to its actions and its attributes are all checked. Hand the declaration to `authorSpace` too.
4
4
  */
5
5
  import {
@@ -27,8 +27,8 @@ formControl({ name: 'q', label: 'Search the docs', hideLabel: true, placeholder:
27
27
  formControl({ name: 'tint', subType: 'color', label: 'Highlight colour', hideLabel: true })
28
28
  ```
29
29
 
30
- A `placeholder` names only a field you type into, and only until something is typed. A swatch, a date, a select or a
31
- checkbox always needs its `label`.
30
+ A `placeholder` names only a field you type into, and only until something is typed. A swatch, a date, a select, a
31
+ checkbox or a switch always needs its `label`.
32
32
 
33
33
  ## Pictures say what they show, or that they only decorate
34
34
 
@@ -111,5 +111,4 @@ can't click something that only appears on focus.
111
111
  - `authorSpace` warnings: zero, as always.
112
112
  - In a test, `page.getByRole('button', { name: 'Close' })` fails when the name is missing.
113
113
  `expect(locator).toMatchAriaSnapshot(…)` pins the whole outline.
114
- - Through MCP, `plitzi_screenshot` with `view: "accessibility"` returns the page's tree and lists everything that has
115
- no name.
114
+ - Through MCP, `plitzi_look` returns the page's tree (its default view) and lists everything that has no name.