umpordez 1.3.2 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +6 -1
- package/cli.mjs +1 -1
- package/create.mjs +179 -116
- package/package.json +1 -1
- package/template/CLAUDE.md +5 -4
- package/template/README.md +3 -3
- package/template/mobile/{{PROJECT_SLUG}}/CLAUDE.md +11 -2
- package/template/mobile/{{PROJECT_SLUG}}/app.json +13 -31
- package/template/mobile/{{PROJECT_SLUG}}/package.json +22 -20
- package/template/mobile/{{PROJECT_SLUG}}/src/components/AnimatedSplash.tsx +5 -1
- package/template/mobile/{{PROJECT_SLUG}}/src/config.ts +11 -0
- package/template/mobile/{{PROJECT_SLUG}}/src/pages/auth/AuthLanding.tsx +55 -50
- package/template-variables.mjs +127 -0
- package/update.mjs +52 -48
- package/template/mobile/{{PROJECT_SLUG}}/src/services/health/index.ts +0 -238
|
@@ -12,49 +12,51 @@
|
|
|
12
12
|
"lint": "eslint src --ext .ts,.tsx",
|
|
13
13
|
"typecheck": "tsc --noEmit"
|
|
14
14
|
},
|
|
15
|
+
"expo": {
|
|
16
|
+
"install": {
|
|
17
|
+
"exclude": ["typescript"]
|
|
18
|
+
}
|
|
19
|
+
},
|
|
15
20
|
"dependencies": {
|
|
16
21
|
"@noble/hashes": "^2.2.0",
|
|
17
|
-
"@react-native-async-storage/async-storage": "
|
|
22
|
+
"@react-native-async-storage/async-storage": "2.2.0",
|
|
18
23
|
"@react-navigation/bottom-tabs": "^7.7.3",
|
|
19
24
|
"@react-navigation/native": "^7.1.19",
|
|
20
25
|
"@react-navigation/native-stack": "^7.6.2",
|
|
21
|
-
"expo": "~57.0.
|
|
26
|
+
"expo": "~57.0.4",
|
|
22
27
|
"expo-apple-authentication": "~57.0.0",
|
|
23
|
-
"expo-auth-session": "~
|
|
28
|
+
"expo-auth-session": "~57.0.2",
|
|
24
29
|
"expo-build-properties": "~57.0.3",
|
|
25
30
|
"expo-constants": "~57.0.3",
|
|
26
31
|
"expo-file-system": "~57.0.0",
|
|
27
|
-
"expo-linking": "~57.0.
|
|
32
|
+
"expo-linking": "~57.0.2",
|
|
28
33
|
"expo-localization": "~57.0.0",
|
|
29
|
-
"expo-share-intent": "^8.0.0",
|
|
30
34
|
"expo-splash-screen": "~57.0.2",
|
|
31
35
|
"expo-sqlite": "~57.0.0",
|
|
32
36
|
"expo-status-bar": "~57.0.0",
|
|
33
37
|
"expo-system-ui": "~57.0.0",
|
|
34
38
|
"expo-web-browser": "~57.0.0",
|
|
35
39
|
"i18next": "^23.16.5",
|
|
36
|
-
"lucide-react-native": "^1.
|
|
37
|
-
"react": "19.
|
|
38
|
-
"react-native": "0.
|
|
40
|
+
"lucide-react-native": "^1.24.0",
|
|
41
|
+
"react": "19.2.3",
|
|
42
|
+
"react-native": "0.86.0",
|
|
39
43
|
"react-i18next": "^15.1.1",
|
|
40
|
-
"react-native-gesture-handler": "~
|
|
41
|
-
"react-native-
|
|
42
|
-
"react-native-health-connect": "^3.5.0",
|
|
43
|
-
"react-native-iap": "^15.2.0",
|
|
44
|
+
"react-native-gesture-handler": "~2.32.0",
|
|
45
|
+
"react-native-iap": "^15.3.6",
|
|
44
46
|
"react-native-nitro-modules": "^0.35.4",
|
|
45
|
-
"react-native-reanimated": "
|
|
46
|
-
"react-native-safe-area-context": "
|
|
47
|
-
"react-native-screens": "
|
|
48
|
-
"react-native-svg": "15.
|
|
47
|
+
"react-native-reanimated": "4.5.0",
|
|
48
|
+
"react-native-safe-area-context": "~5.7.0",
|
|
49
|
+
"react-native-screens": "4.25.2",
|
|
50
|
+
"react-native-svg": "15.15.4",
|
|
49
51
|
"react-native-toast-message": "^2.3.3",
|
|
50
|
-
"react-native-worklets": "
|
|
52
|
+
"react-native-worklets": "0.10.0"
|
|
51
53
|
},
|
|
52
54
|
"devDependencies": {
|
|
53
55
|
"@stylistic/eslint-plugin": "^2.13.0",
|
|
54
|
-
"@types/react": "~19.
|
|
55
|
-
"babel-preset-expo": "~57.0.
|
|
56
|
+
"@types/react": "~19.2.2",
|
|
57
|
+
"babel-preset-expo": "~57.0.2",
|
|
56
58
|
"eslint": "^9.20.1",
|
|
57
|
-
"typescript": "~5.
|
|
59
|
+
"typescript": "~5.9.3",
|
|
58
60
|
"typescript-eslint": "^8.24.0"
|
|
59
61
|
}
|
|
60
62
|
}
|
|
@@ -193,7 +193,11 @@ const RING_SIZE = width * 0.58;
|
|
|
193
193
|
|
|
194
194
|
const s = StyleSheet.create({
|
|
195
195
|
root: {
|
|
196
|
-
|
|
196
|
+
position: 'absolute',
|
|
197
|
+
top: 0,
|
|
198
|
+
left: 0,
|
|
199
|
+
right: 0,
|
|
200
|
+
bottom: 0,
|
|
197
201
|
backgroundColor: BG,
|
|
198
202
|
alignItems: 'center',
|
|
199
203
|
justifyContent: 'center',
|
|
@@ -47,6 +47,15 @@ const GOOGLE_IOS_CLIENT_ID = 'TODO_GOOGLE_IOS_CLIENT_ID';
|
|
|
47
47
|
// appears. Set at scaffold time via `umpordez create`.
|
|
48
48
|
export const SPLASH_ANIMATED = {{SPLASH_ANIMATED}};
|
|
49
49
|
|
|
50
|
+
// Sign-in providers, chosen at scaffold time via `umpordez create`.
|
|
51
|
+
// Email+password always ships; these gate the extra buttons on
|
|
52
|
+
// AuthLanding. Flipping appleAuth back on also requires restoring
|
|
53
|
+
// `usesAppleSignIn` + the `expo-apple-authentication` plugin in
|
|
54
|
+
// app.json (the scaffold strips them when Apple is disabled so iOS
|
|
55
|
+
// builds don't demand the Sign in with Apple capability).
|
|
56
|
+
const GOOGLE_AUTH_ENABLED = {{AUTH_GOOGLE_ENABLED}};
|
|
57
|
+
const APPLE_AUTH_ENABLED = {{AUTH_APPLE_ENABLED}};
|
|
58
|
+
|
|
50
59
|
if (__DEV__) {
|
|
51
60
|
console.log(
|
|
52
61
|
`[config] API: ${baseApiUrl} | prod=${isProdBuild} `
|
|
@@ -62,5 +71,7 @@ export default {
|
|
|
62
71
|
version: APP_VERSION,
|
|
63
72
|
googleClientId: GOOGLE_CLIENT_ID,
|
|
64
73
|
googleIosClientId: GOOGLE_IOS_CLIENT_ID,
|
|
74
|
+
googleAuth: GOOGLE_AUTH_ENABLED,
|
|
75
|
+
appleAuth: APPLE_AUTH_ENABLED,
|
|
65
76
|
scheme: '{{MOBILE_SCHEME}}'
|
|
66
77
|
};
|
|
@@ -47,7 +47,7 @@ export default function AuthLandingScreen({ navigation }: Props) {
|
|
|
47
47
|
})
|
|
48
48
|
]).start();
|
|
49
49
|
|
|
50
|
-
if (Platform.OS === 'ios') {
|
|
50
|
+
if (Platform.OS === 'ios' && config.appleAuth) {
|
|
51
51
|
AppleAuthentication.isAvailableAsync()
|
|
52
52
|
.then(setAppleAvailable)
|
|
53
53
|
.catch(() => setAppleAvailable(false));
|
|
@@ -279,58 +279,63 @@ export default function AuthLandingScreen({ navigation }: Props) {
|
|
|
279
279
|
</TouchableOpacity>
|
|
280
280
|
)}
|
|
281
281
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
s.googleBtn,
|
|
285
|
-
{
|
|
286
|
-
backgroundColor: colors.card,
|
|
287
|
-
borderColor: colors.border
|
|
288
|
-
},
|
|
289
|
-
busy && s.btnDisabled
|
|
290
|
-
]}
|
|
291
|
-
onPress={handleGoogle}
|
|
292
|
-
activeOpacity={0.88}
|
|
293
|
-
disabled={busy}
|
|
294
|
-
>
|
|
295
|
-
{provider === 'google' ? (
|
|
296
|
-
<ActivityIndicator color={colors.text} />
|
|
297
|
-
) : (
|
|
298
|
-
<>
|
|
299
|
-
<GoogleIcon />
|
|
300
|
-
<Text
|
|
301
|
-
style={[
|
|
302
|
-
s.googleTxt,
|
|
303
|
-
{ color: colors.text }
|
|
304
|
-
]}
|
|
305
|
-
>
|
|
306
|
-
{t('auth.landing.google')}
|
|
307
|
-
</Text>
|
|
308
|
-
</>
|
|
309
|
-
)}
|
|
310
|
-
</TouchableOpacity>
|
|
311
|
-
|
|
312
|
-
<View style={s.divRow}>
|
|
313
|
-
<View
|
|
314
|
-
style={[
|
|
315
|
-
s.divLine,
|
|
316
|
-
{ backgroundColor: colors.border }
|
|
317
|
-
]}
|
|
318
|
-
/>
|
|
319
|
-
<Text
|
|
282
|
+
{config.googleAuth && (
|
|
283
|
+
<TouchableOpacity
|
|
320
284
|
style={[
|
|
321
|
-
s.
|
|
322
|
-
{
|
|
285
|
+
s.googleBtn,
|
|
286
|
+
{
|
|
287
|
+
backgroundColor: colors.card,
|
|
288
|
+
borderColor: colors.border
|
|
289
|
+
},
|
|
290
|
+
busy && s.btnDisabled
|
|
323
291
|
]}
|
|
292
|
+
onPress={handleGoogle}
|
|
293
|
+
activeOpacity={0.88}
|
|
294
|
+
disabled={busy}
|
|
324
295
|
>
|
|
325
|
-
{
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
296
|
+
{provider === 'google' ? (
|
|
297
|
+
<ActivityIndicator color={colors.text} />
|
|
298
|
+
) : (
|
|
299
|
+
<>
|
|
300
|
+
<GoogleIcon />
|
|
301
|
+
<Text
|
|
302
|
+
style={[
|
|
303
|
+
s.googleTxt,
|
|
304
|
+
{ color: colors.text }
|
|
305
|
+
]}
|
|
306
|
+
>
|
|
307
|
+
{t('auth.landing.google')}
|
|
308
|
+
</Text>
|
|
309
|
+
</>
|
|
310
|
+
)}
|
|
311
|
+
</TouchableOpacity>
|
|
312
|
+
)}
|
|
313
|
+
|
|
314
|
+
{(config.googleAuth
|
|
315
|
+
|| (Platform.OS === 'ios' && appleAvailable)) && (
|
|
316
|
+
<View style={s.divRow}>
|
|
317
|
+
<View
|
|
318
|
+
style={[
|
|
319
|
+
s.divLine,
|
|
320
|
+
{ backgroundColor: colors.border }
|
|
321
|
+
]}
|
|
322
|
+
/>
|
|
323
|
+
<Text
|
|
324
|
+
style={[
|
|
325
|
+
s.divLabel,
|
|
326
|
+
{ color: colors.textMuted }
|
|
327
|
+
]}
|
|
328
|
+
>
|
|
329
|
+
{t('auth.landing.divider')}
|
|
330
|
+
</Text>
|
|
331
|
+
<View
|
|
332
|
+
style={[
|
|
333
|
+
s.divLine,
|
|
334
|
+
{ backgroundColor: colors.border }
|
|
335
|
+
]}
|
|
336
|
+
/>
|
|
337
|
+
</View>
|
|
338
|
+
)}
|
|
334
339
|
|
|
335
340
|
<TouchableOpacity
|
|
336
341
|
style={[
|
package/template-variables.mjs
CHANGED
|
@@ -39,6 +39,19 @@
|
|
|
39
39
|
// plays the AnimatedSplash on boot or shows
|
|
40
40
|
// up instantly (native splash hides on font
|
|
41
41
|
// load). Consumed by mobile src/config.ts.
|
|
42
|
+
// {{AUTH_GOOGLE_ENABLED}} — "true"/"false" — Google sign-in button in
|
|
43
|
+
// the mobile app. Consumed by src/config.ts.
|
|
44
|
+
// {{AUTH_APPLE_ENABLED}} — "true"/"false" — Apple sign-in button in
|
|
45
|
+
// the mobile app. Consumed by src/config.ts.
|
|
46
|
+
// {{IOS_APPLE_SIGNIN_JSON}} — app.json fragment: `"usesAppleSignIn":
|
|
47
|
+
// true,` (with leading newline+indent) when
|
|
48
|
+
// Apple sign-in is on, empty when off.
|
|
49
|
+
// {{APPLE_AUTH_PLUGIN_JSON}} — app.json fragment: the
|
|
50
|
+
// `"expo-apple-authentication",` plugins
|
|
51
|
+
// entry when Apple sign-in is on, empty
|
|
52
|
+
// when off (the plugin adds the Sign in
|
|
53
|
+
// with Apple entitlement unconditionally,
|
|
54
|
+
// so it must not ship when disabled).
|
|
42
55
|
|
|
43
56
|
// ─── Site landing variants ────────────────────────────────────────────────────
|
|
44
57
|
// The public site ships two landing flavors. The SaaS landing (CTAs →
|
|
@@ -52,6 +65,101 @@
|
|
|
52
65
|
|
|
53
66
|
export const SITE_VARIANT_DIR = 'ui/site/_variants';
|
|
54
67
|
|
|
68
|
+
// ─── Components ──────────────────────────────────────────────────────────────
|
|
69
|
+
// A project is any combination of four components, each picked with a
|
|
70
|
+
// yes/no at create time:
|
|
71
|
+
//
|
|
72
|
+
// server — Admin API + Site API (Express + PostgreSQL)
|
|
73
|
+
// adminUi — React admin webapp (requires server)
|
|
74
|
+
// site — Public site / landing page (EJS + i18n)
|
|
75
|
+
// mobile — Expo + React Native app
|
|
76
|
+
//
|
|
77
|
+
// Both create.mjs and update.mjs derive "which template paths ship"
|
|
78
|
+
// from these flags via the helpers below, so the two tools can never
|
|
79
|
+
// drift. The legacy preset names are kept as manifest labels (old
|
|
80
|
+
// projects only recorded a preset string).
|
|
81
|
+
|
|
82
|
+
export const LEGACY_PRESET_COMPONENTS = {
|
|
83
|
+
everything: { server: true, adminUi: true,
|
|
84
|
+
site: true, mobile: true },
|
|
85
|
+
'web-only': { server: true, adminUi: true,
|
|
86
|
+
site: true, mobile: false },
|
|
87
|
+
'mobile-and-site': { server: false, adminUi: false,
|
|
88
|
+
site: true, mobile: true },
|
|
89
|
+
'mobile-and-server': { server: true, adminUi: false,
|
|
90
|
+
site: false, mobile: true },
|
|
91
|
+
'just-mobile': { server: false, adminUi: false,
|
|
92
|
+
site: false, mobile: true }
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/** Components for a manifest that only recorded a preset name. */
|
|
96
|
+
export function componentsForPreset(preset) {
|
|
97
|
+
return LEGACY_PRESET_COMPONENTS[preset]
|
|
98
|
+
?? LEGACY_PRESET_COMPONENTS.everything;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Manifest label for a component combo. Combos matching a legacy
|
|
103
|
+
* preset keep its exact name (older CLI versions still understand
|
|
104
|
+
* the manifest); anything else is "custom".
|
|
105
|
+
*/
|
|
106
|
+
export function presetNameForComponents(c) {
|
|
107
|
+
for (const [name, def] of
|
|
108
|
+
Object.entries(LEGACY_PRESET_COMPONENTS)) {
|
|
109
|
+
if (def.server === !!c.server
|
|
110
|
+
&& def.adminUi === !!c.adminUi
|
|
111
|
+
&& def.site === !!c.site
|
|
112
|
+
&& def.mobile === !!c.mobile) {
|
|
113
|
+
return name;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return 'custom';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Top-level template dirs a component combo ships. */
|
|
120
|
+
export function includesForComponents(c) {
|
|
121
|
+
const includes = ['doc'];
|
|
122
|
+
if (c.server) {
|
|
123
|
+
includes.push('server', 'misc');
|
|
124
|
+
}
|
|
125
|
+
if (c.adminUi) {
|
|
126
|
+
includes.push('ui/admin');
|
|
127
|
+
}
|
|
128
|
+
if (c.site) {
|
|
129
|
+
includes.push('ui/site');
|
|
130
|
+
}
|
|
131
|
+
if (c.mobile) {
|
|
132
|
+
includes.push('mobile', 'scripts');
|
|
133
|
+
}
|
|
134
|
+
return includes;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Root-level template files a component combo ships. */
|
|
138
|
+
export function rootFilesForComponents(c) {
|
|
139
|
+
const files = ['CLAUDE.md', 'README.md', 'code.md',
|
|
140
|
+
'install.sh', 'dev.sh'];
|
|
141
|
+
if (c.server) {
|
|
142
|
+
files.push('architecture.md', 'seed.sh');
|
|
143
|
+
}
|
|
144
|
+
if (c.server || c.site || c.adminUi) {
|
|
145
|
+
files.push('AGENTS.md');
|
|
146
|
+
}
|
|
147
|
+
return files;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Whether the public site ships the app-marketing landing (store
|
|
152
|
+
* buttons + download CTA) instead of the SaaS landing (signup CTA).
|
|
153
|
+
* New projects store the decision in details.appLanding; legacy
|
|
154
|
+
* manifests fall back to the preset that implied it.
|
|
155
|
+
*/
|
|
156
|
+
export function wantsAppLanding(details, preset) {
|
|
157
|
+
if (typeof details?.appLanding === 'boolean') {
|
|
158
|
+
return details.appLanding;
|
|
159
|
+
}
|
|
160
|
+
return preset === 'mobile-and-site';
|
|
161
|
+
}
|
|
162
|
+
|
|
55
163
|
// [variantPath, canonicalPath] — relative to the project root.
|
|
56
164
|
export const APP_LANDING_SWAPS = [
|
|
57
165
|
['ui/site/_variants/app/home.ejs',
|
|
@@ -275,5 +383,24 @@ export function buildReplacements(details) {
|
|
|
275
383
|
'{{JWT_TOKEN_SECRET}}': details.jwtTokenSecret ?? randomSecret(),
|
|
276
384
|
'{{SPLASH_ANIMATED}}':
|
|
277
385
|
details.splashStyle === 'animated' ? 'true' : 'false',
|
|
386
|
+
// Sign-in providers chosen at scaffold time. Email+password
|
|
387
|
+
// always ships; these gate the extra buttons in the mobile
|
|
388
|
+
// app. Default to enabled so legacy manifests (which never
|
|
389
|
+
// recorded a choice) keep their current behavior on update.
|
|
390
|
+
'{{AUTH_GOOGLE_ENABLED}}':
|
|
391
|
+
details.authGoogle === false ? 'false' : 'true',
|
|
392
|
+
'{{AUTH_APPLE_ENABLED}}':
|
|
393
|
+
details.authApple === false ? 'false' : 'true',
|
|
394
|
+
// app.json fragments — the expo-apple-authentication config
|
|
395
|
+
// plugin adds the Sign in with Apple entitlement
|
|
396
|
+
// unconditionally when listed, so when Apple is disabled the
|
|
397
|
+
// plugin entry (and usesAppleSignIn) must disappear from the
|
|
398
|
+
// JSON entirely, not just flip to false.
|
|
399
|
+
'{{IOS_APPLE_SIGNIN_JSON}}': details.authApple === false
|
|
400
|
+
? ''
|
|
401
|
+
: '\n "usesAppleSignIn": true,',
|
|
402
|
+
'{{APPLE_AUTH_PLUGIN_JSON}}': details.authApple === false
|
|
403
|
+
? ''
|
|
404
|
+
: '\n "expo-apple-authentication",',
|
|
278
405
|
};
|
|
279
406
|
}
|
package/update.mjs
CHANGED
|
@@ -34,7 +34,11 @@ import { fileURLToPath } from 'node:url';
|
|
|
34
34
|
import chalk from 'chalk';
|
|
35
35
|
import {
|
|
36
36
|
buildReplacements,
|
|
37
|
-
APP_LANDING_SWAPS
|
|
37
|
+
APP_LANDING_SWAPS,
|
|
38
|
+
componentsForPreset,
|
|
39
|
+
includesForComponents,
|
|
40
|
+
rootFilesForComponents,
|
|
41
|
+
wantsAppLanding
|
|
38
42
|
} from './template-variables.mjs';
|
|
39
43
|
import { generateIcons, ICON_TARGETS } from './icons.mjs';
|
|
40
44
|
|
|
@@ -42,23 +46,15 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
42
46
|
const TEMPLATE_DIR = path.join(__dirname, 'template');
|
|
43
47
|
const MANIFEST_PATH = '.umpordez/manifest.json';
|
|
44
48
|
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
'doc'],
|
|
55
|
-
'just-mobile': ['mobile', 'scripts', 'doc']
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
// Presets that use the app-marketing landing instead of the SaaS one.
|
|
59
|
-
// Mirror of create.mjs's `appLanding` preset flag — kept in sync
|
|
60
|
-
// manually (see PRESET_INCLUDES note above).
|
|
61
|
-
const APP_LANDING_PRESETS = new Set(['mobile-and-site']);
|
|
49
|
+
// Which components a project shipped. New manifests record the
|
|
50
|
+
// components object in details; legacy ones only stored a preset
|
|
51
|
+
// name — map it through the shared table in template-variables.mjs.
|
|
52
|
+
function componentsFromManifest(manifest) {
|
|
53
|
+
if (manifest.details?.components) {
|
|
54
|
+
return manifest.details.components;
|
|
55
|
+
}
|
|
56
|
+
return componentsForPreset(manifest.preset);
|
|
57
|
+
}
|
|
62
58
|
|
|
63
59
|
const green = chalk.hex('#02e027');
|
|
64
60
|
const cyan = chalk.hex('#00fff9');
|
|
@@ -153,30 +149,26 @@ function applyPathPlaceholders(p, slug) {
|
|
|
153
149
|
}
|
|
154
150
|
|
|
155
151
|
/**
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
* project's existing top-level layout — we only want to update what
|
|
161
|
-
* the project actually shipped.
|
|
162
|
-
*/
|
|
163
|
-
/**
|
|
164
|
-
* Whether a relative path belongs to the preset. Same prune rules
|
|
165
|
-
* as create.mjs's copyTemplate, expressed as a predicate so we can
|
|
166
|
-
* evaluate per-file instead of pruning a directory.
|
|
152
|
+
* Whether a relative path belongs to the project's component combo.
|
|
153
|
+
* Same prune rules as create.mjs's copyTemplate, expressed as a
|
|
154
|
+
* predicate so we can evaluate per-file instead of pruning a
|
|
155
|
+
* directory.
|
|
167
156
|
*/
|
|
168
|
-
// Top-level
|
|
157
|
+
// Top-level dirs/files that ship with every combo (project config
|
|
169
158
|
// like .claude/ that doesn't belong to any single sub-app). Mirrors
|
|
170
|
-
// the ALWAYS_KEEP
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
159
|
+
// the ALWAYS_KEEP sets in create.mjs. Note: rel arrives post
|
|
160
|
+
// path-placeholder pass, so the dot-less `gitignore` the template
|
|
161
|
+
// ships is already `.gitignore` here.
|
|
162
|
+
const ALWAYS_IN_PROJECT = new Set(['.claude']);
|
|
163
|
+
const ALWAYS_ROOT_FILES = new Set(['.gitignore', '.editorconfig']);
|
|
164
|
+
|
|
165
|
+
function isPathInComponents(rel, components) {
|
|
166
|
+
const includes = includesForComponents(components);
|
|
175
167
|
const top = rel.split('/')[0];
|
|
176
|
-
if (
|
|
168
|
+
if (ALWAYS_IN_PROJECT.has(top)) {
|
|
177
169
|
return true;
|
|
178
170
|
}
|
|
179
|
-
// ui is special: only the chosen sub-apps
|
|
171
|
+
// ui is special: only the chosen sub-apps ship.
|
|
180
172
|
if (top === 'ui') {
|
|
181
173
|
const sub = rel.split('/')[1];
|
|
182
174
|
return includes.includes(`ui/${sub}`);
|
|
@@ -184,22 +176,34 @@ function isPathInPreset(rel, preset) {
|
|
|
184
176
|
if (includes.includes(top)) {
|
|
185
177
|
return true;
|
|
186
178
|
}
|
|
187
|
-
// Top-level files
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
|
|
179
|
+
// Top-level files: same filter create.mjs applies at scaffold
|
|
180
|
+
// time, so update never re-adds a root file the combo doesn't
|
|
181
|
+
// ship (e.g. seed.sh on a project without the server).
|
|
182
|
+
if (rel.indexOf('/') === -1) {
|
|
183
|
+
return ALWAYS_ROOT_FILES.has(rel)
|
|
184
|
+
|| rootFilesForComponents(components).includes(rel);
|
|
185
|
+
}
|
|
186
|
+
return false;
|
|
191
187
|
}
|
|
192
188
|
|
|
193
|
-
|
|
189
|
+
/**
|
|
190
|
+
* Walk the template, resolve each file's contents (post-replacement)
|
|
191
|
+
* + final relative path. Returns a map of relative-path → buffer.
|
|
192
|
+
* Skips files outside the project's component combo — we only want
|
|
193
|
+
* to update what the project actually shipped.
|
|
194
|
+
*/
|
|
195
|
+
|
|
196
|
+
async function loadTemplate(details, manifest) {
|
|
194
197
|
const replacements = buildReplacements(details);
|
|
195
198
|
const files = await getAllFiles(TEMPLATE_DIR);
|
|
196
199
|
const out = new Map();
|
|
200
|
+
const components = componentsFromManifest(manifest);
|
|
197
201
|
|
|
198
202
|
for (const abs of files) {
|
|
199
203
|
const relRaw = path.relative(TEMPLATE_DIR, abs);
|
|
200
204
|
const rel = applyPathPlaceholders(relRaw, details.slug);
|
|
201
205
|
|
|
202
|
-
if (
|
|
206
|
+
if (!isPathInComponents(rel, components)) {
|
|
203
207
|
continue;
|
|
204
208
|
}
|
|
205
209
|
|
|
@@ -213,13 +217,13 @@ async function loadTemplate(details, preset) {
|
|
|
213
217
|
}
|
|
214
218
|
|
|
215
219
|
// Landing-page variant resolution — mirror create.mjs's copyTemplate
|
|
216
|
-
// swap. For app-landing
|
|
220
|
+
// swap. For app-landing projects the canonical landing files are
|
|
217
221
|
// sourced from ui/site/_variants/app/ so `umpordez update` compares
|
|
218
222
|
// against the SAME flavor the project was scaffolded with (otherwise
|
|
219
223
|
// it would try to push the SaaS landing over an app project). The
|
|
220
|
-
// _variants/ staging files never ship, so drop them from the map
|
|
221
|
-
// every
|
|
222
|
-
if (
|
|
224
|
+
// _variants/ staging files never ship, so drop them from the map
|
|
225
|
+
// in every case.
|
|
226
|
+
if (wantsAppLanding(details, manifest.preset)) {
|
|
223
227
|
for (const [variant, canonical] of APP_LANDING_SWAPS) {
|
|
224
228
|
if (out.has(variant)) {
|
|
225
229
|
out.set(canonical, out.get(variant));
|
|
@@ -366,7 +370,7 @@ export async function updateProject(projectDirArg, opts = {}) {
|
|
|
366
370
|
));
|
|
367
371
|
console.log('');
|
|
368
372
|
|
|
369
|
-
const template = await loadTemplate(details, manifest
|
|
373
|
+
const template = await loadTemplate(details, manifest);
|
|
370
374
|
const newHashes = {};
|
|
371
375
|
|
|
372
376
|
let acceptAll = !!opts.yes;
|