@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.
- package/MeadowPlatform.podspec +17 -0
- package/Package.swift +23 -0
- package/README.md +147 -2
- package/android/build.gradle +42 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/app/meadow/platform/MeadowAppRatingPlugin.java +38 -0
- package/dist/cli/commands.d.ts +74 -0
- package/dist/cli/commands.d.ts.map +1 -0
- package/dist/cli/commands.js +1062 -0
- package/dist/cli/commands.js.map +1 -0
- package/dist/cli/config.d.ts +123 -0
- package/dist/cli/config.d.ts.map +1 -0
- package/dist/cli/config.js +428 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/meadow.d.ts +8 -0
- package/dist/cli/meadow.d.ts.map +1 -0
- package/dist/cli/meadow.js +463 -0
- package/dist/cli/meadow.js.map +1 -0
- package/dist/cli/recipes.d.ts +10 -0
- package/dist/cli/recipes.d.ts.map +1 -0
- package/dist/cli/recipes.js +575 -0
- package/dist/cli/recipes.js.map +1 -0
- package/dist/cli/sync.d.ts +14 -0
- package/dist/cli/sync.d.ts.map +1 -0
- package/dist/cli/sync.js +310 -0
- package/dist/cli/sync.js.map +1 -0
- package/dist/cli/templates.d.ts +27 -0
- package/dist/cli/templates.d.ts.map +1 -0
- package/dist/cli/templates.js +1246 -0
- package/dist/cli/templates.js.map +1 -0
- package/dist/cli/website.d.ts +5 -0
- package/dist/cli/website.d.ts.map +1 -0
- package/dist/cli/website.js +262 -0
- package/dist/cli/website.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/notifications.d.ts +119 -0
- package/dist/notifications.d.ts.map +1 -0
- package/dist/notifications.js +320 -0
- package/dist/notifications.js.map +1 -0
- package/dist/platform.d.ts +163 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/platform.js +623 -0
- package/dist/platform.js.map +1 -0
- package/dist/purchases.d.ts +88 -0
- package/dist/purchases.d.ts.map +1 -0
- package/dist/purchases.js +296 -0
- package/dist/purchases.js.map +1 -0
- package/dist/ui/app-build.component.d.ts +18 -0
- package/dist/ui/app-build.component.d.ts.map +1 -0
- package/dist/ui/app-build.component.js +56 -0
- package/dist/ui/app-build.component.js.map +1 -0
- package/dist/ui/app-build.model.d.ts +11 -0
- package/dist/ui/app-build.model.d.ts.map +1 -0
- package/dist/ui/app-build.model.js +18 -0
- package/dist/ui/app-build.model.js.map +1 -0
- package/dist/ui/app-links.component.d.ts +14 -0
- package/dist/ui/app-links.component.d.ts.map +1 -0
- package/dist/ui/app-links.component.js +77 -0
- package/dist/ui/app-links.component.js.map +1 -0
- package/dist/ui/app-links.model.d.ts +18 -0
- package/dist/ui/app-links.model.d.ts.map +1 -0
- package/dist/ui/app-links.model.js +17 -0
- package/dist/ui/app-links.model.js.map +1 -0
- package/dist/ui/index.d.ts +5 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +5 -0
- package/dist/ui/index.js.map +1 -0
- package/docs/CLI.md +376 -0
- package/docs/PLATFORM_GUIDE.md +422 -0
- package/docs/POC_FINDINGS.md +218 -0
- package/docs/PROJECT_BRIEF.md +501 -0
- package/docs/RELEASING.md +49 -0
- package/docs/V0.1_PATH.md +310 -0
- package/ios/Sources/MeadowPlatformPlugin/MeadowAppRatingPlugin.swift +39 -0
- package/package.json +89 -4
- 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('&', '&')
|
|
1241
|
+
.replaceAll('<', '<')
|
|
1242
|
+
.replaceAll('>', '>')
|
|
1243
|
+
.replaceAll('"', '"')
|
|
1244
|
+
.replaceAll("'", ''');
|
|
1245
|
+
}
|
|
1246
|
+
//# sourceMappingURL=templates.js.map
|