create-ailk 0.32.0 → 0.33.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 +9 -3
- package/component-catalog.json +12 -1
- package/component-catalog.md +6 -5
- package/dist/export-content.d.ts +6 -3
- package/dist/export-content.js +79 -10
- package/dist/lib/import-mdx.d.ts +42 -0
- package/dist/lib/import-mdx.js +531 -45
- package/dist/seed-flows.d.ts +1 -1
- package/dist/seed-flows.js +9 -4
- package/dist/surfaces.js +1 -0
- package/package.json +3 -3
- package/templates/apps/api/CLAUDE.md +7 -5
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +13 -0
- package/templates/apps/web/app/[locale]/(auth)/__tests__/password-reset.test.tsx +28 -0
- package/templates/apps/web/app/[locale]/(auth)/verify-email/__tests__/actions.test.ts +94 -0
- package/templates/apps/web/app/[locale]/(auth)/verify-email/actions.ts +77 -0
- package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +79 -14
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +7 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-menu.test.tsx +7 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +7 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +21 -0
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +15 -9
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +38 -105
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.invite.test.tsx +72 -58
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/verify.test.tsx +249 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +164 -20
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/by-result.test.tsx +269 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/score.test.tsx +179 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +108 -40
- package/templates/apps/web/app/[locale]/flows/[slug]/verify/page.tsx +153 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/verify.actions.ts +151 -0
- package/templates/apps/web/app/[locale]/meet/confirmed/BookingHandOff.tsx +1 -1
- package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +3 -20
- package/templates/apps/web/app/[locale]/thanks/[slug]/page.tsx +2 -9
- package/templates/apps/web/auth.ts +22 -6
- package/templates/apps/web/lib/__tests__/account-config.test.ts +69 -0
- package/templates/apps/web/lib/__tests__/auth-email.test.ts +99 -1
- package/templates/apps/web/lib/__tests__/telemetry-scrub.test.ts +20 -0
- package/templates/apps/web/lib/account-config.ts +52 -14
- package/templates/apps/web/lib/auth-email.ts +87 -5
- package/templates/apps/web/lib/telemetry-scrub.ts +6 -0
- package/templates/apps/web/messages/en.json +26 -1
- package/templates/database/seed.ts +7 -7
- package/versions.json +48 -48
- package/templates/apps/web/app/[locale]/thanks/[slug]/AccountInviteLink.tsx +0 -89
- package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/AccountInviteLink.test.tsx +0 -104
package/README.md
CHANGED
|
@@ -665,9 +665,15 @@ each directory's own `index.mdx` becomes its `BlogList/index` or
|
|
|
665
665
|
`parent` comes from its path, never its frontmatter, and a file the walk skips
|
|
666
666
|
is listed with the rule it failed. Their fumadocs frontmatter is mapped to
|
|
667
667
|
the stored fields (`date` →
|
|
668
|
-
`publishedAt`, `updated` → `updatedAt`);
|
|
669
|
-
|
|
670
|
-
in
|
|
668
|
+
`publishedAt`, `updated` → `updatedAt`); every other key — the template's
|
|
669
|
+
`featured` and `sidebar`, or a site's own such as `imageAlt` or `sources` —
|
|
670
|
+
is sent in the page's `siteFields` under its own name, and a blog post's
|
|
671
|
+
`tags` are sent as its categories (#10806). A key the store cannot keep is
|
|
672
|
+
named with its reason under `--apply`, in one line per file:
|
|
673
|
+
`<path>: not imported — <key> (<reason>); …`. A hub that predates site
|
|
674
|
+
fields and drops them is named too: `<path>: not kept by the hub: <keys> (…)`.
|
|
675
|
+
`export-content` writes `siteFields` back as top-level frontmatter keys.
|
|
676
|
+
Each outcome carries `root: "content"` or
|
|
671
677
|
`root: "editorial"`, and an editorial line is printed prefixed `editorial: `.
|
|
672
678
|
|
|
673
679
|
```bash
|
package/component-catalog.json
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"importPath": "@working-theory/ui/blocks",
|
|
18
18
|
"rootBarrel": "@working-theory/ui",
|
|
19
19
|
"sourceRoot": "packages/ui/src/blocks",
|
|
20
|
-
"count":
|
|
20
|
+
"count": 108
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
23
|
"name": "chrome",
|
|
@@ -1695,6 +1695,17 @@
|
|
|
1695
1695
|
"descriptionGrade": "strong",
|
|
1696
1696
|
"sectionType": null
|
|
1697
1697
|
},
|
|
1698
|
+
{
|
|
1699
|
+
"name": "ScoreBar",
|
|
1700
|
+
"tier": "blocks",
|
|
1701
|
+
"sourcePath": "packages/ui/src/blocks/ScoreBar.tsx",
|
|
1702
|
+
"importPath": "@working-theory/ui/blocks",
|
|
1703
|
+
"exported": true,
|
|
1704
|
+
"description": "One labelled score bar on a scored magnet's results page: a label (the overall score's, or a dimension's name), the points out of the most a respondent could score, the Level name that share falls in, and one bar filled to the evaluator's own `pct` (`evaluateWaitlistScoring` returns `{ points, max, pct, level }` for the total and per dimension).",
|
|
1705
|
+
"descriptionSource": "doc-comment",
|
|
1706
|
+
"descriptionGrade": "strong",
|
|
1707
|
+
"sectionType": null
|
|
1708
|
+
},
|
|
1698
1709
|
{
|
|
1699
1710
|
"name": "SectionActionRow",
|
|
1700
1711
|
"tier": "blocks",
|
package/component-catalog.md
CHANGED
|
@@ -15,13 +15,13 @@ The component inventory of `@working-theory/ui` and the shells of `@working-theo
|
|
|
15
15
|
3. Check the Primitives, Panels and Shells tables too — a low-level control, a collapsible layout container, or a route-level frame lives there, not under Sections or Blocks.
|
|
16
16
|
4. Only then build. If you do build, you are declaring the capability is genuinely absent.
|
|
17
17
|
|
|
18
|
-
Inventory: **
|
|
18
|
+
Inventory: **325 component files** across 16 tiers — 60 sections, 108 blocks, 19 chrome, 79 primitives, 6 panels, 8 shells, 5 docs-blog-blocks, 1 docs-blog-chrome, 2 docs-blog-shells, 2 scheduling-sections, 6 scheduling-blocks, 5 signal-collectors-sections, 9 signal-collectors-blocks, 2 commerce-sections, 12 commerce-blocks, 1 commerce-panels. 322 distinct names: `SchedulerEmbed`, `Separator`, `Tabs` exist in more than one tier as genuinely different components, listed once per tier with its own source path.
|
|
19
19
|
|
|
20
|
-
Description coverage: **
|
|
20
|
+
Description coverage: **321/325** components carry a capability description derived from source — 45 from the section registry's hand-authored when-to-use text, 241 from the component's own doc-comment header, 35 from its tier barrel's roster line. **4** have none; they are listed under [Undescribed components](#undescribed-components) rather than given a sentence invented from their name.
|
|
21
21
|
|
|
22
|
-
Of those, **
|
|
22
|
+
Of those, **287** are *strong* — a capability sentence you could match a requirement against — and **34** are *weak*: either one short clause, or a long one that spends itself on wiring (client boundaries, class names, which primitives it composes) instead of on what the thing is for. Weak rows are marked `(weak)` and listed in full under [Weak descriptions](#weak-descriptions). Coverage is 99%, but **findable-without-luck coverage is 88%**.
|
|
23
23
|
|
|
24
|
-
Per tier, strong out of total: **Sections** 59/60 · **Blocks**
|
|
24
|
+
Per tier, strong out of total: **Sections** 59/60 · **Blocks** 100/108 · **Chrome** 10/19 · **Primitives** 64/79 (4 undescribed) · **Panels** 5/6 · **Shells** 7/8 · **Docs + Blog Blocks** 5/5 · **Docs + Blog Chrome** 1/1 · **Docs + Blog Shells** 0/2 · **Scheduling Sections** 2/2 · **Scheduling Blocks** 6/6 · **Signal-Collectors Sections** 5/5 · **Signal-Collectors Blocks** 8/9 · **Commerce Sections** 2/2 · **Commerce Blocks** 12/12 · **Commerce Panels** 1/1. A tier low on this line is a tier where a capability search is most likely to come up empty on something that exists.
|
|
25
25
|
|
|
26
26
|
That grade is **mechanical**. It counts what it can measure — how much a description adds beyond the component's own name, how much of it is spent on wiring, whether the extraction came out damaged — and it cannot tell a fluent paragraph about the wrong subject from a useful one. A person reading these tables will find more rows unusable than the weak count admits. Treat it as a floor on the problem, not a measurement of it, and read the source before concluding a capability is absent.
|
|
27
27
|
|
|
@@ -124,7 +124,7 @@ Import: `import { X } from "@working-theory/ui/sections";` (or from the `@workin
|
|
|
124
124
|
|
|
125
125
|
---
|
|
126
126
|
|
|
127
|
-
## Blocks (
|
|
127
|
+
## Blocks (108)
|
|
128
128
|
|
|
129
129
|
Intermediate composition tiles — the pieces sections are built from. Compose primitives and panels; never import a section.
|
|
130
130
|
|
|
@@ -215,6 +215,7 @@ Import: `import { X } from "@working-theory/ui/blocks";` (or from the `@working-
|
|
|
215
215
|
| `ResponsiveSheet` | ONE component, TWO forms, ONE open pair. Renders its `children` as a Radix Dialog at `md` (768px, --breakpoint-md) and up, and as a full-screen Radix Sheet below it — never both at once. | `import { ResponsiveSheet } from "@working-theory/ui/blocks"` |
|
|
216
216
|
| `RowGroup` | The bordered box that groups ListRows with hairline dividers. The reference is `packages-paid/admin/design/demos/first-steps/demo.html`'s `.box` (`background:var(--bg);border:1px solid var(--border); border-radius:6px;overflow:hidden`) with `.row{border-top:…}` and `.row:first-child{border-top:0}` — i.e. hairlines BETWEEN rows, none above the first. | `import { RowGroup } from "@working-theory/ui/blocks"` |
|
|
217
217
|
| `SchedulerEmbed` | A declarative third-party booking iframe. Renders a full-width, border-0, lazy-loaded <iframe> at `src` with a graceful loading skeleton (shown until the iframe's own `load` event fires) and a timeout-gated plain-link fallback beneath it — for environments that block third-party frames (ad blockers, a strict CSP `frame-src`) where the iframe may load blank or never load at all. | `import { SchedulerEmbed } from "@working-theory/ui/blocks"` · `packages/ui/src/blocks/SchedulerEmbed.tsx` |
|
|
218
|
+
| `ScoreBar` | One labelled score bar on a scored magnet's results page: a label (the overall score's, or a dimension's name), the points out of the most a respondent could score, the Level name that share falls in, and one bar filled to the evaluator's own `pct` (`evaluateWaitlistScoring` returns `{ points, max, pct, level }` for the total and per dimension). | `import { ScoreBar } from "@working-theory/ui/blocks"` |
|
|
218
219
|
| `SectionActionRow` | The bottom-anchored, centered action row a section places BENEATH its own content, at the 44×44px target-size floor. It is the block form of the placement established for `stair-steps`: a bounded 1-2 action list rendered after the section body rather than inside the section header. | `import { SectionActionRow } from "@working-theory/ui/blocks"` |
|
|
219
220
|
| `SectionHeader` | Alignment-parameterized section header (eyebrow/subtitle/description/supportLink). The same component serves two ranks (headingLevel="section" \| "subsection") and three alignments (align="start" \| "center" \| "end"). It knows nothing about columns/splits/carousels — it is width-flexible and fills whatever container the section places it in. | `import { SectionHeader } from "@working-theory/ui/blocks"` |
|
|
220
221
|
| `ShareBar` | The reader-facing counterpart to DocsActionBar. Both surfaces share the same SplitButton *affordance + slot* (outline variant, right-justified on the post title row) but fill it with different actions: DocsActionBar carries the AI-ingestion set (Copy page + Open in …), ShareBar carries the social-share set (X · LinkedIn · Bluesky · Copy link). One primitive, two configs. | `import { ShareBar } from "@working-theory/ui/blocks"` |
|
package/dist/export-content.d.ts
CHANGED
|
@@ -6,8 +6,11 @@ export declare class ExportContentRefusedError extends Error {
|
|
|
6
6
|
constructor(message: string);
|
|
7
7
|
}
|
|
8
8
|
export declare const SPOKE_HINT = "the site's read does not serve this workspace's published version \u2014 is the spoke at --store database?";
|
|
9
|
-
/**
|
|
10
|
-
|
|
9
|
+
/**
|
|
10
|
+
* The site-relative file a stored page is written to, and whether it is
|
|
11
|
+
* editorial. A blog post's `parent` is its series' directory (#10811 D1).
|
|
12
|
+
*/
|
|
13
|
+
export declare function fileFor(targetDir: string, type: string, slug: string, locale: string, parent?: string): {
|
|
11
14
|
path: string;
|
|
12
15
|
editorial: boolean;
|
|
13
16
|
};
|
|
@@ -51,5 +54,5 @@ export interface ExportContentArgs {
|
|
|
51
54
|
workspaceId: string | undefined;
|
|
52
55
|
help: boolean;
|
|
53
56
|
}
|
|
54
|
-
export declare const EXPORT_CONTENT_HELP = "create-ailk export-content \u2014 write every published page back to its content file.\n\nUsage:\n AILK_AGENT_KEY=<key> create-ailk export-content [<dir>] [--dry-run] [--api-url <origin>] [--workspace <wksp_id>]\n\nArguments:\n <dir> The site. Defaults to the current dir.\n\nOptions:\n --dry-run Print what would be written, and write nothing.\n --api-url <origin> The API origin that holds the workspace \u2014 on a\n spoke, its hub's. Else AILK_API_URL.\n --workspace <wksp_id> The workspace. Else AILK_WORKSPACE_ID.\n -h, --help Show this help.\n\nThe rollback for a cutover: every page the workspace list reports published\nin the default locale is written to the file migrate-content reads it from\n\u2014 content/en/<Type>/<slug>.mdx (or the legacy content/<Type>/<slug>.mdx that\nexists), apps/web/content/en/blog/<slug>.mdx for a post and the
|
|
57
|
+
export declare const EXPORT_CONTENT_HELP = "create-ailk export-content \u2014 write every published page back to its content file.\n\nUsage:\n AILK_AGENT_KEY=<key> create-ailk export-content [<dir>] [--dry-run] [--api-url <origin>] [--workspace <wksp_id>]\n\nArguments:\n <dir> The site. Defaults to the current dir.\n\nOptions:\n --dry-run Print what would be written, and write nothing.\n --api-url <origin> The API origin that holds the workspace \u2014 on a\n spoke, its hub's. Else AILK_API_URL.\n --workspace <wksp_id> The workspace. Else AILK_WORKSPACE_ID.\n -h, --help Show this help.\n\nThe rollback for a cutover: every page the workspace list reports published\nin the default locale is written to the file migrate-content reads it from\n\u2014 content/en/<Type>/<slug>.mdx (or the legacy content/<Type>/<slug>.mdx that\nexists), apps/web/content/en/blog/<slug>.mdx for a post,\napps/web/content/en/blog/<series>/<slug>.mdx for a post in a series and the\nsame under docs for a docs page; a post or doc goes to whichever of\n<slug>.mdx or <slug>/index.mdx is on disk, <slug>.mdx when neither is \u2014 with\nits words moved back into its sections, so a re-run of migrate-content\nreports every page unchanged. Published edits are\nkept. A page with a draft ahead of its published version fails by name:\npublish or revert it in Admin, then re-run. The site's own API read must\nserve the workspace's published version (on a spoke, the hub's row at\n--store database). Each page prints created, written, unchanged or failed.\nThe agent key needs the content-edit grant; on a spoke, AILK_SPOKE_KEY is\nneeded too. Exit 0 when no page failed, 1 when any did, 2 on a refusal.\n";
|
|
55
58
|
export declare function parseExportContentArgs(argv: string[]): ExportContentArgs;
|
package/dist/export-content.js
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
*
|
|
25
25
|
* Pure module: no `import.meta.url`, so jest imports it directly.
|
|
26
26
|
*/
|
|
27
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
27
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
28
28
|
import { dirname, join, resolve } from "node:path";
|
|
29
29
|
import { parseArgs } from "node:util";
|
|
30
30
|
import { listWorkspacePages, loadSiteWordsCodec, readDraftPage, readPublishedPage, readSiteLocales, resolveWorkspaceAccess, } from "./inventory.js";
|
|
@@ -52,21 +52,32 @@ const EDITORIAL_DIRS = {
|
|
|
52
52
|
const EDITORIAL_RENAMES = { publishedAt: "date", updatedAt: "updated" };
|
|
53
53
|
/** Keys an editorial file does not carry: its path decides both. */
|
|
54
54
|
const EDITORIAL_DROPPED = new Set(["type", "slug"]);
|
|
55
|
+
/**
|
|
56
|
+
* Never written from `siteFields` (#10806 D6): a mapped fumadocs key, an
|
|
57
|
+
* address key, the strip marker, or a prototype name.
|
|
58
|
+
*/
|
|
59
|
+
const NOT_SITE_FIELDS = new Set(["date", "updated", "type", "slug", "oss", "siteFields", "__proto__"]);
|
|
55
60
|
const INDEX_SLUG = "index";
|
|
56
61
|
/** The store's address shapes, restated (create-ailk has no runtime dep): a path built from them stays in the site. */
|
|
57
62
|
const TYPE_SHAPE = /^[A-Z][A-Za-z]*$/;
|
|
58
63
|
const SLUG_SHAPE = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
59
64
|
const LOCALE_SHAPE = /^[a-z]{2}(-[A-Z]{2})?$/;
|
|
65
|
+
/** A blog series' shape (`PARENT` in `lib/import-mdx.ts`), restated: a directory built from it stays in the blog. */
|
|
66
|
+
const SERIES_SHAPE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
60
67
|
/** A pinned item's read times out as the kit's other reads do (`inventory.ts`). */
|
|
61
68
|
const ITEM_READ_TIMEOUT_MS = 15_000;
|
|
62
69
|
export const SPOKE_HINT = "the site's read does not serve this workspace's published version — is the spoke at --store database?";
|
|
63
70
|
// ── File addressing (D6) ────────────────────────────────────────────────────
|
|
64
|
-
/**
|
|
65
|
-
|
|
71
|
+
/**
|
|
72
|
+
* The site-relative file a stored page is written to, and whether it is
|
|
73
|
+
* editorial. A blog post's `parent` is its series' directory (#10811 D1).
|
|
74
|
+
*/
|
|
75
|
+
export function fileFor(targetDir, type, slug, locale, parent) {
|
|
66
76
|
const exists = (path) => existsSync(join(targetDir, path));
|
|
67
77
|
const editorial = EDITORIAL_DIRS[type];
|
|
68
78
|
if (editorial !== undefined && editorial.index === (slug === INDEX_SLUG)) {
|
|
69
|
-
const
|
|
79
|
+
const series = type === "BlogPosting" && parent !== undefined ? `/${parent}` : "";
|
|
80
|
+
const base = `apps/web/content/${locale}/${editorial.dir}${series}`;
|
|
70
81
|
if (editorial.index)
|
|
71
82
|
return { path: `${base}/index.mdx`, editorial: true };
|
|
72
83
|
// The importer reads `<slug>.mdx` first and `<slug>/index.mdx` (#10701);
|
|
@@ -88,11 +99,20 @@ export function recombine(content, sections, codec, editorial, pinned) {
|
|
|
88
99
|
const { body: rawBody, ...rest } = content;
|
|
89
100
|
let body = typeof rawBody === "string" ? rawBody : "";
|
|
90
101
|
const frontmatter = {};
|
|
91
|
-
|
|
102
|
+
const { siteFields, ...stored } = rest;
|
|
103
|
+
for (const [key, value] of Object.entries(editorial ? stored : rest)) {
|
|
92
104
|
if (editorial && EDITORIAL_DROPPED.has(key))
|
|
93
105
|
continue;
|
|
94
106
|
frontmatter[editorial ? (EDITORIAL_RENAMES[key] ?? key) : key] = value;
|
|
95
107
|
}
|
|
108
|
+
// #10806 D6 — an editorial page's site fields are its file's own keys, after
|
|
109
|
+
// the mapped ones; a mapped key always wins.
|
|
110
|
+
if (editorial && siteFields !== null && typeof siteFields === "object" && !Array.isArray(siteFields)) {
|
|
111
|
+
for (const [key, value] of Object.entries(siteFields)) {
|
|
112
|
+
if (!Object.hasOwn(frontmatter, key) && !NOT_SITE_FIELDS.has(key))
|
|
113
|
+
frontmatter[key] = value;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
96
116
|
if (sections !== undefined && sections.length > 0) {
|
|
97
117
|
const applied = codec.applyWords(body, sections);
|
|
98
118
|
if (applied.issues.length > 0) {
|
|
@@ -170,6 +190,15 @@ export async function exportContent(opts) {
|
|
|
170
190
|
if (draft.version !== row.publishedVersionId) {
|
|
171
191
|
return failed("the draft read is not the published version; re-run");
|
|
172
192
|
}
|
|
193
|
+
// #10811 D4 — a blog post's series becomes a directory, so it must be one
|
|
194
|
+
// the importer reads back; the API's value is not printed.
|
|
195
|
+
const stored = type === "BlogPosting" ? draft.content["parent"] : undefined;
|
|
196
|
+
const parent = typeof stored === "string" && SERIES_SHAPE.test(stored) && stored !== slug && stored !== "category"
|
|
197
|
+
? stored
|
|
198
|
+
: undefined;
|
|
199
|
+
if (stored !== undefined && parent === undefined) {
|
|
200
|
+
return failed("its series is not one a file can be written under (lowercase words joined by single hyphens, not its own slug, not category)");
|
|
201
|
+
}
|
|
173
202
|
const published = await readPublishedPage(access, fetch, type, slug, locale);
|
|
174
203
|
if (!published.found || published.publishedVersionId !== row.publishedVersionId) {
|
|
175
204
|
return failed(SPOKE_HINT);
|
|
@@ -177,8 +206,18 @@ export async function exportContent(opts) {
|
|
|
177
206
|
const pinned = await pinnedItems(published.sections ?? [], locale);
|
|
178
207
|
if (typeof pinned === "string")
|
|
179
208
|
return failed(pinned);
|
|
180
|
-
const { path, editorial } = fileFor(targetDir, type, slug, locale);
|
|
181
|
-
|
|
209
|
+
const { path, editorial } = fileFor(targetDir, type, slug, locale, parent);
|
|
210
|
+
let content = draft.content;
|
|
211
|
+
if (parent !== undefined && editorial) {
|
|
212
|
+
const holder = addressHolder(slug, parent);
|
|
213
|
+
if (holder !== undefined) {
|
|
214
|
+
return failed(`${holder} would hold this post's address on import; remove it, then re-run`, path);
|
|
215
|
+
}
|
|
216
|
+
// #10811 D3 — the path carries the series, as it carries type and slug.
|
|
217
|
+
const { parent: _series, ...unparented } = content;
|
|
218
|
+
content = unparented;
|
|
219
|
+
}
|
|
220
|
+
const file = recombine(content, published.sections, codec, editorial, pinned);
|
|
182
221
|
if ("reason" in file)
|
|
183
222
|
return failed(file.reason, path);
|
|
184
223
|
const absolute = join(targetDir, path);
|
|
@@ -190,6 +229,33 @@ export async function exportContent(opts) {
|
|
|
190
229
|
}
|
|
191
230
|
return { type, slug, file: path, result };
|
|
192
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* The first other file, in path order, that the editorial walk reads as
|
|
234
|
+
* this series post's `BlogPosting/<slug>` (#10811 D5): the top-level forms,
|
|
235
|
+
* or the same post under another series. The walk skips `category`, `_`
|
|
236
|
+
* entries and symbolic links, and refuses a series not in its shape or
|
|
237
|
+
* named after its own post, so none of those holds the address.
|
|
238
|
+
*/
|
|
239
|
+
function addressHolder(slug, parent) {
|
|
240
|
+
const blog = `apps/web/content/${locale}/blog`;
|
|
241
|
+
const series = existsSync(join(targetDir, blog))
|
|
242
|
+
? readdirSync(join(targetDir, blog), { withFileTypes: true })
|
|
243
|
+
.filter((d) => d.isDirectory() && SERIES_SHAPE.test(d.name) && ![parent, slug, "category"].includes(d.name))
|
|
244
|
+
.map((d) => `${blog}/${d.name}`)
|
|
245
|
+
: [];
|
|
246
|
+
// Every directory below `blog` a real directory and the file a real file.
|
|
247
|
+
const walked = (path) => {
|
|
248
|
+
const parts = path.slice(blog.length + 1).split("/");
|
|
249
|
+
return parts.every((_, i) => {
|
|
250
|
+
const stat = lstatSync(join(targetDir, blog, ...parts.slice(0, i + 1)), { throwIfNoEntry: false });
|
|
251
|
+
return i === parts.length - 1 ? stat?.isFile() === true : stat?.isDirectory() === true;
|
|
252
|
+
});
|
|
253
|
+
};
|
|
254
|
+
return [blog, ...series]
|
|
255
|
+
.flatMap((dir) => [`${dir}/${slug}.mdx`, `${dir}/${slug}/index.mdx`])
|
|
256
|
+
.filter(walked)
|
|
257
|
+
.sort()[0];
|
|
258
|
+
}
|
|
193
259
|
/**
|
|
194
260
|
* The Live words of every item the page's faq / glossary-index sections pin
|
|
195
261
|
* (#10735 D9), by kind and slug — `undefined` when none pins — or why the
|
|
@@ -261,9 +327,12 @@ Options:
|
|
|
261
327
|
The rollback for a cutover: every page the workspace list reports published
|
|
262
328
|
in the default locale is written to the file migrate-content reads it from
|
|
263
329
|
— content/en/<Type>/<slug>.mdx (or the legacy content/<Type>/<slug>.mdx that
|
|
264
|
-
exists), apps/web/content/en/blog/<slug>.mdx for a post
|
|
265
|
-
|
|
266
|
-
|
|
330
|
+
exists), apps/web/content/en/blog/<slug>.mdx for a post,
|
|
331
|
+
apps/web/content/en/blog/<series>/<slug>.mdx for a post in a series and the
|
|
332
|
+
same under docs for a docs page; a post or doc goes to whichever of
|
|
333
|
+
<slug>.mdx or <slug>/index.mdx is on disk, <slug>.mdx when neither is — with
|
|
334
|
+
its words moved back into its sections, so a re-run of migrate-content
|
|
335
|
+
reports every page unchanged. Published edits are
|
|
267
336
|
kept. A page with a draft ahead of its published version fails by name:
|
|
268
337
|
publish or revert it in Admin, then re-run. The site's own API read must
|
|
269
338
|
serve the workspace's published version (on a spoke, the hub's row at
|
package/dist/lib/import-mdx.d.ts
CHANGED
|
@@ -32,6 +32,19 @@ export interface MdxAdapterModule {
|
|
|
32
32
|
data: Record<string, unknown>;
|
|
33
33
|
content: string;
|
|
34
34
|
};
|
|
35
|
+
/**
|
|
36
|
+
* The site-field rule from the adapter's validation package (#10806 D4);
|
|
37
|
+
* without it the editorial walk sends no `siteFields` and no `tags`, and
|
|
38
|
+
* names the keys it would have sent.
|
|
39
|
+
*/
|
|
40
|
+
siteFields?: SiteFieldRules;
|
|
41
|
+
}
|
|
42
|
+
/** The validation package's site-field rule, typed structurally. */
|
|
43
|
+
export interface SiteFieldRules {
|
|
44
|
+
refusal: (key: string, value: unknown) => string | null;
|
|
45
|
+
publicPageTags: (tags: readonly unknown[]) => string[];
|
|
46
|
+
maxKeys: number;
|
|
47
|
+
maxBytes: number;
|
|
35
48
|
}
|
|
36
49
|
export interface ImportMdxOptions {
|
|
37
50
|
/** The site's content root (`content/`). */
|
|
@@ -88,6 +101,35 @@ interface At {
|
|
|
88
101
|
export declare function outcomePath(at: At): string;
|
|
89
102
|
/** The whole report, one line per page and a final count line. */
|
|
90
103
|
export declare function formatImportMdxReport(result: ImportMdxResult): string;
|
|
104
|
+
/**
|
|
105
|
+
* A section string's inline HTML as the constrained Markdown the upstream
|
|
106
|
+
* renderers read (#10804 D3): a link with an allowed href becomes
|
|
107
|
+
* `[label](href)`, `<br>` a newline, a block or list item a paragraph break,
|
|
108
|
+
* and every other tag goes, its text kept. A string with no tag comes back
|
|
109
|
+
* byte-identical; the result never holds one. `notCarried` names, sorted,
|
|
110
|
+
* every tag the layout does not keep.
|
|
111
|
+
*/
|
|
112
|
+
export declare function inlineHtmlToMarkdown(value: string): {
|
|
113
|
+
text: string;
|
|
114
|
+
notCarried: string[];
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* Each section with every string in its `data` that holds a tag converted by
|
|
118
|
+
* `inlineHtmlToMarkdown` (#10804 D2); keys and tag-free strings are kept as
|
|
119
|
+
* they are. `notCarried` is the sorted union of the names it did not carry.
|
|
120
|
+
*/
|
|
121
|
+
export declare function sectionsWithoutHtml<T>(sections: readonly T[]): {
|
|
122
|
+
sections: T[];
|
|
123
|
+
notCarried: string[];
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* The first section-string or key rule of the hub's structure write
|
|
127
|
+
* (`checkSectionData`) the layout breaks, as `<message> (sections[i].data<path>)`,
|
|
128
|
+
* or `undefined` (#10804 D5). The depth and size bounds are not restated.
|
|
129
|
+
*/
|
|
130
|
+
export declare function layoutRefusal(layout: {
|
|
131
|
+
sections: readonly unknown[];
|
|
132
|
+
}): string | undefined;
|
|
91
133
|
export declare function importMdx(opts: ImportMdxOptions): Promise<ImportMdxResult>;
|
|
92
134
|
/**
|
|
93
135
|
* The site's installed adapter package, resolved from its `apps/web` — never
|