create-better-fullstack 2.1.3 → 2.1.5
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/dist/add-handler-BNSL6HdM.mjs +257 -0
- package/dist/{addons-setup-C_xrNtkL.mjs → addons-setup-CyrP1IV-.mjs} +1 -1
- package/dist/addons-setup-DyMm42a5.mjs +5 -0
- package/dist/cli.mjs +2 -2
- package/dist/{doctor-BFSSbS-U.mjs → doctor-DBoq7bZ9.mjs} +2 -2
- package/dist/{errors-D9yiiGVq.mjs → errors-Cyol8zbN.mjs} +18 -2
- package/dist/{config-validation-C4glouQh.mjs → file-formatter-B3dsev2l.mjs} +372 -456
- package/dist/{generated-checks-DUvVXWId.mjs → generated-checks-C8hn9w2i.mjs} +1 -1
- package/dist/index.d.mts +1012 -108
- package/dist/index.mjs +7 -8
- package/dist/{install-dependencies-DDGF-zDG.mjs → install-dependencies-CgNh-aOy.mjs} +2 -2
- package/dist/{mcp-BXLhb7wW.mjs → mcp-CsW3i66c.mjs} +2 -3
- package/dist/mcp-entry.mjs +195 -10
- package/dist/{run-DRzP53v3.mjs → run-BYse4yJy.mjs} +61 -61
- package/dist/run-_cf_sFwM.mjs +10 -0
- package/dist/templates-CnTOtKjm.mjs +497 -0
- package/package.json +3 -3
- package/dist/add-handler-9F-AsGM-.mjs +0 -151
- package/dist/addons-setup-C8eaCaH5.mjs +0 -5
- package/dist/compatibility-rules-D7zYNVjC.mjs +0 -372
- package/dist/run-BmRKR2wG.mjs +0 -11
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { c as isSilent, g as updateBtsConfig, l as runWithContextAsync, m as readBtsConfig, n as UserCancelledError, t as CLIError, x as getDefaultConfig } from "./errors-Cyol8zbN.mjs";
|
|
3
|
+
import { t as renderTitle } from "./render-title-zvyKC1ej.mjs";
|
|
4
|
+
import { t as setupAddons } from "./addons-setup-CyrP1IV-.mjs";
|
|
5
|
+
import "./file-formatter-B3dsev2l.mjs";
|
|
6
|
+
import { c as applyDependencyVersionChannel, t as installDependencies, u as getAddonsToAdd } from "./install-dependencies-CgNh-aOy.mjs";
|
|
7
|
+
import { a as applyStackUpdate, o as planStackUpdate } from "./mcp-entry.mjs";
|
|
8
|
+
import { intro, log, outro } from "@clack/prompts";
|
|
9
|
+
import pc from "picocolors";
|
|
10
|
+
import fs from "fs-extra";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { EMBEDDED_TEMPLATES, VirtualFileSystem, processAddonTemplates, processAddonsDeps } from "@better-fullstack/template-generator";
|
|
13
|
+
import { writeTreeToFilesystem } from "@better-fullstack/template-generator/fs-writer";
|
|
14
|
+
|
|
15
|
+
//#region src/helpers/core/add-handler.ts
|
|
16
|
+
const ADD_CONTROL_KEYS = new Set([
|
|
17
|
+
"projectDir",
|
|
18
|
+
"install",
|
|
19
|
+
"dryRun"
|
|
20
|
+
]);
|
|
21
|
+
function buildStackUpdateRequest(input) {
|
|
22
|
+
const request = {};
|
|
23
|
+
for (const [key, value] of Object.entries(input)) {
|
|
24
|
+
if (ADD_CONTROL_KEYS.has(key)) continue;
|
|
25
|
+
if (value === void 0 || value === false) continue;
|
|
26
|
+
if (Array.isArray(value) && value.length === 0) continue;
|
|
27
|
+
request[key] = value;
|
|
28
|
+
}
|
|
29
|
+
return request;
|
|
30
|
+
}
|
|
31
|
+
function getNewAddons(input, currentConfig) {
|
|
32
|
+
const requestedAddons = (input.addons ?? []).filter((addon) => addon !== "none");
|
|
33
|
+
const existingAddons = new Set(currentConfig.addons ?? []);
|
|
34
|
+
return requestedAddons.filter((addon) => !existingAddons.has(addon));
|
|
35
|
+
}
|
|
36
|
+
function formatCount(count, noun) {
|
|
37
|
+
return `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
38
|
+
}
|
|
39
|
+
function countDependencyChanges(plan) {
|
|
40
|
+
return Object.values(plan.dependencyChanges).reduce((count, deps) => count + Object.keys(deps).length, 0);
|
|
41
|
+
}
|
|
42
|
+
function countEnvChanges(plan) {
|
|
43
|
+
return Object.values(plan.envChanges).reduce((count, keys) => count + keys.length, 0);
|
|
44
|
+
}
|
|
45
|
+
function logStackUpdateSummary(plan, dryRun) {
|
|
46
|
+
if (isSilent()) return;
|
|
47
|
+
log.info(pc.cyan(dryRun ? "Stack update plan:" : "Stack update result:"));
|
|
48
|
+
const requestedFields = Object.keys(plan.requestedChanges);
|
|
49
|
+
if (requestedFields.length > 0) log.info(pc.dim(`Requested: ${requestedFields.sort().join(", ")}`));
|
|
50
|
+
if (plan.graphSummary) log.info(pc.dim(`Stack: ${plan.graphSummary}`));
|
|
51
|
+
if (plan.filesToAdd.length + plan.filesToPatch.length === 0 && plan.manualReviewBlockers.length === 0) log.info(pc.dim("No stack changes to apply."));
|
|
52
|
+
else log.info(pc.dim(`Files: ${formatCount(plan.filesToAdd.length, "file add")}, ${formatCount(plan.filesToPatch.length, "file update")}, ${formatCount(plan.filesUnchanged.length, "unchanged file")}`));
|
|
53
|
+
const dependencyCount = countDependencyChanges(plan);
|
|
54
|
+
if (dependencyCount > 0) log.info(pc.dim(`Dependencies: ${formatCount(dependencyCount, "change")}`));
|
|
55
|
+
const envCount = countEnvChanges(plan);
|
|
56
|
+
if (envCount > 0) log.info(pc.dim(`Env vars: ${formatCount(envCount, "addition")}`));
|
|
57
|
+
for (const adjustment of plan.compatibilityAdjustments) log.info(pc.dim(`Adjusted: ${adjustment}`));
|
|
58
|
+
for (const blocker of plan.manualReviewBlockers) log.warn(pc.yellow(`Manual review: ${blocker}`));
|
|
59
|
+
}
|
|
60
|
+
function buildAddonSetupConfig(projectDir, projectName, currentConfig, plan, addonsToSetup) {
|
|
61
|
+
const baseConfig = getDefaultConfig();
|
|
62
|
+
return {
|
|
63
|
+
...baseConfig,
|
|
64
|
+
...currentConfig,
|
|
65
|
+
...plan.proposedConfig,
|
|
66
|
+
projectName,
|
|
67
|
+
projectDir,
|
|
68
|
+
relativePath: ".",
|
|
69
|
+
packageManager: plan.proposedConfig.packageManager || currentConfig.packageManager || baseConfig.packageManager,
|
|
70
|
+
addons: addonsToSetup,
|
|
71
|
+
frontend: plan.proposedConfig.frontend || currentConfig.frontend || baseConfig.frontend,
|
|
72
|
+
examples: plan.proposedConfig.examples || currentConfig.examples || [],
|
|
73
|
+
rustLibraries: plan.proposedConfig.rustLibraries || currentConfig.rustLibraries || [],
|
|
74
|
+
pythonAi: plan.proposedConfig.pythonAi || currentConfig.pythonAi || [],
|
|
75
|
+
aiDocs: plan.proposedConfig.aiDocs || currentConfig.aiDocs || []
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
async function runStackUpdateAdd(input, projectDir, projectName, currentConfig, request) {
|
|
79
|
+
const dryRun = input.dryRun ?? false;
|
|
80
|
+
const result = dryRun ? await planStackUpdate(projectDir, request) : await applyStackUpdate(projectDir, request);
|
|
81
|
+
if (!result.success) throw new CLIError(result.error);
|
|
82
|
+
logStackUpdateSummary(result, dryRun);
|
|
83
|
+
if (dryRun) {
|
|
84
|
+
if (!isSilent()) outro(pc.magenta("Dry run complete. No files were written."));
|
|
85
|
+
return {
|
|
86
|
+
success: true,
|
|
87
|
+
addedAddons: [],
|
|
88
|
+
projectDir
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const addonsToSetup = getNewAddons(input, currentConfig);
|
|
92
|
+
const setupConfig = buildAddonSetupConfig(projectDir, projectName, currentConfig, result, addonsToSetup);
|
|
93
|
+
const setupWarnings = addonsToSetup.length > 0 ? await setupAddons(setupConfig) : [];
|
|
94
|
+
await applyDependencyVersionChannel(projectDir, result.proposedConfig.versionChannel);
|
|
95
|
+
let installFailed = false;
|
|
96
|
+
if (input.install) {
|
|
97
|
+
if (result.proposedConfig.ecosystem === "typescript" || result.proposedConfig.ecosystem === "react-native") installFailed = !(await installDependencies({
|
|
98
|
+
projectDir,
|
|
99
|
+
packageManager: setupConfig.packageManager
|
|
100
|
+
})).success;
|
|
101
|
+
else if (!isSilent()) log.warn(pc.yellow(`Automatic --install is only supported for JavaScript package-manager installs. Run '${result.installCommand}' instead.`));
|
|
102
|
+
}
|
|
103
|
+
if (!isSilent()) {
|
|
104
|
+
if (addonsToSetup.length > 0) log.success(pc.green(`Successfully added: ${addonsToSetup.join(", ")}`));
|
|
105
|
+
else if ((input.addons ?? []).some((addon) => addon !== "none")) log.info(pc.dim("No new addons selected."));
|
|
106
|
+
log.success(pc.green("Stack update applied."));
|
|
107
|
+
for (const warning of setupWarnings) log.warn(pc.yellow(warning));
|
|
108
|
+
if (!input.install) log.info(pc.yellow(`Run '${result.installCommand}' to install new dependencies.`));
|
|
109
|
+
else if (installFailed) log.warn(pc.yellow(`Dependency installation failed. Run '${result.installCommand}' after resolving the error above.`));
|
|
110
|
+
outro(pc.magenta("Project updated successfully!"));
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
success: true,
|
|
114
|
+
addedAddons: addonsToSetup,
|
|
115
|
+
projectDir,
|
|
116
|
+
setupWarnings: setupWarnings.length > 0 ? setupWarnings : void 0
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
async function addHandler(input, options = {}) {
|
|
120
|
+
const { silent = false } = options;
|
|
121
|
+
return runWithContextAsync({ silent }, async () => {
|
|
122
|
+
try {
|
|
123
|
+
return await addHandlerInternal(input);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if (error instanceof UserCancelledError) {
|
|
126
|
+
if (isSilent()) return {
|
|
127
|
+
success: false,
|
|
128
|
+
addedAddons: [],
|
|
129
|
+
projectDir: "",
|
|
130
|
+
error: error.message
|
|
131
|
+
};
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (error instanceof CLIError) {
|
|
135
|
+
if (isSilent()) return {
|
|
136
|
+
success: false,
|
|
137
|
+
addedAddons: [],
|
|
138
|
+
projectDir: "",
|
|
139
|
+
error: error.message
|
|
140
|
+
};
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
if (isSilent()) return {
|
|
144
|
+
success: false,
|
|
145
|
+
addedAddons: [],
|
|
146
|
+
projectDir: "",
|
|
147
|
+
error: error instanceof Error ? error.message : String(error)
|
|
148
|
+
};
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
async function addHandlerInternal(input) {
|
|
154
|
+
const projectDir = path.resolve(input.projectDir || process.cwd());
|
|
155
|
+
if (!isSilent()) {
|
|
156
|
+
renderTitle();
|
|
157
|
+
intro(pc.magenta("Update your Better Fullstack project"));
|
|
158
|
+
}
|
|
159
|
+
const btsConfig = await readBtsConfig(projectDir);
|
|
160
|
+
if (!btsConfig) throw new CLIError(`No Better Fullstack project found in ${projectDir}. Make sure bts.jsonc exists.`);
|
|
161
|
+
const projectName = path.basename(projectDir);
|
|
162
|
+
if (!isSilent()) log.info(pc.dim(`Detected project: ${projectName}`));
|
|
163
|
+
const stackUpdateRequest = buildStackUpdateRequest(input);
|
|
164
|
+
if (Object.keys(stackUpdateRequest).length > 0 || input.dryRun) return runStackUpdateAdd(input, projectDir, projectName, btsConfig, stackUpdateRequest);
|
|
165
|
+
const existingAddons = btsConfig.addons || [];
|
|
166
|
+
let addonsToAdd = [];
|
|
167
|
+
if (input.addons && input.addons.length > 0) addonsToAdd = input.addons.filter((addon) => addon !== "none" && !existingAddons.includes(addon));
|
|
168
|
+
else addonsToAdd = (await getAddonsToAdd(btsConfig.frontend || [], existingAddons, btsConfig.auth)).filter((addon) => addon !== "none");
|
|
169
|
+
if (addonsToAdd.length === 0) {
|
|
170
|
+
if (!isSilent()) {
|
|
171
|
+
log.info(pc.dim("No new addons selected."));
|
|
172
|
+
outro(pc.magenta("Nothing to add."));
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
success: true,
|
|
176
|
+
addedAddons: [],
|
|
177
|
+
projectDir
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
if (!isSilent()) log.info(pc.cyan(`Adding addons: ${addonsToAdd.join(", ")}`));
|
|
181
|
+
const baseConfig = getDefaultConfig();
|
|
182
|
+
const config = {
|
|
183
|
+
...baseConfig,
|
|
184
|
+
...btsConfig,
|
|
185
|
+
projectName,
|
|
186
|
+
projectDir,
|
|
187
|
+
relativePath: ".",
|
|
188
|
+
packageManager: input.packageManager || btsConfig.packageManager || baseConfig.packageManager,
|
|
189
|
+
addons: addonsToAdd,
|
|
190
|
+
frontend: btsConfig.frontend || baseConfig.frontend,
|
|
191
|
+
examples: btsConfig.examples || [],
|
|
192
|
+
rustLibraries: btsConfig.rustLibraries || [],
|
|
193
|
+
pythonAi: btsConfig.pythonAi || [],
|
|
194
|
+
aiDocs: btsConfig.aiDocs || []
|
|
195
|
+
};
|
|
196
|
+
const vfs = new VirtualFileSystem();
|
|
197
|
+
const packageJsonPaths = await collectPackageJsonPaths(projectDir);
|
|
198
|
+
for (const pkgPath of packageJsonPaths) {
|
|
199
|
+
const fullPath = path.join(projectDir, pkgPath);
|
|
200
|
+
const content = await fs.readFile(fullPath, "utf-8");
|
|
201
|
+
vfs.writeFile(pkgPath, content);
|
|
202
|
+
}
|
|
203
|
+
await processAddonTemplates(vfs, EMBEDDED_TEMPLATES, config);
|
|
204
|
+
processAddonsDeps(vfs, config);
|
|
205
|
+
await writeTreeToFilesystem({
|
|
206
|
+
root: vfs.toTree(projectName),
|
|
207
|
+
fileCount: vfs.getFileCount(),
|
|
208
|
+
directoryCount: vfs.getDirectoryCount(),
|
|
209
|
+
config
|
|
210
|
+
}, projectDir);
|
|
211
|
+
const setupWarnings = await setupAddons(config);
|
|
212
|
+
await applyDependencyVersionChannel(projectDir, config.versionChannel);
|
|
213
|
+
const configUpdates = { addons: [...new Set([...existingAddons, ...addonsToAdd])] };
|
|
214
|
+
if (input.webDeploy !== void 0) configUpdates.webDeploy = input.webDeploy;
|
|
215
|
+
if (input.serverDeploy !== void 0) configUpdates.serverDeploy = input.serverDeploy;
|
|
216
|
+
await updateBtsConfig(projectDir, configUpdates);
|
|
217
|
+
let addonInstallFailed = false;
|
|
218
|
+
if (input.install) addonInstallFailed = !(await installDependencies({
|
|
219
|
+
projectDir,
|
|
220
|
+
packageManager: config.packageManager
|
|
221
|
+
})).success;
|
|
222
|
+
if (!isSilent()) {
|
|
223
|
+
log.success(pc.green(`Successfully added: ${addonsToAdd.join(", ")}`));
|
|
224
|
+
for (const warning of setupWarnings) log.warn(pc.yellow(warning));
|
|
225
|
+
const installCmd = config.packageManager === "npm" ? "npm install" : `${config.packageManager} install`;
|
|
226
|
+
if (!input.install) log.info(pc.yellow(`Run '${installCmd}' to install new dependencies.`));
|
|
227
|
+
else if (addonInstallFailed) log.warn(pc.yellow(`Dependency installation failed. Run '${installCmd}' after resolving the error above.`));
|
|
228
|
+
outro(pc.magenta("Addons added successfully!"));
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
success: true,
|
|
232
|
+
addedAddons: addonsToAdd,
|
|
233
|
+
projectDir,
|
|
234
|
+
setupWarnings: setupWarnings.length > 0 ? setupWarnings : void 0
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
async function collectPackageJsonPaths(projectDir) {
|
|
238
|
+
const results = [];
|
|
239
|
+
async function walk(currentDir) {
|
|
240
|
+
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
|
241
|
+
for (const entry of entries) {
|
|
242
|
+
if (entry.name === "node_modules" || entry.name === ".git" || entry.name === ".turbo") continue;
|
|
243
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
244
|
+
if (entry.isDirectory()) {
|
|
245
|
+
await walk(fullPath);
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (entry.isFile() && entry.name === "package.json") results.push(path.relative(projectDir, fullPath).replaceAll(path.sep, "/"));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
await walk(projectDir);
|
|
252
|
+
if (!results.includes("package.json") && await fs.pathExists(path.join(projectDir, "package.json"))) results.push("package.json");
|
|
253
|
+
return results;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
//#endregion
|
|
257
|
+
export { addHandler };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { c as isSilent, m as readBtsConfig, r as exitCancelled } from "./errors-
|
|
2
|
+
import { c as isSilent, m as readBtsConfig, r as exitCancelled } from "./errors-Cyol8zbN.mjs";
|
|
3
3
|
import { autocompleteMultiselect, group, isCancel, log, multiselect, select, spinner } from "@clack/prompts";
|
|
4
4
|
import pc from "picocolors";
|
|
5
5
|
import fs from "fs-extra";
|
package/dist/cli.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
//#region src/cli.ts
|
|
3
|
-
if (process.argv[2] === "mcp" && process.argv.length === 3) import("./mcp-
|
|
4
|
-
else import("./run-
|
|
3
|
+
if (process.argv[2] === "mcp" && process.argv.length === 3) import("./mcp-CsW3i66c.mjs").then((m) => m.startMcpServer());
|
|
4
|
+
else import("./run-_cf_sFwM.mjs").then((m) => m.createBtsCli().run());
|
|
5
5
|
|
|
6
6
|
//#endregion
|
|
7
7
|
export { };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as handleError, m as readBtsConfig } from "./errors-
|
|
2
|
+
import { a as handleError, m as readBtsConfig } from "./errors-Cyol8zbN.mjs";
|
|
3
3
|
import { t as renderTitle } from "./render-title-zvyKC1ej.mjs";
|
|
4
|
-
import { t as runGeneratedChecks } from "./generated-checks-
|
|
4
|
+
import { t as runGeneratedChecks } from "./generated-checks-C8hn9w2i.mjs";
|
|
5
5
|
import { intro, log, spinner } from "@clack/prompts";
|
|
6
6
|
import pc from "picocolors";
|
|
7
7
|
import fs from "fs-extra";
|
|
@@ -73,7 +73,7 @@ const TOOL_LABELS = {
|
|
|
73
73
|
"spring-data-jpa": "Spring Data JPA"
|
|
74
74
|
};
|
|
75
75
|
function getSelectedGraphParts(config) {
|
|
76
|
-
return (config.stackParts ?? []).filter((part) => part.source !== "provided");
|
|
76
|
+
return (config.stackParts ?? []).filter((part) => part.source !== "provided" && part.toolId !== "none");
|
|
77
77
|
}
|
|
78
78
|
function getGraphPart(config, role, ecosystem) {
|
|
79
79
|
return getSelectedGraphParts(config).find((part) => part.role === role && (!ecosystem || part.ecosystem === ecosystem));
|
|
@@ -244,6 +244,22 @@ const MOBILE_CONFIG_FIELDS = [
|
|
|
244
244
|
"mobileOTA",
|
|
245
245
|
"mobileDeepLinking"
|
|
246
246
|
];
|
|
247
|
+
const MOBILE_CONFIG_FIELD_ROLES = {
|
|
248
|
+
mobileNavigation: "navigation",
|
|
249
|
+
mobileUI: "ui",
|
|
250
|
+
mobileStorage: "storage",
|
|
251
|
+
mobileTesting: "testing",
|
|
252
|
+
mobilePush: "push",
|
|
253
|
+
mobileOTA: "ota",
|
|
254
|
+
mobileDeepLinking: "deepLinking"
|
|
255
|
+
};
|
|
256
|
+
function hasExplicitMobileGraphField(stackParts, field) {
|
|
257
|
+
const role = MOBILE_CONFIG_FIELD_ROLES[field];
|
|
258
|
+
return stackParts.some((part) => {
|
|
259
|
+
if (part.source === "provided" || part.role !== role || part.ecosystem !== "react-native") return false;
|
|
260
|
+
return (part.ownerPartId ? stackParts.find((candidate) => candidate.id === part.ownerPartId) : void 0)?.role === "mobile";
|
|
261
|
+
});
|
|
262
|
+
}
|
|
247
263
|
function normalizeGraphConfigForPersistence(projectConfig, stackParts) {
|
|
248
264
|
if (!stackParts) return projectConfig;
|
|
249
265
|
const legacyConfig = (0, types_exports.stackPartsToLegacyProjectConfigPartial)(stackParts);
|
|
@@ -349,7 +365,7 @@ function normalizeGraphConfigForPersistence(projectConfig, stackParts) {
|
|
|
349
365
|
if (hasNativeFrontend) for (const field of MOBILE_CONFIG_FIELDS) {
|
|
350
366
|
const projectValue = projectConfig[field];
|
|
351
367
|
const legacyValue = legacyConfig[field];
|
|
352
|
-
if (projectValue !== void 0 && projectValue !== "none" && legacyValue === "none") normalized[field] = projectValue;
|
|
368
|
+
if (projectValue !== void 0 && projectValue !== "none" && legacyValue === "none" && !hasExplicitMobileGraphField(stackParts, field)) normalized[field] = projectValue;
|
|
353
369
|
}
|
|
354
370
|
if (!selectedEcosystems.has("react-native") && !hasNativeFrontend) {
|
|
355
371
|
normalized.mobileNavigation = "none";
|