blume 1.6.0 → 1.6.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 +15 -0
- package/dist/cli/index.js +434 -150
- package/dist/cli/index.js.map +59 -56
- package/dist/types/core/data.d.ts +10 -1
- package/dist/types/core/i18n-ui.d.ts +2 -0
- package/dist/types/core/schema.d.ts +5 -0
- package/dist/types/core/types.d.ts +6 -0
- package/dist/types/openapi/references.d.ts +5 -0
- package/docs/07-faq.mdx +9 -9
- package/docs/advanced/api-reference.mdx +10 -1
- package/docs/advanced/custom-pages.mdx +3 -1
- package/docs/advanced/graphql.mdx +1 -1
- package/docs/configuration/ai.mdx +4 -0
- package/docs/configuration/seo.mdx +3 -3
- package/docs/configuration/theming.mdx +6 -0
- package/docs/content/components.mdx +8 -1
- package/package.json +53 -53
- package/src/astro/examples.ts +29 -2
- package/src/astro/generate.ts +99 -61
- package/src/astro/index.ts +7 -0
- package/src/astro/markdown-negotiation.ts +1 -1
- package/src/astro/runtime-modules.ts +196 -0
- package/src/astro/templates.ts +241 -38
- package/src/cli/commands/build.ts +7 -1
- package/src/cli/commands/dev.ts +6 -3
- package/src/cli/host-args.ts +18 -0
- package/src/cli/index.ts +2 -1
- package/src/components/copy-feedback.ts +93 -9
- package/src/components/islands/ask-ai.tsx +4 -1
- package/src/components/islands/hooks.ts +3 -1
- package/src/components/layout/PageActions.astro +25 -14
- package/src/core/data.ts +10 -1
- package/src/core/define-components.ts +2 -0
- package/src/core/i18n-ui.ts +1 -0
- package/src/core/includes.ts +2 -1
- package/src/core/manifest.ts +10 -0
- package/src/core/schema.ts +13 -5
- package/src/core/types.ts +6 -0
- package/src/core/ui-packs/ar.ts +1 -0
- package/src/core/ui-packs/bg.ts +1 -0
- package/src/core/ui-packs/bn.ts +1 -0
- package/src/core/ui-packs/ca.ts +1 -0
- package/src/core/ui-packs/cs.ts +1 -0
- package/src/core/ui-packs/da.ts +1 -0
- package/src/core/ui-packs/de.ts +1 -0
- package/src/core/ui-packs/el.ts +1 -0
- package/src/core/ui-packs/es.ts +1 -0
- package/src/core/ui-packs/fa.ts +1 -0
- package/src/core/ui-packs/fi.ts +1 -0
- package/src/core/ui-packs/fr.ts +1 -0
- package/src/core/ui-packs/he.ts +1 -0
- package/src/core/ui-packs/hi.ts +1 -0
- package/src/core/ui-packs/hr.ts +1 -0
- package/src/core/ui-packs/hu.ts +1 -0
- package/src/core/ui-packs/id.ts +1 -0
- package/src/core/ui-packs/it.ts +1 -0
- package/src/core/ui-packs/ja.ts +1 -0
- package/src/core/ui-packs/ko.ts +1 -0
- package/src/core/ui-packs/nl.ts +1 -0
- package/src/core/ui-packs/no.ts +1 -0
- package/src/core/ui-packs/pl.ts +1 -0
- package/src/core/ui-packs/pt-br.ts +1 -0
- package/src/core/ui-packs/pt.ts +1 -0
- package/src/core/ui-packs/ro.ts +1 -0
- package/src/core/ui-packs/ru.ts +1 -0
- package/src/core/ui-packs/sk.ts +1 -0
- package/src/core/ui-packs/sr.ts +1 -0
- package/src/core/ui-packs/sv.ts +1 -0
- package/src/core/ui-packs/th.ts +1 -0
- package/src/core/ui-packs/tr.ts +1 -0
- package/src/core/ui-packs/uk.ts +1 -0
- package/src/core/ui-packs/vi.ts +1 -0
- package/src/core/ui-packs/zh-tw.ts +1 -0
- package/src/core/ui-packs/zh.ts +1 -0
- package/src/core/version-cut.ts +5 -3
- package/src/deploy/vercel-negotiation.ts +49 -6
- package/src/og/card.ts +1 -1
- package/src/openapi/references.ts +8 -0
- package/src/openapi/render-mdx.ts +18 -4
- package/src/openapi/scalar.ts +0 -4
- package/src/registry/eject.ts +36 -17
- package/src/theme/entry.ts +2 -2
- package/src/theme/sources.ts +49 -0
package/src/core/version-cut.ts
CHANGED
|
@@ -177,12 +177,14 @@ export const insertArchivedVersion = async (
|
|
|
177
177
|
const indent = text.slice(lineStart).match(/^\s*/u)?.[0] ?? "";
|
|
178
178
|
const rest = text.slice(insertAt);
|
|
179
179
|
// Match the array's authored shape: empty stays bare, an inline array gets
|
|
180
|
-
// an inline entry, a multiline array gets its own indented line
|
|
180
|
+
// an inline entry, a multiline array gets its own indented line — on the
|
|
181
|
+
// file's own line ending, so a CRLF config doesn't gain a lone LF.
|
|
182
|
+
const eol = /^\r?\n/u.exec(rest)?.[0];
|
|
181
183
|
let entry: string;
|
|
182
184
|
if (rest.trimStart().startsWith("]")) {
|
|
183
185
|
entry = `{ id: "${id}" }`;
|
|
184
|
-
} else if (
|
|
185
|
-
entry =
|
|
186
|
+
} else if (eol) {
|
|
187
|
+
entry = `${eol}${indent} { id: "${id}" },`;
|
|
186
188
|
} else {
|
|
187
189
|
entry = `{ id: "${id}" }, `;
|
|
188
190
|
}
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
* into it so a content-page request that prefers `text/markdown` is rewritten
|
|
10
10
|
* (not redirected) to the page's prerendered `.md` mirror — the deployed
|
|
11
11
|
* counterpart of the dev-server rewrite in `astro/markdown-negotiation.ts`.
|
|
12
|
+
* The same routing config also answers a *missing* page: a request that
|
|
13
|
+
* prefers Markdown (or asks for a `.md` URL no page backs) gets the
|
|
14
|
+
* prerendered Markdown 404 body with the 404 status, instead of the HTML shell.
|
|
12
15
|
*/
|
|
13
16
|
|
|
14
17
|
/**
|
|
@@ -49,6 +52,32 @@ const ACCEPT_MARKDOWN_CONDITION: VercelRoute["has"] = [
|
|
|
49
52
|
|
|
50
53
|
const VARY_ACCEPT = { vary: "Accept" };
|
|
51
54
|
|
|
55
|
+
/** Where the prerendered Markdown 404 (`pages/404.md.ts`) lands. */
|
|
56
|
+
const NOT_FOUND_MARKDOWN_DEST = "/404.md";
|
|
57
|
+
|
|
58
|
+
/** The adapter's own not-found fallback — the anchor the Markdown 404 precedes. */
|
|
59
|
+
const NOT_FOUND_HTML_DEST = "/404.html";
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Miss-phase routes that answer a missing page with the Markdown 404 body: any
|
|
63
|
+
* path when the client prefers Markdown, and any `.md`/`.mdx` URL (a request
|
|
64
|
+
* for a raw-Markdown mirror that has no page wants Markdown back, not the HTML
|
|
65
|
+
* shell). Both keep the 404 status. Spliced immediately before the adapter's
|
|
66
|
+
* `/404.html` fallback, so they run after every server route (the MCP
|
|
67
|
+
* endpoint, server islands, images) has had its turn and never hijack a
|
|
68
|
+
* request one of those would have answered.
|
|
69
|
+
*/
|
|
70
|
+
const NOT_FOUND_MARKDOWN_ROUTES: readonly VercelRoute[] = [
|
|
71
|
+
{
|
|
72
|
+
dest: NOT_FOUND_MARKDOWN_DEST,
|
|
73
|
+
has: ACCEPT_MARKDOWN_CONDITION,
|
|
74
|
+
headers: VARY_ACCEPT,
|
|
75
|
+
src: "^/.*$",
|
|
76
|
+
status: 404,
|
|
77
|
+
},
|
|
78
|
+
{ dest: NOT_FOUND_MARKDOWN_DEST, src: "^/.*\\.mdx?$", status: 404 },
|
|
79
|
+
];
|
|
80
|
+
|
|
52
81
|
/**
|
|
53
82
|
* Vercel rejects route `src` patterns longer than 4096 characters, so route
|
|
54
83
|
* alternations are split across as many route entries as needed. The budget
|
|
@@ -186,12 +215,14 @@ export const TRAILING_SLASH_REDIRECT: VercelRoute = {
|
|
|
186
215
|
* user-authored route of that identical shape would be semantically equal to
|
|
187
216
|
* the one re-added); the homepage `Link` route by its three-field
|
|
188
217
|
* continue-with-link shape (the Build Output config is adapter-generated, so
|
|
189
|
-
* no user-authored route competes in this file)
|
|
218
|
+
* no user-authored route competes in this file); the Markdown 404 routes by
|
|
219
|
+
* their `/404.md` destination.
|
|
190
220
|
*/
|
|
191
221
|
const isNegotiationRoute = (route: VercelRoute): boolean =>
|
|
192
222
|
route.has?.some(
|
|
193
223
|
(condition) => condition.value === ACCEPT_MARKDOWN_HEADER_VALUE
|
|
194
224
|
) === true ||
|
|
225
|
+
(route.dest === NOT_FOUND_MARKDOWN_DEST && route.status === 404) ||
|
|
195
226
|
(route.continue === true &&
|
|
196
227
|
route.headers?.vary === "Accept" &&
|
|
197
228
|
isString(route.src) &&
|
|
@@ -213,17 +244,21 @@ const isNegotiationRoute = (route: VercelRoute): boolean =>
|
|
|
213
244
|
* platform's mechanism for extensionless static files (e.g. the Web Bot Auth
|
|
214
245
|
* signature directory). The trailing-slash 308 redirect is always spliced in
|
|
215
246
|
* alongside, so slashed duplicates of every page collapse onto the canonical
|
|
216
|
-
* slashless URL.
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
247
|
+
* slashless URL. With `notFoundMarkdown` (the build emitted `404.md`), the
|
|
248
|
+
* Markdown 404 routes go into the miss phase right before the adapter's
|
|
249
|
+
* `/404.html` fallback — and nowhere when that fallback is absent, since a
|
|
250
|
+
* `dest` with no file behind it would serve nothing. Returns the updated JSON
|
|
251
|
+
* text (tab-indented, like the adapter's own output), or `null` when there is
|
|
252
|
+
* nowhere safe to splice: an unparsable config, no `routes` array, or no
|
|
253
|
+
* `handle: "filesystem"` marker to anchor the splice.
|
|
220
254
|
*/
|
|
221
255
|
export const injectNegotiationRoutes = (
|
|
222
256
|
configText: string,
|
|
223
257
|
routePaths: readonly string[],
|
|
224
258
|
homeLinkHeader?: string | null,
|
|
225
259
|
contentTypeOverrides?: Record<string, string>,
|
|
226
|
-
homeTokens?: number
|
|
260
|
+
homeTokens?: number,
|
|
261
|
+
notFoundMarkdown = false
|
|
227
262
|
): string | null => {
|
|
228
263
|
const overrideEntries = Object.entries(contentTypeOverrides ?? {});
|
|
229
264
|
let config: {
|
|
@@ -272,6 +307,14 @@ export const injectNegotiationRoutes = (
|
|
|
272
307
|
...rewriteRoutes,
|
|
273
308
|
TRAILING_SLASH_REDIRECT
|
|
274
309
|
);
|
|
310
|
+
if (notFoundMarkdown) {
|
|
311
|
+
const fallbackIndex = routes.findIndex(
|
|
312
|
+
(route) => route.status === 404 && route.dest === NOT_FOUND_HTML_DEST
|
|
313
|
+
);
|
|
314
|
+
if (fallbackIndex !== -1) {
|
|
315
|
+
routes.splice(fallbackIndex, 0, ...NOT_FOUND_MARKDOWN_ROUTES);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
275
318
|
config.routes = routes;
|
|
276
319
|
return `${JSON.stringify(config, null, "\t")}\n`;
|
|
277
320
|
};
|
package/src/og/card.ts
CHANGED
|
@@ -81,7 +81,7 @@ export interface OgCardOptions {
|
|
|
81
81
|
accent?: string;
|
|
82
82
|
/** Brand/site name shown in the top-left lockup. */
|
|
83
83
|
brand?: string;
|
|
84
|
-
/** Muted subtitle under the headline (
|
|
84
|
+
/** Muted subtitle under the headline (the page description, else the site's). */
|
|
85
85
|
description?: string;
|
|
86
86
|
/**
|
|
87
87
|
* Inlined SVG markup of the configured logo, painted into the brand
|
|
@@ -54,6 +54,11 @@ export interface ReferenceSource {
|
|
|
54
54
|
includeInSearch: boolean;
|
|
55
55
|
/** Whether generated pages emit noindex metadata and stay out of the sitemap. */
|
|
56
56
|
noindex: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Whether operation meta descriptions end with the generated English
|
|
59
|
+
* "Reference for …" sentence, or carry the spec's own prose alone.
|
|
60
|
+
*/
|
|
61
|
+
seoDescriptionSuffix: boolean;
|
|
57
62
|
/** Local path or `http(s)` URL, verbatim from config. */
|
|
58
63
|
spec: string;
|
|
59
64
|
/**
|
|
@@ -132,6 +137,7 @@ interface Block {
|
|
|
132
137
|
label?: string;
|
|
133
138
|
noindex: boolean;
|
|
134
139
|
route?: string;
|
|
140
|
+
seoDescriptionSuffix: boolean;
|
|
135
141
|
spec: string;
|
|
136
142
|
}[];
|
|
137
143
|
spec?: string;
|
|
@@ -146,6 +152,7 @@ const sourcesOf = (block: Block): Block["sources"] => {
|
|
|
146
152
|
includeInLlms: true,
|
|
147
153
|
includeInSearch: true,
|
|
148
154
|
noindex: false,
|
|
155
|
+
seoDescriptionSuffix: true,
|
|
149
156
|
spec: block.spec,
|
|
150
157
|
});
|
|
151
158
|
}
|
|
@@ -192,6 +199,7 @@ const referencesFor = (
|
|
|
192
199
|
renderer,
|
|
193
200
|
route,
|
|
194
201
|
scalar: block.scalar,
|
|
202
|
+
seoDescriptionSuffix: source.seoDescriptionSuffix,
|
|
195
203
|
slug: routeSlug(route),
|
|
196
204
|
spec: source.spec,
|
|
197
205
|
theme: block.theme,
|
|
@@ -174,12 +174,23 @@ const GRAPHQL_MEMBER_PHRASES = {
|
|
|
174
174
|
/**
|
|
175
175
|
* The spec's own prose for the operation, followed by the endpoint it documents
|
|
176
176
|
* — so every operation page carries a distinct, self-describing meta
|
|
177
|
-
* description even when the spec's summaries are terse.
|
|
177
|
+
* description even when the spec's summaries are terse. With the suffix
|
|
178
|
+
* switched off (`seoDescriptionSuffix: false`, for sites whose prose isn't
|
|
179
|
+
* English) the description is the prose alone, or the page `title` — a
|
|
180
|
+
* language-neutral `GET /pets`, channel, or field name — when the operation
|
|
181
|
+
* has no prose at all, so no page ships an empty description.
|
|
178
182
|
*/
|
|
179
183
|
const operationDescription = (
|
|
180
184
|
spec: ApiSpecData,
|
|
181
|
-
operation: ApiOperationRef
|
|
185
|
+
operation: ApiOperationRef,
|
|
186
|
+
options: { suffix: boolean; title: string }
|
|
182
187
|
): string => {
|
|
188
|
+
if (!options.suffix) {
|
|
189
|
+
return clip(
|
|
190
|
+
plainProse(operation.description || operation.summary) || options.title,
|
|
191
|
+
META_DESCRIPTION_MAX
|
|
192
|
+
);
|
|
193
|
+
}
|
|
183
194
|
// AsyncAPI operations act on a channel, not an HTTP endpoint; GraphQL pages
|
|
184
195
|
// document a root field or a named type.
|
|
185
196
|
let suffix: string;
|
|
@@ -210,7 +221,7 @@ export const operationMdx = (
|
|
|
210
221
|
operation: ApiOperationRef,
|
|
211
222
|
reference?: Pick<
|
|
212
223
|
ReferenceSource,
|
|
213
|
-
"includeInLlms" | "includeInSearch" | "noindex"
|
|
224
|
+
"includeInLlms" | "includeInSearch" | "noindex" | "seoDescriptionSuffix"
|
|
214
225
|
>
|
|
215
226
|
): RenderedPage => {
|
|
216
227
|
const method = operation.method.toUpperCase();
|
|
@@ -242,7 +253,10 @@ export const operationMdx = (
|
|
|
242
253
|
searchFlags.exclude = true;
|
|
243
254
|
}
|
|
244
255
|
const seo: RenderedPageData["seo"] = {
|
|
245
|
-
description: operationDescription(spec, operation
|
|
256
|
+
description: operationDescription(spec, operation, {
|
|
257
|
+
suffix: reference?.seoDescriptionSuffix !== false,
|
|
258
|
+
title,
|
|
259
|
+
}),
|
|
246
260
|
};
|
|
247
261
|
if (reference?.noindex) {
|
|
248
262
|
seo.noindex = true;
|
package/src/openapi/scalar.ts
CHANGED
|
@@ -144,9 +144,6 @@ export const buildReferenceFiles = async (options: {
|
|
|
144
144
|
warnings.push(spec.warning);
|
|
145
145
|
}
|
|
146
146
|
const pagePath = referencePagePath(ref.route);
|
|
147
|
-
// Relative path from the page back to src/generated/data.json: a page one
|
|
148
|
-
// directory deep (api/events.astro) needs an extra "../".
|
|
149
|
-
const depth = pagePath.split("/").length - 1;
|
|
150
147
|
files.push({
|
|
151
148
|
content: scalarReferenceTemplate({
|
|
152
149
|
configuration: {
|
|
@@ -157,7 +154,6 @@ export const buildReferenceFiles = async (options: {
|
|
|
157
154
|
// hideTestRequestButton, orderSchemaPropertiesBy, and the rest).
|
|
158
155
|
...ref.scalar,
|
|
159
156
|
},
|
|
160
|
-
dataImport: `${"../".repeat(depth + 1)}generated/data.json`,
|
|
161
157
|
noindex: ref.noindex,
|
|
162
158
|
route: ref.route,
|
|
163
159
|
title: ref.label,
|
package/src/registry/eject.ts
CHANGED
|
@@ -9,7 +9,12 @@ import { buildRawMarkdown } from "../ai/markdown.ts";
|
|
|
9
9
|
import { buildMcpData } from "../ai/mcp/data.ts";
|
|
10
10
|
import { buildMcpDiscovery, buildMcpServerCard } from "../ai/mcp/discovery.ts";
|
|
11
11
|
import { planComponentSlots } from "../astro/component-slots.ts";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
EXAMPLE_SCAN_GLOB,
|
|
14
|
+
discoverExamples,
|
|
15
|
+
exampleMarkdownLookup,
|
|
16
|
+
exampleScanRoots,
|
|
17
|
+
} from "../astro/examples.ts";
|
|
13
18
|
import {
|
|
14
19
|
buildRuntimeData,
|
|
15
20
|
collectStaged,
|
|
@@ -61,6 +66,7 @@ import {
|
|
|
61
66
|
tailwindEntryTemplate,
|
|
62
67
|
} from "../theme/entry.ts";
|
|
63
68
|
import { buildThemeCss } from "../theme/palette.ts";
|
|
69
|
+
import { rebaseSourceDirectives } from "../theme/sources.ts";
|
|
64
70
|
import { twoslashCss } from "../theme/twoslash.ts";
|
|
65
71
|
|
|
66
72
|
const toPosix = (path: string): string => path.split("\\").join("/");
|
|
@@ -203,7 +209,7 @@ const mcpFiles = async (
|
|
|
203
209
|
path: join(genDir, "mcp-data.json"),
|
|
204
210
|
},
|
|
205
211
|
{
|
|
206
|
-
content: mcpEndpointTemplate(
|
|
212
|
+
content: mcpEndpointTemplate(),
|
|
207
213
|
path: join(srcDir, "pages", mcpPageFile(route)),
|
|
208
214
|
},
|
|
209
215
|
{
|
|
@@ -251,13 +257,21 @@ const changelogFiles = (
|
|
|
251
257
|
};
|
|
252
258
|
|
|
253
259
|
/** Contents of the configured `examples.css`, or `""` when unset/absent. */
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
260
|
+
/**
|
|
261
|
+
* Read a user stylesheet that eject inlines into a generated entry under
|
|
262
|
+
* `genDir`, re-rooting its relative `@source` paths from the user's file.
|
|
263
|
+
* Resolves to an empty string when the file is unset or absent.
|
|
264
|
+
*/
|
|
265
|
+
const readUserCss = async (
|
|
266
|
+
file: string | null,
|
|
267
|
+
genDir: string
|
|
268
|
+
): Promise<string> => {
|
|
269
|
+
if (!(file && existsSync(file))) {
|
|
270
|
+
return "";
|
|
271
|
+
}
|
|
272
|
+
const css = await readFile(file, "utf-8");
|
|
273
|
+
return rebaseSourceDirectives(css, { from: file, to: genDir });
|
|
274
|
+
};
|
|
261
275
|
|
|
262
276
|
/**
|
|
263
277
|
* The per-example preview route `<Component />` iframes embed, nested under
|
|
@@ -331,10 +345,11 @@ export const eject = async (
|
|
|
331
345
|
context.pagesRoot ? discoverPages(context.pagesRoot) : Promise.resolve([]),
|
|
332
346
|
detectNeedsReact(root),
|
|
333
347
|
detectUsesMath(root),
|
|
334
|
-
context.themeFile
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
348
|
+
readUserCss(context.themeFile, genDir),
|
|
349
|
+
readUserCss(
|
|
350
|
+
config.examples.css ? join(root, config.examples.css) : null,
|
|
351
|
+
genDir
|
|
352
|
+
),
|
|
338
353
|
buildRawMarkdown(project),
|
|
339
354
|
discoverIslands(root),
|
|
340
355
|
]);
|
|
@@ -387,9 +402,11 @@ export const eject = async (
|
|
|
387
402
|
contentRoot: relContext.contentRoot,
|
|
388
403
|
contentRoutes: project.manifest.routes.map((route) => route.path),
|
|
389
404
|
context: relContext,
|
|
390
|
-
dataPath: "./src/generated/data.json",
|
|
391
405
|
examplesPath: "./src/generated/examples.ts",
|
|
392
406
|
examplesThemePath: "./src/generated/examples.css",
|
|
407
|
+
// No CLI publishes the runtime data modules in memory after eject, so
|
|
408
|
+
// the config aliases each to the JSON snapshot written below.
|
|
409
|
+
generatedModulesDir: "./src/generated",
|
|
393
410
|
integrationBridge: ejectIntegrationBridge(
|
|
394
411
|
config,
|
|
395
412
|
root,
|
|
@@ -398,7 +415,6 @@ export const eject = async (
|
|
|
398
415
|
needsReact,
|
|
399
416
|
needsSvelte,
|
|
400
417
|
needsVue,
|
|
401
|
-
openapiPath: "./src/generated/openapi.json",
|
|
402
418
|
pages: relPages,
|
|
403
419
|
searchClientPath: "./src/generated/search-client.ts",
|
|
404
420
|
themePath: "./src/generated/app.css",
|
|
@@ -452,7 +468,9 @@ export const eject = async (
|
|
|
452
468
|
// Relative sources keep the ejected app portable.
|
|
453
469
|
content: examplesEntryTemplate({
|
|
454
470
|
configTokens: buildThemeCss(config.theme),
|
|
455
|
-
sources:
|
|
471
|
+
sources: exampleScanRoots(root, examples.dir).map(
|
|
472
|
+
(dir) => `${relative(genDir, dir)}/${EXAMPLE_SCAN_GLOB}`
|
|
473
|
+
),
|
|
456
474
|
userCss: userExamplesCss,
|
|
457
475
|
}),
|
|
458
476
|
path: join(genDir, "examples.css"),
|
|
@@ -513,7 +531,8 @@ export const eject = async (
|
|
|
513
531
|
if (config.seo.og.enabled) {
|
|
514
532
|
files.push({
|
|
515
533
|
content: ogEndpointTemplate(
|
|
516
|
-
customOgRoutes(pages, config.title, config.seo.og.titles)
|
|
534
|
+
customOgRoutes(pages, config.title, config.seo.og.titles),
|
|
535
|
+
{ pageDescriptions: config.seo.og.description !== false }
|
|
517
536
|
),
|
|
518
537
|
path: join(srcDir, "pages", "og", "[...slug].png.ts"),
|
|
519
538
|
});
|
package/src/theme/entry.ts
CHANGED
|
@@ -835,7 +835,7 @@ ${options.userTheme}
|
|
|
835
835
|
interface ExamplesEntryOptions {
|
|
836
836
|
/** Config-derived token overrides (`:root { --blume-accent: ... }`). */
|
|
837
837
|
configTokens: string;
|
|
838
|
-
/** Globs to scan for utility classes (
|
|
838
|
+
/** Globs to scan for utility classes (the project and examples directory). */
|
|
839
839
|
sources: string[];
|
|
840
840
|
/** Raw contents of the configured `examples.css`, if any. */
|
|
841
841
|
userCss: string;
|
|
@@ -855,7 +855,7 @@ export const examplesEntryTemplate = (options: ExamplesEntryOptions): string =>
|
|
|
855
855
|
`/* Generated by Blume. Do not edit. */
|
|
856
856
|
@import "tailwindcss";
|
|
857
857
|
|
|
858
|
-
/* Scan the
|
|
858
|
+
/* Scan the project (and an out-of-root examples directory) for utility classes. */
|
|
859
859
|
${options.sources.map((source) => `@source "${source}";`).join("\n")}
|
|
860
860
|
|
|
861
861
|
${DARK_VARIANT}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { dirname, isAbsolute, relative, resolve } from "pathe";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A Tailwind `@source "…"` / `@source not "…"` directive with a quoted path.
|
|
5
|
+
* `@source inline("…")` never matches: the quote must directly follow the
|
|
6
|
+
* keyword (or `not`), and `inline(` sits in between.
|
|
7
|
+
*/
|
|
8
|
+
const SOURCE_DIRECTIVE =
|
|
9
|
+
/@source(?<not>\s+not)?\s+(?<quote>["'])(?<path>[^"']+)\k<quote>/gu;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Rewrite the relative `@source` paths in a user stylesheet so they still
|
|
13
|
+
* point where the author meant once the sheet is inlined into a generated
|
|
14
|
+
* Tailwind entry. Tailwind resolves `@source` relative to the stylesheet that
|
|
15
|
+
* declares it, and Blume splices `theme.css` / `examples.css` verbatim into
|
|
16
|
+
* `.blume/src/generated/*.css` (or `src/generated/*.css` after eject), which
|
|
17
|
+
* would silently re-root them there. Resolving each path against the user's
|
|
18
|
+
* file and re-expressing it relative to the generated directory keeps the
|
|
19
|
+
* standard contract — write `@source "../../packages/ui"` next to the file
|
|
20
|
+
* that says it — and lets a monorepo scan sibling workspace packages for
|
|
21
|
+
* utility classes without any knowledge of Blume's internal layout. Absolute
|
|
22
|
+
* paths pass through untouched.
|
|
23
|
+
*/
|
|
24
|
+
export const rebaseSourceDirectives = (
|
|
25
|
+
css: string,
|
|
26
|
+
options: {
|
|
27
|
+
/** The user's stylesheet the CSS was read from. */
|
|
28
|
+
from: string;
|
|
29
|
+
/** The directory of the generated entry the CSS is inlined into. */
|
|
30
|
+
to: string;
|
|
31
|
+
}
|
|
32
|
+
): string => {
|
|
33
|
+
const base = dirname(options.from);
|
|
34
|
+
return css.replace(
|
|
35
|
+
SOURCE_DIRECTIVE,
|
|
36
|
+
(
|
|
37
|
+
directive: string,
|
|
38
|
+
not: string | undefined,
|
|
39
|
+
quote: string,
|
|
40
|
+
path: string
|
|
41
|
+
): string => {
|
|
42
|
+
if (isAbsolute(path)) {
|
|
43
|
+
return directive;
|
|
44
|
+
}
|
|
45
|
+
const rebased = relative(options.to, resolve(base, path));
|
|
46
|
+
return `@source${not ?? ""} ${quote}${rebased}${quote}`;
|
|
47
|
+
}
|
|
48
|
+
);
|
|
49
|
+
};
|