@parche/core 0.3.0-alpha.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/LICENSE +21 -0
- package/README.md +11 -0
- package/package.json +55 -0
- package/src/components/DynamicRenderer.astro +96 -0
- package/src/components/LayoutRenderer.astro +65 -0
- package/src/components/SectionWrapper.astro +46 -0
- package/src/components/common/LocaleSwitcher.astro +168 -0
- package/src/components/common/OptimizedImage.astro +41 -0
- package/src/components/common/ThemePanel.astro +152 -0
- package/src/components/common/ThemeSelector.astro +67 -0
- package/src/components/common/ThemeToggle.astro +82 -0
- package/src/config/font-variables.ts +29 -0
- package/src/config/fonts.ts +18 -0
- package/src/content/index.ts +11 -0
- package/src/content/schemas.ts +253 -0
- package/src/integration/index.ts +156 -0
- package/src/integration/registry.ts +193 -0
- package/src/integration/types.ts +193 -0
- package/src/integration/virtual.d.ts +326 -0
- package/src/integration/vite-plugin-parche.ts +432 -0
- package/src/layouts/BaseLayout.astro +121 -0
- package/src/routes/404.astro +46 -0
- package/src/routes/[...slug].astro +216 -0
- package/src/routes/middleware.ts +18 -0
- package/src/styles/base.css +221 -0
- package/src/styles/semantic.css +95 -0
- package/src/styles/shadcn-compat.css +120 -0
- package/src/styles/tokens.css +237 -0
- package/src/types/config.ts +102 -0
- package/src/utils/i18n.ts +79 -0
- package/src/utils/layout.ts +46 -0
- package/src/utils/metadata.ts +277 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import type { ParcheUserConfig, ResolvedRegistry, ParcheManifest } from './types.js';
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const coreDir = path.resolve(__dirname, '..');
|
|
7
|
+
|
|
8
|
+
function corePath(...segments: string[]): string {
|
|
9
|
+
return path.resolve(coreDir, ...segments);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Keep the first entry per theme `value` (parche order = precedence). */
|
|
13
|
+
function dedupeThemes(
|
|
14
|
+
themes: Array<{ label: string; value: string }>,
|
|
15
|
+
): Array<{ label: string; value: string }> {
|
|
16
|
+
const seen = new Set<string>();
|
|
17
|
+
return themes.filter((t) => (seen.has(t.value) ? false : (seen.add(t.value), true)));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The always-present base look (no data-theme). Themes are added by parches. */
|
|
21
|
+
const DEFAULT_THEME = { label: 'Default', value: '' };
|
|
22
|
+
|
|
23
|
+
/** Built-in core component registry */
|
|
24
|
+
const CORE_MODULES: Record<string, string> = {
|
|
25
|
+
// Theme / i18n engine controls (consumed by the ui parche's Header)
|
|
26
|
+
'parche:components/ThemeToggle': corePath('components/common/ThemeToggle.astro'),
|
|
27
|
+
'parche:components/ThemeSelector': corePath('components/common/ThemeSelector.astro'),
|
|
28
|
+
'parche:components/OptimizedImage': corePath('components/common/OptimizedImage.astro'),
|
|
29
|
+
'parche:components/LocaleSwitcher': corePath('components/common/LocaleSwitcher.astro'),
|
|
30
|
+
'parche:components/ThemePanel': corePath('components/common/ThemePanel.astro'),
|
|
31
|
+
|
|
32
|
+
// Layouts
|
|
33
|
+
'parche:layouts/BaseLayout': corePath('layouts/BaseLayout.astro'),
|
|
34
|
+
|
|
35
|
+
// DynamicRenderer & LayoutRenderer
|
|
36
|
+
'parche:DynamicRenderer': corePath('components/DynamicRenderer.astro'),
|
|
37
|
+
'parche:LayoutRenderer': corePath('components/LayoutRenderer.astro'),
|
|
38
|
+
|
|
39
|
+
// Utils (named exports)
|
|
40
|
+
'parche:utils/metadata': corePath('utils/metadata.ts'),
|
|
41
|
+
'parche:utils/i18n': corePath('utils/i18n.ts'),
|
|
42
|
+
'parche:utils/layout': corePath('utils/layout.ts'),
|
|
43
|
+
// Note: layout/Header, layout/Footer and the contact/content templates are
|
|
44
|
+
// now provided by the ui parche — core no longer ships chrome or primitives.
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** Modules that use named exports instead of default export */
|
|
48
|
+
const NAMED_EXPORT_MODULES = new Set([
|
|
49
|
+
'parche:utils/metadata',
|
|
50
|
+
'parche:utils/i18n',
|
|
51
|
+
'parche:utils/layout',
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Convert an override key ('widgets:hero:Hero') to a virtual module ID ('parche:widgets/hero/Hero')
|
|
56
|
+
*/
|
|
57
|
+
function overrideKeyToVirtualId(key: string): string {
|
|
58
|
+
return 'parche:' + key.replace(/:/g, '/');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Build the complete resolved registry from user config.
|
|
63
|
+
*/
|
|
64
|
+
export function createRegistry(
|
|
65
|
+
userConfig: ParcheUserConfig,
|
|
66
|
+
rootDir: string,
|
|
67
|
+
astroI18n?: { locales?: Array<string | { path: string; codes: string[] }>; defaultLocale?: string },
|
|
68
|
+
): ResolvedRegistry {
|
|
69
|
+
const modules: Record<string, string> = { ...CORE_MODULES };
|
|
70
|
+
|
|
71
|
+
// Add config module
|
|
72
|
+
const configPath = userConfig.config || './src/config.ts';
|
|
73
|
+
modules['parche:config'] = path.resolve(rootDir, configPath);
|
|
74
|
+
|
|
75
|
+
// Register parches (order = precedence: later wins). Each parche contributes
|
|
76
|
+
// primitives / widgets / templates / routes / config to the system.
|
|
77
|
+
const parches = userConfig.parches ?? [];
|
|
78
|
+
const providedPrimitives = new Set<string>();
|
|
79
|
+
const providedWidgets = new Set<string>();
|
|
80
|
+
const apps: ParcheManifest[] = [];
|
|
81
|
+
const contributedStyles: string[] = [];
|
|
82
|
+
const contributedThemes: Array<{ label: string; value: string }> = [];
|
|
83
|
+
const contentGlobs: string[] = [];
|
|
84
|
+
|
|
85
|
+
for (const parche of parches) {
|
|
86
|
+
if (parche.styles) contributedStyles.push(...parche.styles);
|
|
87
|
+
if (parche.themes) contributedThemes.push(...parche.themes);
|
|
88
|
+
if (parche.content) contentGlobs.push(...parche.content);
|
|
89
|
+
if (parche.primitives) {
|
|
90
|
+
for (const [name, absPath] of Object.entries(parche.primitives)) {
|
|
91
|
+
modules[`parche:primitives/${name}`] = absPath;
|
|
92
|
+
providedPrimitives.add(name);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (parche.widgets) {
|
|
96
|
+
for (const [name, absPath] of Object.entries(parche.widgets)) {
|
|
97
|
+
modules[`parche:widgets/${name}`] = absPath;
|
|
98
|
+
providedWidgets.add(name);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (parche.templates) {
|
|
102
|
+
for (const [name, absPath] of Object.entries(parche.templates)) {
|
|
103
|
+
modules[`parche:templates/${name}`] = absPath;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (parche.namedExportModules) {
|
|
107
|
+
for (const id of parche.namedExportModules) NAMED_EXPORT_MODULES.add(id);
|
|
108
|
+
}
|
|
109
|
+
// A parche that injects routes / resolves slugs / exposes config is an "app".
|
|
110
|
+
if (parche.routes || parche.resolver || parche.config) {
|
|
111
|
+
apps.push(parche);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Add user-defined templates
|
|
116
|
+
if (userConfig.routes?.templates) {
|
|
117
|
+
for (const [name, userPath] of Object.entries(userConfig.routes.templates)) {
|
|
118
|
+
modules[`parche:templates/${name}`] = path.resolve(rootDir, userPath);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Add user-defined layouts
|
|
123
|
+
if (userConfig.routes?.layouts) {
|
|
124
|
+
for (const [name, userPath] of Object.entries(userConfig.routes.layouts)) {
|
|
125
|
+
modules[`parche:layouts/${name}`] = path.resolve(rootDir, userPath);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Validate parche requirements (V1: capability presence).
|
|
130
|
+
const missing: string[] = [];
|
|
131
|
+
for (const parche of parches) {
|
|
132
|
+
for (const name of parche.requires?.primitives ?? []) {
|
|
133
|
+
if (!providedPrimitives.has(name)) missing.push(`"${parche.name}" requires primitive "${name}" (parche:primitives/${name})`);
|
|
134
|
+
}
|
|
135
|
+
for (const name of parche.requires?.widgets ?? []) {
|
|
136
|
+
if (!providedWidgets.has(name)) missing.push(`"${parche.name}" requires widget "${name}" (parche:widgets/${name})`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (missing.length) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
'[parche] Unsatisfied parche requirements — add a parche that provides them:\n - ' + missing.join('\n - '),
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Apply user overrides (these take priority)
|
|
146
|
+
if (userConfig.overrides) {
|
|
147
|
+
for (const [key, overridePath] of Object.entries(userConfig.overrides)) {
|
|
148
|
+
const virtualId = overrideKeyToVirtualId(key);
|
|
149
|
+
modules[virtualId] = path.resolve(rootDir, overridePath);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Resolve i18n config from Astro's official i18n settings
|
|
154
|
+
const resolvedLocales = (astroI18n?.locales ?? ['en']).map((loc) =>
|
|
155
|
+
typeof loc === 'string' ? loc : loc.codes[0],
|
|
156
|
+
);
|
|
157
|
+
const i18n = {
|
|
158
|
+
locales: resolvedLocales,
|
|
159
|
+
defaultLocale: astroI18n?.defaultLocale ?? 'en',
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// Resolve themes: the base look plus whatever the imported parches contribute.
|
|
163
|
+
// `themes.available` still overrides explicitly, for full manual control.
|
|
164
|
+
const themes = userConfig.themes?.available ?? dedupeThemes([DEFAULT_THEME, ...contributedThemes]);
|
|
165
|
+
const showPanel = userConfig.themes?.showPanel ?? themes.length > 1;
|
|
166
|
+
|
|
167
|
+
// Aggregate the CSS to bundle: what the parches contribute (e.g. themes) plus
|
|
168
|
+
// an optional user entry. A site ships only the CSS of the parches it imports.
|
|
169
|
+
const styleEntries = [...contributedStyles];
|
|
170
|
+
if (userConfig.styles?.entry) {
|
|
171
|
+
styleEntries.push(path.resolve(rootDir, userConfig.styles.entry));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Collect app resolvers
|
|
175
|
+
const resolvers: Array<{ appName: string; entrypoint: string }> = [];
|
|
176
|
+
for (const app of apps) {
|
|
177
|
+
if (app.resolver) {
|
|
178
|
+
resolvers.push({ appName: app.name, entrypoint: app.resolver.entrypoint });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
modules,
|
|
184
|
+
namedExportModules: NAMED_EXPORT_MODULES,
|
|
185
|
+
i18n,
|
|
186
|
+
themes,
|
|
187
|
+
showPanel,
|
|
188
|
+
styleEntries,
|
|
189
|
+
contentGlobs,
|
|
190
|
+
apps,
|
|
191
|
+
resolvers,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
export interface UIRegistry {
|
|
2
|
+
atoms: Record<string, string>;
|
|
3
|
+
widgets: Record<string, string>;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Widget props system (Zod v4 + meta)
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
/** Field-level metadata passed via z.string().meta({ ... }) */
|
|
11
|
+
export interface FieldMeta {
|
|
12
|
+
/** Override the auto-generated label */
|
|
13
|
+
label?: string;
|
|
14
|
+
/** Help text shown below the field */
|
|
15
|
+
help?: string;
|
|
16
|
+
/** Placeholder for text/textarea inputs */
|
|
17
|
+
placeholder?: string;
|
|
18
|
+
/** Force a specific input type: 'textarea', 'icon', 'color', 'url' */
|
|
19
|
+
input?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Group definition for organising fields in the builder form */
|
|
23
|
+
export interface FieldGroup {
|
|
24
|
+
key: string;
|
|
25
|
+
label: string;
|
|
26
|
+
fields: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Widget-level metadata — classification + builder UI config */
|
|
30
|
+
export interface WidgetMeta {
|
|
31
|
+
widget: {
|
|
32
|
+
label: string;
|
|
33
|
+
description?: string;
|
|
34
|
+
category?: string;
|
|
35
|
+
icon?: string;
|
|
36
|
+
thumbnail?: string;
|
|
37
|
+
tags?: string[];
|
|
38
|
+
};
|
|
39
|
+
ui?: {
|
|
40
|
+
groups?: FieldGroup[];
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Capabilities a parche needs from the system (validated at setup). */
|
|
45
|
+
export interface ParcheRequires {
|
|
46
|
+
/** Primitive names that must exist (parche:primitives/{name}) */
|
|
47
|
+
primitives?: string[];
|
|
48
|
+
/** Widget names that must exist (parche:widgets/{name}) */
|
|
49
|
+
widgets?: string[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A parche (plugin). Contributes capabilities to the Parche host and declares
|
|
54
|
+
* what it requires. Primitive-packs, widget-packs and apps are all parches —
|
|
55
|
+
* they differ only in what they provide.
|
|
56
|
+
*/
|
|
57
|
+
export interface ParcheManifest {
|
|
58
|
+
/** Unique identifier (e.g. 'primitives', 'ui', 'blog') */
|
|
59
|
+
name: string;
|
|
60
|
+
/** Primitives to register: name → absolute path (parche:primitives/{name}) */
|
|
61
|
+
primitives?: Record<string, string>;
|
|
62
|
+
/** Widgets to register: virtual ID suffix → absolute path (parche:widgets/{name}) */
|
|
63
|
+
widgets?: Record<string, string>;
|
|
64
|
+
/** Templates to register: virtual ID suffix → absolute path */
|
|
65
|
+
templates?: Record<string, string>;
|
|
66
|
+
/**
|
|
67
|
+
* CSS files this parche contributes (absolute paths). Aggregated into the
|
|
68
|
+
* styles entry so a site bundles only the CSS of the parches it imports.
|
|
69
|
+
* A theme is just a parche that contributes its override CSS here.
|
|
70
|
+
*/
|
|
71
|
+
styles?: string[];
|
|
72
|
+
/**
|
|
73
|
+
* Theme entries this parche adds to the switcher: { label, value }. The
|
|
74
|
+
* `value` is the `data-theme` attribute the theme's CSS is scoped to.
|
|
75
|
+
*/
|
|
76
|
+
themes?: Array<{ label: string; value: string }>;
|
|
77
|
+
/**
|
|
78
|
+
* Absolute globs of this parche's own component files, so Tailwind scans them
|
|
79
|
+
* and generates the utility classes they use. A parche must contribute these
|
|
80
|
+
* for its classes to survive being installed from npm (relative `@source`
|
|
81
|
+
* paths can't reach sibling packages once published). Typically:
|
|
82
|
+
* content: [path.resolve(dir, '**\/*.astro')]
|
|
83
|
+
*/
|
|
84
|
+
content?: string[];
|
|
85
|
+
/** Routes to inject */
|
|
86
|
+
routes?: Array<{ pattern: string; entrypoint: string }>;
|
|
87
|
+
/** App config exposed via virtual module parche:app/{name} */
|
|
88
|
+
config?: Record<string, unknown>;
|
|
89
|
+
/** Module IDs that use named exports (export *) instead of default */
|
|
90
|
+
namedExportModules?: string[];
|
|
91
|
+
/**
|
|
92
|
+
* Content resolver for root-level routes.
|
|
93
|
+
* When routes would conflict with the catch-all (e.g. /%slug%), the parche
|
|
94
|
+
* registers a resolver instead; the catch-all calls it before treating a
|
|
95
|
+
* slug as a page. The entrypoint must export:
|
|
96
|
+
* resolve(slug, locale, opts) → { template, collection, entryId, props, metadata } | null
|
|
97
|
+
* getPaths(locales, defaultLocale, opts) → Array<{ params, props }>
|
|
98
|
+
*/
|
|
99
|
+
resolver?: {
|
|
100
|
+
entrypoint: string;
|
|
101
|
+
};
|
|
102
|
+
/** What this parche needs the system to provide. */
|
|
103
|
+
requires?: ParcheRequires;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** @deprecated Use ParcheManifest. Kept as an alias for existing app factories. */
|
|
107
|
+
export type ParcheApp = ParcheManifest;
|
|
108
|
+
|
|
109
|
+
export interface ParcheI18nConfig {
|
|
110
|
+
/** Supported locales (e.g. ['en', 'es']) */
|
|
111
|
+
locales: string[];
|
|
112
|
+
/** Default locale — served without URL prefix (e.g. 'en') */
|
|
113
|
+
defaultLocale: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface ParcheRoutesConfig {
|
|
117
|
+
/**
|
|
118
|
+
* Enable the built-in catch-all page route ([...slug].astro).
|
|
119
|
+
* This route renders pages from your JSON content collections using DynamicRenderer.
|
|
120
|
+
* Must be explicitly set to true to enable.
|
|
121
|
+
*/
|
|
122
|
+
pages: boolean;
|
|
123
|
+
/** Additional templates: { templateName: './src/templates/MyTemplate.astro' } */
|
|
124
|
+
templates?: Record<string, string>;
|
|
125
|
+
/** Additional layouts: { layoutName: './src/layouts/MyLayout.astro' } */
|
|
126
|
+
layouts?: Record<string, string>;
|
|
127
|
+
/** Override the injected catch-all route entrypoint */
|
|
128
|
+
catchAllRoute?: string;
|
|
129
|
+
/** Override the injected 404 page entrypoint */
|
|
130
|
+
notFoundRoute?: string;
|
|
131
|
+
/** Override the injected middleware entrypoint */
|
|
132
|
+
middleware?: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface ParcheStylesConfig {
|
|
136
|
+
/**
|
|
137
|
+
* Path to an extra CSS file imported by injected routes, on top of any CSS
|
|
138
|
+
* the parches contribute. Use it for project-wide styles.
|
|
139
|
+
* Default: nothing beyond what the imported parches (e.g. themes) provide.
|
|
140
|
+
*/
|
|
141
|
+
entry?: string;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface ParcheThemesConfig {
|
|
145
|
+
/** Available themes for ThemeSelector. Each entry: { label, value } */
|
|
146
|
+
available?: Array<{ label: string; value: string }>;
|
|
147
|
+
/** Show the floating theme panel. Default: true when multiple themes are available */
|
|
148
|
+
showPanel?: boolean;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface ParcheSeoConfig {
|
|
152
|
+
/** Allow AI crawlers (GPTBot, CCBot, anthropic-ai, ClaudeBot) in robots.txt. Default: true */
|
|
153
|
+
allowAICrawlers?: boolean;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface ParcheUserConfig {
|
|
157
|
+
/** Override any component using namespaced keys: 'widgets:hero:Hero', 'primitives:Button', etc.
|
|
158
|
+
* Values are paths to .astro component files. */
|
|
159
|
+
overrides?: Record<string, string>;
|
|
160
|
+
/** Path to user config file (default: './src/config.ts') */
|
|
161
|
+
config?: string;
|
|
162
|
+
/** Parches (plugins): primitive-packs, widget-packs and apps. Order = precedence. */
|
|
163
|
+
parches?: ParcheManifest[];
|
|
164
|
+
/** Route injection config */
|
|
165
|
+
routes?: ParcheRoutesConfig;
|
|
166
|
+
/** Theme config */
|
|
167
|
+
themes?: ParcheThemesConfig;
|
|
168
|
+
/** Styles config */
|
|
169
|
+
styles?: ParcheStylesConfig;
|
|
170
|
+
/** SEO build-time config (robots.txt generation, etc.) */
|
|
171
|
+
seo?: ParcheSeoConfig;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface ResolvedRegistry {
|
|
175
|
+
/** Map of virtual module ID → absolute file path */
|
|
176
|
+
modules: Record<string, string>;
|
|
177
|
+
/** Set of virtual IDs that use named exports (export *) instead of default */
|
|
178
|
+
namedExportModules: Set<string>;
|
|
179
|
+
/** Resolved i18n config */
|
|
180
|
+
i18n: ParcheI18nConfig;
|
|
181
|
+
/** Resolved themes config */
|
|
182
|
+
themes: Array<{ label: string; value: string }>;
|
|
183
|
+
/** Whether to show the floating theme panel */
|
|
184
|
+
showPanel: boolean;
|
|
185
|
+
/** Absolute CSS paths to import via parche:config/styles (parche-contributed + user entry) */
|
|
186
|
+
styleEntries: string[];
|
|
187
|
+
/** Absolute globs of parche component files for Tailwind to scan (@source) */
|
|
188
|
+
contentGlobs: string[];
|
|
189
|
+
/** Registered apps */
|
|
190
|
+
apps: ParcheApp[];
|
|
191
|
+
/** App resolvers — modules that export resolve() and getPaths() */
|
|
192
|
+
resolvers: Array<{ appName: string; entrypoint: string }>;
|
|
193
|
+
}
|