@parche/core 0.3.0-alpha.0 → 0.4.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/package.json +1 -1
- package/src/components/DynamicRenderer.astro +97 -23
- package/src/components/LayoutRenderer.astro +23 -3
- package/src/components/SectionWrapper.astro +9 -8
- package/src/integration/index.ts +225 -10
- package/src/integration/registry.ts +131 -15
- package/src/integration/types.ts +60 -4
- package/src/integration/virtual.d.ts +30 -199
- package/src/integration/vite-plugin-parche.ts +117 -44
- package/src/routes/[...slug].astro +15 -15
- package/src/styles/semantic.css +1 -1
- package/src/types/config.ts +13 -18
- package/src/utils/i18n.ts +39 -14
- package/src/utils/layout.ts +31 -21
package/package.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
-
import {
|
|
2
|
+
import { loadWidgets } from 'parche:registry/widgets';
|
|
3
|
+
import { fullBleedWidgets } from 'parche:config/layout';
|
|
3
4
|
import SectionWrapper from './SectionWrapper.astro';
|
|
4
5
|
|
|
5
6
|
interface WrapperConfig {
|
|
@@ -25,43 +26,116 @@ interface Props {
|
|
|
25
26
|
|
|
26
27
|
const { sections, Wrapper } = Astro.props;
|
|
27
28
|
|
|
28
|
-
// ---- Image resolution:
|
|
29
|
-
|
|
29
|
+
// ---- Image resolution: @/assets/images/foo.png → optimized import ----
|
|
30
|
+
// Lazy glob: a loader exists for every image, but a module (and its build-time
|
|
31
|
+
// optimization) is only produced for the ones actually referenced. We collect
|
|
32
|
+
// just the paths these sections use, resolve them once, then substitute
|
|
33
|
+
// synchronously during render.
|
|
34
|
+
const imageLoaders = import.meta.glob<{ default: ImageMetadata }>(
|
|
30
35
|
'/src/assets/images/**/*.{png,jpg,jpeg,gif,svg,webp,avif}',
|
|
31
|
-
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
/** Plain objects/arrays are the only things we recurse into — Dates, class
|
|
39
|
+
* instances, etc. are left untouched (and never hold image paths). */
|
|
40
|
+
function isPlainObject(v: any): boolean {
|
|
41
|
+
if (v === null || typeof v !== 'object') return false;
|
|
42
|
+
const proto = Object.getPrototypeOf(v);
|
|
43
|
+
return proto === Object.prototype || proto === null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Gather every '@/assets/…' string used anywhere in the section props. */
|
|
47
|
+
function collectAssetPaths(value: any, acc: Set<string>): void {
|
|
48
|
+
if (typeof value === 'string') {
|
|
49
|
+
if (value.startsWith('@/assets/')) acc.add(value);
|
|
50
|
+
} else if (Array.isArray(value)) {
|
|
51
|
+
for (const v of value) collectAssetPaths(v, acc);
|
|
52
|
+
} else if (isPlainObject(value)) {
|
|
53
|
+
for (const v of Object.values(value)) collectAssetPaths(v, acc);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Load only the widget components these sections reference (lazy catalog).
|
|
58
|
+
const widgets = await loadWidgets(sections.map((s) => s.widget));
|
|
59
|
+
|
|
60
|
+
const assetPaths = new Set<string>();
|
|
61
|
+
for (const section of sections) collectAssetPaths(section.props ?? {}, assetPaths);
|
|
62
|
+
|
|
63
|
+
const resolvedImages = new Map<string, string>();
|
|
64
|
+
await Promise.all(
|
|
65
|
+
[...assetPaths].map(async (src) => {
|
|
66
|
+
const loader = imageLoaders[src.replace('@/', '/src/')];
|
|
67
|
+
if (loader) {
|
|
68
|
+
const mod = await loader();
|
|
69
|
+
resolvedImages.set(src, mod.default.src);
|
|
70
|
+
}
|
|
71
|
+
}),
|
|
32
72
|
);
|
|
33
73
|
|
|
34
74
|
function resolveImage(src: string): string {
|
|
35
|
-
|
|
36
|
-
const fsPath = src.replace('@/', '/src/');
|
|
37
|
-
const meta = imageModules[fsPath]?.default;
|
|
38
|
-
return meta?.src ?? src;
|
|
75
|
+
return resolvedImages.get(src) ?? src;
|
|
39
76
|
}
|
|
40
77
|
|
|
41
|
-
/**
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
78
|
+
/** Substitute resolved image paths at any depth, reusing every subtree that
|
|
79
|
+
* didn't change so props with no images are passed through, not deep-cloned. */
|
|
80
|
+
function resolveDeep(value: any): any {
|
|
81
|
+
if (typeof value === 'string') return resolveImage(value);
|
|
82
|
+
if (Array.isArray(value)) {
|
|
83
|
+
let changed = false;
|
|
84
|
+
const out = value.map((v) => {
|
|
85
|
+
const r = resolveDeep(v);
|
|
86
|
+
if (r !== v) changed = true;
|
|
87
|
+
return r;
|
|
88
|
+
});
|
|
89
|
+
return changed ? out : value;
|
|
90
|
+
}
|
|
91
|
+
if (isPlainObject(value)) {
|
|
92
|
+
let changed = false;
|
|
93
|
+
const out: Record<string, any> = {};
|
|
94
|
+
for (const [k, v] of Object.entries(value)) {
|
|
95
|
+
const r = resolveDeep(v);
|
|
96
|
+
if (r !== v) changed = true;
|
|
97
|
+
out[k] = r;
|
|
49
98
|
}
|
|
99
|
+
return changed ? out : value;
|
|
50
100
|
}
|
|
51
|
-
return
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function resolveProps(props: Record<string, any>): Record<string, any> {
|
|
105
|
+
return resolveDeep(props);
|
|
52
106
|
}
|
|
53
107
|
|
|
54
|
-
// Widgets that render full-bleed and manage their own padding — no wrapper by
|
|
55
|
-
|
|
108
|
+
// Widgets that render full-bleed and manage their own padding — no wrapper by
|
|
109
|
+
// default. The list is contributed by the parches (manifest `fullBleed`), not
|
|
110
|
+
// hardcoded here; core knows no widget names. Match the key as registered, or
|
|
111
|
+
// its short form (a parche may register `hero/Hero` while declaring `Hero`).
|
|
112
|
+
const noWrapperWidgets = new Set(fullBleedWidgets);
|
|
56
113
|
function widgetSkipsWrapper(name: string): boolean {
|
|
57
|
-
|
|
58
|
-
|
|
114
|
+
if (noWrapperWidgets.has(name)) return true;
|
|
115
|
+
const short = name.includes('/') ? name.split('/').pop()! : name;
|
|
116
|
+
return noWrapperWidgets.has(short);
|
|
59
117
|
}
|
|
60
118
|
---
|
|
61
119
|
|
|
62
120
|
{sections.map((section, index) => {
|
|
63
|
-
const Widget =
|
|
64
|
-
if (!Widget)
|
|
121
|
+
const Widget = widgets[section.widget];
|
|
122
|
+
if (!Widget) {
|
|
123
|
+
if (import.meta.env.DEV) {
|
|
124
|
+
console.warn(
|
|
125
|
+
`[parche] Unknown widget "${section.widget}" — not registered by any parche. ` +
|
|
126
|
+
`Check the widget name or that a parche provides it.`,
|
|
127
|
+
);
|
|
128
|
+
return (
|
|
129
|
+
<div
|
|
130
|
+
data-parche-missing-widget={section.widget}
|
|
131
|
+
style="margin:.5rem;padding:.75rem 1rem;border:2px dashed #dc2626;border-radius:.5rem;background:#fef2f2;color:#991b1b;font:14px/1.5 ui-monospace,SFMono-Regular,monospace"
|
|
132
|
+
>
|
|
133
|
+
⚠ Parche: unknown widget <strong>{section.widget}</strong> — not registered by any parche.
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
65
139
|
|
|
66
140
|
const props = resolveProps(section.props ?? {});
|
|
67
141
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
import {
|
|
2
|
+
import { loadWidgets } from 'parche:registry/widgets';
|
|
3
3
|
import DynamicRenderer from 'parche:DynamicRenderer';
|
|
4
4
|
|
|
5
5
|
interface WrapperConfig {
|
|
@@ -37,6 +37,10 @@ const {
|
|
|
37
37
|
SectionWrapper: PreviewWrapper,
|
|
38
38
|
mainId,
|
|
39
39
|
} = Astro.props;
|
|
40
|
+
|
|
41
|
+
// Load only the layout widgets this layout references ('layout/Main' is a
|
|
42
|
+
// synthetic marker with no loader — skipped). Lazy catalog.
|
|
43
|
+
const widgets = await loadWidgets(layoutSections.map((s) => s.widget));
|
|
40
44
|
---
|
|
41
45
|
|
|
42
46
|
{layoutSections.map((section) => {
|
|
@@ -58,8 +62,24 @@ const {
|
|
|
58
62
|
);
|
|
59
63
|
}
|
|
60
64
|
|
|
61
|
-
const Widget =
|
|
62
|
-
if (!Widget)
|
|
65
|
+
const Widget = widgets[section.widget];
|
|
66
|
+
if (!Widget) {
|
|
67
|
+
if (import.meta.env.DEV) {
|
|
68
|
+
console.warn(
|
|
69
|
+
`[parche] Unknown layout widget "${section.widget}" — not registered by any parche. ` +
|
|
70
|
+
`Check the layout definition or the parche providing this widget.`,
|
|
71
|
+
);
|
|
72
|
+
return (
|
|
73
|
+
<div
|
|
74
|
+
data-parche-missing-widget={section.widget}
|
|
75
|
+
style="margin:.5rem;padding:.75rem 1rem;border:2px dashed #dc2626;border-radius:.5rem;background:#fef2f2;color:#991b1b;font:14px/1.5 ui-monospace,SFMono-Regular,monospace"
|
|
76
|
+
>
|
|
77
|
+
⚠ Parche: unknown layout widget <strong>{section.widget}</strong> — not registered by any parche.
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
63
83
|
|
|
64
84
|
return <Widget {...(section.props ?? {})} />;
|
|
65
85
|
})}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
/**
|
|
3
3
|
* Default section wrapper used by DynamicRenderer.
|
|
4
|
-
* Provides: configurable HTML tag, background layer, isDark per-widget,
|
|
5
|
-
* responsive container
|
|
4
|
+
* Provides: configurable HTML tag, a raw-HTML background layer, isDark per-widget,
|
|
5
|
+
* a responsive container, and class overrides via twMerge.
|
|
6
|
+
*
|
|
7
|
+
* Section rhythm/backgrounds are authored as raw HTML via `bg` in page content —
|
|
8
|
+
* a gradient div, an inline SVG (dots/grid), a glow, whatever the section needs.
|
|
6
9
|
*/
|
|
7
10
|
import type { HTMLTag } from 'astro/types';
|
|
8
11
|
import { twMerge } from 'tailwind-merge';
|
|
@@ -10,6 +13,7 @@ import { twMerge } from 'tailwind-merge';
|
|
|
10
13
|
interface Props {
|
|
11
14
|
id?: string;
|
|
12
15
|
isDark?: boolean;
|
|
16
|
+
/** Raw HTML background layer (a div, gradient, inline SVG, pattern…). */
|
|
13
17
|
bg?: string;
|
|
14
18
|
classes?: Record<string, unknown>;
|
|
15
19
|
as?: string;
|
|
@@ -23,21 +27,18 @@ const containerClass = (classes.container as string) ?? '';
|
|
|
23
27
|
|
|
24
28
|
<Tag class="relative not-prose scroll-mt-[72px]" {...id ? { id } : {}}>
|
|
25
29
|
{bg && (
|
|
26
|
-
<div class="absolute inset-0 pointer-events-none -z-[1]" aria-hidden="true">
|
|
30
|
+
<div class="absolute inset-0 pointer-events-none -z-[1] overflow-hidden" aria-hidden="true">
|
|
27
31
|
<Fragment set:html={bg} />
|
|
28
32
|
</div>
|
|
29
33
|
)}
|
|
30
34
|
{isDark && !bg && (
|
|
31
35
|
<div class="absolute inset-0 pointer-events-none -z-[1]" aria-hidden="true">
|
|
32
|
-
<div class="absolute inset-0 bg-neutral-900 dark:bg-transparent"
|
|
36
|
+
<div class="absolute inset-0 bg-neutral-900 dark:bg-transparent"></div>
|
|
33
37
|
</div>
|
|
34
38
|
)}
|
|
35
39
|
<div
|
|
36
40
|
class:list={[
|
|
37
|
-
twMerge(
|
|
38
|
-
'relative mx-auto max-w-7xl px-4 md:px-6 py-12 md:py-16 lg:py-20',
|
|
39
|
-
containerClass,
|
|
40
|
-
),
|
|
41
|
+
twMerge('relative mx-auto max-w-7xl px-4 md:px-6 py-12 md:py-16 lg:py-20', containerClass),
|
|
41
42
|
{ dark: isDark },
|
|
42
43
|
]}
|
|
43
44
|
>
|
package/src/integration/index.ts
CHANGED
|
@@ -2,19 +2,176 @@ import type { AstroIntegration } from 'astro';
|
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import fs from 'node:fs';
|
|
5
|
+
import { z } from 'zod';
|
|
5
6
|
import { vitePluginParche } from './vite-plugin-parche.js';
|
|
6
7
|
import { createRegistry } from './registry.js';
|
|
7
|
-
import
|
|
8
|
+
import { siteConfigSchema, type SiteConfig } from '../types/config.js';
|
|
9
|
+
import type { ParcheUserConfig, ParchePreset, ParcheSeoConfig, UIRegistry, ParcheApp, ParcheManifest, ParcheRequires } from './types.js';
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Preset composition (`extends`)
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
function isPlainObject(v: unknown): v is Record<string, unknown> {
|
|
16
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Deep-merge two config fragments. Objects merge recursively; primitives and
|
|
21
|
+
* arrays are replaced by the override (last wins) — except `parches`, which the
|
|
22
|
+
* caller concatenates. `undefined` on the override never clobbers the base.
|
|
23
|
+
*/
|
|
24
|
+
function deepMerge<T>(base: T, over: T): T {
|
|
25
|
+
if (over === undefined) return base;
|
|
26
|
+
if (isPlainObject(base) && isPlainObject(over)) {
|
|
27
|
+
const out: Record<string, unknown> = { ...base };
|
|
28
|
+
for (const key of Object.keys(over)) {
|
|
29
|
+
out[key] = deepMerge((base as Record<string, unknown>)[key], (over as Record<string, unknown>)[key]);
|
|
30
|
+
}
|
|
31
|
+
return out as T;
|
|
32
|
+
}
|
|
33
|
+
return over;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Merge an override config onto a base, concatenating `parches` (base first). */
|
|
37
|
+
function mergeConfig(base: ParchePreset, over: ParchePreset): ParchePreset {
|
|
38
|
+
const merged = deepMerge(base, over);
|
|
39
|
+
const baseParches = base.parches ?? [];
|
|
40
|
+
const overParches = over.parches ?? [];
|
|
41
|
+
if (baseParches.length || overParches.length) {
|
|
42
|
+
merged.parches = [...baseParches, ...overParches];
|
|
43
|
+
}
|
|
44
|
+
return merged;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Identity helper that types and freezes a reusable config fragment for
|
|
49
|
+
* `extends`. Authoring a preset through it gets you inference and a clear
|
|
50
|
+
* boundary; it does no work beyond returning the object.
|
|
51
|
+
*/
|
|
52
|
+
export function parchePreset(preset: ParchePreset): ParchePreset {
|
|
53
|
+
return preset;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Fold a config's `extends` chain into a single flat config (presets first). */
|
|
57
|
+
export function resolveExtends(userConfig: ParcheUserConfig): ParcheUserConfig {
|
|
58
|
+
if (!userConfig.extends) return userConfig;
|
|
59
|
+
const presets = Array.isArray(userConfig.extends) ? userConfig.extends : [userConfig.extends];
|
|
60
|
+
let base: ParchePreset = {};
|
|
61
|
+
for (const preset of presets) base = mergeConfig(base, preset);
|
|
62
|
+
const { extends: _drop, ...rest } = userConfig;
|
|
63
|
+
return mergeConfig(base, rest) as ParcheUserConfig;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Shape validation for the `parche({ ... })` options. `.strict()` turns a typo'd
|
|
67
|
+
// option name (e.g. `parchez`) or a malformed value into a friendly error at
|
|
68
|
+
// config time, instead of a deep, cryptic Vite failure later. The `parches`
|
|
69
|
+
// array holds manifests (validated structurally by the registry), so it's `any`.
|
|
70
|
+
const userConfigSchema = z
|
|
71
|
+
.object({
|
|
72
|
+
overrides: z.record(z.string(), z.string()).optional(),
|
|
73
|
+
config: z.string().optional(),
|
|
74
|
+
parches: z.array(z.any()).optional(),
|
|
75
|
+
routes: z
|
|
76
|
+
.object({
|
|
77
|
+
pages: z.boolean().optional(),
|
|
78
|
+
templates: z.record(z.string(), z.string()).optional(),
|
|
79
|
+
layouts: z.record(z.string(), z.string()).optional(),
|
|
80
|
+
catchAllRoute: z.string().optional(),
|
|
81
|
+
notFoundRoute: z.string().optional(),
|
|
82
|
+
middleware: z.string().optional(),
|
|
83
|
+
})
|
|
84
|
+
.strict()
|
|
85
|
+
.optional(),
|
|
86
|
+
themes: z
|
|
87
|
+
.object({
|
|
88
|
+
available: z.array(z.object({ label: z.string(), value: z.string() })).optional(),
|
|
89
|
+
showPanel: z.boolean().optional(),
|
|
90
|
+
})
|
|
91
|
+
.strict()
|
|
92
|
+
.optional(),
|
|
93
|
+
styles: z.object({ entry: z.string().optional() }).strict().optional(),
|
|
94
|
+
seo: z.object({ allowAICrawlers: z.boolean().optional() }).strict().optional(),
|
|
95
|
+
})
|
|
96
|
+
.strict();
|
|
97
|
+
|
|
98
|
+
function validateUserConfig(userConfig: ParcheUserConfig): void {
|
|
99
|
+
const result = userConfigSchema.safeParse(userConfig);
|
|
100
|
+
if (!result.success) {
|
|
101
|
+
const issues = result.error.issues
|
|
102
|
+
.map((i) => ` - ${i.path.join('.') || '(root)'}: ${i.message}`)
|
|
103
|
+
.join('\n');
|
|
104
|
+
throw new Error(`[parche] Invalid parche() options:\n${issues}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Config resolution
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
/** Runtime context passed to the function form of `parche()`. */
|
|
113
|
+
export interface ParcheConfigContext {
|
|
114
|
+
/** Astro command driving this run. */
|
|
115
|
+
command: 'dev' | 'build' | 'preview' | 'sync';
|
|
116
|
+
/** Convenience alias: 'development' for dev, 'production' otherwise. */
|
|
117
|
+
mode: 'development' | 'production';
|
|
118
|
+
/** Environment variables, for env-based / white-label branching. */
|
|
119
|
+
env: Record<string, string | undefined>;
|
|
120
|
+
/** Tenant id from PARCHE_TENANT (multi-tenant / white-label), if set. */
|
|
121
|
+
tenant: string | undefined;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
type SiteConfigInput = Parameters<typeof siteConfigSchema.parse>[0];
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The Parche config object: integration options (parches, routes, themes,
|
|
128
|
+
* styles, overrides, extends) plus, optionally, the site identity inline. Omit
|
|
129
|
+
* the site fields and point `config` at a separate file instead — both styles
|
|
130
|
+
* are equally supported. `seo` carries the site SEO fields and the build-time
|
|
131
|
+
* `allowAICrawlers`.
|
|
132
|
+
*/
|
|
133
|
+
export type ParcheConfig = Omit<ParcheUserConfig, 'seo'> &
|
|
134
|
+
Partial<Omit<SiteConfigInput, 'seo'>> & {
|
|
135
|
+
seo?: (SiteConfigInput extends { seo?: infer S } ? S : never) & ParcheSeoConfig;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/** `parche()` accepts a config object or a function of the runtime context. */
|
|
139
|
+
export type ParcheConfigInput = ParcheConfig | ((ctx: ParcheConfigContext) => ParcheConfig);
|
|
140
|
+
|
|
141
|
+
interface PreparedConfig {
|
|
142
|
+
/** Integration options only (site data stripped), extends already folded. */
|
|
143
|
+
userConfig: ParcheUserConfig;
|
|
144
|
+
/** Inline site config, when the site identity was given inline. */
|
|
145
|
+
inlineSiteConfig?: SiteConfig;
|
|
146
|
+
/** robots.txt AI-crawler policy. */
|
|
147
|
+
allowAICrawlers: boolean;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Shared integration body. `prepare` turns the runtime context into the resolved
|
|
152
|
+
* options; everything downstream (registry, route injection, robots.txt) is
|
|
153
|
+
* identical regardless of how the config was authored.
|
|
154
|
+
*/
|
|
155
|
+
function createIntegration(prepare: (ctx: ParcheConfigContext) => PreparedConfig): AstroIntegration {
|
|
10
156
|
let resolvedSiteUrl = '';
|
|
157
|
+
let allowAICrawlers = true;
|
|
11
158
|
return {
|
|
12
159
|
name: 'parche',
|
|
13
160
|
hooks: {
|
|
14
|
-
'astro:config:setup': ({ updateConfig, config, injectRoute, addMiddleware }) => {
|
|
161
|
+
'astro:config:setup': ({ command, updateConfig, config, injectRoute, addMiddleware }) => {
|
|
162
|
+
const ctx: ParcheConfigContext = {
|
|
163
|
+
command,
|
|
164
|
+
mode: command === 'dev' ? 'development' : 'production',
|
|
165
|
+
env: process.env,
|
|
166
|
+
tenant: process.env.PARCHE_TENANT,
|
|
167
|
+
};
|
|
168
|
+
const prepared = prepare(ctx);
|
|
169
|
+
const resolved = prepared.userConfig;
|
|
170
|
+
allowAICrawlers = prepared.allowAICrawlers;
|
|
171
|
+
validateUserConfig(resolved);
|
|
15
172
|
resolvedSiteUrl = config.site ?? '';
|
|
16
173
|
const rootDir = fileURLToPath(config.root);
|
|
17
|
-
const resolvedRegistry = createRegistry(
|
|
174
|
+
const resolvedRegistry = createRegistry(resolved, rootDir, config.i18n, prepared.inlineSiteConfig);
|
|
18
175
|
|
|
19
176
|
// Resolve @core/* alias for backward compatibility with widget internal imports
|
|
20
177
|
const coreDir = path.resolve(
|
|
@@ -25,24 +182,24 @@ export default function parche(userConfig: ParcheUserConfig = {}): AstroIntegrat
|
|
|
25
182
|
const routesDir = path.resolve(coreDir, 'routes');
|
|
26
183
|
|
|
27
184
|
// Inject routes only when explicitly enabled via routes.pages: true
|
|
28
|
-
if (
|
|
185
|
+
if (resolved.routes?.pages) {
|
|
29
186
|
// Catch-all page route
|
|
30
187
|
injectRoute({
|
|
31
188
|
pattern: '[...slug]',
|
|
32
|
-
entrypoint:
|
|
189
|
+
entrypoint: resolved.routes?.catchAllRoute
|
|
33
190
|
?? path.resolve(routesDir, '[...slug].astro'),
|
|
34
191
|
});
|
|
35
192
|
|
|
36
193
|
// 404 page
|
|
37
194
|
injectRoute({
|
|
38
195
|
pattern: '404',
|
|
39
|
-
entrypoint:
|
|
196
|
+
entrypoint: resolved.routes?.notFoundRoute
|
|
40
197
|
?? path.resolve(routesDir, '404.astro'),
|
|
41
198
|
});
|
|
42
199
|
|
|
43
200
|
// Middleware for i18n locale resolution
|
|
44
201
|
addMiddleware({
|
|
45
|
-
entrypoint:
|
|
202
|
+
entrypoint: resolved.routes?.middleware
|
|
46
203
|
?? path.resolve(routesDir, 'middleware.ts'),
|
|
47
204
|
order: 'pre',
|
|
48
205
|
});
|
|
@@ -78,13 +235,71 @@ export default function parche(userConfig: ParcheUserConfig = {}): AstroIntegrat
|
|
|
78
235
|
},
|
|
79
236
|
|
|
80
237
|
'astro:build:done': ({ dir }) => {
|
|
81
|
-
const allowAICrawlers = userConfig.seo?.allowAICrawlers ?? true;
|
|
82
238
|
processRobotsTxt(dir, resolvedSiteUrl, allowAICrawlers);
|
|
83
239
|
},
|
|
84
240
|
},
|
|
85
241
|
};
|
|
86
242
|
}
|
|
87
243
|
|
|
244
|
+
/**
|
|
245
|
+
* The Parche Astro integration. One entry, two equally supported styles for the
|
|
246
|
+
* site identity:
|
|
247
|
+
*
|
|
248
|
+
* • Inline — pass `site` (and optionally metadata/seo/organization) right here.
|
|
249
|
+
* It's validated and served as `parche:config`; no separate file needed.
|
|
250
|
+
* • Separate file — omit `site` and point `config` at a file (default
|
|
251
|
+
* `./parche.config.ts` at the project root). The parches stay in
|
|
252
|
+
* astro.config; everything else lives in that file (authored with
|
|
253
|
+
* `defineConfig` from `@parche/core/config`).
|
|
254
|
+
*
|
|
255
|
+
* The argument may also be a function of the runtime context
|
|
256
|
+
* (`(ctx) => config`) for env-based / conditional / multi-tenant setups, and any
|
|
257
|
+
* config may `extends` a `parchePreset(...)`.
|
|
258
|
+
*
|
|
259
|
+
* @example
|
|
260
|
+
* // astro.config.mjs
|
|
261
|
+
* import parche from '@parche/core';
|
|
262
|
+
* export default defineConfig({
|
|
263
|
+
* integrations: [parche({ parches: [createUI()], config: './parche.config.ts', routes: { pages: true } })],
|
|
264
|
+
* });
|
|
265
|
+
*/
|
|
266
|
+
export default function parche(input: ParcheConfigInput = {}): AstroIntegration {
|
|
267
|
+
return createIntegration((ctx) => prepareParcheConfig(input, ctx));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Resolve a config input (object or function) into the pieces the integration
|
|
272
|
+
* needs: the integration options, an optional inline site config, and the
|
|
273
|
+
* robots policy. Pure — exported for testing. Folds `extends`, splits the site
|
|
274
|
+
* identity out of the options, and picks inline vs separate-file mode.
|
|
275
|
+
*/
|
|
276
|
+
export function prepareParcheConfig(
|
|
277
|
+
input: ParcheConfigInput,
|
|
278
|
+
ctx: ParcheConfigContext,
|
|
279
|
+
): PreparedConfig {
|
|
280
|
+
const cfg = typeof input === 'function' ? input(ctx) : input;
|
|
281
|
+
// Fold `extends` first (a preset may seed site data or parches), then split
|
|
282
|
+
// the site identity out of the integration options.
|
|
283
|
+
const merged = resolveExtends(cfg as unknown as ParcheUserConfig) as unknown as ParcheConfig;
|
|
284
|
+
const { site, metadata, seo, organization, config: configPath, ...rest } =
|
|
285
|
+
merged as ParcheConfig & { config?: string };
|
|
286
|
+
const userOpts = rest as ParcheUserConfig;
|
|
287
|
+
const { allowAICrawlers = true, ...siteSeo } = (seo ?? {}) as Record<string, unknown>;
|
|
288
|
+
|
|
289
|
+
if (site) {
|
|
290
|
+
// Inline mode: validate + serve the site identity as parche:config.
|
|
291
|
+
const inlineSiteConfig = siteConfigSchema.parse({ site, metadata, seo: siteSeo, organization });
|
|
292
|
+
return { userConfig: userOpts, inlineSiteConfig, allowAICrawlers: allowAICrawlers as boolean };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Separate-file mode: site identity comes from `config` (or the default
|
|
296
|
+
// ./parche.config.ts). Only the robots policy is read from seo here.
|
|
297
|
+
return {
|
|
298
|
+
userConfig: { ...userOpts, config: configPath },
|
|
299
|
+
allowAICrawlers: allowAICrawlers as boolean,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
88
303
|
const ROBOTS_MARKER = '# === PARCHE:AUTO-GENERATED';
|
|
89
304
|
|
|
90
305
|
function processRobotsTxt(outDir: URL, siteUrl: string, allowAICrawlers: boolean) {
|
|
@@ -153,4 +368,4 @@ function buildAutoGeneratedRobots(siteUrl: string, allowAICrawlers: boolean): st
|
|
|
153
368
|
return lines.join('\n');
|
|
154
369
|
}
|
|
155
370
|
|
|
156
|
-
export type { ParcheUserConfig, UIRegistry, ParcheApp, ParcheManifest, ParcheRequires };
|
|
371
|
+
export type { ParcheUserConfig, ParchePreset, UIRegistry, ParcheApp, ParcheManifest, ParcheRequires };
|