blume 1.4.3 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +16 -12
- package/dist/cli/index.js +1784 -633
- package/dist/cli/index.js.map +111 -106
- package/dist/types/ai/component-markdown.d.ts +14 -4
- package/dist/types/core/config-input.d.ts +80 -28
- package/dist/types/core/config.d.ts +2 -1
- package/dist/types/core/data.d.ts +19 -3
- package/dist/types/core/diagnostics.d.ts +5 -1
- package/dist/types/core/i18n-ui.d.ts +12 -0
- package/dist/types/core/schema.d.ts +112 -15
- package/dist/types/core/sources/types.d.ts +3 -1
- package/dist/types/core/standard-schema.d.ts +7 -3
- package/dist/types/core/types.d.ts +43 -2
- package/dist/types/core/ui-packs/index.d.ts +9 -1
- package/dist/types/openapi/references.d.ts +6 -5
- package/dist/types/seo/x-handle.d.ts +3 -2
- package/dist/types/theme/fonts.d.ts +11 -2
- package/docs/advanced/api-reference.mdx +8 -6
- package/docs/advanced/custom-pages.mdx +5 -1
- package/docs/configuration/index.mdx +1 -1
- package/docs/configuration/search.mdx +2 -0
- package/docs/configuration/seo.mdx +1 -1
- package/docs/configuration/theming.mdx +4 -2
- package/docs/content/i18n.mdx +1 -1
- package/docs/content/meta.mdx +2 -1
- package/docs/content/meta.ts +1 -0
- package/docs/content/navigation.mdx +35 -1
- package/docs/content/versioning.mdx +106 -0
- package/docs/reference/cli.mdx +2 -1
- package/docs/reference/frontmatter.mdx +3 -0
- package/package.json +3 -1
- package/skills/blume-migrate/SKILL.md +2 -2
- package/skills/blume-migrate/references/docusaurus.md +1 -1
- package/skills/blume-migrate/references/fumadocs.md +1 -1
- package/skills/blume-migrate/references/mintlify.md +1 -1
- package/src/ai/agent-readability.ts +37 -10
- package/src/ai/ask-context.ts +5 -1
- package/src/ai/ask.ts +10 -1
- package/src/ai/component-markdown.ts +80 -43
- package/src/ai/llms.ts +40 -16
- package/src/ai/mcp/data.ts +48 -12
- package/src/ai/mcp/discovery.ts +28 -11
- package/src/ai/mcp/server.ts +183 -38
- package/src/ai/mcp/tools.ts +3 -3
- package/src/ai/skills.ts +32 -9
- package/src/ai/visibility.ts +2 -2
- package/src/astro/component-slots.ts +2 -0
- package/src/astro/examples.ts +6 -2
- package/src/astro/generate.ts +64 -34
- package/src/astro/integration.ts +13 -2
- package/src/astro/islands.ts +16 -9
- package/src/astro/templates.ts +181 -40
- package/src/audit/agent.ts +2 -2
- package/src/audit/checks/content.ts +26 -11
- package/src/audit/checks/dns-aid.ts +3 -0
- package/src/audit/checks/indexability.ts +24 -6
- package/src/audit/checks/llms.ts +9 -4
- package/src/audit/checks/network.ts +2 -0
- package/src/audit/checks/social.ts +18 -10
- package/src/audit/crawl.ts +37 -9
- package/src/audit/report.ts +20 -19
- package/src/audit/run.ts +5 -2
- package/src/audit/snapshot.ts +2 -4
- package/src/audit/types.ts +25 -3
- package/src/blume-modules.d.ts +5 -1
- package/src/cli/commands/audit.ts +9 -4
- package/src/cli/commands/build.ts +15 -9
- package/src/cli/commands/dev.ts +2 -0
- package/src/cli/commands/doctor.ts +2 -0
- package/src/cli/commands/eval.ts +7 -3
- package/src/cli/commands/init.ts +9 -9
- package/src/cli/commands/mcp-stdio.ts +3 -0
- package/src/cli/commands/translate.ts +14 -3
- package/src/cli/commands/version.ts +85 -0
- package/src/cli/dev-lock.ts +31 -10
- package/src/cli/eject-scripts.ts +17 -2
- package/src/cli/index.ts +2 -0
- package/src/cli/init/questions.ts +1 -1
- package/src/cli/init/scaffold.ts +22 -15
- package/src/cli/internal-error.ts +1 -0
- package/src/components/content/auto-type-table.ts +3 -0
- package/src/components/content/diff.ts +9 -5
- package/src/components/content/github-info.ts +2 -0
- package/src/components/islands/ask-ai.tsx +33 -25
- package/src/components/islands/hooks.ts +5 -1
- package/src/components/islands/webmcp.ts +49 -12
- package/src/components/layout/Fonts.astro +23 -3
- package/src/components/layout/Header.astro +25 -1
- package/src/components/layout/NavSelector.astro +11 -2
- package/src/components/layout/NavTree.astro +4 -2
- package/src/components/layout/PageLayout.astro +72 -3
- package/src/components/layout/ReferenceLayout.astro +2 -1
- package/src/components/layout/RootLayout.astro +20 -1
- package/src/components/layout/Search.astro +77 -13
- package/src/components/layout/VersionBanner.astro +39 -0
- package/src/components/layout/analytics-client.ts +8 -5
- package/src/components/layout/hydration-hint.ts +1 -1
- package/src/components/layout/nav-utils.ts +1 -4
- package/src/components/layout/overrides.ts +25 -12
- package/src/components/layout/search/algolia.ts +18 -5
- package/src/components/layout/search/endpoint.ts +3 -0
- package/src/components/layout/search/flexsearch.ts +23 -7
- package/src/components/layout/search/orama-cloud.ts +1 -1
- package/src/components/layout/search/orama.ts +4 -1
- package/src/components/layout/search/pagefind.ts +2 -0
- package/src/components/layout/search/types.ts +13 -1
- package/src/components/layout/search/typesense.ts +19 -3
- package/src/components/openapi/ApiOverview.astro +32 -6
- package/src/components/openapi/AsyncApiOperation.astro +237 -0
- package/src/components/openapi/Bindings.astro +89 -0
- package/src/components/openapi/MethodBadge.astro +3 -0
- package/src/components/openapi/Operation.astro +7 -2
- package/src/components/openapi/PanelTabs.astro +131 -0
- package/src/components/openapi/ParametersTable.astro +2 -0
- package/src/components/openapi/RequestPanel.astro +12 -119
- package/src/components/openapi/async-snippets.ts +174 -0
- package/src/components/openapi/async.ts +348 -0
- package/src/components/openapi/helpers.ts +52 -20
- package/src/components/openapi/security.ts +102 -29
- package/src/components/openapi/snippets.ts +11 -11
- package/src/core/component-overrides.ts +28 -23
- package/src/core/config-input.ts +89 -28
- package/src/core/config.ts +20 -7
- package/src/core/content.ts +3 -1
- package/src/core/data.ts +19 -3
- package/src/core/define-components.ts +5 -0
- package/src/core/diagnostics.ts +46 -38
- package/src/core/frontmatter.ts +33 -7
- package/src/core/graph.ts +137 -53
- package/src/core/i18n-ui.ts +15 -0
- package/src/core/i18n.ts +16 -8
- package/src/core/last-modified.ts +49 -0
- package/src/core/load-module.ts +1 -0
- package/src/core/manifest.ts +92 -3
- package/src/core/meta.ts +44 -14
- package/src/core/nav-diagnostics.ts +3 -3
- package/src/core/navigation.ts +247 -67
- package/src/core/project-graph.ts +26 -3
- package/src/core/schema.ts +214 -68
- package/src/core/sources/assets.ts +2 -0
- package/src/core/sources/cache.ts +6 -0
- package/src/core/sources/github-releases.ts +39 -31
- package/src/core/sources/mdx-remote.ts +4 -0
- package/src/core/sources/normalize.ts +67 -20
- package/src/core/sources/notion.ts +49 -17
- package/src/core/sources/portable-text.ts +32 -11
- package/src/core/sources/sanity.ts +68 -14
- package/src/core/sources/types.ts +4 -0
- package/src/core/sources/watch.ts +1 -1
- package/src/core/standard-schema.ts +9 -3
- package/src/core/text-width.ts +26 -0
- package/src/core/tsconfig-aliases.ts +9 -5
- package/src/core/types.ts +45 -2
- package/src/core/ui-packs/index.ts +9 -1
- package/src/core/version-cut.ts +301 -0
- package/src/core/version.ts +2 -0
- package/src/core/versions.ts +170 -0
- package/src/deploy/adapter-output.ts +5 -2
- package/src/deploy/cloudflare-negotiation.ts +25 -10
- package/src/deploy/sitemap.ts +33 -1
- package/src/deploy/vercel-negotiation.ts +45 -18
- package/src/eval/report.ts +4 -4
- package/src/eval/run.ts +2 -2
- package/src/eval/schema.ts +1 -1
- package/src/markdown/base-links.ts +6 -6
- package/src/markdown/directives.ts +7 -1
- package/src/markdown/heading-anchors.ts +17 -6
- package/src/markdown/index.ts +73 -24
- package/src/markdown/inline-code.ts +14 -2
- package/src/markdown/language-icon.ts +6 -2
- package/src/markdown/mdast.ts +18 -4
- package/src/markdown/package-commands.ts +6 -8
- package/src/markdown/table-wrap.ts +4 -1
- package/src/markdown/twoslash.ts +2 -0
- package/src/og/card.ts +33 -12
- package/src/og/derive.ts +43 -27
- package/src/openapi/asyncapi.ts +366 -0
- package/src/openapi/model.ts +126 -57
- package/src/openapi/parse.ts +97 -5
- package/src/openapi/references.ts +12 -10
- package/src/openapi/render-mdx.ts +73 -34
- package/src/openapi/scalar.ts +6 -8
- package/src/openapi/source.ts +98 -28
- package/src/registry/eject.ts +7 -2
- package/src/search/documents.ts +25 -5
- package/src/search/facets.ts +7 -5
- package/src/search/orama-index.ts +66 -20
- package/src/search/popular.ts +10 -5
- package/src/search/providers.ts +2 -2
- package/src/search/sync/index.ts +2 -0
- package/src/search/sync/typesense.ts +4 -2
- package/src/seo/jsonld.ts +24 -6
- package/src/seo/x-handle.ts +8 -3
- package/src/theme/chrome-icons.ts +7 -2
- package/src/theme/entry.ts +24 -2
- package/src/theme/fonts.ts +83 -7
- package/src/theme/icons.ts +4 -2
- package/src/theme/palette.ts +22 -14
- package/src/translate/meta.ts +15 -6
- package/src/translate/report.ts +9 -5
- package/src/translate/run.ts +10 -4
- package/src/translate/validate.ts +52 -17
- package/src/translate/work-list.ts +0 -0
package/src/core/types.ts
CHANGED
|
@@ -121,9 +121,24 @@ export interface PageRecord {
|
|
|
121
121
|
locale: string;
|
|
122
122
|
/**
|
|
123
123
|
* Locale-agnostic logical route shared by every translation (e.g.
|
|
124
|
-
* `/guides/x`
|
|
124
|
+
* `/guides/x`, or `/v1.0/guides/x` under versioning — the key is
|
|
125
|
+
* version-specific, so translations group within their version).
|
|
126
|
+
* Pages with the same key are translations of each other.
|
|
125
127
|
*/
|
|
126
128
|
translationKey: string;
|
|
129
|
+
/**
|
|
130
|
+
* Resolved docs version: an archived id (`v1.0`) for snapshot pages, `""`
|
|
131
|
+
* for the current (unprefixed) docs — including every page of an
|
|
132
|
+
* unversioned project.
|
|
133
|
+
*/
|
|
134
|
+
version: string;
|
|
135
|
+
/**
|
|
136
|
+
* Version- and locale-agnostic logical route (e.g. `/guides/x` for
|
|
137
|
+
* `/v1.0/guides/x`). Pages with the same key and locale are the same
|
|
138
|
+
* logical page across versions — this drives the switcher's same-page
|
|
139
|
+
* navigation and the canonical-to-latest lookup.
|
|
140
|
+
*/
|
|
141
|
+
versionKey: string;
|
|
127
142
|
/**
|
|
128
143
|
* True for entries filled in from the fallback locale to pad a locale's
|
|
129
144
|
* navigation for pages it hasn't translated yet. The record's content —
|
|
@@ -151,6 +166,7 @@ export interface PageRecord {
|
|
|
151
166
|
* schemas (schema output, so transforms apply). Present only when the
|
|
152
167
|
* project opts in and the page carries at least one value.
|
|
153
168
|
*/
|
|
169
|
+
// oxlint-disable-next-line anti-slop/no-unsafe-dictionary-type -- values are outputs of arbitrary user-supplied zod schemas (transforms included), so no narrower type exists
|
|
154
170
|
custom?: Record<string, unknown>;
|
|
155
171
|
headings: Heading[];
|
|
156
172
|
/** Whether the file is `.md`/`.mdx`. */
|
|
@@ -180,7 +196,12 @@ export type NavNode =
|
|
|
180
196
|
label: string;
|
|
181
197
|
badge?: string;
|
|
182
198
|
directory?: DirectoryMode;
|
|
183
|
-
|
|
199
|
+
/**
|
|
200
|
+
* The group's resolved render mode. Always stamped by both builders
|
|
201
|
+
* (generated and explicit-config sidebars), so the renderer can rely
|
|
202
|
+
* on it per node.
|
|
203
|
+
*/
|
|
204
|
+
display: SidebarDisplay;
|
|
184
205
|
icon?: string;
|
|
185
206
|
route?: string;
|
|
186
207
|
/**
|
|
@@ -266,6 +287,12 @@ export interface ContentGraph {
|
|
|
266
287
|
navigation: Navigation;
|
|
267
288
|
/** Navigation per locale; one entry per configured locale under i18n. */
|
|
268
289
|
navigationByLocale: Record<string, Navigation>;
|
|
290
|
+
/**
|
|
291
|
+
* Navigation per archived version, keyed by version id and then locale code
|
|
292
|
+
* (`""` on a single-locale site). The current version's trees are
|
|
293
|
+
* `navigation`/`navigationByLocale`; empty when versioning is off.
|
|
294
|
+
*/
|
|
295
|
+
navigationByVersion: Record<string, Record<string, Navigation>>;
|
|
269
296
|
/** Map of route -> pageId for fast lookup and duplicate detection. */
|
|
270
297
|
routes: Map<string, string>;
|
|
271
298
|
diagnostics: Diagnostic[];
|
|
@@ -289,6 +316,13 @@ export interface LocaleSwitchOption {
|
|
|
289
316
|
untranslated: boolean;
|
|
290
317
|
}
|
|
291
318
|
|
|
319
|
+
/** A docs version a logical page exists in, for the switcher and canonicals. */
|
|
320
|
+
export interface VersionAlternate {
|
|
321
|
+
/** Version id; `""` for the current (unprefixed) docs. */
|
|
322
|
+
version: string;
|
|
323
|
+
path: string;
|
|
324
|
+
}
|
|
325
|
+
|
|
292
326
|
/** A route entry written to `blume.manifest.json`. */
|
|
293
327
|
export interface RouteManifestEntry {
|
|
294
328
|
id: string;
|
|
@@ -313,6 +347,15 @@ export interface RouteManifestEntry {
|
|
|
313
347
|
locale: string;
|
|
314
348
|
/** Locales this logical page is genuinely translated into (excludes fallbacks). */
|
|
315
349
|
alternates: RouteAlternate[];
|
|
350
|
+
/** Resolved docs version (`""` for the current docs; see `PageRecord.version`). */
|
|
351
|
+
version: string;
|
|
352
|
+
/**
|
|
353
|
+
* Versions this logical page exists in within this route's locale — the
|
|
354
|
+
* current version first, then archived versions in configured order. Drives
|
|
355
|
+
* the switcher's same-page navigation and the canonical-to-latest lookup.
|
|
356
|
+
* Empty when versioning is off.
|
|
357
|
+
*/
|
|
358
|
+
versionAlternates: VersionAlternate[];
|
|
316
359
|
/** True when this route renders fallback content for a missing translation. */
|
|
317
360
|
fallback?: boolean;
|
|
318
361
|
/** Resolved "last updated" ISO date, when the feature is enabled. */
|
|
@@ -47,7 +47,15 @@ import { zh } from "./zh.ts";
|
|
|
47
47
|
* `zh-TW`). A locale's pack is merged onto English by {@link resolveUIStrings},
|
|
48
48
|
* so a pack only needs the keys it translates.
|
|
49
49
|
*/
|
|
50
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Starter packs keyed by locale code. Open-keyed on purpose: resolvers look
|
|
52
|
+
* packs up by whatever locale a project configures, not only the codes here.
|
|
53
|
+
*/
|
|
54
|
+
interface UiPackRegistry {
|
|
55
|
+
[locale: string]: UIStringsOverride;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const UI_PACKS: UiPackRegistry = {
|
|
51
59
|
ar,
|
|
52
60
|
bg,
|
|
53
61
|
bn,
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { cp, readdir, readFile, rm } from "node:fs/promises";
|
|
3
|
+
|
|
4
|
+
import { join, relative } from "pathe";
|
|
5
|
+
|
|
6
|
+
import { stripBasePath } from "./base-path.ts";
|
|
7
|
+
import { writeTextAtomic } from "./fs-atomic.ts";
|
|
8
|
+
import { localizeRoute } from "./i18n.ts";
|
|
9
|
+
import type { BlumeProject } from "./project-graph.ts";
|
|
10
|
+
import { scanProject } from "./project-graph.ts";
|
|
11
|
+
import { VERSION_ID } from "./schema.ts";
|
|
12
|
+
import { nextFenceState } from "./sources/normalize.ts";
|
|
13
|
+
import type { FenceState } from "./sources/normalize.ts";
|
|
14
|
+
import { VERSION_LIKE, versionizeRoute } from "./versions.ts";
|
|
15
|
+
|
|
16
|
+
/** What `cutVersion` did, for the CLI to report. */
|
|
17
|
+
export interface CutResult {
|
|
18
|
+
/** Files copied into the snapshot. */
|
|
19
|
+
copied: number;
|
|
20
|
+
/** Markdown pages whose root-absolute links were rewritten, with counts. */
|
|
21
|
+
rewritten: { file: string; count: number }[];
|
|
22
|
+
/** Whether `blume.config.ts` was updated in place. */
|
|
23
|
+
configUpdated: boolean;
|
|
24
|
+
/** Ready-to-paste config snippet when in-place update was not possible. */
|
|
25
|
+
configSnippet: string | null;
|
|
26
|
+
/** Absolute path of the created snapshot directory. */
|
|
27
|
+
dir: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A `cutVersion` failure the CLI reports as a user error, not a crash. */
|
|
31
|
+
export class CutError extends Error {
|
|
32
|
+
constructor(message: string) {
|
|
33
|
+
super(message);
|
|
34
|
+
this.name = "CutError";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Inline code spans are syntax being *shown*, not links — blank them before
|
|
39
|
+
// matching so replacements can splice into the real line by index (the mask
|
|
40
|
+
// preserves length). Mirrors the content-assets rewriter.
|
|
41
|
+
const INLINE_CODE = /`[^`]*`/gu;
|
|
42
|
+
// A markdown link/image target or an HTML href/src attribute whose value is
|
|
43
|
+
// root-absolute. Named groups carry the prefix (kept) and target (rewritten).
|
|
44
|
+
const ROOT_LINK = /(?<prefix>\]\(|href="|src=")(?<target>\/[^\s"')]*)/gu;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Build the link-rewrite table: every current-version route (basePath
|
|
48
|
+
* stripped, since authors write root-absolute links as if mounted at root)
|
|
49
|
+
* mapped to the same page's route inside the new snapshot. Only pages the
|
|
50
|
+
* snapshot actually contains qualify — filesystem pages under the content
|
|
51
|
+
* root. Spec-rendered references (`/api`, `/events`) and remote sources
|
|
52
|
+
* aren't copied, so links to them keep pointing at the live pages instead of
|
|
53
|
+
* a 404 inside the snapshot.
|
|
54
|
+
*/
|
|
55
|
+
const buildRouteRewrites = (
|
|
56
|
+
project: BlumeProject,
|
|
57
|
+
id: string
|
|
58
|
+
): Map<string, string> => {
|
|
59
|
+
const { basePath, i18n } = project.config;
|
|
60
|
+
const { contentRoot } = project.context;
|
|
61
|
+
const rewrites = new Map<string, string>();
|
|
62
|
+
for (const page of project.graph.pages) {
|
|
63
|
+
// `sourcePath` is set by the filesystem adapter only, so its absence
|
|
64
|
+
// already excludes generated and remote pages.
|
|
65
|
+
const { sourcePath } = page;
|
|
66
|
+
if (
|
|
67
|
+
page.version !== "" ||
|
|
68
|
+
!sourcePath ||
|
|
69
|
+
relative(contentRoot, sourcePath).startsWith("..")
|
|
70
|
+
) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const logical = versionizeRoute(page.versionKey, id);
|
|
74
|
+
rewrites.set(
|
|
75
|
+
stripBasePath(basePath, page.route),
|
|
76
|
+
i18n ? localizeRoute(logical, page.locale, i18n) : logical
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
return rewrites;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/** Rewrite one line's root-absolute internal links via the rewrite table. */
|
|
83
|
+
const rewriteLine = (line: string, rewrites: Map<string, string>): string => {
|
|
84
|
+
const masked = line.replaceAll(INLINE_CODE, (span) =>
|
|
85
|
+
" ".repeat(span.length)
|
|
86
|
+
);
|
|
87
|
+
let out = "";
|
|
88
|
+
let cursor = 0;
|
|
89
|
+
let count = 0;
|
|
90
|
+
for (const match of masked.matchAll(ROOT_LINK)) {
|
|
91
|
+
const target = match.groups?.target ?? "";
|
|
92
|
+
// An anchor or query stays attached to the rewritten path.
|
|
93
|
+
const hash = target.search(/[#?]/u);
|
|
94
|
+
const path = hash === -1 ? target : target.slice(0, hash);
|
|
95
|
+
const suffix = hash === -1 ? "" : target.slice(hash);
|
|
96
|
+
const bare = path !== "/" && path.endsWith("/") ? path.slice(0, -1) : path;
|
|
97
|
+
const replacement = rewrites.get(bare);
|
|
98
|
+
if (replacement === undefined) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const offset = (match.index ?? 0) + (match.groups?.prefix?.length ?? 0);
|
|
102
|
+
out += line.slice(cursor, offset) + replacement + suffix;
|
|
103
|
+
cursor = offset + target.length;
|
|
104
|
+
count += 1;
|
|
105
|
+
}
|
|
106
|
+
return count === 0 ? line : out + line.slice(cursor);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/** A page's link-rewritten text and how many of its lines changed. */
|
|
110
|
+
export interface SnapshotRewrite {
|
|
111
|
+
text: string;
|
|
112
|
+
count: number;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Rewrite a copied page's root-absolute internal links to their snapshot
|
|
117
|
+
* equivalents, skipping fenced and inline code. Relative links need no
|
|
118
|
+
* rewriting — the whole tree copies together, so they stay self-contained.
|
|
119
|
+
*/
|
|
120
|
+
export const rewriteSnapshotLinks = (
|
|
121
|
+
source: string,
|
|
122
|
+
rewrites: Map<string, string>
|
|
123
|
+
): SnapshotRewrite => {
|
|
124
|
+
let fence: FenceState = null;
|
|
125
|
+
let count = 0;
|
|
126
|
+
const lines = source.split("\n").map((line) => {
|
|
127
|
+
const next = nextFenceState(line, fence);
|
|
128
|
+
const inFence = fence !== null || next !== null;
|
|
129
|
+
fence = next;
|
|
130
|
+
if (inFence) {
|
|
131
|
+
return line;
|
|
132
|
+
}
|
|
133
|
+
const rewrittenLine = rewriteLine(line, rewrites);
|
|
134
|
+
if (rewrittenLine !== line) {
|
|
135
|
+
count += 1;
|
|
136
|
+
}
|
|
137
|
+
return rewrittenLine;
|
|
138
|
+
});
|
|
139
|
+
return { count, text: lines.join("\n") };
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/** The config entry to add, as a paste-ready snippet for the fallback path. */
|
|
143
|
+
const snippetFor = (id: string, hasVersions: boolean): string =>
|
|
144
|
+
hasVersions
|
|
145
|
+
? `Add to versions.archived in blume.config.ts (newest first):\n\n { id: "${id}" },\n`
|
|
146
|
+
: `Add to blume.config.ts:\n\n versions: {\n archived: [{ id: "${id}" }],\n current: { label: "…" },\n },\n`;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Best-effort in-place config update: insert the new id at the head of an
|
|
150
|
+
* existing \`archived: [\` array literal, preserving indentation. Anything
|
|
151
|
+
* fancier (computed arrays, config spread across files) falls back to a
|
|
152
|
+
* printed snippet — the config is user-authored TypeScript, and string-level
|
|
153
|
+
* surgery beyond this simple shape risks corrupting it.
|
|
154
|
+
*/
|
|
155
|
+
export const insertArchivedVersion = async (
|
|
156
|
+
configPath: string,
|
|
157
|
+
id: string
|
|
158
|
+
): Promise<boolean> => {
|
|
159
|
+
let text: string;
|
|
160
|
+
try {
|
|
161
|
+
text = await readFile(configPath, "utf-8");
|
|
162
|
+
} catch {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
const match = text.match(/(?<lead>archived:\s*\[)/u);
|
|
166
|
+
if (!match || match.index === undefined) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
const insertAt = match.index + (match.groups?.lead?.length ?? 0);
|
|
170
|
+
// Indentation: one level deeper than the line holding `archived:`.
|
|
171
|
+
const lineStart = text.lastIndexOf("\n", match.index) + 1;
|
|
172
|
+
const indent = text.slice(lineStart).match(/^\s*/u)?.[0] ?? "";
|
|
173
|
+
const rest = text.slice(insertAt);
|
|
174
|
+
// Match the array's authored shape: empty stays bare, an inline array gets
|
|
175
|
+
// an inline entry, a multiline array gets its own indented line.
|
|
176
|
+
let entry: string;
|
|
177
|
+
if (rest.trimStart().startsWith("]")) {
|
|
178
|
+
entry = `{ id: "${id}" }`;
|
|
179
|
+
} else if (rest.startsWith("\n")) {
|
|
180
|
+
entry = `\n${indent} { id: "${id}" },`;
|
|
181
|
+
} else {
|
|
182
|
+
entry = `{ id: "${id}" }, `;
|
|
183
|
+
}
|
|
184
|
+
await writeTextAtomic(configPath, text.slice(0, insertAt) + entry + rest);
|
|
185
|
+
return true;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Freeze the current docs as an archived version: copy the content tree into
|
|
190
|
+
* `<contentRoot>/<id>/` (excluding existing snapshots), rewrite root-absolute
|
|
191
|
+
* internal links so the copy is self-contained, and register the id in
|
|
192
|
+
* `blume.config.ts` (or print the snippet to paste).
|
|
193
|
+
*/
|
|
194
|
+
export const cutVersion = async (
|
|
195
|
+
root: string,
|
|
196
|
+
id: string,
|
|
197
|
+
options: { force?: boolean } = {}
|
|
198
|
+
): Promise<CutResult> => {
|
|
199
|
+
if (!VERSION_ID.test(id)) {
|
|
200
|
+
throw new CutError(
|
|
201
|
+
`Version ids must start with a letter (e.g. "v1.0") and contain only letters, digits, dots, hyphens, and underscores — got "${id}".`
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const project = await scanProject(root);
|
|
206
|
+
const errors = project.diagnostics.filter(
|
|
207
|
+
(diagnostic) => diagnostic.severity === "error"
|
|
208
|
+
);
|
|
209
|
+
if (errors.length > 0) {
|
|
210
|
+
throw new CutError(
|
|
211
|
+
`The project has ${errors.length} error diagnostic(s) — fix them before cutting a version (run \`blume validate\`).`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
// A missing content root already fails inside `scanProject` (the filesystem
|
|
215
|
+
// source's own validation), so only the version-level checks remain here.
|
|
216
|
+
const { contentRoot } = project.context;
|
|
217
|
+
if (project.config.versions?.archived.some((version) => version.id === id)) {
|
|
218
|
+
throw new CutError(
|
|
219
|
+
`Version "${id}" is already registered in versions.archived.`
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const dir = join(contentRoot, id);
|
|
224
|
+
if (existsSync(dir)) {
|
|
225
|
+
if (!options.force) {
|
|
226
|
+
throw new CutError(
|
|
227
|
+
`${dir} already exists — pass --force to overwrite the snapshot.`
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
await rm(dir, { force: true, recursive: true });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Existing snapshots must not nest inside the new one: configured ids, plus
|
|
234
|
+
// any version-shaped directory (`v1.0/`) that never made it into the config —
|
|
235
|
+
// e.g. a prior cut whose config update fell back to a printed snippet. A
|
|
236
|
+
// genuine content folder that merely looks like a version already draws the
|
|
237
|
+
// rename-or-register diagnostic, so skipping it here is consistent.
|
|
238
|
+
const excluded = new Set([
|
|
239
|
+
id,
|
|
240
|
+
"node_modules",
|
|
241
|
+
...(project.config.versions?.archived.map((version) => version.id) ?? []),
|
|
242
|
+
]);
|
|
243
|
+
const entries = await readdir(contentRoot, { withFileTypes: true });
|
|
244
|
+
let copied = 0;
|
|
245
|
+
await Promise.all(
|
|
246
|
+
entries.map(async (entry) => {
|
|
247
|
+
if (
|
|
248
|
+
excluded.has(entry.name) ||
|
|
249
|
+
entry.name.startsWith(".") ||
|
|
250
|
+
(entry.isDirectory() && VERSION_LIKE.test(entry.name))
|
|
251
|
+
) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
await cp(join(contentRoot, entry.name), join(dir, entry.name), {
|
|
255
|
+
recursive: true,
|
|
256
|
+
});
|
|
257
|
+
})
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
// Rewrite root-absolute internal links in every copied markdown page.
|
|
261
|
+
const rewrites = buildRouteRewrites(project, id);
|
|
262
|
+
const rewritten: { file: string; count: number }[] = [];
|
|
263
|
+
const walk = async (current: string): Promise<void> => {
|
|
264
|
+
const children = await readdir(current, { withFileTypes: true });
|
|
265
|
+
await Promise.all(
|
|
266
|
+
children.map(async (child) => {
|
|
267
|
+
const abs = join(current, child.name);
|
|
268
|
+
if (child.isDirectory()) {
|
|
269
|
+
await walk(abs);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
copied += 1;
|
|
273
|
+
if (!/\.mdx?$/u.test(child.name)) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const source = await readFile(abs, "utf-8");
|
|
277
|
+
const { text, count } = rewriteSnapshotLinks(source, rewrites);
|
|
278
|
+
if (count > 0) {
|
|
279
|
+
await writeTextAtomic(abs, text);
|
|
280
|
+
rewritten.push({ count, file: relative(dir, abs) });
|
|
281
|
+
}
|
|
282
|
+
})
|
|
283
|
+
);
|
|
284
|
+
};
|
|
285
|
+
await walk(dir);
|
|
286
|
+
|
|
287
|
+
const configPath = join(root, "blume.config.ts");
|
|
288
|
+
const configUpdated =
|
|
289
|
+
project.config.versions !== undefined &&
|
|
290
|
+
(await insertArchivedVersion(configPath, id));
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
configSnippet: configUpdated
|
|
294
|
+
? null
|
|
295
|
+
: snippetFor(id, project.config.versions !== undefined),
|
|
296
|
+
configUpdated,
|
|
297
|
+
copied,
|
|
298
|
+
dir,
|
|
299
|
+
rewritten,
|
|
300
|
+
};
|
|
301
|
+
};
|
package/src/core/version.ts
CHANGED
|
@@ -16,6 +16,8 @@ let cached: string | undefined;
|
|
|
16
16
|
export const getBlumeVersion = (): string => {
|
|
17
17
|
if (cached === undefined) {
|
|
18
18
|
const pkgPath = join(packageRoot(), "package.json");
|
|
19
|
+
// SAFETY: this reads blume's own package.json, which always declares a
|
|
20
|
+
// `version` (publishing requires it).
|
|
19
21
|
cached = (JSON.parse(readFileSync(pkgPath, "utf-8")) as { version: string })
|
|
20
22
|
.version;
|
|
21
23
|
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { localizeRoute } from "./i18n.ts";
|
|
2
|
+
import type {
|
|
3
|
+
ArchivedVersionConfig,
|
|
4
|
+
ResolvedConfig,
|
|
5
|
+
ResolvedI18nConfig,
|
|
6
|
+
ResolvedVersionsConfig,
|
|
7
|
+
} from "./schema.ts";
|
|
8
|
+
import type { Diagnostic, PageRecord } from "./types.ts";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Version logic, centralized. Every seam that needs to reason about docs
|
|
12
|
+
* versions (content discovery, navigation, manifest, runtime generation, the
|
|
13
|
+
* catch-all) goes through these helpers so the routing rules live in exactly
|
|
14
|
+
* one place — the same contract `./i18n.ts` holds for locales.
|
|
15
|
+
*
|
|
16
|
+
* The current version is the empty string `""`: the latest docs live at the
|
|
17
|
+
* content root with unprefixed URLs, and only archived snapshots have a
|
|
18
|
+
* directory and a URL segment.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Top-level folders that look like a version (`v1`, `V2.0`) — used by the
|
|
23
|
+
* unconfigured-snapshot diagnostic and by `blume version` to keep such folders
|
|
24
|
+
* out of new snapshots.
|
|
25
|
+
*/
|
|
26
|
+
export const VERSION_LIKE = /^v\d/iu;
|
|
27
|
+
|
|
28
|
+
/** True when the project opts into versioning. */
|
|
29
|
+
export const versionsEnabled = (
|
|
30
|
+
config: ResolvedConfig
|
|
31
|
+
): config is ResolvedConfig & { versions: ResolvedVersionsConfig } =>
|
|
32
|
+
config.versions !== undefined;
|
|
33
|
+
|
|
34
|
+
/** All archived version ids, in configured (switcher) order. */
|
|
35
|
+
export const archivedIds = (versions: ResolvedVersionsConfig): string[] =>
|
|
36
|
+
versions.archived.map((version) => version.id);
|
|
37
|
+
|
|
38
|
+
/** Switcher label for a version: the current label for `""`, else per config. */
|
|
39
|
+
export const versionLabel = (
|
|
40
|
+
id: string,
|
|
41
|
+
versions: ResolvedVersionsConfig
|
|
42
|
+
): string => {
|
|
43
|
+
if (id === "") {
|
|
44
|
+
return versions.current.label;
|
|
45
|
+
}
|
|
46
|
+
const archived = versions.archived.find((version) => version.id === id);
|
|
47
|
+
return archived?.label ?? id;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** The archived version config for an id (`undefined` for current/unknown). */
|
|
51
|
+
export const archivedVersion = (
|
|
52
|
+
id: string,
|
|
53
|
+
versions: ResolvedVersionsConfig
|
|
54
|
+
): ArchivedVersionConfig | undefined =>
|
|
55
|
+
versions.archived.find((version) => version.id === id);
|
|
56
|
+
|
|
57
|
+
/** A resolved version (`""` for current) and the version-stripped remainder. */
|
|
58
|
+
export interface DetectedVersion {
|
|
59
|
+
version: string;
|
|
60
|
+
rest: string[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** {@link DetectedVersion} for a slash-joined source ref. */
|
|
64
|
+
export interface DetectedVersionRef {
|
|
65
|
+
version: string;
|
|
66
|
+
rest: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Detect a leading archived-version directory in a path's segments. The
|
|
71
|
+
* current version lives at the content root, so only archived ids are matched
|
|
72
|
+
* as a leading segment. Returns the resolved version (`""` for current) and
|
|
73
|
+
* the remaining (version-stripped) segments. Runs BEFORE locale detection —
|
|
74
|
+
* the snapshot directory is outermost on disk, so the locale parser must see
|
|
75
|
+
* a version-stripped path.
|
|
76
|
+
*/
|
|
77
|
+
export const detectVersion = (
|
|
78
|
+
parts: string[],
|
|
79
|
+
versions: ResolvedVersionsConfig
|
|
80
|
+
): DetectedVersion => {
|
|
81
|
+
const [first] = parts;
|
|
82
|
+
if (first && versions.archived.some((version) => version.id === first)) {
|
|
83
|
+
return { rest: parts.slice(1), version: first };
|
|
84
|
+
}
|
|
85
|
+
return { rest: parts, version: "" };
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* {@link detectVersion} over a slash-joined source ref (`v1.0/fr/x.mdx`):
|
|
90
|
+
* returns the version and the version-stripped ref for the locale parser and
|
|
91
|
+
* route mapping to consume.
|
|
92
|
+
*/
|
|
93
|
+
export const detectVersionRef = (
|
|
94
|
+
ref: string,
|
|
95
|
+
versions: ResolvedVersionsConfig
|
|
96
|
+
): DetectedVersionRef => {
|
|
97
|
+
const { version, rest } = detectVersion(ref.split("/"), versions);
|
|
98
|
+
return { rest: rest.join("/"), version };
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Prefix a version-agnostic logical route with its version: `/guides/x`
|
|
103
|
+
* becomes `/v1.0/guides/x`, `/` becomes `/v1.0`. The current version (`""`)
|
|
104
|
+
* is the identity.
|
|
105
|
+
*/
|
|
106
|
+
export const versionizeRoute = (
|
|
107
|
+
logicalRoute: string,
|
|
108
|
+
version: string
|
|
109
|
+
): string => {
|
|
110
|
+
if (!version) {
|
|
111
|
+
return logicalRoute;
|
|
112
|
+
}
|
|
113
|
+
return logicalRoute === "/" ? `/${version}` : `/${version}${logicalRoute}`;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The localized root route of a version: `/`, `/fr`, `/v1.0`, or `/fr/v1.0`.
|
|
118
|
+
* The locale prefix stays outermost, matching how page routes compose
|
|
119
|
+
* (`localizeRoute(versionizeRoute(...))`).
|
|
120
|
+
*/
|
|
121
|
+
export const versionRoot = (
|
|
122
|
+
version: string,
|
|
123
|
+
locale?: string,
|
|
124
|
+
i18n?: ResolvedI18nConfig
|
|
125
|
+
): string => {
|
|
126
|
+
const logical = versionizeRoute("/", version);
|
|
127
|
+
if (i18n && locale !== undefined) {
|
|
128
|
+
return localizeRoute(logical, locale, i18n);
|
|
129
|
+
}
|
|
130
|
+
return logical;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Warn about top-level content folders shaped like a version (`v1`, `v2.0`)
|
|
135
|
+
* that aren't declared in `versions.archived`. Without this they're silently
|
|
136
|
+
* treated as current-version content under a `/<folder>/…` route — usually a
|
|
137
|
+
* snapshot that wasn't registered in config.
|
|
138
|
+
*/
|
|
139
|
+
export const versionsDiagnostics = (
|
|
140
|
+
pages: PageRecord[],
|
|
141
|
+
versions: ResolvedVersionsConfig
|
|
142
|
+
): Diagnostic[] => {
|
|
143
|
+
// Exact-id comparison, matching `detectVersion`: a folder that differs from
|
|
144
|
+
// a configured id only by case (`V1.0/` vs `v1.0`) is NOT routed as that
|
|
145
|
+
// snapshot, so it must still warn. `VERSION_LIKE` is case-insensitive on
|
|
146
|
+
// its own.
|
|
147
|
+
const configured = new Set(versions.archived.map((version) => version.id));
|
|
148
|
+
const seen = new Set<string>();
|
|
149
|
+
const diagnostics: Diagnostic[] = [];
|
|
150
|
+
for (const page of pages) {
|
|
151
|
+
// The version-looking folder is the first segment of the source-local ref
|
|
152
|
+
// (e.g. `v1.0/guide.md`), not the namespaced id (`filesystem:v1.0/guide.md`).
|
|
153
|
+
const [first] = page.source.ref.split("/");
|
|
154
|
+
if (
|
|
155
|
+
first &&
|
|
156
|
+
!seen.has(first) &&
|
|
157
|
+
VERSION_LIKE.test(first) &&
|
|
158
|
+
!configured.has(first)
|
|
159
|
+
) {
|
|
160
|
+
seen.add(first);
|
|
161
|
+
diagnostics.push({
|
|
162
|
+
code: "BLUME_VERSIONS_UNCONFIGURED_VERSION",
|
|
163
|
+
message: `Folder "${first}/" looks like a version, but "${first}" is not in versions.archived — its pages are treated as current-version content at /${first}/….`,
|
|
164
|
+
severity: "warning",
|
|
165
|
+
suggestion: `Add { id: "${first}" } to versions.archived, or rename the folder if it isn't a snapshot.`,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return diagnostics;
|
|
170
|
+
};
|
|
@@ -8,6 +8,9 @@ import type { ProjectContext } from "../core/types.ts";
|
|
|
8
8
|
|
|
9
9
|
type Adapter = NonNullable<ResolvedConfig["deployment"]["adapter"]>;
|
|
10
10
|
|
|
11
|
+
/** An optional per-adapter path, keyed by the closed adapter union. */
|
|
12
|
+
type AdapterPathMap = { [A in Adapter]?: string };
|
|
13
|
+
|
|
11
14
|
/**
|
|
12
15
|
* Top-level directory each server adapter writes its deploy bundle into, for
|
|
13
16
|
* `.gitignore` — the bundle is a build artifact, and the platform's own state
|
|
@@ -15,7 +18,7 @@ type Adapter = NonNullable<ResolvedConfig["deployment"]["adapter"]>;
|
|
|
15
18
|
* whole directory is ignored. `node` and `cloudflare` emit into `dist/`, which
|
|
16
19
|
* `blume init` already ignores.
|
|
17
20
|
*/
|
|
18
|
-
export const ADAPTER_IGNORE_DIRS:
|
|
21
|
+
export const ADAPTER_IGNORE_DIRS: AdapterPathMap = {
|
|
19
22
|
netlify: ".netlify/",
|
|
20
23
|
vercel: ".vercel/",
|
|
21
24
|
};
|
|
@@ -40,7 +43,7 @@ export const ADAPTER_IGNORE_DIRS: Partial<Record<Adapter, string>> = {
|
|
|
40
43
|
* chunks and `node_modules`. Given the right root it writes its Build Output
|
|
41
44
|
* tree straight to `<root>/.vercel/output`, so there is nothing left to move.
|
|
42
45
|
*/
|
|
43
|
-
export const ADAPTER_OUTPUT_PATHS:
|
|
46
|
+
export const ADAPTER_OUTPUT_PATHS: AdapterPathMap = {
|
|
44
47
|
netlify: ".netlify/v1",
|
|
45
48
|
};
|
|
46
49
|
|
|
@@ -75,6 +75,25 @@ const FALLBACK_RULES = ["/*", "!/_astro/*", "!/*.md", "!/*.mdx", "!/*.txt"];
|
|
|
75
75
|
const basePrefix = (base?: string): string =>
|
|
76
76
|
base && base !== "/" ? base.replace(/\/$/u, "") : "";
|
|
77
77
|
|
|
78
|
+
/** A value as `JSON.parse` produces it (the wrangler config's value space). */
|
|
79
|
+
type JsonValue =
|
|
80
|
+
| string
|
|
81
|
+
| number
|
|
82
|
+
| boolean
|
|
83
|
+
| null
|
|
84
|
+
| JsonValue[]
|
|
85
|
+
| { [key: string]: JsonValue };
|
|
86
|
+
|
|
87
|
+
/** Narrow a parsed JSON value to a plain (non-array) object. */
|
|
88
|
+
const isJsonObject = (
|
|
89
|
+
value: JsonValue | undefined
|
|
90
|
+
): value is { [key: string]: JsonValue } =>
|
|
91
|
+
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
92
|
+
|
|
93
|
+
/** Narrow a parsed JSON value to a string. */
|
|
94
|
+
const isJsonString = (value: JsonValue | undefined): value is string =>
|
|
95
|
+
typeof value === "string";
|
|
96
|
+
|
|
78
97
|
const isNegativeRule = (rule: string): boolean => rule.startsWith("!");
|
|
79
98
|
|
|
80
99
|
const ruleBody = (rule: string): string =>
|
|
@@ -163,7 +182,7 @@ const coveredBy = (rule: string, other: string): boolean =>
|
|
|
163
182
|
* validation error at deploy time.
|
|
164
183
|
*/
|
|
165
184
|
export const mergeRunWorkerFirstRules = (
|
|
166
|
-
existing:
|
|
185
|
+
existing: JsonValue | undefined,
|
|
167
186
|
added: readonly string[]
|
|
168
187
|
): string[] | true => {
|
|
169
188
|
if (existing === true) {
|
|
@@ -466,29 +485,25 @@ export const injectWorkerNegotiation = (
|
|
|
466
485
|
if (options.routePaths.length === 0) {
|
|
467
486
|
return null;
|
|
468
487
|
}
|
|
469
|
-
let config:
|
|
488
|
+
let config: JsonValue;
|
|
470
489
|
try {
|
|
471
490
|
config = JSON.parse(wranglerText);
|
|
472
491
|
} catch {
|
|
473
492
|
return null;
|
|
474
493
|
}
|
|
475
|
-
if (
|
|
494
|
+
if (!isJsonObject(config)) {
|
|
476
495
|
return null;
|
|
477
496
|
}
|
|
478
497
|
const { main } = config;
|
|
479
498
|
if (
|
|
480
|
-
|
|
499
|
+
!isJsonString(main) ||
|
|
481
500
|
main.length === 0 ||
|
|
482
501
|
main === NEGOTIATION_WORKER_FILE
|
|
483
502
|
) {
|
|
484
503
|
return null;
|
|
485
504
|
}
|
|
486
|
-
const assets = config
|
|
487
|
-
if (
|
|
488
|
-
assets === null ||
|
|
489
|
-
typeof assets !== "object" ||
|
|
490
|
-
typeof assets.binding !== "string"
|
|
491
|
-
) {
|
|
505
|
+
const { assets } = config;
|
|
506
|
+
if (!isJsonObject(assets) || !isJsonString(assets.binding)) {
|
|
492
507
|
return null;
|
|
493
508
|
}
|
|
494
509
|
const {
|