@meadow-studio/platform 0.0.0-stage → 0.1.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.
Files changed (79) hide show
  1. package/MeadowPlatform.podspec +17 -0
  2. package/Package.swift +23 -0
  3. package/README.md +147 -2
  4. package/android/build.gradle +42 -0
  5. package/android/src/main/AndroidManifest.xml +1 -0
  6. package/android/src/main/java/app/meadow/platform/MeadowAppRatingPlugin.java +38 -0
  7. package/dist/cli/commands.d.ts +74 -0
  8. package/dist/cli/commands.d.ts.map +1 -0
  9. package/dist/cli/commands.js +1062 -0
  10. package/dist/cli/commands.js.map +1 -0
  11. package/dist/cli/config.d.ts +123 -0
  12. package/dist/cli/config.d.ts.map +1 -0
  13. package/dist/cli/config.js +428 -0
  14. package/dist/cli/config.js.map +1 -0
  15. package/dist/cli/meadow.d.ts +8 -0
  16. package/dist/cli/meadow.d.ts.map +1 -0
  17. package/dist/cli/meadow.js +463 -0
  18. package/dist/cli/meadow.js.map +1 -0
  19. package/dist/cli/recipes.d.ts +10 -0
  20. package/dist/cli/recipes.d.ts.map +1 -0
  21. package/dist/cli/recipes.js +575 -0
  22. package/dist/cli/recipes.js.map +1 -0
  23. package/dist/cli/sync.d.ts +14 -0
  24. package/dist/cli/sync.d.ts.map +1 -0
  25. package/dist/cli/sync.js +310 -0
  26. package/dist/cli/sync.js.map +1 -0
  27. package/dist/cli/templates.d.ts +27 -0
  28. package/dist/cli/templates.d.ts.map +1 -0
  29. package/dist/cli/templates.js +1246 -0
  30. package/dist/cli/templates.js.map +1 -0
  31. package/dist/cli/website.d.ts +5 -0
  32. package/dist/cli/website.d.ts.map +1 -0
  33. package/dist/cli/website.js +262 -0
  34. package/dist/cli/website.js.map +1 -0
  35. package/dist/index.d.ts +5 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +5 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/notifications.d.ts +119 -0
  40. package/dist/notifications.d.ts.map +1 -0
  41. package/dist/notifications.js +320 -0
  42. package/dist/notifications.js.map +1 -0
  43. package/dist/platform.d.ts +163 -0
  44. package/dist/platform.d.ts.map +1 -0
  45. package/dist/platform.js +623 -0
  46. package/dist/platform.js.map +1 -0
  47. package/dist/purchases.d.ts +88 -0
  48. package/dist/purchases.d.ts.map +1 -0
  49. package/dist/purchases.js +296 -0
  50. package/dist/purchases.js.map +1 -0
  51. package/dist/ui/app-build.component.d.ts +18 -0
  52. package/dist/ui/app-build.component.d.ts.map +1 -0
  53. package/dist/ui/app-build.component.js +56 -0
  54. package/dist/ui/app-build.component.js.map +1 -0
  55. package/dist/ui/app-build.model.d.ts +11 -0
  56. package/dist/ui/app-build.model.d.ts.map +1 -0
  57. package/dist/ui/app-build.model.js +18 -0
  58. package/dist/ui/app-build.model.js.map +1 -0
  59. package/dist/ui/app-links.component.d.ts +14 -0
  60. package/dist/ui/app-links.component.d.ts.map +1 -0
  61. package/dist/ui/app-links.component.js +77 -0
  62. package/dist/ui/app-links.component.js.map +1 -0
  63. package/dist/ui/app-links.model.d.ts +18 -0
  64. package/dist/ui/app-links.model.d.ts.map +1 -0
  65. package/dist/ui/app-links.model.js +17 -0
  66. package/dist/ui/app-links.model.js.map +1 -0
  67. package/dist/ui/index.d.ts +5 -0
  68. package/dist/ui/index.d.ts.map +1 -0
  69. package/dist/ui/index.js +5 -0
  70. package/dist/ui/index.js.map +1 -0
  71. package/docs/CLI.md +376 -0
  72. package/docs/PLATFORM_GUIDE.md +422 -0
  73. package/docs/POC_FINDINGS.md +218 -0
  74. package/docs/PROJECT_BRIEF.md +501 -0
  75. package/docs/RELEASING.md +49 -0
  76. package/docs/V0.1_PATH.md +310 -0
  77. package/ios/Sources/MeadowPlatformPlugin/MeadowAppRatingPlugin.swift +39 -0
  78. package/package.json +89 -4
  79. package/schema/meadow-config.schema.json +250 -0
@@ -0,0 +1,1246 @@
1
+ import { websiteFiles } from './website.js';
2
+ export const TOOLCHAIN = Object.freeze({
3
+ angular: '22.1.6',
4
+ angularBuild: '22.1.8',
5
+ capacitor: '8.4.3',
6
+ capacitorApp: '8.1.1',
7
+ capacitorFirebase: '8.5.1',
8
+ firebase: '12.19.0',
9
+ googleServicesGradle: '4.5.0',
10
+ firebaseCrashlyticsGradle: '3.0.8',
11
+ revenuecat: '13.5.1',
12
+ supabase: '2.116.0',
13
+ localData: '8.1.1',
14
+ localNotifications: '8.3.1',
15
+ jeepSqlite: '2.8.0',
16
+ sqlJs: '1.11.0',
17
+ node: '24.15.0',
18
+ typescript: '6.0.3',
19
+ playwright: '1.63.0',
20
+ });
21
+ export const GENERATED_HEADER = '// Generated by meadow sync. Do not edit directly.';
22
+ const ACTIONS = Object.freeze({
23
+ checkout: 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1', // v7
24
+ setupNode: 'actions/setup-node@820762786026740c76f36085b0efc47a31fe5020', // v7
25
+ setupJava: 'actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6', // v6
26
+ uploadArtifact: 'actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f', // v6
27
+ });
28
+ export function generatedFiles(config, platformVersion, requestedPlatformSpec) {
29
+ const { app } = config;
30
+ const platformSpec = requestedPlatformSpec ?? platformVersion;
31
+ const packageJson = {
32
+ name: app.slug,
33
+ version: '0.1.0',
34
+ private: true,
35
+ type: 'module',
36
+ engines: { node: '>=24.15 <25', npm: '>=11 <12' },
37
+ scripts: {
38
+ ng: 'ng',
39
+ 'services:config': 'node scripts/generate-meadow-service-config.mjs',
40
+ prestart: 'npm run services:config',
41
+ start: 'ng serve',
42
+ 'prestart:visual': 'npm run services:config',
43
+ 'start:visual': 'ng serve --configuration visual',
44
+ build: 'npm run build:web',
45
+ 'prebuild:web': 'npm run services:config',
46
+ 'build:web': 'ng build --configuration production',
47
+ 'prebuild:device': 'npm run services:config',
48
+ 'build:device': 'ng build --configuration device',
49
+ watch: 'ng build --watch --configuration development',
50
+ lint: 'eslint . --max-warnings=0',
51
+ format: "prettier --write \"src/**/*.{ts,html,scss}\" \"e2e/**/*.ts\" \"*.{json,js,mjs,ts,md}\" \".github/workflows/*.yml\"",
52
+ 'format:check': "prettier --check \"src/**/*.{ts,html,scss}\" \"e2e/**/*.ts\" \"*.{json,js,mjs,ts,md}\" \".github/workflows/*.yml\"",
53
+ test: 'npm run test:unit',
54
+ 'test:unit': 'ng test --no-watch --no-progress',
55
+ 'test:unit:coverage': 'ng test --no-watch --no-progress --coverage',
56
+ 'test:visual': 'playwright test',
57
+ 'test:visual:update': 'playwright test --update-snapshots',
58
+ 'test:visual:report': 'playwright show-report',
59
+ 'website:start': 'npm start --prefix website',
60
+ 'website:build': 'npm run build --prefix website',
61
+ 'website:check': 'npm run build --prefix website',
62
+ 'native:init': config.app.platforms.map((platform) => `cap add ${platform}`).join(' && '),
63
+ 'cap:sync': 'npm run build:device && cap sync',
64
+ ...(config.app.platforms.includes('android')
65
+ ? {
66
+ 'build:android:ci': 'npm run build:device && cap sync android && cd android && ./gradlew assembleDebug --no-daemon --console=plain',
67
+ }
68
+ : {}),
69
+ ...(config.app.platforms.includes('ios')
70
+ ? { 'build:ios:ci': 'sh scripts/build-ios-ci.sh' }
71
+ : {}),
72
+ sync: 'meadow sync',
73
+ 'sync:check': 'meadow sync --check',
74
+ check: 'npm run sync:check && npm run format:check && npm run lint && npm run test:unit && npm run build:web && npm run website:check',
75
+ 'release:check': 'meadow release-check all',
76
+ },
77
+ dependencies: {
78
+ '@angular/common': TOOLCHAIN.angular,
79
+ '@angular/compiler': TOOLCHAIN.angular,
80
+ '@angular/core': TOOLCHAIN.angular,
81
+ '@angular/forms': TOOLCHAIN.angular,
82
+ '@angular/platform-browser': TOOLCHAIN.angular,
83
+ '@angular/router': TOOLCHAIN.angular,
84
+ ...(config.services.firebase.enabled
85
+ ? {
86
+ '@capacitor-firebase/analytics': TOOLCHAIN.capacitorFirebase,
87
+ '@capacitor-firebase/crashlytics': TOOLCHAIN.capacitorFirebase,
88
+ firebase: TOOLCHAIN.firebase,
89
+ }
90
+ : {}),
91
+ ...(config.app.platforms.includes('android')
92
+ ? { '@capacitor/android': TOOLCHAIN.capacitor }
93
+ : {}),
94
+ '@capacitor/app': TOOLCHAIN.capacitorApp,
95
+ '@capacitor/core': TOOLCHAIN.capacitor,
96
+ ...(config.app.platforms.includes('ios') ? { '@capacitor/ios': TOOLCHAIN.capacitor } : {}),
97
+ '@meadow-studio/platform': platformSpec,
98
+ ...(config.services.revenuecat.enabled
99
+ ? { '@revenuecat/purchases-capacitor': TOOLCHAIN.revenuecat }
100
+ : {}),
101
+ ...(config.services.supabase.enabled ? { '@supabase/supabase-js': TOOLCHAIN.supabase } : {}),
102
+ ...(config.services.notifications.enabled
103
+ ? { '@capacitor/local-notifications': TOOLCHAIN.localNotifications }
104
+ : {}),
105
+ rxjs: '7.8.2',
106
+ tslib: '2.8.1',
107
+ 'zone.js': '0.16.0',
108
+ },
109
+ devDependencies: {
110
+ '@angular/build': TOOLCHAIN.angularBuild,
111
+ '@angular/cli': TOOLCHAIN.angularBuild,
112
+ '@angular/compiler-cli': TOOLCHAIN.angular,
113
+ '@capacitor/assets': '3.0.5',
114
+ '@capacitor/cli': TOOLCHAIN.capacitor,
115
+ '@eslint/js': '10.0.1',
116
+ '@playwright/test': TOOLCHAIN.playwright,
117
+ '@vitest/coverage-v8': '4.1.11',
118
+ 'angular-eslint': '22.5.0',
119
+ eslint: '10.10.0',
120
+ jsdom: '30.0.1',
121
+ prettier: '3.9.6',
122
+ typescript: TOOLCHAIN.typescript,
123
+ 'typescript-eslint': '8.70.0',
124
+ vitest: '4.1.11',
125
+ },
126
+ overrides: {
127
+ '@capacitor/assets': {
128
+ '@capacitor/cli': TOOLCHAIN.capacitor,
129
+ sharp: '0.35.4',
130
+ },
131
+ xcode: {
132
+ uuid: '11.1.1',
133
+ },
134
+ },
135
+ };
136
+ return new Map([
137
+ ['package.json', json(packageJson)],
138
+ ['meadow.config.json', json(config)],
139
+ ['.nvmrc', `${TOOLCHAIN.node}\n`],
140
+ ['.npmrc', 'engine-strict=true\nfund=false\naudit=true\n'],
141
+ ['.gitignore', gitignore()],
142
+ ['angular.json', angularJson(app.slug)],
143
+ ...managedProjectFiles(config),
144
+ ['tsconfig.json', tsconfig()],
145
+ ['tsconfig.app.json', tsconfigApp()],
146
+ ['tsconfig.spec.json', tsconfigSpec()],
147
+ ['vitest.config.ts', vitestConfig()],
148
+ ['eslint.config.js', eslintConfig()],
149
+ ['prettier.config.mjs', prettierConfig()],
150
+ ['.prettierignore', prettierIgnore()],
151
+ ['AGENTS.md', appAgentInstructions(config)],
152
+ ['README.md', projectReadme(config)],
153
+ ['public/.gitkeep', ''],
154
+ ['src/index.html', indexHtml(config)],
155
+ ['src/main.ts', mainTs()],
156
+ ['src/styles.scss', globalStyles()],
157
+ ['src/app/app.config.ts', appConfigTs()],
158
+ ['src/app/app.routes.ts', appRoutesTs()],
159
+ ['src/app/app.ts', appTs(config)],
160
+ ['src/app/app.html', appHtml()],
161
+ ['src/app/app.scss', appScss()],
162
+ ['src/app/app.spec.ts', appSpecTs()],
163
+ ['src/app/pages/home/home.page.ts', homeTs()],
164
+ ['src/app/pages/home/home.page.html', homeHtml(config)],
165
+ ['src/app/pages/home/home.page.scss', homeScss()],
166
+ ['src/app/pages/settings/settings.page.ts', settingsTs()],
167
+ ['src/app/pages/settings/settings.page.html', settingsHtml()],
168
+ ['src/app/pages/settings/settings.page.scss', settingsScss()],
169
+ ['e2e/visual/app.visual.spec.ts', visualSpec()],
170
+ ['e2e/visual/visual-fixtures.ts', visualFixtures()],
171
+ ['scripts/build-ios-ci.sh', iosBuildScript()],
172
+ ['scripts/generate-meadow-service-config.mjs', serviceConfigScript()],
173
+ ['docs/RELEASE_CHECKLIST.md', releaseChecklist(config)],
174
+ ['docs/CI.md', ciDocumentation()],
175
+ ['.github/workflows/ci.yml', githubWorkflow(config)],
176
+ ['.github/workflows/visual-baselines.yml', baselineWorkflow()],
177
+ ['.github/workflows/visual-failures.yml', visualFailuresWorkflow()],
178
+ ['.github/scripts/report-visual-failures.mjs', visualFailureReportScript()],
179
+ ['.env.example', envExample(config)],
180
+ ...websiteFiles(config),
181
+ ]);
182
+ }
183
+ export function managedProjectFiles(config) {
184
+ return new Map([
185
+ ['capacitor.config.ts', capacitorConfig(config)],
186
+ ['src/environments/environment.ts', environmentFile('development', false)],
187
+ ['src/environments/environment.production.ts', environmentFile('production', false)],
188
+ ['src/environments/environment.visual.ts', environmentFile('test', true)],
189
+ ['src/app/meadow/meadow-runtime-config.generated.ts', meadowRuntimeConfig(config)],
190
+ ['src/app/meadow/app-analytics.generated.ts', appAnalytics(config)],
191
+ ['scripts/generate-meadow-service-config.mjs', serviceConfigScript()],
192
+ ['playwright.config.ts', playwrightConfig(config)],
193
+ ]);
194
+ }
195
+ function json(value) {
196
+ return `${JSON.stringify(value, null, 2)}\n`;
197
+ }
198
+ function angularJson(slug) {
199
+ return json({
200
+ $schema: './node_modules/@angular/cli/lib/config/schema.json',
201
+ version: 1,
202
+ newProjectRoot: 'projects',
203
+ projects: {
204
+ [slug]: {
205
+ projectType: 'application',
206
+ schematics: { '@schematics/angular:component': { style: 'scss' } },
207
+ root: '',
208
+ sourceRoot: 'src',
209
+ prefix: 'app',
210
+ architect: {
211
+ build: {
212
+ builder: '@angular/build:application',
213
+ options: {
214
+ browser: 'src/main.ts',
215
+ polyfills: ['zone.js'],
216
+ tsConfig: 'tsconfig.app.json',
217
+ inlineStyleLanguage: 'scss',
218
+ assets: [{ glob: '**/*', input: 'public' }],
219
+ styles: ['src/styles.scss'],
220
+ },
221
+ configurations: {
222
+ production: {
223
+ budgets: [
224
+ { type: 'initial', maximumWarning: '500kB', maximumError: '1MB' },
225
+ { type: 'anyComponentStyle', maximumWarning: '8kB', maximumError: '16kB' },
226
+ ],
227
+ outputHashing: 'all',
228
+ fileReplacements: [
229
+ {
230
+ replace: 'src/environments/environment.ts',
231
+ with: 'src/environments/environment.production.ts',
232
+ },
233
+ ],
234
+ },
235
+ development: {
236
+ optimization: false,
237
+ extractLicenses: false,
238
+ sourceMap: true,
239
+ },
240
+ device: {
241
+ optimization: true,
242
+ extractLicenses: true,
243
+ sourceMap: true,
244
+ outputHashing: 'all',
245
+ },
246
+ visual: {
247
+ optimization: false,
248
+ extractLicenses: false,
249
+ sourceMap: true,
250
+ fileReplacements: [
251
+ {
252
+ replace: 'src/environments/environment.ts',
253
+ with: 'src/environments/environment.visual.ts',
254
+ },
255
+ ],
256
+ },
257
+ },
258
+ defaultConfiguration: 'production',
259
+ },
260
+ serve: {
261
+ builder: '@angular/build:dev-server',
262
+ configurations: {
263
+ production: { buildTarget: `${slug}:build:production` },
264
+ development: { buildTarget: `${slug}:build:development` },
265
+ visual: { buildTarget: `${slug}:build:visual` },
266
+ },
267
+ defaultConfiguration: 'development',
268
+ },
269
+ test: {
270
+ builder: '@angular/build:unit-test',
271
+ options: {
272
+ runner: 'vitest',
273
+ runnerConfig: 'vitest.config.ts',
274
+ tsConfig: 'tsconfig.spec.json',
275
+ buildTarget: `${slug}:build:development`,
276
+ },
277
+ },
278
+ },
279
+ },
280
+ },
281
+ });
282
+ }
283
+ function capacitorConfig(config) {
284
+ return `${GENERATED_HEADER}
285
+ import type { CapacitorConfig } from '@capacitor/cli';
286
+
287
+ const config: CapacitorConfig = {
288
+ appId: ${JSON.stringify(config.app.identifiers.android)},
289
+ appName: ${JSON.stringify(config.app.name)},
290
+ webDir: ${JSON.stringify(`dist/${config.app.slug}/browser`)},
291
+ ios: { contentInset: 'automatic' },
292
+ android: { adjustMarginsForEdgeToEdge: 'auto' },
293
+ ${config.services.firebase.enabled ? `experimental: {
294
+ ios: {
295
+ spm: {
296
+ packageOptions: {
297
+ '@capacitor-firebase/crashlytics': { symlink: true },
298
+ },
299
+ },
300
+ },
301
+ },` : ''}
302
+ };
303
+
304
+ export default config;
305
+ `;
306
+ }
307
+ function meadowRuntimeConfig(config) {
308
+ const eventNames = Object.keys(config.analytics.events);
309
+ const propertyNames = [
310
+ ...new Set(Object.values(config.analytics.events).flatMap((event) => Object.keys(event.properties))),
311
+ ].sort();
312
+ const runtimeId = config.app.identifiers.android;
313
+ const firebaseAvailable = config.services.firebase.enabled && config.services.firebase.configured;
314
+ const purchasesAvailable = config.services.revenuecat.enabled &&
315
+ config.services.revenuecat.configured &&
316
+ Boolean(config.services.revenuecat.entitlementId);
317
+ return `${GENERATED_HEADER}
318
+ import type { MeadowPlatformConfig } from '@meadow-studio/platform';
319
+
320
+ ${purchasesAvailable ? `interface MeadowGeneratedServiceConfig {
321
+ readonly revenueCatIosApiKey?: string;
322
+ readonly revenueCatAndroidApiKey?: string;
323
+ }
324
+
325
+ const serviceConfig =
326
+ (globalThis as typeof globalThis & { readonly __MEADOW_SERVICE_CONFIG__?: MeadowGeneratedServiceConfig })
327
+ .__MEADOW_SERVICE_CONFIG__ ?? {};` : ''}
328
+
329
+ export const meadowProject = ${JSON.stringify({
330
+ name: config.app.name,
331
+ slug: config.app.slug,
332
+ identifiers: config.app.identifiers,
333
+ platforms: config.app.platforms,
334
+ devices: config.app.devices,
335
+ orientation: config.app.orientation,
336
+ links: config.links,
337
+ store: config.store,
338
+ services: config.services,
339
+ release: config.release,
340
+ environments: config.environments,
341
+ }, null, 2)} as const;
342
+
343
+ export type MeadowBuildEnvironmentName = keyof typeof meadowProject.environments;
344
+
345
+ export function createMeadowRuntimeConfig(
346
+ environmentName: MeadowBuildEnvironmentName,
347
+ ): MeadowPlatformConfig {
348
+ const environment = meadowProject.environments[environmentName];
349
+ return {
350
+ app: {
351
+ name: meadowProject.name,
352
+ id: ${JSON.stringify(runtimeId)},
353
+ ${config.links.supportEmail ? `supportEmail: ${JSON.stringify(config.links.supportEmail)},` : ''}
354
+ ${config.links.privacyUrl ? `privacyUrl: ${JSON.stringify(config.links.privacyUrl)},` : ''}
355
+ },
356
+ environment: environment.runtime,
357
+ analytics: {
358
+ enabled: ${firebaseAvailable ? 'environment.analytics' : 'false'},
359
+ allowedEvents: ${JSON.stringify(eventNames)},
360
+ allowedProperties: ${JSON.stringify(propertyNames)},
361
+ },
362
+ crashReporting: {
363
+ enabled: ${firebaseAvailable ? 'environment.crashlytics' : 'false'},
364
+ },
365
+ notifications: {
366
+ enabled: ${config.services.notifications.enabled && config.services.notifications.configured},
367
+ },
368
+ ${purchasesAvailable ? `purchases: {
369
+ enabled: true,
370
+ apiKeys: {
371
+ ios: serviceConfig.revenueCatIosApiKey,
372
+ android: serviceConfig.revenueCatAndroidApiKey,
373
+ },
374
+ entitlementId: ${JSON.stringify(config.services.revenuecat.entitlementId)},
375
+ ${config.services.revenuecat.offeringId ? `offeringId: ${JSON.stringify(config.services.revenuecat.offeringId)},` : ''}
376
+ productIds: ${JSON.stringify(config.services.revenuecat.productIds)},
377
+ },` : ''}
378
+ };
379
+ }
380
+ `;
381
+ }
382
+ function appAnalytics(config) {
383
+ const eventEntries = Object.entries(config.analytics.events);
384
+ const eventTypes = eventEntries
385
+ .map(([eventName, event]) => {
386
+ const properties = Object.entries(event.properties)
387
+ .map(([propertyName, property]) => {
388
+ const optional = property.required ? '' : '?';
389
+ return ` readonly ${propertyName}${optional}: ${analyticsPropertyType(property)};`;
390
+ })
391
+ .join('\n');
392
+ return ` readonly ${eventName}: {\n${properties}\n };`;
393
+ })
394
+ .join('\n');
395
+ const eventMap = eventEntries.length ? `{\n${eventTypes}\n}` : 'Record<never, never>';
396
+ return `${GENERATED_HEADER}
397
+ import { Injectable, inject } from '@angular/core';
398
+ import { MeadowAnalyticsService, type MeadowAnalyticsParams } from '@meadow-studio/platform';
399
+
400
+ export type AppAnalyticsEvents = ${eventMap};
401
+
402
+ export type AppAnalyticsEventName = keyof AppAnalyticsEvents;
403
+ type RequiredKeys<Value> = {
404
+ [Key in keyof Value]-?: object extends Pick<Value, Key> ? never : Key;
405
+ }[keyof Value];
406
+ type EventArguments<Value> = [RequiredKeys<Value>] extends [never]
407
+ ? [properties?: Value]
408
+ : [properties: Value];
409
+
410
+ @Injectable({ providedIn: 'root' })
411
+ export class AppAnalytics {
412
+ private readonly analytics = inject(MeadowAnalyticsService);
413
+
414
+ track<Event extends AppAnalyticsEventName>(
415
+ event: Event,
416
+ ...[properties]: EventArguments<AppAnalyticsEvents[Event]>
417
+ ): void {
418
+ this.analytics.track(event, properties as MeadowAnalyticsParams | undefined);
419
+ }
420
+ }
421
+ `;
422
+ }
423
+ function analyticsPropertyType(property) {
424
+ if (property.values?.length)
425
+ return property.values.map((value) => JSON.stringify(value)).join(' | ');
426
+ if (property.type === 'integer')
427
+ return 'number';
428
+ return property.type;
429
+ }
430
+ function tsconfig() {
431
+ return json({
432
+ compileOnSave: false,
433
+ compilerOptions: {
434
+ outDir: './dist/out-tsc',
435
+ forceConsistentCasingInFileNames: true,
436
+ strict: true,
437
+ noImplicitOverride: true,
438
+ noPropertyAccessFromIndexSignature: true,
439
+ noImplicitReturns: true,
440
+ noFallthroughCasesInSwitch: true,
441
+ sourceMap: true,
442
+ declaration: false,
443
+ experimentalDecorators: true,
444
+ moduleResolution: 'bundler',
445
+ importHelpers: true,
446
+ target: 'ES2022',
447
+ module: 'preserve',
448
+ lib: ['ES2022', 'dom'],
449
+ },
450
+ angularCompilerOptions: {
451
+ enableI18nLegacyMessageIdFormat: false,
452
+ strictInjectionParameters: true,
453
+ strictInputAccessModifiers: true,
454
+ strictTemplates: true,
455
+ },
456
+ });
457
+ }
458
+ function tsconfigApp() {
459
+ return json({
460
+ extends: './tsconfig.json',
461
+ compilerOptions: { outDir: './out-tsc/app', types: [] },
462
+ files: ['src/main.ts'],
463
+ include: ['src/**/*.d.ts'],
464
+ });
465
+ }
466
+ function tsconfigSpec() {
467
+ return json({
468
+ extends: './tsconfig.json',
469
+ compilerOptions: { outDir: './out-tsc/spec', types: ['vitest/globals'] },
470
+ include: ['src/**/*.spec.ts', 'src/**/*.d.ts'],
471
+ });
472
+ }
473
+ function vitestConfig() {
474
+ return `import { defineConfig } from 'vitest/config';
475
+
476
+ export default defineConfig({
477
+ test: {
478
+ coverage: {
479
+ thresholds: {
480
+ lines: 80,
481
+ functions: 80,
482
+ statements: 80,
483
+ branches: 70,
484
+ },
485
+ },
486
+ },
487
+ });
488
+ `;
489
+ }
490
+ function eslintConfig() {
491
+ return `import eslint from '@eslint/js';
492
+ import angular from 'angular-eslint';
493
+ import tseslint from 'typescript-eslint';
494
+
495
+ export default tseslint.config(
496
+ { ignores: ['dist/**', 'coverage/**', 'playwright-report/**', 'test-results/**', 'android/**', 'ios/**'] },
497
+ {
498
+ files: ['**/*.ts'],
499
+ extends: [eslint.configs.recommended, ...tseslint.configs.recommended, ...angular.configs.tsRecommended],
500
+ processor: angular.processInlineTemplates,
501
+ rules: {
502
+ '@angular-eslint/directive-selector': ['error', { type: 'attribute', prefix: 'app', style: 'camelCase' }],
503
+ '@angular-eslint/component-selector': ['error', { type: 'element', prefix: 'app', style: 'kebab-case' }],
504
+ },
505
+ },
506
+ {
507
+ files: ['**/*.html'],
508
+ extends: [...angular.configs.templateRecommended, ...angular.configs.templateAccessibility],
509
+ },
510
+ );
511
+ `;
512
+ }
513
+ function prettierConfig() {
514
+ return `export default {
515
+ printWidth: 100,
516
+ singleQuote: true,
517
+ overrides: [{ files: '*.html', options: { parser: 'angular' } }],
518
+ };
519
+ `;
520
+ }
521
+ function prettierIgnore() {
522
+ return `meadow.config.json
523
+ capacitor.config.ts
524
+ playwright.config.ts
525
+ src/environments/environment*.ts
526
+ src/app/meadow/*.generated.ts
527
+ `;
528
+ }
529
+ function mainTs() {
530
+ return `import { bootstrapApplication } from '@angular/platform-browser';
531
+ import { App } from './app/app';
532
+ import { appConfig } from './app/app.config';
533
+
534
+ bootstrapApplication(App, appConfig).catch((error: unknown) => console.error(error));
535
+ `;
536
+ }
537
+ function appConfigTs() {
538
+ return `import { ApplicationConfig } from '@angular/core';
539
+ import { provideRouter, withComponentInputBinding } from '@angular/router';
540
+ import { provideMeadowPlatform } from '@meadow-studio/platform';
541
+ import { environment } from '../environments/environment';
542
+ import { createMeadowRuntimeConfig } from './meadow/meadow-runtime-config.generated';
543
+ import { routes } from './app.routes';
544
+
545
+ export const appConfig: ApplicationConfig = {
546
+ providers: [
547
+ provideRouter(routes, withComponentInputBinding()),
548
+ provideMeadowPlatform(createMeadowRuntimeConfig(environment.meadowEnvironmentName)),
549
+ ],
550
+ };
551
+ `;
552
+ }
553
+ function appRoutesTs() {
554
+ return `import { Routes } from '@angular/router';
555
+
556
+ export const routes: Routes = [
557
+ {
558
+ path: '',
559
+ loadComponent: () => import('./pages/home/home.page').then((module) => module.HomePage),
560
+ },
561
+ {
562
+ path: 'settings',
563
+ loadComponent: () =>
564
+ import('./pages/settings/settings.page').then((module) => module.SettingsPage),
565
+ },
566
+ { path: '**', redirectTo: '' },
567
+ ];
568
+ `;
569
+ }
570
+ function appTs(config) {
571
+ return `import { ChangeDetectionStrategy, Component } from '@angular/core';
572
+ import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
573
+
574
+ @Component({
575
+ selector: 'app-root',
576
+ imports: [RouterLink, RouterLinkActive, RouterOutlet],
577
+ templateUrl: './app.html',
578
+ styleUrl: './app.scss',
579
+ changeDetection: ChangeDetectionStrategy.OnPush,
580
+ })
581
+ export class App {
582
+ protected readonly appName = ${JSON.stringify(config.app.name)};
583
+ }
584
+ `;
585
+ }
586
+ function appHtml() {
587
+ return `<div class="app-shell">
588
+ <header class="app-header">
589
+ <a class="brand" routerLink="/">{{ appName }}</a>
590
+ <nav aria-label="Primary navigation">
591
+ <a routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">Home</a>
592
+ <a routerLink="/settings" routerLinkActive="active">Settings</a>
593
+ </nav>
594
+ </header>
595
+ <main><router-outlet /></main>
596
+ </div>
597
+ `;
598
+ }
599
+ function appScss() {
600
+ return `.app-shell { min-height: 100dvh; }
601
+ .app-header { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 1rem 1.25rem; border-bottom: 1px solid var(--border); }
602
+ .brand { color: inherit; font-weight: 750; text-decoration: none; }
603
+ nav { display: flex; gap: 0.35rem; }
604
+ nav a { border-radius: 999px; color: var(--muted); padding: 0.55rem 0.8rem; text-decoration: none; }
605
+ nav a.active { background: var(--surface-strong); color: var(--text); }
606
+ main { margin: 0 auto; max-width: 48rem; padding: 2rem 1.25rem 5rem; }
607
+ `;
608
+ }
609
+ function appSpecTs() {
610
+ return `import { TestBed } from '@angular/core/testing';
611
+ import { provideRouter } from '@angular/router';
612
+ import { describe, expect, it } from 'vitest';
613
+ import { App } from './app';
614
+
615
+ describe('App', () => {
616
+ it('creates the application shell', async () => {
617
+ await TestBed.configureTestingModule({ imports: [App], providers: [provideRouter([])] }).compileComponents();
618
+ const fixture = TestBed.createComponent(App);
619
+ fixture.detectChanges();
620
+ expect(fixture.nativeElement.querySelector('main')).not.toBeNull();
621
+ });
622
+ });
623
+ `;
624
+ }
625
+ function homeTs() {
626
+ return `import { ChangeDetectionStrategy, Component } from '@angular/core';
627
+
628
+ @Component({
629
+ selector: 'app-home-page',
630
+ templateUrl: './home.page.html',
631
+ styleUrl: './home.page.scss',
632
+ changeDetection: ChangeDetectionStrategy.OnPush,
633
+ })
634
+ export class HomePage {}
635
+ `;
636
+ }
637
+ function homeHtml(config) {
638
+ return `<section class="hero">
639
+ <p class="eyebrow">Ready to build</p>
640
+ <h1>${escapeHtml(config.app.name)}</h1>
641
+ <p>${escapeHtml(config.app.description || 'Your release-ready Meadow app starts here.')}</p>
642
+ </section>
643
+
644
+ <section aria-labelledby="next-heading">
645
+ <h2 id="next-heading">Next step</h2>
646
+ <p>Replace this screen with the smallest useful version of your idea.</p>
647
+ </section>
648
+ `;
649
+ }
650
+ function homeScss() {
651
+ return `.hero { padding: 3.5rem 0 2.5rem; }
652
+ .eyebrow { color: var(--accent); font-size: 0.78rem; font-weight: 750; letter-spacing: 0.12em; text-transform: uppercase; }
653
+ h1 { font-size: clamp(2.4rem, 12vw, 4.8rem); letter-spacing: -0.055em; line-height: 0.95; margin: 0.4rem 0 1rem; }
654
+ .hero > p:last-child { color: var(--muted); font-size: 1.1rem; max-width: 34rem; }
655
+ section + section { border-top: 1px solid var(--border); padding-top: 2rem; }
656
+ `;
657
+ }
658
+ function settingsTs() {
659
+ return `import { AsyncPipe } from '@angular/common';
660
+ import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
661
+ import { MEADOW_PLATFORM_CONFIG, MeadowAppInfoService } from '@meadow-studio/platform';
662
+
663
+ @Component({
664
+ selector: 'app-settings-page',
665
+ imports: [AsyncPipe],
666
+ templateUrl: './settings.page.html',
667
+ styleUrl: './settings.page.scss',
668
+ changeDetection: ChangeDetectionStrategy.OnPush,
669
+ })
670
+ export class SettingsPage {
671
+ protected readonly config = inject(MEADOW_PLATFORM_CONFIG);
672
+ protected readonly info = inject(MeadowAppInfoService).get();
673
+ }
674
+ `;
675
+ }
676
+ function settingsHtml() {
677
+ return `<section>
678
+ <p class="eyebrow">Application</p>
679
+ <h1>Settings</h1>
680
+ <dl>
681
+ <div><dt>Name</dt><dd>{{ config.app.name }}</dd></div>
682
+ @if (info | async; as appInfo) {
683
+ <div><dt>Version</dt><dd>{{ appInfo.version ?? 'Development' }}</dd></div>
684
+ <div><dt>Build</dt><dd>{{ appInfo.buildNumber ?? 'Local' }}</dd></div>
685
+ }
686
+ </dl>
687
+ </section>
688
+ `;
689
+ }
690
+ function settingsScss() {
691
+ return `.eyebrow { color: var(--accent); font-size: 0.78rem; font-weight: 750; letter-spacing: 0.12em; text-transform: uppercase; }
692
+ h1 { font-size: clamp(2.2rem, 10vw, 3.8rem); letter-spacing: -0.045em; margin: 0.35rem 0 2rem; }
693
+ dl { background: var(--surface); border: 1px solid var(--border); border-radius: 1rem; margin: 0; overflow: hidden; }
694
+ dl div { display: flex; justify-content: space-between; gap: 2rem; padding: 1rem; }
695
+ dl div + div { border-top: 1px solid var(--border); }
696
+ dt { color: var(--muted); }
697
+ dd { margin: 0; text-align: right; }
698
+ `;
699
+ }
700
+ function environmentFile(environmentName, visual) {
701
+ return `${GENERATED_HEADER}
702
+ export const environment = {
703
+ meadowEnvironmentName: ${JSON.stringify(environmentName)} as const,
704
+ visual: ${visual},
705
+ };
706
+ `;
707
+ }
708
+ function indexHtml(config) {
709
+ return `<!doctype html>
710
+ <html lang="en">
711
+ <head>
712
+ <meta charset="utf-8" />
713
+ <title>${escapeHtml(config.app.name)}</title>
714
+ <base href="/" />
715
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
716
+ <meta name="theme-color" content="#171717" />
717
+ <script src="meadow-service-config.js"></script>
718
+ </head>
719
+ <body>
720
+ <app-root></app-root>
721
+ </body>
722
+ </html>
723
+ `;
724
+ }
725
+ function globalStyles() {
726
+ return `:root {
727
+ color-scheme: light;
728
+ --background: #f6f5f2;
729
+ --surface: #ffffff;
730
+ --surface-strong: #e9e7e1;
731
+ --text: #171717;
732
+ --muted: #66645f;
733
+ --border: #d8d5cd;
734
+ --accent: #b43b24;
735
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
736
+ }
737
+ * { box-sizing: border-box; }
738
+ html { background: var(--background); color: var(--text); }
739
+ body { margin: 0; min-width: 20rem; }
740
+ button, input, textarea, select { font: inherit; }
741
+ @media (prefers-reduced-motion: reduce) {
742
+ *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; scroll-behavior: auto !important; transition-duration: 0.01ms !important; }
743
+ }
744
+ `;
745
+ }
746
+ function visualSpec() {
747
+ return `import { expect, test } from '@playwright/test';
748
+ import { openVisualApp, settleVisuals } from './visual-fixtures';
749
+
750
+ test('home', async ({ page }) => {
751
+ await openVisualApp(page, '/');
752
+ await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
753
+ await settleVisuals(page);
754
+ await expect(page).toHaveScreenshot('home.png');
755
+ });
756
+
757
+ test('settings', async ({ page }) => {
758
+ await openVisualApp(page, '/settings');
759
+ await expect(page.getByRole('heading', { name: 'Settings' })).toBeVisible();
760
+ await settleVisuals(page);
761
+ await expect(page).toHaveScreenshot('settings.png');
762
+ });
763
+ `;
764
+ }
765
+ function visualFixtures() {
766
+ return `import { expect, Page } from '@playwright/test';
767
+
768
+ const FIXED_NOW = new Date('2026-01-15T12:00:00+01:00');
769
+
770
+ export async function openVisualApp(page: Page, path: string): Promise<void> {
771
+ await page.clock.install({ time: FIXED_NOW });
772
+ await page.route('**/*', async (route) => {
773
+ const url = new URL(route.request().url());
774
+ if (url.hostname === '127.0.0.1') await route.continue();
775
+ else await route.abort();
776
+ });
777
+ await page.addInitScript(() => localStorage.clear());
778
+ await page.goto(path);
779
+ await expect(page.locator('app-root')).toBeVisible();
780
+ }
781
+
782
+ export async function settleVisuals(page: Page): Promise<void> {
783
+ await page.evaluate(async () => {
784
+ await document.fonts.ready;
785
+ await Promise.all(
786
+ Array.from(document.images).map(async (image) => {
787
+ if (image.complete && image.naturalWidth > 0) await image.decode().catch(() => undefined);
788
+ }),
789
+ );
790
+ await new Promise<void>((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
791
+ });
792
+ }
793
+ `;
794
+ }
795
+ function playwrightConfig(config) {
796
+ const projects = config.ci.visualViewports
797
+ .map((viewport) => viewport === 'phone'
798
+ ? ` { name: 'phone-chromium', use: { ...devices['Desktop Chrome'], viewport: { width: 393, height: 852 }, deviceScaleFactor: 1, hasTouch: true } }`
799
+ : ` { name: 'tablet-chromium', use: { ...devices['Desktop Chrome'], viewport: { width: 820, height: 1180 }, deviceScaleFactor: 1, hasTouch: true } }`)
800
+ .join(',\n');
801
+ return `${GENERATED_HEADER}
802
+ import { defineConfig, devices } from '@playwright/test';
803
+
804
+ export default defineConfig({
805
+ testDir: './e2e/visual',
806
+ outputDir: 'test-results/visual',
807
+ snapshotPathTemplate: '{testDir}/__screenshots__/{projectName}/{arg}{ext}',
808
+ fullyParallel: false,
809
+ forbidOnly: !!process.env['CI'],
810
+ retries: 0,
811
+ workers: process.env['CI'] ? 1 : undefined,
812
+ reporter: process.env['CI']
813
+ ? [['github'], ['html', { outputFolder: 'playwright-report', open: 'never' }]]
814
+ : [['line'], ['html', { outputFolder: 'playwright-report', open: 'never' }]],
815
+ expect: {
816
+ timeout: 10_000,
817
+ toHaveScreenshot: { animations: 'disabled', caret: 'hide', maxDiffPixelRatio: 0.001, scale: 'css' },
818
+ },
819
+ use: {
820
+ baseURL: 'http://127.0.0.1:4200',
821
+ colorScheme: 'light',
822
+ locale: 'en-US',
823
+ timezoneId: 'Europe/Copenhagen',
824
+ reducedMotion: 'reduce',
825
+ trace: 'retain-on-failure',
826
+ },
827
+ projects: [
828
+ ${projects},
829
+ ],
830
+ webServer: {
831
+ command: 'npm run start:visual -- --host 127.0.0.1 --port 4200',
832
+ url: 'http://127.0.0.1:4200',
833
+ reuseExistingServer: false,
834
+ timeout: 180_000,
835
+ },
836
+ });
837
+ `;
838
+ }
839
+ function githubWorkflow(config) {
840
+ return `name: CI
841
+
842
+ on:
843
+ push:
844
+ branches: [main]
845
+ pull_request:
846
+ branches: [main]
847
+ workflow_dispatch:
848
+
849
+ permissions:
850
+ contents: read
851
+
852
+ concurrency:
853
+ group: ci-\${{ github.workflow }}-\${{ github.event.pull_request.number || github.ref }}
854
+ cancel-in-progress: true
855
+
856
+ env:
857
+ CI: true
858
+
859
+ jobs:
860
+ verify:
861
+ name: Quality, unit tests, and web build
862
+ runs-on: ubuntu-24.04
863
+ timeout-minutes: 15
864
+ steps:
865
+ - uses: ${ACTIONS.checkout} # v7
866
+ - uses: ${ACTIONS.setupNode} # v7
867
+ with:
868
+ node-version: 24.15.0
869
+ cache: npm
870
+ - run: npm ci
871
+ - run: npm audit --audit-level=high
872
+ - run: npm run sync:check
873
+ - run: npm run format:check
874
+ - run: npm run lint
875
+ - run: npm run test:unit:coverage
876
+ - run: npm run build:web
877
+ - run: npm run website:check
878
+ - uses: ${ACTIONS.uploadArtifact} # v6
879
+ if: failure()
880
+ with:
881
+ name: coverage
882
+ path: coverage/
883
+ retention-days: 14
884
+
885
+ visual:
886
+ name: Visual regression
887
+ runs-on: macos-26
888
+ timeout-minutes: 15
889
+ steps:
890
+ - uses: ${ACTIONS.checkout} # v7
891
+ - uses: ${ACTIONS.setupNode} # v7
892
+ with:
893
+ node-version: 24.15.0
894
+ cache: npm
895
+ - run: npm ci
896
+ - run: npx playwright install chromium
897
+ - run: npm run test:visual
898
+ - uses: ${ACTIONS.uploadArtifact} # v6
899
+ if: always()
900
+ with:
901
+ name: playwright-report
902
+ path: |
903
+ playwright-report/
904
+ test-results/
905
+ if-no-files-found: ignore
906
+ retention-days: 14
907
+
908
+ ${config.app.platforms.includes('android') ? ` android:
909
+ name: Android build
910
+ runs-on: ubuntu-24.04
911
+ timeout-minutes: 25
912
+ steps:
913
+ - uses: ${ACTIONS.checkout} # v7
914
+ - uses: ${ACTIONS.setupNode} # v7
915
+ with:
916
+ node-version: 24.15.0
917
+ cache: npm
918
+ - uses: ${ACTIONS.setupJava} # v6
919
+ with:
920
+ distribution: temurin
921
+ java-version: 21
922
+ cache: gradle
923
+ - run: npm ci
924
+ - run: npm run build:android:ci
925
+ - uses: ${ACTIONS.uploadArtifact} # v6
926
+ with:
927
+ name: android-debug-apk
928
+ path: android/app/build/outputs/apk/debug/app-debug.apk
929
+ if-no-files-found: error
930
+ retention-days: 14
931
+ ` : ''}
932
+
933
+ ${config.app.platforms.includes('ios') ? ` ios:
934
+ name: iOS build
935
+ runs-on: macos-26
936
+ timeout-minutes: 30
937
+ steps:
938
+ - uses: ${ACTIONS.checkout} # v7
939
+ - uses: ${ACTIONS.setupNode} # v7
940
+ with:
941
+ node-version: 24.15.0
942
+ cache: npm
943
+ - run: npm ci
944
+ - run: npm run build:ios:ci
945
+ ` : ''}
946
+ `;
947
+ }
948
+ function baselineWorkflow() {
949
+ return `name: Generate visual baselines
950
+
951
+ on:
952
+ workflow_dispatch:
953
+
954
+ permissions:
955
+ contents: read
956
+
957
+ jobs:
958
+ baselines:
959
+ runs-on: macos-26
960
+ timeout-minutes: 15
961
+ steps:
962
+ - uses: ${ACTIONS.checkout} # v7
963
+ - uses: ${ACTIONS.setupNode} # v7
964
+ with:
965
+ node-version: 24.15.0
966
+ cache: npm
967
+ - run: npm ci
968
+ - run: npx playwright install chromium
969
+ - run: npm run test:visual:update
970
+ - uses: ${ACTIONS.uploadArtifact} # v6
971
+ with:
972
+ name: visual-baselines
973
+ path: e2e/visual/__screenshots__/
974
+ retention-days: 7
975
+ `;
976
+ }
977
+ function visualFailuresWorkflow() {
978
+ return `name: Report visual failures
979
+
980
+ on:
981
+ workflow_run:
982
+ workflows: [CI]
983
+ types: [completed]
984
+
985
+ permissions:
986
+ actions: read
987
+ contents: read
988
+ pull-requests: write
989
+
990
+ jobs:
991
+ comment:
992
+ if: \${{ github.event.workflow_run.conclusion == 'failure' && github.event.workflow_run.event == 'pull_request' }}
993
+ runs-on: ubuntu-24.04
994
+ timeout-minutes: 10
995
+ steps:
996
+ - uses: ${ACTIONS.checkout} # v7; trusted default branch
997
+ - name: Report failed screenshot comparisons
998
+ env:
999
+ GH_TOKEN: \${{ github.token }}
1000
+ SOURCE_RUN_ID: \${{ github.event.workflow_run.id }}
1001
+ FALLBACK_PR: \${{ github.event.workflow_run.pull_requests[0].number }}
1002
+ HEAD_BRANCH: \${{ github.event.workflow_run.head_branch }}
1003
+ run: |
1004
+ if gh run download "$SOURCE_RUN_ID" --repo "$GITHUB_REPOSITORY" --name playwright-report --dir visual-results; then
1005
+ node .github/scripts/report-visual-failures.mjs visual-results/test-results/visual
1006
+ if [ -f visual-failures.md ]; then
1007
+ pr_number="$FALLBACK_PR"
1008
+ if [ -z "$pr_number" ]; then
1009
+ pr_number="$(gh pr list --repo "$GITHUB_REPOSITORY" --head "$HEAD_BRANCH" --state open --json number --jq '.[0].number // empty')"
1010
+ fi
1011
+ if [ -n "$pr_number" ]; then
1012
+ gh pr comment "$pr_number" --repo "$GITHUB_REPOSITORY" --body-file visual-failures.md
1013
+ fi
1014
+ fi
1015
+ fi
1016
+ `;
1017
+ }
1018
+ function visualFailureReportScript() {
1019
+ return `import { existsSync, readdirSync, writeFileSync } from 'node:fs';
1020
+ import { join, relative, resolve } from 'node:path';
1021
+
1022
+ const root = resolve(process.argv[2] ?? 'visual-results/test-results/visual');
1023
+ if (!existsSync(root)) process.exit(0);
1024
+
1025
+ function files(directory) {
1026
+ return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
1027
+ const path = join(directory, entry.name);
1028
+ if (entry.isSymbolicLink()) return [];
1029
+ return entry.isDirectory() ? files(path) : entry.isFile() ? [path] : [];
1030
+ });
1031
+ }
1032
+
1033
+ const failures = files(root).filter((path) => path.endsWith('-actual.png')).sort();
1034
+ if (failures.length) {
1035
+ const runUrl = (process.env.GITHUB_SERVER_URL ?? 'https://github.com') + '/' +
1036
+ process.env.GITHUB_REPOSITORY + '/actions/runs/' + process.env.SOURCE_RUN_ID;
1037
+ const names = failures.map((path) => '- ' + relative(root, path));
1038
+ writeFileSync('visual-failures.md', [
1039
+ '## Screenshot checks failed',
1040
+ '',
1041
+ failures.length + ' screenshot comparison' + (failures.length === 1 ? '' : 's') + ' failed:',
1042
+ '',
1043
+ ...names,
1044
+ '',
1045
+ '[Download expected, actual, and diff images with the Playwright report](' + runUrl + ').',
1046
+ '',
1047
+ 'Open the **playwright-report** artifact on that run for the images and test details.',
1048
+ ].join('\\n') + '\\n');
1049
+ }
1050
+ `;
1051
+ }
1052
+ function iosBuildScript() {
1053
+ return `#!/bin/sh
1054
+ set -eu
1055
+ npm run build:device
1056
+ npx cap sync ios
1057
+ xcodebuild -project ios/App/App.xcodeproj -scheme App -sdk iphonesimulator -configuration Debug CODE_SIGNING_ALLOWED=NO build
1058
+ `;
1059
+ }
1060
+ function gitignore() {
1061
+ return `/.angular/cache
1062
+ /.cache
1063
+ /.build
1064
+ /coverage
1065
+ /dist
1066
+ /website/dist
1067
+ /node_modules
1068
+ /playwright-report
1069
+ /test-results
1070
+ /www
1071
+ /android/.gradle
1072
+ /android/build
1073
+ /android/app/build
1074
+ /ios/App/DerivedData
1075
+ public/meadow-service-config.js
1076
+ /.env.meadow.local
1077
+ *.jks
1078
+ *.keystore
1079
+ *.p12
1080
+ *.mobileprovision
1081
+ *.xcarchive
1082
+ .DS_Store
1083
+ npm-debug.log*
1084
+ `;
1085
+ }
1086
+ function serviceConfigScript() {
1087
+ return `import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
1088
+ import { dirname, resolve } from 'node:path';
1089
+
1090
+ const root = process.cwd();
1091
+ const localPath = resolve(root, '.env.meadow.local');
1092
+ const local = existsSync(localPath) ? parseEnv(readFileSync(localPath, 'utf8')) : {};
1093
+ const value = (name) => process.env[name] ?? local[name] ?? '';
1094
+ const outputPath = resolve(root, 'public/meadow-service-config.js');
1095
+ const config = {
1096
+ revenueCatIosApiKey: value('MEADOW_REVENUECAT_IOS_API_KEY'),
1097
+ revenueCatAndroidApiKey: value('MEADOW_REVENUECAT_ANDROID_API_KEY'),
1098
+ };
1099
+ mkdirSync(dirname(outputPath), { recursive: true });
1100
+ writeFileSync(
1101
+ outputPath,
1102
+ \`globalThis.__MEADOW_SERVICE_CONFIG__ = Object.freeze(\${JSON.stringify(config)});\\n\`,
1103
+ );
1104
+
1105
+ function parseEnv(source) {
1106
+ return Object.fromEntries(
1107
+ source
1108
+ .split(/\\r?\\n/)
1109
+ .map((line) => line.trim())
1110
+ .filter((line) => line && !line.startsWith('#') && line.includes('='))
1111
+ .map((line) => {
1112
+ const separator = line.indexOf('=');
1113
+ const key = line.slice(0, separator).trim();
1114
+ const raw = line.slice(separator + 1).trim();
1115
+ const quoted =
1116
+ (raw.startsWith('"') && raw.endsWith('"')) ||
1117
+ (raw.startsWith("'") && raw.endsWith("'"));
1118
+ return [key, quoted ? raw.slice(1, -1) : raw];
1119
+ }),
1120
+ );
1121
+ }
1122
+ `;
1123
+ }
1124
+ function envExample(config) {
1125
+ return `# Copy to .env.meadow.local. Never commit the local file.
1126
+ MEADOW_REVENUECAT_IOS_API_KEY=
1127
+ MEADOW_REVENUECAT_ANDROID_API_KEY=
1128
+ MEADOW_SUPABASE_URL=
1129
+ MEADOW_SUPABASE_PUBLISHABLE_KEY=
1130
+ ${config.services.supabase.enabled ? 'MEADOW_SUPABASE_AUTH=email-otp\n' : ''}`;
1131
+ }
1132
+ function releaseChecklist(config) {
1133
+ return `# Release checklist
1134
+
1135
+ - [ ] Replace placeholder icons and splash assets.
1136
+ - [ ] Confirm the iOS application ID is \`${config.app.identifiers.ios}\` and Android application ID is \`${config.app.identifiers.android}\`.
1137
+ - [ ] Add working privacy, terms, and support links.
1138
+ - [ ] Complete the website privacy draft with the app's actual data, events, retention, hosting, deletion process, publisher address, and effective date; review support and terms; remove draft notices and noindex tags before publishing.
1139
+ - [ ] Configure the website's domain, DNS, TLS, and hosting, then verify its live links.
1140
+ - [ ] Set \`links.websiteUrl\` to the live companion website before a production release.
1141
+ - [ ] Review Firebase Analytics and Crashlytics consent copy.
1142
+ - [ ] Confirm both Firebase consent choices start unset and withdrawing either choice disables its collection.
1143
+ ${config.services.firebase.enabled ? '- [ ] Verify a real allowlisted event in Firebase DebugView and a Crashlytics test report on a device.\n' : ''}${config.services.firebase.enabled && config.app.platforms.includes('ios') ? '- [ ] Configure and verify the iOS Crashlytics dSYM upload build phase for readable production reports.\n' : ''}
1144
+ - [ ] Verify App Store and Play Store privacy disclosures.
1145
+ - [ ] Review screenshots and store metadata.
1146
+ - [ ] Configure signing outside the repository.
1147
+ ${config.services.revenuecat.enabled ? '- [ ] Verify RevenueCat production keys, offering, products, prices, legal copy, purchase, cancellation/pending behavior, restore, and entitlement state in both store sandboxes.\n' : ''}${config.services.supabase.enabled ? '- [ ] Run Supabase database tests and advisors; verify RLS, grants, storage policies, SMTP, and account deletion.\n' : ''}${config.services.notifications.enabled ? '- [ ] Verify the app-owned notification permission explanation, denial behavior, schedule, replacement, cancellation, Android channel (if enabled), and tap navigation on each enabled platform.\n' : ''}- [ ] Run \`npm run release:check\`.
1148
+ `;
1149
+ }
1150
+ function ciDocumentation() {
1151
+ return `# Continuous integration
1152
+
1153
+ Every pull request runs formatting, linting, Vitest unit tests, a production web build,
1154
+ the companion website build, Playwright screenshot comparisons, Android compilation,
1155
+ and an unsigned iOS simulator build. Successful Android builds upload a debug APK as a
1156
+ downloadable Actions artifact. Visual failures retain the Playwright report, expected/actual/diff
1157
+ images, and a trusted follow-up workflow comments on the PR with failed screenshot names and a
1158
+ link to the run.
1159
+
1160
+ \`@meadow-studio/platform\` is installed at its exact public npm version. App CI needs no Meadow package
1161
+ secret or access to the platform source repository. Commit the generated lockfile so CI installs
1162
+ the same release as local development.
1163
+
1164
+ Reference screenshots are reviewed source files. CI never updates them automatically. Generate
1165
+ candidate macOS baselines with the manual **Generate visual baselines** workflow, download the
1166
+ artifact, inspect it, and commit intentional changes.
1167
+
1168
+ Production vendor credentials are not used by pull-request jobs. Tests must use deterministic
1169
+ adapters and local fixtures.
1170
+ `;
1171
+ }
1172
+ function projectReadme(config) {
1173
+ return `# ${config.app.name}
1174
+
1175
+ ${config.app.description || 'A Meadow application.'}
1176
+
1177
+ ## Start
1178
+
1179
+ \`\`\`sh
1180
+ npm install
1181
+ npm start
1182
+ \`\`\`
1183
+
1184
+ ## Verify
1185
+
1186
+ \`\`\`sh
1187
+ npm run check
1188
+ npm run test:visual
1189
+ npm run build:android:ci
1190
+ npm run build:ios:ci
1191
+ \`\`\`
1192
+
1193
+ Run \`meadow doctor\` for configuration guidance and \`meadow release-check all\` before release.
1194
+
1195
+ The companion website lives in \`website/\` as a second project in this repository.
1196
+ Use \`npm run website:start\` to preview it and \`npm run website:check\` to build it.
1197
+ The site starts with home, support, privacy, and terms pages; replace draft content before publication.
1198
+ `;
1199
+ }
1200
+ export const MEADOW_AGENT_SECTION_START = '<!-- meadow-platform:start -->';
1201
+ export const MEADOW_AGENT_SECTION_END = '<!-- meadow-platform:end -->';
1202
+ export function meadowAgentSection(config) {
1203
+ return `${MEADOW_AGENT_SECTION_START}
1204
+ ## Meadow Platform
1205
+
1206
+ This application uses Meadow Platform to keep its shared app infrastructure and release workflow
1207
+ consistent with the other Meadow applications.
1208
+
1209
+ Before changing infrastructure, integrations, CI, or release setup:
1210
+
1211
+ 1. Check the pinned \`@meadow-studio/platform\` version in \`package.json\`.
1212
+ 2. Read \`node_modules/@meadow-studio/platform/docs/PLATFORM_GUIDE.md\` for that version's capabilities,
1213
+ ownership boundaries, and maturity limits.
1214
+ 3. Read \`meadow.config.json\` and run \`meadow doctor\` before guessing about project setup.
1215
+ 4. Prefer existing Meadow commands, services, and recipes when they cover the need.
1216
+
1217
+ Treat \`meadow.config.json\` as the source of truth for project identity, targets, releases,
1218
+ environments, links, capabilities, provider identifiers, and analytics definitions. Run \`meadow sync\`
1219
+ after changing it and never hand-edit files marked as generated by that command.
1220
+
1221
+ The companion website is in \`website/\`. Its page content and styles belong to this repository;
1222
+ \`website/site.config.json\` is synchronized from \`meadow.config.json\`.
1223
+
1224
+ Keep product domain logic, pages, visual design, analytics events, credentials, provider policies,
1225
+ and store content in this application. Do not infer that an installed or configured provider is a
1226
+ complete Meadow runtime capability; verify it in the platform guide.
1227
+
1228
+ Treat screenshot changes as product changes requiring review. Run the app's relevant unit, visual,
1229
+ web, and native checks before considering work complete.
1230
+ ${MEADOW_AGENT_SECTION_END}`;
1231
+ }
1232
+ function appAgentInstructions(config) {
1233
+ return `# ${config.app.name} working context
1234
+
1235
+ ${meadowAgentSection(config)}
1236
+ `;
1237
+ }
1238
+ function escapeHtml(value) {
1239
+ return value
1240
+ .replaceAll('&', '&amp;')
1241
+ .replaceAll('<', '&lt;')
1242
+ .replaceAll('>', '&gt;')
1243
+ .replaceAll('"', '&quot;')
1244
+ .replaceAll("'", '&#39;');
1245
+ }
1246
+ //# sourceMappingURL=templates.js.map