handoff-app 2.0.0-4 → 2.0.0-5

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 (209) hide show
  1. package/AGENTS.md +7 -0
  2. package/CLAUDE.md +7 -0
  3. package/README.md +302 -378
  4. package/dist/app-builder/build.js +2 -2
  5. package/dist/app-builder/nav-shell.js +8 -15
  6. package/dist/app-builder/vercel-output.js +4 -4
  7. package/dist/cli/make.js +2 -2
  8. package/dist/cli.js +38 -6
  9. package/dist/commands/build/app.js +1 -5
  10. package/dist/commands/build/components.js +1 -5
  11. package/dist/commands/build/index.js +1 -5
  12. package/dist/commands/checkout/index.d.ts +15 -12
  13. package/dist/commands/checkout/index.js +15 -54
  14. package/dist/commands/compat/deprecations.d.ts +46 -0
  15. package/dist/commands/compat/deprecations.js +62 -0
  16. package/dist/commands/compat/push-args.d.ts +48 -0
  17. package/dist/commands/compat/push-args.js +73 -0
  18. package/dist/commands/db/migrate.js +1 -5
  19. package/dist/commands/dev/index.js +1 -5
  20. package/dist/commands/eject/config.js +1 -5
  21. package/dist/commands/eject/pages.js +1 -5
  22. package/dist/commands/eject/theme.js +1 -5
  23. package/dist/commands/fetch/index.js +1 -5
  24. package/dist/commands/index.js +4 -0
  25. package/dist/commands/login/index.js +1 -5
  26. package/dist/commands/logout/index.js +1 -5
  27. package/dist/commands/make/component.js +1 -5
  28. package/dist/commands/make/page.js +3 -7
  29. package/dist/commands/make/template.js +1 -5
  30. package/dist/commands/publish/index.d.ts +18 -12
  31. package/dist/commands/publish/index.js +18 -54
  32. package/dist/commands/pull/index.d.ts +13 -0
  33. package/dist/commands/pull/index.js +20 -0
  34. package/dist/commands/push/index.d.ts +23 -0
  35. package/dist/commands/push/index.js +58 -0
  36. package/dist/commands/scaffold/index.js +1 -5
  37. package/dist/commands/start/index.js +1 -5
  38. package/dist/commands/types.d.ts +10 -0
  39. package/dist/commands/utils.d.ts +74 -6
  40. package/dist/commands/utils.js +89 -10
  41. package/dist/commands/validate/components.js +1 -5
  42. package/dist/config/index.d.ts +2 -1
  43. package/dist/config/index.js +2 -1
  44. package/dist/config/loader.d.ts +19 -6
  45. package/dist/config/loader.js +43 -13
  46. package/dist/config/normalizers/page.d.ts +4 -4
  47. package/dist/config/normalizers/page.js +2 -2
  48. package/dist/config/runtime.js +9 -25
  49. package/dist/index.d.ts +69 -22
  50. package/dist/index.js +148 -35
  51. package/dist/nav/resolver.js +3 -1
  52. package/dist/nav/workspace.js +2 -2
  53. package/dist/registry/auth/access-tokens.d.ts +11 -3
  54. package/dist/registry/auth/access-tokens.js +29 -3
  55. package/dist/registry/auth/index.d.ts +4 -3
  56. package/dist/registry/auth/index.js +10 -2
  57. package/dist/registry/auth/sync-secret.d.ts +27 -0
  58. package/dist/registry/auth/sync-secret.js +65 -0
  59. package/dist/registry/auth/types.d.ts +20 -1
  60. package/dist/registry/auth/types.js +3 -1
  61. package/dist/registry/auth/validation.d.ts +11 -0
  62. package/dist/registry/auth/validation.js +18 -1
  63. package/dist/registry/checkout/assets.d.ts +1 -1
  64. package/dist/registry/checkout/assets.js +21 -6
  65. package/dist/registry/checkout/index.d.ts +8 -1
  66. package/dist/registry/checkout/index.js +39 -6
  67. package/dist/registry/checkout/tokens.d.ts +1 -1
  68. package/dist/registry/checkout/tokens.js +12 -5
  69. package/dist/registry/connection.d.ts +10 -0
  70. package/dist/registry/connection.js +52 -7
  71. package/dist/registry/content-kinds.d.ts +27 -0
  72. package/dist/registry/content-kinds.js +33 -0
  73. package/dist/registry/db/client.d.ts +6 -1
  74. package/dist/registry/db/client.js +16 -3
  75. package/dist/registry/db/schema.d.ts +37 -3
  76. package/dist/registry/db/schema.js +9 -7
  77. package/dist/registry/publish/assets.d.ts +2 -2
  78. package/dist/registry/publish/assets.js +46 -35
  79. package/dist/registry/publish/index.d.ts +29 -5
  80. package/dist/registry/publish/index.js +87 -34
  81. package/dist/registry/publish/tokens.d.ts +2 -2
  82. package/dist/registry/publish/tokens.js +30 -24
  83. package/dist/registry/selection.d.ts +12 -0
  84. package/dist/registry/selection.js +22 -0
  85. package/dist/store/registry.d.ts +48 -0
  86. package/dist/store/registry.js +75 -12
  87. package/dist/transformers/preview/types.d.ts +2 -2
  88. package/dist/utils/markdown.d.ts +10 -0
  89. package/dist/utils/markdown.js +17 -0
  90. package/dist/utils/menu-shell.js +14 -7
  91. package/dist/utils/pages.d.ts +22 -0
  92. package/dist/utils/pages.js +73 -0
  93. package/dist/utils/path.d.ts +5 -0
  94. package/dist/utils/path.js +9 -0
  95. package/docs/api.md +1 -3
  96. package/docs/cli.md +1 -1
  97. package/drizzle/0002_add_pages.sql +5 -3
  98. package/drizzle/meta/0002_snapshot.json +17 -8
  99. package/drizzle/meta/0003_snapshot.json +17 -8
  100. package/drizzle/meta/0004_snapshot.json +17 -8
  101. package/drizzle/meta/0005_snapshot.json +17 -8
  102. package/package.json +1 -1
  103. package/src/app/components/util/index.ts +5 -23
  104. package/src/app/lib/docs-api/backend.ts +11 -0
  105. package/src/app/lib/docs-api/errors.ts +8 -7
  106. package/src/app/lib/docs-api/handler.ts +2 -3
  107. package/src/app/lib/docs-api/index.ts +2 -0
  108. package/src/app/lib/docs-api/page-discovery.ts +126 -0
  109. package/src/app/lib/docs-api/page-rendering.ts +17 -0
  110. package/src/app/lib/docs-api/page-search.ts +191 -0
  111. package/src/app/lib/docs-api/records.ts +11 -19
  112. package/src/app/lib/docs-api/registry.ts +90 -11
  113. package/src/app/lib/docs-api/search.ts +115 -0
  114. package/src/app/lib/registry-api/allowlist.ts +7 -5
  115. package/src/app/lib/registry-api/asset-transfer.ts +58 -31
  116. package/src/app/lib/registry-api/auth.ts +89 -9
  117. package/src/app/lib/registry-api/entity-routes.ts +17 -7
  118. package/src/app/lib/registry-api/errors.ts +10 -4
  119. package/src/app/lib/registry-api/handler.ts +13 -15
  120. package/src/app/lib/registry-api/meta.ts +4 -0
  121. package/src/app/lib/registry-api/revalidate.ts +31 -12
  122. package/src/app/lib/registry-api/store.ts +89 -26
  123. package/src/app/lib/registry-api/sync-compat.ts +166 -0
  124. package/src/app/lib/registry-api/token-transfer.ts +44 -12
  125. package/src/app/lib/registry-api/transfer-batch.ts +116 -0
  126. package/src/app/lib/registry-api/transfer.ts +62 -35
  127. package/src/app/lib/registry-api/validation.ts +33 -1
  128. package/src/app/lib/registry-connection.ts +1 -1
  129. package/src/app/next.config.mjs +6 -0
  130. package/src/app/pages/[...slug]/index.tsx +37 -22
  131. package/src/app/pages/api/account/tokens/index.ts +4 -13
  132. package/src/app/pages/api/docs/search/pages.json.ts +25 -0
  133. package/src/app/pages/api/oauth/revoke.ts +14 -7
  134. package/src/app/pages/api/registry/components/[id]/files/[...filePath].ts +1 -1
  135. package/src/app/pages/api/registry/components/[id].ts +2 -2
  136. package/src/app/pages/api/registry/components/index.ts +1 -1
  137. package/src/app/pages/api/registry/pages/[...id].ts +20 -0
  138. package/src/app/pages/api/registry/pages/index.ts +19 -0
  139. package/src/app/pages/api/registry/patterns/[id]/files/[...filePath].ts +1 -1
  140. package/src/app/pages/api/registry/patterns/[id].ts +2 -2
  141. package/src/app/pages/api/registry/patterns/index.ts +1 -1
  142. package/src/app/pages/api/registry/transfer/assets/blobs/have.ts +2 -2
  143. package/src/app/pages/api/sync/upload.ts +21 -0
  144. package/src/app/pages/index/index.tsx +14 -0
  145. package/src/app/pages/index.tsx +30 -5
  146. package/src/app-builder/build.ts +2 -2
  147. package/src/app-builder/nav-shell.ts +8 -13
  148. package/src/app-builder/vercel-output.ts +4 -4
  149. package/src/cli/make.ts +2 -2
  150. package/src/cli.ts +30 -6
  151. package/src/commands/build/app.ts +2 -3
  152. package/src/commands/build/components.ts +2 -3
  153. package/src/commands/build/index.ts +2 -3
  154. package/src/commands/checkout/index.ts +18 -44
  155. package/src/commands/compat/deprecations.ts +79 -0
  156. package/src/commands/compat/push-args.ts +106 -0
  157. package/src/commands/db/migrate.ts +2 -3
  158. package/src/commands/dev/index.ts +2 -3
  159. package/src/commands/eject/config.ts +2 -3
  160. package/src/commands/eject/pages.ts +2 -3
  161. package/src/commands/eject/theme.ts +2 -3
  162. package/src/commands/fetch/index.ts +2 -3
  163. package/src/commands/index.ts +4 -0
  164. package/src/commands/login/index.ts +2 -3
  165. package/src/commands/logout/index.ts +2 -3
  166. package/src/commands/make/component.ts +2 -3
  167. package/src/commands/make/page.ts +4 -5
  168. package/src/commands/make/template.ts +2 -3
  169. package/src/commands/publish/index.ts +21 -44
  170. package/src/commands/pull/index.ts +32 -0
  171. package/src/commands/push/index.ts +115 -0
  172. package/src/commands/scaffold/index.ts +2 -3
  173. package/src/commands/start/index.ts +2 -3
  174. package/src/commands/types.ts +11 -1
  175. package/src/commands/utils.ts +80 -11
  176. package/src/commands/validate/components.ts +2 -3
  177. package/src/config/index.ts +2 -1
  178. package/src/config/loader.ts +57 -12
  179. package/src/config/normalizers/page.ts +4 -4
  180. package/src/config/runtime.ts +9 -24
  181. package/src/index.ts +183 -42
  182. package/src/nav/resolver.ts +3 -1
  183. package/src/nav/workspace.ts +2 -2
  184. package/src/registry/auth/access-tokens.ts +29 -5
  185. package/src/registry/auth/index.ts +7 -1
  186. package/src/registry/auth/sync-secret.ts +67 -0
  187. package/src/registry/auth/types.ts +24 -1
  188. package/src/registry/auth/validation.ts +18 -0
  189. package/src/registry/checkout/assets.ts +24 -7
  190. package/src/registry/checkout/index.ts +41 -5
  191. package/src/registry/checkout/tokens.ts +12 -6
  192. package/src/registry/connection.ts +53 -5
  193. package/src/registry/content-kinds.ts +39 -0
  194. package/src/registry/db/client.ts +22 -2
  195. package/src/registry/db/schema.ts +9 -7
  196. package/src/registry/publish/assets.ts +47 -37
  197. package/src/registry/publish/index.ts +102 -38
  198. package/src/registry/publish/tokens.ts +35 -26
  199. package/src/registry/selection.ts +19 -0
  200. package/src/store/registry.ts +118 -13
  201. package/src/transformers/preview/types.ts +2 -2
  202. package/src/utils/README.md +1 -0
  203. package/src/utils/markdown.ts +11 -0
  204. package/src/utils/menu-shell.ts +14 -7
  205. package/src/utils/pages.ts +52 -0
  206. package/src/utils/path.ts +15 -6
  207. package/templates/default/package.json.tpl +4 -0
  208. package/config/docs/foundations/logos.md +0 -7
  209. package/src/app/lib/api/redact.ts +0 -46
@@ -31,7 +31,7 @@ var __asyncValues = (this && this.__asyncValues) || function (o) {
31
31
  function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
32
32
  };
33
33
  Object.defineProperty(exports, "__esModule", { value: true });
34
- exports.createRegistryStore = exports.RegistryAssetStore = exports.RegistryTokenStore = exports.RegistryPageStore = exports.RegistryPatternStore = exports.RegistryComponentStore = void 0;
34
+ exports.createRegistryStore = exports.RegistryAssetStore = exports.RegistryTokenStore = exports.filterPublishedPageIds = exports.searchPageCandidates = exports.RegistryPageStore = exports.RegistryPatternStore = exports.RegistryComponentStore = void 0;
35
35
  const drizzle_orm_1 = require("drizzle-orm");
36
36
  const schema_1 = require("../registry/db/schema");
37
37
  /**
@@ -65,11 +65,7 @@ class RegistryComponentStore {
65
65
  get(id) {
66
66
  return __awaiter(this, void 0, void 0, function* () {
67
67
  var _a, _b;
68
- const rows = yield this.context.db
69
- .select({ record: schema_1.components.record })
70
- .from(schema_1.components)
71
- .where((0, drizzle_orm_1.eq)(schema_1.components.id, id))
72
- .limit(1);
68
+ const rows = yield this.context.db.select({ record: schema_1.components.record }).from(schema_1.components).where((0, drizzle_orm_1.eq)(schema_1.components.id, id)).limit(1);
73
69
  return (_b = (_a = rows[0]) === null || _a === void 0 ? void 0 : _a.record) !== null && _b !== void 0 ? _b : null;
74
70
  });
75
71
  }
@@ -112,11 +108,7 @@ class RegistryPatternStore {
112
108
  get(id) {
113
109
  return __awaiter(this, void 0, void 0, function* () {
114
110
  var _a, _b;
115
- const rows = yield this.context.db
116
- .select({ record: schema_1.patterns.record })
117
- .from(schema_1.patterns)
118
- .where((0, drizzle_orm_1.eq)(schema_1.patterns.id, id))
119
- .limit(1);
111
+ const rows = yield this.context.db.select({ record: schema_1.patterns.record }).from(schema_1.patterns).where((0, drizzle_orm_1.eq)(schema_1.patterns.id, id)).limit(1);
120
112
  return (_b = (_a = rows[0]) === null || _a === void 0 ? void 0 : _a.record) !== null && _b !== void 0 ? _b : null;
121
113
  });
122
114
  }
@@ -179,6 +171,72 @@ class RegistryPageStore {
179
171
  }
180
172
  }
181
173
  exports.RegistryPageStore = RegistryPageStore;
174
+ /** Escape the wildcards Postgres reads in an `ILIKE` pattern, so a term cannot widen its own match. */
175
+ const likePattern = (term) => `%${term.replace(/[\\%_]/g, (char) => `\\${char}`)}%`;
176
+ /**
177
+ * Candidate pages for a search, filtered and capped in SQL.
178
+ *
179
+ * `enabled` and `menuTitle` are fields in the `record` JSON, so the query uses JSON operators. An
180
+ * absent `enabled` field means enabled. The body filter excludes frontmatter, so an excluded field
181
+ * such as `metaTitle` cannot consume a candidate slot.
182
+ *
183
+ * The filter and the projection share one body expression, so every term SQL matched is inside the text that the
184
+ * server ranks.
185
+ *
186
+ * The body expression is empty for an external-link page. Search reads such a page by title, menu title, and
187
+ * description only. The exclusion must happen in SQL. An external body otherwise satisfies the filter and takes one
188
+ * of the `limit` rows. It pushes a real candidate past the cap before the server can drop it.
189
+ *
190
+ * `C` collation orders IDs by code point, as the merge in registry search does. Both orders must
191
+ * agree, or the cap can keep different pages in each runtime mode.
192
+ */
193
+ const searchPageCandidates = (db, query) => __awaiter(void 0, void 0, void 0, function* () {
194
+ // No term means no candidate, and an empty list would build `array[]`, which Postgres cannot type.
195
+ if (query.terms.length === 0) {
196
+ return [];
197
+ }
198
+ const patterns = drizzle_orm_1.sql.join(query.terms.map((term) => (0, drizzle_orm_1.sql) `${likePattern(term)}`), (0, drizzle_orm_1.sql) `, `);
199
+ const matchesAnyTerm = (text) => (0, drizzle_orm_1.sql) `${text} ilike any (array[${patterns}])`;
200
+ // This mirrors `record.external ? '' : markdown` in `toSearchablePage`. The transfer endpoint stores a published
201
+ // record verbatim, so `external` can hold any JSON. The list holds every value that JS reads as falsy, and
202
+ // `coalesce` prevents a NULL result for an absent key. The case blanks the external side, so an unrecognized
203
+ // value keeps its body.
204
+ const isExternal = (0, drizzle_orm_1.sql) `
205
+ coalesce(${schema_1.pages.record}->'external', 'false'::jsonb) not in (
206
+ 'null'::jsonb, 'false'::jsonb, '""'::jsonb, '0'::jsonb
207
+ )
208
+ `;
209
+ const storedBody = (0, drizzle_orm_1.sql) `left(coalesce(${schema_1.pageFiles.body}, ''), ${query.bodyLength})`;
210
+ const searchableBody = (0, drizzle_orm_1.sql) `
211
+ case
212
+ when ${isExternal} then ''
213
+ else ${storedBody}
214
+ end
215
+ `;
216
+ return db
217
+ .select({ record: schema_1.pages.record, body: searchableBody })
218
+ .from(schema_1.pages)
219
+ .leftJoin(schema_1.pageFiles, (0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.pageFiles.pageId, schema_1.pages.id), (0, drizzle_orm_1.eq)(schema_1.pageFiles.kind, 'markdown')))
220
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.sql) `${schema_1.pages.record}->>'enabled' is distinct from 'false'`, query.group ? (0, drizzle_orm_1.sql) `lower(${schema_1.pages.group}) = lower(${query.group})` : undefined, (0, drizzle_orm_1.or)(matchesAnyTerm(schema_1.pages.title), matchesAnyTerm(schema_1.pages.description), matchesAnyTerm((0, drizzle_orm_1.sql) `${schema_1.pages.record}->>'menuTitle'`), matchesAnyTerm(searchableBody))))
221
+ .orderBy((0, drizzle_orm_1.sql) `${schema_1.pages.id} collate "C"`)
222
+ .limit(query.limit);
223
+ });
224
+ exports.searchPageCandidates = searchPageCandidates;
225
+ /**
226
+ * The candidate IDs that have a published page. Search asks only about packaged default IDs, because
227
+ * no other ID can replace a default. This keeps the query bounded as the registry grows.
228
+ *
229
+ * A published page replaces the default with the same ID even when it does not match the search
230
+ * terms, so this query ignores the term filter.
231
+ */
232
+ const filterPublishedPageIds = (db, candidateIds) => __awaiter(void 0, void 0, void 0, function* () {
233
+ if (candidateIds.length === 0) {
234
+ return [];
235
+ }
236
+ const rows = yield db.select({ id: schema_1.pages.id }).from(schema_1.pages).where((0, drizzle_orm_1.inArray)(schema_1.pages.id, candidateIds));
237
+ return rows.map((row) => row.id);
238
+ });
239
+ exports.filterPublishedPageIds = filterPublishedPageIds;
182
240
  class RegistryTokenStore {
183
241
  constructor(context) {
184
242
  this.context = context;
@@ -203,7 +261,12 @@ class RegistryTokenStore {
203
261
  getArtifacts(id) {
204
262
  return __awaiter(this, void 0, void 0, function* () {
205
263
  const rows = yield this.context.db
206
- .select({ path: schema_1.tokenArtifacts.path, format: schema_1.tokenArtifacts.format, content: schema_1.tokenArtifacts.content, contentType: schema_1.tokenArtifacts.contentType })
264
+ .select({
265
+ path: schema_1.tokenArtifacts.path,
266
+ format: schema_1.tokenArtifacts.format,
267
+ content: schema_1.tokenArtifacts.content,
268
+ contentType: schema_1.tokenArtifacts.contentType,
269
+ })
207
270
  .from(schema_1.tokenArtifacts)
208
271
  .where((0, drizzle_orm_1.eq)(schema_1.tokenArtifacts.tokenSetId, id));
209
272
  return rows
@@ -254,9 +254,9 @@ export type PageObject = {
254
254
  };
255
255
  };
256
256
  export type PageListObject = {
257
- /** Stable page id — its slug path (e.g. `guides/setup`), derived from the file location (id ≡ route). */
257
+ /** Stable page id derived from the file location; `index` identifies the root home page. */
258
258
  id: string;
259
- /** Logical route the page is served at (e.g. `/guides/setup`). */
259
+ /** Logical route the page is served at (e.g. `/guides/setup`, or `/` for `index`). */
260
260
  path: string;
261
261
  /**
262
262
  * Absolute filesystem path of the source `.md` (workspace store only). Workspace-only and stripped
@@ -0,0 +1,10 @@
1
+ import matter from 'gray-matter';
2
+ /**
3
+ * Parse markdown frontmatter without the process-lifetime cache in gray-matter.
4
+ *
5
+ * `matter(text)` caches every document it parses, keyed by the whole input string, and only when the call passes no
6
+ * options. Nothing clears that cache, so a long-running server keeps a copy of every distinct document it parsed.
7
+ * Passing `{}` skips the cache and matches the defaults of the parser, so the result does not change. Use this
8
+ * wherever a request, a regeneration, or a watch can parse markdown again.
9
+ */
10
+ export declare const parseMarkdown: (text: string) => matter.GrayMatterFile<string>;
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.parseMarkdown = void 0;
7
+ const gray_matter_1 = __importDefault(require("gray-matter"));
8
+ /**
9
+ * Parse markdown frontmatter without the process-lifetime cache in gray-matter.
10
+ *
11
+ * `matter(text)` caches every document it parses, keyed by the whole input string, and only when the call passes no
12
+ * options. Nothing clears that cache, so a long-running server keeps a copy of every distinct document it parsed.
13
+ * Passing `{}` skips the cache and matches the defaults of the parser, so the result does not change. Use this
14
+ * wherever a request, a regeneration, or a watch can parse markdown again.
15
+ */
16
+ const parseMarkdown = (text) => (0, gray_matter_1.default)(text, {});
17
+ exports.parseMarkdown = parseMarkdown;
@@ -28,25 +28,32 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
28
28
  Object.defineProperty(exports, "__esModule", { value: true });
29
29
  exports.buildMenuShell = exports.buildMenuFromDirectory = exports.buildTokensFoundationsMenu = exports.KNOWN_PATHS = void 0;
30
30
  const fs = __importStar(require("fs"));
31
- const gray_matter_1 = __importDefault(require("gray-matter"));
32
31
  const lodash_1 = require("lodash");
33
32
  const path_1 = __importDefault(require("path"));
33
+ const markdown_1 = require("./markdown");
34
+ const pages_1 = require("./pages");
34
35
  /**
35
36
  * Paths that have dedicated route files and are therefore excluded from auto-scanned submenus.
36
37
  * Single source of truth — the docs app imports this (re-exported there as `knownPaths`).
37
38
  */
38
39
  exports.KNOWN_PATHS = [
40
+ 'account',
41
+ 'account/tokens',
42
+ 'account/users',
39
43
  'assets',
40
44
  'assets/fonts',
41
45
  'assets/icons',
42
46
  'assets/logos',
47
+ 'cli/device',
43
48
  'foundations',
44
49
  'foundations/colors',
45
50
  'foundations/icons',
46
51
  'foundations/effects',
47
- 'foundations/logos',
48
52
  'foundations/logo',
49
53
  'foundations/typography',
54
+ 'install',
55
+ 'login',
56
+ 'reset-password',
50
57
  'system',
51
58
  'system/component',
52
59
  'system/tokens',
@@ -101,12 +108,12 @@ const buildMenuFromDirectory = (dirPath, urlPrefix) => {
101
108
  }
102
109
  }
103
110
  else if (entry.endsWith('.md') && entry !== 'index.md') {
104
- const slug = entry.replace('.md', '');
111
+ const slug = (0, pages_1.stripMarkdownExtension)(entry);
105
112
  const fullSlugPath = `${urlPrefix}/${slug}`.replace(/^\/+/, '');
106
113
  if (exports.KNOWN_PATHS.indexOf(fullSlugPath) >= 0)
107
114
  continue;
108
115
  const contents = fs.readFileSync(fullPath, 'utf-8');
109
- const { data: metadata } = (0, gray_matter_1.default)(contents);
116
+ const { data: metadata } = (0, markdown_1.parseMarkdown)(contents);
110
117
  if (metadata.enabled === false)
111
118
  continue;
112
119
  items.push({
@@ -151,11 +158,11 @@ const buildMenuShell = (options) => {
151
158
  continue;
152
159
  }
153
160
  const contents = fs.readFileSync(search, 'utf-8');
154
- const { data: metadata } = (0, gray_matter_1.default)(contents);
161
+ const { data: metadata } = (0, markdown_1.parseMarkdown)(contents);
155
162
  if (metadata.enabled === false) {
156
163
  continue;
157
164
  }
158
- const filepath = `/${fileName.replace('.md', '')}`;
165
+ const filepath = `/${(0, pages_1.stripMarkdownExtension)(fileName)}`;
159
166
  const subSections = [];
160
167
  if (metadata.menu) {
161
168
  for (const key of Object.keys(metadata.menu)) {
@@ -183,7 +190,7 @@ const buildMenuShell = (options) => {
183
190
  }
184
191
  else {
185
192
  // No frontmatter menu: auto-scan the matching directory (working pages win over docs).
186
- const dirName = fileName.replace('.md', '');
193
+ const dirName = (0, pages_1.stripMarkdownExtension)(fileName);
187
194
  const nestedFromDocs = (0, exports.buildMenuFromDirectory)(path_1.default.resolve(docRoot, dirName), `/${dirName}`);
188
195
  const nestedFromPages = workingPagesDir
189
196
  ? (0, exports.buildMenuFromDirectory)(path_1.default.resolve(workingPagesDir, dirName), `/${dirName}`)
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Strip a single trailing `.md` extension.
3
+ *
4
+ * Anchored to the end on purpose: an unanchored `replace('.md', '')` removes the FIRST `.md`
5
+ * substring, so a compound name like `notes.mdx.md` becomes `notesx.md` instead of `notes.mdx`.
6
+ * The route, nav link, and discovered id must all agree, so every subsystem strips the extension
7
+ * through this one function.
8
+ */
9
+ export declare const stripMarkdownExtension: (fileName: string) => string;
10
+ export interface CollectPageSlugsOptions {
11
+ /**
12
+ * Keep the root-level `index.md` (the home page entity, id `index`). Nested `index.md` files are
13
+ * always skipped because section indexes are owned by dedicated routes. Defaults to `false`.
14
+ */
15
+ includeRootIndex?: boolean;
16
+ }
17
+ /**
18
+ * Recursively collect page slug segments (path relative to `root`, minus the trailing `.md`) for
19
+ * every markdown page under `root`. Shared by page discovery, the routing catch-all, and the nav
20
+ * shell so all three derive identical ids from the same files.
21
+ */
22
+ export declare const collectPageSlugSegments: (root: string, options?: CollectPageSlugsOptions, relativeParts?: string[]) => string[][];
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.collectPageSlugSegments = exports.stripMarkdownExtension = void 0;
30
+ const fs = __importStar(require("fs"));
31
+ const path_1 = __importDefault(require("path"));
32
+ const content_kinds_1 = require("../registry/content-kinds");
33
+ /**
34
+ * Strip a single trailing `.md` extension.
35
+ *
36
+ * Anchored to the end on purpose: an unanchored `replace('.md', '')` removes the FIRST `.md`
37
+ * substring, so a compound name like `notes.mdx.md` becomes `notesx.md` instead of `notes.mdx`.
38
+ * The route, nav link, and discovered id must all agree, so every subsystem strips the extension
39
+ * through this one function.
40
+ */
41
+ const stripMarkdownExtension = (fileName) => fileName.replace(/\.md$/, '');
42
+ exports.stripMarkdownExtension = stripMarkdownExtension;
43
+ /** True for a page `.md` file; an `index.md` counts only as the root home page when `includeRootIndex` is set. */
44
+ const isCollectablePage = (entry, isRoot, includeRootIndex) => {
45
+ if (!entry.endsWith('.md'))
46
+ return false;
47
+ if (entry === `${content_kinds_1.HOME_PAGE_ID}.md`)
48
+ return isRoot && includeRootIndex;
49
+ return true;
50
+ };
51
+ /**
52
+ * Recursively collect page slug segments (path relative to `root`, minus the trailing `.md`) for
53
+ * every markdown page under `root`. Shared by page discovery, the routing catch-all, and the nav
54
+ * shell so all three derive identical ids from the same files.
55
+ */
56
+ const collectPageSlugSegments = (root, options = {}, relativeParts = []) => {
57
+ var _a;
58
+ if (!fs.existsSync(root))
59
+ return [];
60
+ const includeRootIndex = (_a = options.includeRootIndex) !== null && _a !== void 0 ? _a : false;
61
+ const results = [];
62
+ for (const entry of fs.readdirSync(root)) {
63
+ const fullPath = path_1.default.join(root, entry);
64
+ if (fs.lstatSync(fullPath).isDirectory()) {
65
+ results.push(...(0, exports.collectPageSlugSegments)(fullPath, options, [...relativeParts, entry]));
66
+ }
67
+ else if (isCollectablePage(entry, relativeParts.length === 0, includeRootIndex)) {
68
+ results.push([...relativeParts, (0, exports.stripMarkdownExtension)(entry)]);
69
+ }
70
+ }
71
+ return results;
72
+ };
73
+ exports.collectPageSlugSegments = collectPageSlugSegments;
@@ -15,6 +15,11 @@ export declare function normalizePathForCompare(inputPath: string): string;
15
15
  * True when both paths resolve to the same normalized filesystem path.
16
16
  */
17
17
  export declare function arePathsEqual(pathA: string, pathB: string): boolean;
18
+ /**
19
+ * The directory handoff commands run against - `HANDOFF_WORKING_PATH` when set, otherwise the
20
+ * process working directory. Relative paths passed on the command line (e.g. `-c`) resolve from here.
21
+ */
22
+ export declare function resolveWorkingPath(): string;
18
23
  /**
19
24
  * True when childPath is equal to or within parentPath.
20
25
  */
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.generateFilesystemSafeId = generateFilesystemSafeId;
7
7
  exports.normalizePathForCompare = normalizePathForCompare;
8
8
  exports.arePathsEqual = arePathsEqual;
9
+ exports.resolveWorkingPath = resolveWorkingPath;
9
10
  exports.isPathInside = isPathInside;
10
11
  const path_1 = __importDefault(require("path"));
11
12
  /**
@@ -49,6 +50,14 @@ function normalizePathForCompare(inputPath) {
49
50
  function arePathsEqual(pathA, pathB) {
50
51
  return normalizePathForCompare(pathA) === normalizePathForCompare(pathB);
51
52
  }
53
+ /**
54
+ * The directory handoff commands run against - `HANDOFF_WORKING_PATH` when set, otherwise the
55
+ * process working directory. Relative paths passed on the command line (e.g. `-c`) resolve from here.
56
+ */
57
+ function resolveWorkingPath() {
58
+ const configured = process.env.HANDOFF_WORKING_PATH;
59
+ return configured ? path_1.default.resolve(configured) : process.cwd();
60
+ }
52
61
  /**
53
62
  * True when childPath is equal to or within parentPath.
54
63
  */
package/docs/api.md CHANGED
@@ -19,9 +19,7 @@ https://github.com/Convertiv/handoff-0-6-0/
19
19
  ```js
20
20
  import Handoff from 'handoff-app';
21
21
 
22
- const handoff = new Handoff({
23
- // You can customize the configuration here
24
- });
22
+ const handoff = new Handoff();
25
23
  handoff.fetch();
26
24
  ```
27
25
 
package/docs/cli.md CHANGED
@@ -16,7 +16,7 @@ but the various kinds of configurations can be tailored or extended.
16
16
 
17
17
  Handoff has 4 configuration areas -
18
18
 
19
- `handoff.config.*` - Defines the general handoff configuration. Supported files are `handoff.config.ts`, `handoff.config.js`, `handoff.config.cjs`, and `handoff.config.json` (resolved in that precedence order).
19
+ `handoff.config.*` - Defines the general handoff configuration. Supported files are `handoff.config.ts`, `handoff.config.js`, `handoff.config.cjs`, and `handoff.config.json` (resolved in that precedence order). Pass `-c, --config <file>` to load one specific file instead, resolved from the working directory (`HANDOFF_WORKING_PATH`, or the directory the command runs in).
20
20
  `pages` - Markdown files that will create or customize pages in the documentation
21
21
  app
22
22
  `exportables` - JSON schemas for each component in your figma file that you
@@ -3,12 +3,14 @@ CREATE TABLE "page_files" (
3
3
  "path" text NOT NULL,
4
4
  "kind" text NOT NULL,
5
5
  "content" text,
6
+ "body" text NOT NULL,
6
7
  "storage_ref" text,
7
8
  "content_type" text NOT NULL,
8
9
  "created_at" timestamp with time zone DEFAULT now() NOT NULL,
9
10
  "updated_at" timestamp with time zone DEFAULT now() NOT NULL,
10
- CONSTRAINT "page_files_page_id_path_pk" PRIMARY KEY("page_id","path"),
11
- CONSTRAINT "page_files_kind_not_declaration" CHECK ("page_files"."kind" <> 'declaration')
11
+ CONSTRAINT "page_files_page_id_pk" PRIMARY KEY("page_id"),
12
+ CONSTRAINT "page_files_kind_markdown" CHECK ("page_files"."kind" = 'markdown'),
13
+ CONSTRAINT "page_files_canonical_path" CHECK ("page_files"."path" = "page_files"."page_id" || '.md')
12
14
  );
13
15
  --> statement-breakpoint
14
16
  CREATE TABLE "pages" (
@@ -25,4 +27,4 @@ CREATE TABLE "pages" (
25
27
  );
26
28
  --> statement-breakpoint
27
29
  ALTER TABLE "page_files" ADD CONSTRAINT "page_files_page_id_pages_id_fk" FOREIGN KEY ("page_id") REFERENCES "public"."pages"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
28
- CREATE INDEX "pages_group_idx" ON "pages" USING btree ("group");
30
+ CREATE INDEX "pages_group_idx" ON "pages" USING btree ("group");
@@ -480,6 +480,12 @@
480
480
  "primaryKey": false,
481
481
  "notNull": false
482
482
  },
483
+ "body": {
484
+ "name": "body",
485
+ "type": "text",
486
+ "primaryKey": false,
487
+ "notNull": true
488
+ },
483
489
  "storage_ref": {
484
490
  "name": "storage_ref",
485
491
  "type": "text",
@@ -524,20 +530,23 @@
524
530
  }
525
531
  },
526
532
  "compositePrimaryKeys": {
527
- "page_files_page_id_path_pk": {
528
- "name": "page_files_page_id_path_pk",
533
+ "page_files_page_id_pk": {
534
+ "name": "page_files_page_id_pk",
529
535
  "columns": [
530
- "page_id",
531
- "path"
536
+ "page_id"
532
537
  ]
533
538
  }
534
539
  },
535
540
  "uniqueConstraints": {},
536
541
  "policies": {},
537
542
  "checkConstraints": {
538
- "page_files_kind_not_declaration": {
539
- "name": "page_files_kind_not_declaration",
540
- "value": "\"page_files\".\"kind\" <> 'declaration'"
543
+ "page_files_kind_markdown": {
544
+ "name": "page_files_kind_markdown",
545
+ "value": "\"page_files\".\"kind\" = 'markdown'"
546
+ },
547
+ "page_files_canonical_path": {
548
+ "name": "page_files_canonical_path",
549
+ "value": "\"page_files\".\"path\" = \"page_files\".\"page_id\" || '.md'"
541
550
  }
542
551
  },
543
552
  "isRLSEnabled": false
@@ -832,4 +841,4 @@
832
841
  "schemas": {},
833
842
  "tables": {}
834
843
  }
835
- }
844
+ }
@@ -480,6 +480,12 @@
480
480
  "primaryKey": false,
481
481
  "notNull": false
482
482
  },
483
+ "body": {
484
+ "name": "body",
485
+ "type": "text",
486
+ "primaryKey": false,
487
+ "notNull": true
488
+ },
483
489
  "storage_ref": {
484
490
  "name": "storage_ref",
485
491
  "type": "text",
@@ -524,20 +530,23 @@
524
530
  }
525
531
  },
526
532
  "compositePrimaryKeys": {
527
- "page_files_page_id_path_pk": {
528
- "name": "page_files_page_id_path_pk",
533
+ "page_files_page_id_pk": {
534
+ "name": "page_files_page_id_pk",
529
535
  "columns": [
530
- "page_id",
531
- "path"
536
+ "page_id"
532
537
  ]
533
538
  }
534
539
  },
535
540
  "uniqueConstraints": {},
536
541
  "policies": {},
537
542
  "checkConstraints": {
538
- "page_files_kind_not_declaration": {
539
- "name": "page_files_kind_not_declaration",
540
- "value": "\"page_files\".\"kind\" <> 'declaration'"
543
+ "page_files_kind_markdown": {
544
+ "name": "page_files_kind_markdown",
545
+ "value": "\"page_files\".\"kind\" = 'markdown'"
546
+ },
547
+ "page_files_canonical_path": {
548
+ "name": "page_files_canonical_path",
549
+ "value": "\"page_files\".\"path\" = \"page_files\".\"page_id\" || '.md'"
541
550
  }
542
551
  },
543
552
  "isRLSEnabled": false
@@ -1020,4 +1029,4 @@
1020
1029
  "schemas": {},
1021
1030
  "tables": {}
1022
1031
  }
1023
- }
1032
+ }
@@ -709,6 +709,12 @@
709
709
  "primaryKey": false,
710
710
  "notNull": false
711
711
  },
712
+ "body": {
713
+ "name": "body",
714
+ "type": "text",
715
+ "primaryKey": false,
716
+ "notNull": true
717
+ },
712
718
  "storage_ref": {
713
719
  "name": "storage_ref",
714
720
  "type": "text",
@@ -753,20 +759,23 @@
753
759
  }
754
760
  },
755
761
  "compositePrimaryKeys": {
756
- "page_files_page_id_path_pk": {
757
- "name": "page_files_page_id_path_pk",
762
+ "page_files_page_id_pk": {
763
+ "name": "page_files_page_id_pk",
758
764
  "columns": [
759
- "page_id",
760
- "path"
765
+ "page_id"
761
766
  ]
762
767
  }
763
768
  },
764
769
  "uniqueConstraints": {},
765
770
  "policies": {},
766
771
  "checkConstraints": {
767
- "page_files_kind_not_declaration": {
768
- "name": "page_files_kind_not_declaration",
769
- "value": "\"page_files\".\"kind\" <> 'declaration'"
772
+ "page_files_kind_markdown": {
773
+ "name": "page_files_kind_markdown",
774
+ "value": "\"page_files\".\"kind\" = 'markdown'"
775
+ },
776
+ "page_files_canonical_path": {
777
+ "name": "page_files_canonical_path",
778
+ "value": "\"page_files\".\"path\" = \"page_files\".\"page_id\" || '.md'"
770
779
  }
771
780
  },
772
781
  "isRLSEnabled": false
@@ -1249,4 +1258,4 @@
1249
1258
  "schemas": {},
1250
1259
  "tables": {}
1251
1260
  }
1252
- }
1261
+ }
@@ -709,6 +709,12 @@
709
709
  "primaryKey": false,
710
710
  "notNull": false
711
711
  },
712
+ "body": {
713
+ "name": "body",
714
+ "type": "text",
715
+ "primaryKey": false,
716
+ "notNull": true
717
+ },
712
718
  "storage_ref": {
713
719
  "name": "storage_ref",
714
720
  "type": "text",
@@ -753,20 +759,23 @@
753
759
  }
754
760
  },
755
761
  "compositePrimaryKeys": {
756
- "page_files_page_id_path_pk": {
757
- "name": "page_files_page_id_path_pk",
762
+ "page_files_page_id_pk": {
763
+ "name": "page_files_page_id_pk",
758
764
  "columns": [
759
- "page_id",
760
- "path"
765
+ "page_id"
761
766
  ]
762
767
  }
763
768
  },
764
769
  "uniqueConstraints": {},
765
770
  "policies": {},
766
771
  "checkConstraints": {
767
- "page_files_kind_not_declaration": {
768
- "name": "page_files_kind_not_declaration",
769
- "value": "\"page_files\".\"kind\" <> 'declaration'"
772
+ "page_files_kind_markdown": {
773
+ "name": "page_files_kind_markdown",
774
+ "value": "\"page_files\".\"kind\" = 'markdown'"
775
+ },
776
+ "page_files_canonical_path": {
777
+ "name": "page_files_canonical_path",
778
+ "value": "\"page_files\".\"path\" = \"page_files\".\"page_id\" || '.md'"
770
779
  }
771
780
  },
772
781
  "isRLSEnabled": false
@@ -1923,4 +1932,4 @@
1923
1932
  "schemas": {},
1924
1933
  "tables": {}
1925
1934
  }
1926
- }
1935
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "handoff-app",
3
- "version": "2.0.0-4",
3
+ "version": "2.0.0-5",
4
4
  "description": "Automated documentation toolchain for building client side documentation from figma",
5
5
  "author ": {
6
6
  "name": "Convertiv",