@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.
@@ -0,0 +1,67 @@
1
+ ---
2
+ import { themes as configuredThemes } from 'parche:config/themes';
3
+
4
+ interface ThemeOption {
5
+ label: string;
6
+ value: string;
7
+ }
8
+
9
+ interface Props {
10
+ /** Override the themes list from config */
11
+ themes?: ThemeOption[];
12
+ }
13
+
14
+ const { themes = configuredThemes } = Astro.props;
15
+ ---
16
+
17
+ {themes.length > 1 && (
18
+ <theme-selector>
19
+ <div class="flex items-center gap-1 bg-surface border border-border rounded-lg p-0.5">
20
+ {themes.map((theme) => (
21
+ <button
22
+ type="button"
23
+ data-theme-value={theme.value}
24
+ class="px-2.5 py-1 text-xs font-medium rounded-md transition-colors cursor-pointer text-muted hover:text-heading"
25
+ >
26
+ {theme.label}
27
+ </button>
28
+ ))}
29
+ </div>
30
+ </theme-selector>
31
+ )}
32
+
33
+ <script>
34
+ class ThemeSelectorElement extends HTMLElement {
35
+ connectedCallback() {
36
+ const buttons = this.querySelectorAll<HTMLButtonElement>('[data-theme-value]');
37
+ const currentTheme = document.documentElement.getAttribute('data-theme') || '';
38
+
39
+ const updateActive = (active: string) => {
40
+ buttons.forEach((btn) => {
41
+ const isActive = btn.dataset.themeValue === active;
42
+ btn.classList.toggle('bg-primary', isActive);
43
+ btn.classList.toggle('text-white', isActive);
44
+ btn.classList.toggle('text-muted', !isActive);
45
+ });
46
+ };
47
+
48
+ updateActive(currentTheme);
49
+
50
+ buttons.forEach((btn) => {
51
+ btn.addEventListener('click', () => {
52
+ const value = btn.dataset.themeValue || '';
53
+ if (value) {
54
+ document.documentElement.setAttribute('data-theme', value);
55
+ localStorage.setItem('site-theme', value);
56
+ } else {
57
+ document.documentElement.removeAttribute('data-theme');
58
+ localStorage.removeItem('site-theme');
59
+ }
60
+ updateActive(value);
61
+ });
62
+ });
63
+ }
64
+ }
65
+
66
+ customElements.define('theme-selector', ThemeSelectorElement);
67
+ </script>
@@ -0,0 +1,82 @@
1
+ ---
2
+ /**
3
+ * ThemeToggle — HTML Web Component (no Shadow DOM)
4
+ * Toggles .dark class on <html>, persists to localStorage.
5
+ * Static HTML is functional without JS (shows both icons, JS hides one).
6
+ */
7
+ ---
8
+
9
+ <theme-toggle class="inline-flex">
10
+ <button
11
+ type="button"
12
+ aria-label="Toggle dark mode"
13
+ class="relative inline-flex items-center justify-center w-8 h-8 rounded-md text-muted hover:text-heading hover:bg-black/5 dark:hover:bg-white/5 transition-colors cursor-pointer"
14
+ >
15
+ <!-- Sun icon (visible in dark mode) -->
16
+ <svg
17
+ class="sun-icon absolute w-6 h-6 transition-transform"
18
+ xmlns="http://www.w3.org/2000/svg"
19
+ viewBox="0 0 24 24"
20
+ fill="none"
21
+ stroke="currentColor"
22
+ stroke-width="2"
23
+ stroke-linecap="round"
24
+ stroke-linejoin="round"
25
+ >
26
+ <circle cx="12" cy="12" r="5"></circle>
27
+ <line x1="12" y1="1" x2="12" y2="3"></line>
28
+ <line x1="12" y1="21" x2="12" y2="23"></line>
29
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
30
+ <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
31
+ <line x1="1" y1="12" x2="3" y2="12"></line>
32
+ <line x1="21" y1="12" x2="23" y2="12"></line>
33
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
34
+ <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
35
+ </svg>
36
+ <!-- Moon icon (visible in light mode) -->
37
+ <svg
38
+ class="moon-icon absolute w-5 h-5 transition-transform"
39
+ xmlns="http://www.w3.org/2000/svg"
40
+ viewBox="0 0 24 24"
41
+ fill="none"
42
+ stroke="currentColor"
43
+ stroke-width="2"
44
+ stroke-linecap="round"
45
+ stroke-linejoin="round"
46
+ >
47
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
48
+ </svg>
49
+ </button>
50
+ </theme-toggle>
51
+
52
+ <script>
53
+ class ThemeToggle extends HTMLElement {
54
+ connectedCallback() {
55
+ const button = this.querySelector('button');
56
+ if (!button) return;
57
+
58
+ this.updateIcons();
59
+
60
+ button.addEventListener('click', () => {
61
+ document.documentElement.classList.toggle('dark');
62
+ const isDark = document.documentElement.classList.contains('dark');
63
+ localStorage.setItem('theme', isDark ? 'dark' : 'light');
64
+ this.updateIcons();
65
+ });
66
+ }
67
+
68
+ updateIcons() {
69
+ const isDark = document.documentElement.classList.contains('dark');
70
+ const sun = this.querySelector('.sun-icon') as HTMLElement;
71
+ const moon = this.querySelector('.moon-icon') as HTMLElement;
72
+ if (sun && moon) {
73
+ sun.style.opacity = isDark ? '1' : '0';
74
+ sun.style.transform = isDark ? 'rotate(0deg) scale(1)' : 'rotate(-90deg) scale(0)';
75
+ moon.style.opacity = isDark ? '0' : '1';
76
+ moon.style.transform = isDark ? 'rotate(90deg) scale(0)' : 'rotate(0deg) scale(1)';
77
+ }
78
+ }
79
+ }
80
+
81
+ customElements.define('theme-toggle', ThemeToggle);
82
+ </script>
@@ -0,0 +1,29 @@
1
+ export interface ParcheFontDef {
2
+ cssVariable: string;
3
+ name: string;
4
+ weights: number[];
5
+ fallbacks: string[];
6
+ preload?: boolean;
7
+ }
8
+
9
+ const sans = ['ui-sans-serif', 'system-ui', 'sans-serif'];
10
+ const serif = ['ui-serif', 'Georgia', 'serif'];
11
+ const mono = ['ui-monospace', 'SFMono-Regular', 'monospace'];
12
+
13
+ /**
14
+ * Single source of truth for Parche's font set. BaseLayout renders a <Font>
15
+ * for each entry; the `parcheFonts` config helper builds the astro.config array
16
+ * from the same list, so the two never drift.
17
+ */
18
+ export const parcheFontDefs: ParcheFontDef[] = [
19
+ { cssVariable: '--font-sans', name: 'Geist', weights: [400, 500, 600, 700], fallbacks: sans, preload: true },
20
+ { cssVariable: '--font-serif', name: 'Lora', weights: [400, 700], fallbacks: serif },
21
+ { cssVariable: '--font-mono', name: 'JetBrains Mono', weights: [400], fallbacks: mono },
22
+ { cssVariable: '--font-heading-alt', name: 'Literata', weights: [400, 700], fallbacks: serif },
23
+ { cssVariable: '--font-body-alt', name: 'Libre Franklin', weights: [400, 500, 600, 700], fallbacks: sans },
24
+ { cssVariable: '--font-rounded', name: 'Nunito', weights: [400, 500, 600, 700], fallbacks: sans },
25
+ { cssVariable: '--font-tech', name: 'Schibsted Grotesk', weights: [400, 500, 600, 700], fallbacks: sans },
26
+ { cssVariable: '--font-tech-body', name: 'DM Sans', weights: [400, 500, 600, 700], fallbacks: sans },
27
+ ];
28
+
29
+ export const parcheFontVariables = parcheFontDefs.map((f) => f.cssVariable);
@@ -0,0 +1,18 @@
1
+ import { fontProviders } from 'astro/config';
2
+ import { parcheFontDefs } from './font-variables.js';
3
+
4
+ /**
5
+ * Parche's default font set. Assign into your astro.config `fonts`:
6
+ *
7
+ * import { parcheFonts } from '@parche/core/fonts';
8
+ * export default defineConfig({ fonts: parcheFonts, ... });
9
+ *
10
+ * BaseLayout renders the matching <Font> tags automatically.
11
+ */
12
+ export const parcheFonts = parcheFontDefs.map((f) => ({
13
+ provider: fontProviders.google(),
14
+ name: f.name,
15
+ cssVariable: f.cssVariable,
16
+ weights: f.weights,
17
+ fallbacks: f.fallbacks,
18
+ }));
@@ -0,0 +1,11 @@
1
+ export {
2
+ sectionSchema,
3
+ metadataSchema,
4
+ pageSchema,
5
+ navigationSchema,
6
+ layoutSchema,
7
+ createCollections,
8
+ collections,
9
+ } from './schemas.js';
10
+
11
+ export type { SectionEntry, MetadataEntry, PageEntry, NavigationEntry, LayoutEntry } from './schemas.js';
@@ -0,0 +1,253 @@
1
+ import { z } from 'zod';
2
+ import { defineCollection } from 'astro:content';
3
+ import { glob } from 'astro/loaders';
4
+
5
+ /**
6
+ * Schema for page-level SEO/metadata overrides.
7
+ * All fields are optional — the system resolves fallbacks at render time
8
+ * (e.g. metadata.title ?? page.title).
9
+ */
10
+ export const metadataSchema = z.object({
11
+ // Meta basics (override page-level title/description for SEO)
12
+ title: z.string().optional(),
13
+ description: z.string().optional(),
14
+ canonical: z.string().optional(),
15
+ keywords: z.string().optional(),
16
+
17
+ // Indexing & robots
18
+ noindex: z.boolean().default(false),
19
+ nofollow: z.boolean().default(false),
20
+ robots: z
21
+ .object({
22
+ maxSnippet: z.number().optional(),
23
+ maxImagePreview: z.enum(['none', 'standard', 'large']).optional(),
24
+ maxVideoPreview: z.number().optional(),
25
+ })
26
+ .optional(),
27
+
28
+ // Open Graph
29
+ ogTitle: z.string().optional(),
30
+ ogDescription: z.string().optional(),
31
+ ogImage: z.string().optional(),
32
+ ogType: z.enum(['website', 'article', 'product', 'profile']).default('website'),
33
+
34
+ // Twitter Card
35
+ twitterCard: z.enum(['summary', 'summary_large_image', 'player', 'app']).default('summary_large_image'),
36
+
37
+ // Article (relevant when ogType='article')
38
+ article: z
39
+ .object({
40
+ author: z.string().optional(),
41
+ publishedDate: z.string().optional(),
42
+ modifiedDate: z.string().optional(),
43
+ section: z.string().optional(),
44
+ tags: z.array(z.string()).optional(),
45
+ })
46
+ .optional(),
47
+
48
+ // Custom structured data escape hatch
49
+ jsonLd: z.unknown().optional(),
50
+ });
51
+
52
+ /**
53
+ * Base schema for page content entries.
54
+ * Users can extend this with `.extend({ myField: z.string() })`.
55
+ */
56
+ /**
57
+ * Schema for a single section (shared by pages and layouts).
58
+ */
59
+ export const sectionSchema = z.object({
60
+ widget: z.string(),
61
+ props: z.record(z.string(), z.unknown()).optional(),
62
+ wrapper: z.union([
63
+ z.literal(false),
64
+ z.object({
65
+ id: z.string().optional(),
66
+ isDark: z.boolean().optional(),
67
+ bg: z.string().optional(),
68
+ classes: z.record(z.string(), z.unknown()).optional(),
69
+ as: z.string().optional(),
70
+ }),
71
+ ]).optional(),
72
+ });
73
+
74
+ export const pageSchema = z.object({
75
+ title: z.string(),
76
+ description: z.string().optional(),
77
+ urlSlug: z.string().optional(),
78
+ template: z.string().default('dynamic'),
79
+ layout: z.string().optional(),
80
+ metadata: metadataSchema.optional(),
81
+ sections: z.array(sectionSchema).optional(),
82
+ body: z.string().optional(),
83
+ formLabels: z.record(z.string(), z.string()).optional(),
84
+ });
85
+
86
+ /**
87
+ * Schema for a single navigation link (used in dropdowns, mega menus, etc.)
88
+ */
89
+ const navLinkSchema = z.object({
90
+ label: z.string(),
91
+ href: z.string(),
92
+ icon: z.string().optional(),
93
+ description: z.string().optional(),
94
+ });
95
+
96
+ /**
97
+ * Schema for a group of links (used in dropdowns and mega menu columns).
98
+ */
99
+ const navGroupSchema = z.object({
100
+ title: z.string().optional(),
101
+ links: z.array(navLinkSchema),
102
+ });
103
+
104
+ /**
105
+ * Schema for mega menu configuration.
106
+ */
107
+ const megaMenuSchema = z.object({
108
+ columns: z.number().min(1).max(4).default(3),
109
+ featured: z.object({
110
+ title: z.string(),
111
+ description: z.string().optional(),
112
+ image: z.string().optional(),
113
+ href: z.string(),
114
+ }).optional(),
115
+ footer: z.string().optional(),
116
+ });
117
+
118
+ /**
119
+ * Schema for a top-level header link.
120
+ * - href only → simple link
121
+ * - children without mega → dropdown
122
+ * - children + mega → mega menu
123
+ */
124
+ const headerLinkSchema = z.object({
125
+ label: z.string(),
126
+ href: z.string().optional(),
127
+ children: z.array(navGroupSchema).optional(),
128
+ mega: megaMenuSchema.optional(),
129
+ });
130
+
131
+ /**
132
+ * Schema for a header CTA action button.
133
+ */
134
+ const headerActionSchema = z.object({
135
+ label: z.string(),
136
+ href: z.string(),
137
+ variant: z.enum(['primary', 'secondary', 'ghost']).default('primary'),
138
+ icon: z.string().optional(),
139
+ });
140
+
141
+ /**
142
+ * Schema for the announcement bar above the header.
143
+ */
144
+ const announcementSchema = z.object({
145
+ text: z.string(),
146
+ href: z.string().optional(),
147
+ icon: z.string().optional(),
148
+ dismissible: z.boolean().default(true),
149
+ aside: z.string().optional(),
150
+ class: z.string().optional(),
151
+ });
152
+
153
+ /**
154
+ * Schema for the header logo (text or image).
155
+ */
156
+ const logoSchema = z.union([
157
+ z.string(),
158
+ z.object({
159
+ src: z.string(),
160
+ alt: z.string().default('Logo'),
161
+ width: z.number().optional(),
162
+ height: z.number().optional(),
163
+ }),
164
+ ]);
165
+
166
+ /**
167
+ * Base schema for navigation entries.
168
+ */
169
+ export const navigationSchema = z.object({
170
+ header: z.object({
171
+ logo: logoSchema.optional(),
172
+ links: z.array(headerLinkSchema),
173
+ actions: z.array(headerActionSchema).optional(),
174
+ announcement: announcementSchema.optional(),
175
+ }),
176
+ footer: z.object({
177
+ columns: z.array(
178
+ z.object({
179
+ title: z.string(),
180
+ links: z.array(z.object({ label: z.string(), href: z.string() })),
181
+ }),
182
+ ),
183
+ secondaryLinks: z.array(z.object({ label: z.string(), href: z.string() })).optional(),
184
+ socialLinks: z.array(z.object({
185
+ label: z.string(),
186
+ href: z.string(),
187
+ icon: z.string().optional(),
188
+ })).optional(),
189
+ footNote: z.string().optional(),
190
+ copyright: z.string().optional(),
191
+ }),
192
+ });
193
+
194
+ /**
195
+ * Schema for layout entries (same sections format as pages).
196
+ */
197
+ export const layoutSchema = z.object({
198
+ sections: z.array(sectionSchema),
199
+ });
200
+
201
+ export type MetadataEntry = z.infer<typeof metadataSchema>;
202
+ export type PageEntry = z.infer<typeof pageSchema>;
203
+ export type NavigationEntry = z.infer<typeof navigationSchema>;
204
+ export type LayoutEntry = z.infer<typeof layoutSchema>;
205
+ export type SectionEntry = z.infer<typeof sectionSchema>;
206
+
207
+ /**
208
+ * Ready-to-use collections for a standard Parche project.
209
+ *
210
+ * Usage in content.config.ts:
211
+ * export { collections } from '@parche/core/content';
212
+ *
213
+ * Or extend:
214
+ * import { createCollections, pageSchema } from '@parche/core/content';
215
+ * export const collections = createCollections({
216
+ * pageSchema: pageSchema.extend({ author: z.string() }),
217
+ * });
218
+ */
219
+ export function createCollections(options?: {
220
+ pageSchema?: z.ZodType;
221
+ navigationSchema?: z.ZodType;
222
+ layoutSchema?: z.ZodType;
223
+ pagesBase?: string;
224
+ navigationBase?: string;
225
+ layoutsBase?: string;
226
+ }) {
227
+ return {
228
+ pages: defineCollection({
229
+ loader: glob({
230
+ pattern: '**/*.{json,md}',
231
+ base: options?.pagesBase ?? './src/content/pages',
232
+ }),
233
+ schema: options?.pageSchema ?? pageSchema,
234
+ }),
235
+ navigation: defineCollection({
236
+ loader: glob({
237
+ pattern: '*.json',
238
+ base: options?.navigationBase ?? './src/content/navigation',
239
+ }),
240
+ schema: options?.navigationSchema ?? navigationSchema,
241
+ }),
242
+ layouts: defineCollection({
243
+ loader: glob({
244
+ pattern: '**/*.{yaml,yml,json}',
245
+ base: options?.layoutsBase ?? './src/content/layouts',
246
+ }),
247
+ schema: options?.layoutSchema ?? layoutSchema,
248
+ }),
249
+ };
250
+ }
251
+
252
+ /** Default collections — import directly if no customization needed */
253
+ export const collections = createCollections();
@@ -0,0 +1,156 @@
1
+ import type { AstroIntegration } from 'astro';
2
+ import { fileURLToPath } from 'node:url';
3
+ import path from 'node:path';
4
+ import fs from 'node:fs';
5
+ import { vitePluginParche } from './vite-plugin-parche.js';
6
+ import { createRegistry } from './registry.js';
7
+ import type { ParcheUserConfig, UIRegistry, ParcheApp, ParcheManifest, ParcheRequires } from './types.js';
8
+
9
+ export default function parche(userConfig: ParcheUserConfig = {}): AstroIntegration {
10
+ let resolvedSiteUrl = '';
11
+ return {
12
+ name: 'parche',
13
+ hooks: {
14
+ 'astro:config:setup': ({ updateConfig, config, injectRoute, addMiddleware }) => {
15
+ resolvedSiteUrl = config.site ?? '';
16
+ const rootDir = fileURLToPath(config.root);
17
+ const resolvedRegistry = createRegistry(userConfig, rootDir, config.i18n);
18
+
19
+ // Resolve @core/* alias for backward compatibility with widget internal imports
20
+ const coreDir = path.resolve(
21
+ path.dirname(fileURLToPath(import.meta.url)),
22
+ '..',
23
+ );
24
+
25
+ const routesDir = path.resolve(coreDir, 'routes');
26
+
27
+ // Inject routes only when explicitly enabled via routes.pages: true
28
+ if (userConfig.routes?.pages) {
29
+ // Catch-all page route
30
+ injectRoute({
31
+ pattern: '[...slug]',
32
+ entrypoint: userConfig.routes?.catchAllRoute
33
+ ?? path.resolve(routesDir, '[...slug].astro'),
34
+ });
35
+
36
+ // 404 page
37
+ injectRoute({
38
+ pattern: '404',
39
+ entrypoint: userConfig.routes?.notFoundRoute
40
+ ?? path.resolve(routesDir, '404.astro'),
41
+ });
42
+
43
+ // Middleware for i18n locale resolution
44
+ addMiddleware({
45
+ entrypoint: userConfig.routes?.middleware
46
+ ?? path.resolve(routesDir, 'middleware.ts'),
47
+ order: 'pre',
48
+ });
49
+ }
50
+
51
+ // Inject routes from registered apps (with i18n locale prefixes)
52
+ const nonDefaultLocales = resolvedRegistry.i18n.locales.filter(
53
+ (l) => l !== resolvedRegistry.i18n.defaultLocale,
54
+ );
55
+ for (const app of resolvedRegistry.apps) {
56
+ if (app.routes) {
57
+ for (const route of app.routes) {
58
+ // Default locale route (no prefix)
59
+ injectRoute({ pattern: route.pattern, entrypoint: route.entrypoint });
60
+ // Non-default locale routes (prefixed)
61
+ for (const locale of nonDefaultLocales) {
62
+ injectRoute({ pattern: `${locale}/${route.pattern}`, entrypoint: route.entrypoint });
63
+ }
64
+ }
65
+ }
66
+ }
67
+
68
+ updateConfig({
69
+ vite: {
70
+ plugins: [vitePluginParche(resolvedRegistry)],
71
+ resolve: {
72
+ alias: {
73
+ '@core': coreDir,
74
+ },
75
+ },
76
+ },
77
+ });
78
+ },
79
+
80
+ 'astro:build:done': ({ dir }) => {
81
+ const allowAICrawlers = userConfig.seo?.allowAICrawlers ?? true;
82
+ processRobotsTxt(dir, resolvedSiteUrl, allowAICrawlers);
83
+ },
84
+ },
85
+ };
86
+ }
87
+
88
+ const ROBOTS_MARKER = '# === PARCHE:AUTO-GENERATED';
89
+
90
+ function processRobotsTxt(outDir: URL, siteUrl: string, allowAICrawlers: boolean) {
91
+ const robotsPath = path.join(fileURLToPath(outDir), 'robots.txt');
92
+ // Also check the static client dir for SSR builds
93
+ const clientDir = path.join(fileURLToPath(outDir), 'client');
94
+ const robotsClientPath = fs.existsSync(clientDir)
95
+ ? path.join(clientDir, 'robots.txt')
96
+ : null;
97
+
98
+ const targetPath = fs.existsSync(robotsPath)
99
+ ? robotsPath
100
+ : robotsClientPath && fs.existsSync(robotsClientPath)
101
+ ? robotsClientPath
102
+ : null;
103
+
104
+ const generated = buildAutoGeneratedRobots(siteUrl, allowAICrawlers);
105
+
106
+ if (targetPath) {
107
+ const content = fs.readFileSync(targetPath, 'utf-8');
108
+ const markerIdx = content.indexOf(ROBOTS_MARKER);
109
+ if (markerIdx !== -1) {
110
+ // Replace everything from the marker onwards
111
+ const before = content.slice(0, markerIdx);
112
+ fs.writeFileSync(targetPath, before + ROBOTS_MARKER + '\n' + generated, 'utf-8');
113
+ }
114
+ // If no marker, don't touch the file — user has full control
115
+ } else {
116
+ // No robots.txt exists — generate a complete one
117
+ const outPath = robotsClientPath
118
+ ? path.join(clientDir, 'robots.txt')
119
+ : robotsPath;
120
+ const fullContent = `User-agent: *\nAllow: /\n\n${ROBOTS_MARKER}\n${generated}`;
121
+ fs.writeFileSync(outPath, fullContent, 'utf-8');
122
+ }
123
+ }
124
+
125
+ function buildAutoGeneratedRobots(siteUrl: string, allowAICrawlers: boolean): string {
126
+ const lines: string[] = [];
127
+
128
+ if (!allowAICrawlers) {
129
+ lines.push(
130
+ '',
131
+ '# AI Crawlers',
132
+ 'User-agent: GPTBot',
133
+ 'Disallow: /',
134
+ '',
135
+ 'User-agent: Google-Extended',
136
+ 'Disallow: /',
137
+ '',
138
+ 'User-agent: CCBot',
139
+ 'Disallow: /',
140
+ '',
141
+ 'User-agent: anthropic-ai',
142
+ 'Disallow: /',
143
+ '',
144
+ 'User-agent: ClaudeBot',
145
+ 'Disallow: /',
146
+ );
147
+ }
148
+
149
+ if (siteUrl) {
150
+ lines.push('', `Sitemap: ${siteUrl.replace(/\/$/, '')}/sitemap-index.xml`);
151
+ }
152
+
153
+ return lines.join('\n');
154
+ }
155
+
156
+ export type { ParcheUserConfig, UIRegistry, ParcheApp, ParcheManifest, ParcheRequires };