From 6f525185ab7bdc0768ff4fd948e9859a5e8fa8ab Mon Sep 17 00:00:00 2001 From: Frank Calise Date: Thu, 25 Jul 2024 17:07:14 -0400 Subject: [PATCH 01/14] feat(cli): experimental expo-router flag --- boilerplate/.gitignore | 1 + boilerplate/app/components/ListItem.tsx | 9 +- boilerplate/src/app/_layout.tsx | 45 ++++++++++ boilerplate/src/app/index.tsx | 76 +++++++++++++++++ boilerplate/tsconfig.json | 2 +- src/commands/new.ts | 105 ++++++++++++++++++++++-- 6 files changed, 227 insertions(+), 11 deletions(-) create mode 100644 boilerplate/src/app/_layout.tsx create mode 100644 boilerplate/src/app/index.tsx diff --git a/boilerplate/.gitignore b/boilerplate/.gitignore index a5c60cce3..19458b4ce 100644 --- a/boilerplate/.gitignore +++ b/boilerplate/.gitignore @@ -71,6 +71,7 @@ buck-out/ bin/Exponent.app /android /ios +expo-env.d.ts ## Secrets npm-debug.* diff --git a/boilerplate/app/components/ListItem.tsx b/boilerplate/app/components/ListItem.tsx index 31e422cdd..ad9ae8e13 100644 --- a/boilerplate/app/components/ListItem.tsx +++ b/boilerplate/app/components/ListItem.tsx @@ -104,7 +104,10 @@ interface ListItemActionProps { * @param {ListItemProps} props - The props for the `ListItem` component. * @returns {JSX.Element} The rendered `ListItem` component. */ -export function ListItem(props: ListItemProps) { +export const ListItem = React.forwardRef(function ListItem( + props: ListItemProps, + ref, +) { const { bottomSeparator, children, @@ -138,7 +141,7 @@ export function ListItem(props: ListItemProps) { const $touchableStyles = [$styles.row, $touchableStyle, { minHeight: height }, style] return ( - + ) -} +}) /** * @param {ListItemActionProps} props - The props for the `ListItemAction` component. diff --git a/boilerplate/src/app/_layout.tsx b/boilerplate/src/app/_layout.tsx new file mode 100644 index 000000000..71ef7e495 --- /dev/null +++ b/boilerplate/src/app/_layout.tsx @@ -0,0 +1,45 @@ +import React from "react" +import { Slot, SplashScreen } from "expo-router" +import { GestureHandlerRootView } from "react-native-gesture-handler" +import { useInitialRootStore } from "src/models" +import { useFonts } from "@expo-google-fonts/space-grotesk" +import { customFontsToLoad } from "src/theme" + +SplashScreen.preventAutoHideAsync() + +if (__DEV__) { + // Load Reactotron configuration in development. We don't want to + // include this in our production bundle, so we are using `if (__DEV__)` + // to only execute this in development. + require("src/devtools/ReactotronConfig.ts") +} + +export { ErrorBoundary } from "src/components/ErrorBoundary/ErrorBoundary" + +export default function Root() { + // Wait for stores to load and render our layout inside of it so we have access + // to auth info etc + const { rehydrated } = useInitialRootStore() + + const [fontsLoaded, fontError] = useFonts(customFontsToLoad) + + const loaded = fontsLoaded && rehydrated + + React.useEffect(() => { + if (fontError) throw fontError + }, [fontError]) + + React.useEffect(() => { + if (loaded) { + SplashScreen.hideAsync() + } + }, [loaded]) + + if (!loaded) { + return null + } + + return +} + +const $root: ViewStyle = { flex: 1 } \ No newline at end of file diff --git a/boilerplate/src/app/index.tsx b/boilerplate/src/app/index.tsx new file mode 100644 index 000000000..441faf865 --- /dev/null +++ b/boilerplate/src/app/index.tsx @@ -0,0 +1,76 @@ +import { observer } from "mobx-react-lite" +import React from "react" +import { Image, ImageStyle, TextStyle, View, ViewStyle } from "react-native" +import { Text } from "src/components" +import { isRTL } from "../i18n" +import { colors, spacing } from "../theme" +import { useSafeAreaInsetsStyle } from "../utils/useSafeAreaInsetsStyle" + +const welcomeLogo = require("../../assets/images/logo.png") +const welcomeFace = require("../../assets/images/welcome-face.png") + +export default observer(function WelcomeScreen() { + const $bottomContainerInsets = useSafeAreaInsetsStyle(["bottom"]) + + return ( + + + + + + + + + + + + + ) +}) + +const $container: ViewStyle = { + flex: 1, + backgroundColor: colors.background, +} + +const $topContainer: ViewStyle = { + flexShrink: 1, + flexGrow: 1, + flexBasis: "57%", + justifyContent: "center", + paddingHorizontal: spacing.lg, +} + +const $bottomContainer: ViewStyle = { + flexShrink: 1, + flexGrow: 0, + flexBasis: "43%", + backgroundColor: colors.palette.neutral100, + borderTopLeftRadius: 16, + borderTopRightRadius: 16, + paddingHorizontal: spacing.lg, + justifyContent: "space-around", +} +const $welcomeLogo: ImageStyle = { + height: 88, + width: "100%", + marginBottom: spacing.xxl, +} + +const $welcomeFace: ImageStyle = { + height: 169, + width: 269, + position: "absolute", + bottom: -47, + right: -80, + transform: [{ scaleX: isRTL ? -1 : 1 }], +} + +const $welcomeHeading: TextStyle = { + marginBottom: spacing.md, +} diff --git a/boilerplate/tsconfig.json b/boilerplate/tsconfig.json index 6cd07d5ff..635f68baa 100644 --- a/boilerplate/tsconfig.json +++ b/boilerplate/tsconfig.json @@ -32,6 +32,6 @@ "module": "commonjs" } }, - "include": ["index.js", "App.tsx", "app", "types", "plugins", "app.config.ts"], + "include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"], "exclude": ["node_modules", "test/**/*"] } diff --git a/src/commands/new.ts b/src/commands/new.ts index b9f0e24ce..716e60124 100644 --- a/src/commands/new.ts +++ b/src/commands/new.ts @@ -451,6 +451,7 @@ module.exports = { // #region Experimental Features parsing let newArch let expoVersion + let expoRouter = false const experimentalFlags = options.experimental?.split(",") ?? [] log(`experimentalFlags: ${experimentalFlags}`) @@ -458,7 +459,15 @@ module.exports = { if (flag === "new-arch") { newArch = true } else if (flag.indexOf("expo-") > -1) { - expoVersion = flag.substring(5) + if (flag !== "expo-router") { + expoVersion = flag.substring(5) + } else { + // user wants to convert to expo-router + // force demo code removal for easier conversion + // maybe one day convert the demo app + expoRouter = true + removeDemo = true + } } }) // #endregion @@ -568,7 +577,29 @@ module.exports = { .replace(/HelloWorld/g, projectName) .replace(/hello-world/g, projectNameKebab) - // - If we need native dirs, change up start scripts from Expo Go variation to expo run:platform. + // add in expo-router package + if (expoRouter) { + // find "expo-localization" line and append "expo-router" line after it + packageJsonRaw = packageJsonRaw.replace( + /"expo-localization": ".*",/g, + `"expo-localization": "~15.0.3",${EOL} "expo-router": "~3.5.17",`, + ) + + // replace "main" entry point from App.js to "expo-router/entry" + packageJsonRaw = packageJsonRaw.replace(/"main": ".*",/g, `"main": "expo-router/entry",`) + + // replace format and lint scripts + packageJsonRaw = packageJsonRaw.replace( + /"format": ".*",/g, + `"format": "prettier --write \\"src/**/*.{js,jsx,json,md,ts,tsx}\\"",`, + ) + packageJsonRaw = packageJsonRaw.replace( + /"lint": ".*",/g, + `"lint": "eslint src test --fix --ext .js,.ts,.tsx && npm run format",`, + ) + } + + // If we need native dirs, change up start scripts from Expo Go variation to expo run:platform. packageJsonRaw = packageJsonRaw .replace(/start --android/g, "run:android") .replace(/start --ios/g, "run:ios") @@ -583,7 +614,7 @@ module.exports = { packageJsonRaw = packageJsonRaw.replace(/"expo": ".*"/g, `"expo": "${tagVersion}"`) } - // - If we're removing the demo code, clean up some dependencies that are no longer needed + // If we're removing the demo code, clean up some dependencies that are no longer needed if (removeDemo) { log(`Removing demo dependencies... ${demoDependenciesToRemove.join(", ")}`) packageJsonRaw = findAndRemoveDependencies(packageJsonRaw, demoDependenciesToRemove) @@ -594,9 +625,8 @@ module.exports = { packageJsonRaw = findAndRemoveDependencies(packageJsonRaw, mstDependenciesToRemove) } - // - Then write it back out. + // Then write it back out. const packageJson = JSON.parse(packageJsonRaw) - write("./package.json", packageJson) // #endregion @@ -673,8 +703,8 @@ module.exports = { startSpinner(unboxingMessage) await packager.install({ ...packagerOptions, onProgress: log }) - // if we're using the canary build, we need to install the canary versions of supporting Expo packages - if (expoVersion) { + // if we're using the Expo Go or canary build, we need to install the canary versions of supporting Expo packages + if (expoVersion || workflow === "expo") { await system.run("npx expo install --fix", { onProgress: log }) } stopSpinner(unboxingMessage, "🧶") @@ -713,6 +743,11 @@ module.exports = { appJson.expo.plugins[1][1].android.newArchEnabled = true } + if (expoRouter) { + appJson.expo.experiments.typedRoutes = true + appJson.expo.plugins.push("expo-router") + } + write("./app.json", appJson) } catch (e) { log(e) @@ -787,7 +822,63 @@ module.exports = { } stopSpinner(`Removing MobX-State-Tree code`, "🛠️") } + // #endregion + + // #region Expo Router edits + /** + * instructions mostly adapted from https://ignitecookbook.com/docs/recipes/ExpoRouter + * TODO + * fix generators + * remove the resetNavigation command from reactotronConfig + */ + if (expoRouter) { + // mv ALL files under app/ to src/ + await system.run(log(`mv app/* src/`)) + + // replace app/ with src/ in files + const expoRouterFilesToFix = [ + "tsconfig.json", + "test/i18n.test.ts", + "src/devtools/ReactotronConfig.ts", + "src/components/Toggle.tsx", + "src/components/ListView.tsx", + "src/screens/WelcomeScreen.tsx", + ] + expoRouterFilesToFix.forEach((file) => { + const filePath = path(targetPath, file) + let fileContents = read(filePath) + fileContents = fileContents.replace(/app\//g, "src/") + write(filePath, fileContents) + }) + // work in reactotron custom commands + const reactotronConfigPath = path(targetPath, "src/devtools/ReactotronConfig.ts") + let reactotronConfig = read(reactotronConfigPath) + reactotronConfig = reactotronConfig + .replace( + /import { goBack, resetRoot, navigate }.*/g, + 'import { router } from "expo-router"', + ) + .replace(/navigate\(route as any\).*/g, "router.push(route)") + .replace(/goBack\(\).*/g, " router.back()") + + write(reactotronConfigPath, reactotronConfig) + + // some clean up + await system.run( + log(` + \\rm src/app.tsx + mkdir src/components/ErrorBoundary + mv src/screens/ErrorScreen/* src/components/ErrorBoundary + rm -rf src/screens + rm -rf src/navigators + rm -rf app + `), + ) + } else { + // remove src/ dir since not using expo-router + await system.run(log(`rm -rf src`)) + } // #endregion // #region Format generator templates EOL for Windows From 7c883d001c79bdfdc097b1c5ca71fe7c608c85a4 Mon Sep 17 00:00:00 2001 From: Frank Calise Date: Sun, 4 Aug 2024 11:11:38 -0400 Subject: [PATCH 02/14] chore: todo notes for expo router screen gen --- src/commands/new.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/new.ts b/src/commands/new.ts index 716e60124..8e513788b 100644 --- a/src/commands/new.ts +++ b/src/commands/new.ts @@ -828,7 +828,7 @@ module.exports = { /** * instructions mostly adapted from https://ignitecookbook.com/docs/recipes/ExpoRouter * TODO - * fix generators + * set up a proper screen generator (depends on PR #2726) * remove the resetNavigation command from reactotronConfig */ if (expoRouter) { From 4a1fa9c3707e5570955f3e8dff022b4752051d2e Mon Sep 17 00:00:00 2001 From: Frank Calise Date: Sun, 4 Aug 2024 11:36:56 -0400 Subject: [PATCH 03/14] fix(cli): always do expo install --fix check --- src/commands/new.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/commands/new.ts b/src/commands/new.ts index 8e513788b..f74339de5 100644 --- a/src/commands/new.ts +++ b/src/commands/new.ts @@ -701,12 +701,12 @@ module.exports = { if (shouldFreshInstallDeps) { const unboxingMessage = `Installing ${packagerName} dependencies (wow these are heavy)` startSpinner(unboxingMessage) + + // do base install await packager.install({ ...packagerOptions, onProgress: log }) + // now that expo is installed, we can run their install --fix for best Expo SDK compatibility + await system.run("npx expo install --fix", { onProgress: log }) - // if we're using the Expo Go or canary build, we need to install the canary versions of supporting Expo packages - if (expoVersion || workflow === "expo") { - await system.run("npx expo install --fix", { onProgress: log }) - } stopSpinner(unboxingMessage, "🧶") } From 05f8d967b26af4e7421f6d2c6970d9b5b55ebd92 Mon Sep 17 00:00:00 2001 From: Frank Calise Date: Mon, 5 Aug 2024 12:07:37 -0400 Subject: [PATCH 04/14] fix(cli): prompt about expo-router, fix exp flags output in buildCliCommand --- src/commands/new.ts | 50 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/src/commands/new.ts b/src/commands/new.ts index f74339de5..db694a5a7 100644 --- a/src/commands/new.ts +++ b/src/commands/new.ts @@ -451,7 +451,7 @@ module.exports = { // #region Experimental Features parsing let newArch let expoVersion - let expoRouter = false + let expoRouter const experimentalFlags = options.experimental?.split(",") ?? [] log(`experimentalFlags: ${experimentalFlags}`) @@ -474,6 +474,26 @@ module.exports = { // #region Prompt to enable experimental features + // Expo Router + const defaultExpoRouter = false + let experimentalExpoRouter = useDefault(expoRouter) ? defaultExpoRouter : boolFlag(expoRouter) + if (experimentalExpoRouter === undefined) { + const expoRouterResponse = await prompt.ask<{ experimentalExpoRouter: boolean }>(() => ({ + type: "confirm", + name: "experimentalExpoRouter", + message: "❗EXPERIMENTAL❗Would you use Expo Router for navigation?", + initial: defaultExpoRouter, + format: prettyPrompt.format.boolean, + prefix, + })) + experimentalExpoRouter = expoRouterResponse.experimentalExpoRouter + + // update experimental flags if needed for buildCliCommand output + if (experimentalExpoRouter && !experimentalFlags.includes("expo-router")) { + experimentalFlags.push("expo-router") + } + } + // New Architecture const defaultNewArch = false let experimentalNewArch = useDefault(newArch) ? defaultNewArch : boolFlag(newArch) @@ -487,7 +507,12 @@ module.exports = { prefix, })) experimentalNewArch = newArchResponse.experimentalNewArch + // update experimental flags if needed for buildCliCommand output + if (experimentalNewArch && !experimentalFlags.includes("new-arch")) { + experimentalFlags.push("new-arch") + } } + // #endregion // #region Debug @@ -578,7 +603,7 @@ module.exports = { .replace(/hello-world/g, projectNameKebab) // add in expo-router package - if (expoRouter) { + if (experimentalExpoRouter) { // find "expo-localization" line and append "expo-router" line after it packageJsonRaw = packageJsonRaw.replace( /"expo-localization": ".*",/g, @@ -743,7 +768,7 @@ module.exports = { appJson.expo.plugins[1][1].android.newArchEnabled = true } - if (expoRouter) { + if (experimentalExpoRouter) { appJson.expo.experiments.typedRoutes = true appJson.expo.plugins.push("expo-router") } @@ -753,7 +778,7 @@ module.exports = { log(e) p(yellow("Unable to configure app.json.")) } - stopSpinner(" Configuring app.json", "") + stopSpinner(" Configuring app.json", "⚙️") // #endregion // #region Run Format @@ -831,8 +856,10 @@ module.exports = { * set up a proper screen generator (depends on PR #2726) * remove the resetNavigation command from reactotronConfig */ - if (expoRouter) { + if (experimentalExpoRouter) { // mv ALL files under app/ to src/ + const expoRouterMsg = " Recalibrating compass with Expo Router" + startSpinner(expoRouterMsg) await system.run(log(`mv app/* src/`)) // replace app/ with src/ in files @@ -840,15 +867,19 @@ module.exports = { "tsconfig.json", "test/i18n.test.ts", "src/devtools/ReactotronConfig.ts", - "src/components/Toggle.tsx", + "src/components/Toggle/Switch.tsx", "src/components/ListView.tsx", "src/screens/WelcomeScreen.tsx", ] expoRouterFilesToFix.forEach((file) => { const filePath = path(targetPath, file) let fileContents = read(filePath) - fileContents = fileContents.replace(/app\//g, "src/") - write(filePath, fileContents) + try { + fileContents = fileContents.replace(/app\//g, "src/") + write(filePath, fileContents) + } catch (e) { + log(`Unable to locate ${file}.`) + } }) // work in reactotron custom commands @@ -875,6 +906,7 @@ module.exports = { rm -rf app `), ) + stopSpinner(expoRouterMsg, "🧭") } else { // remove src/ dir since not using expo-router await system.run(log(`rm -rf src`)) @@ -977,7 +1009,7 @@ module.exports = { toolbox, }) - p2(`For next time: here are the Ignite options you picked!`) + p2(`For next time, here are the Ignite options you picked:`) // create a multi-line string of the command, where each --flag is on it's own line const prettyCliCommand = cliCommand From 599f1f21a0d2f229baac3a3a056a5820dea4a1a2 Mon Sep 17 00:00:00 2001 From: Frank Calise Date: Tue, 6 Aug 2024 17:36:57 -0400 Subject: [PATCH 05/14] fix(expo-router): mst directives for template file --- boilerplate/src/app/_layout.tsx | 3 +++ boilerplate/src/app/index.tsx | 3 +++ 2 files changed, 6 insertions(+) diff --git a/boilerplate/src/app/_layout.tsx b/boilerplate/src/app/_layout.tsx index 71ef7e495..ef0b46cca 100644 --- a/boilerplate/src/app/_layout.tsx +++ b/boilerplate/src/app/_layout.tsx @@ -1,6 +1,7 @@ import React from "react" import { Slot, SplashScreen } from "expo-router" import { GestureHandlerRootView } from "react-native-gesture-handler" +// @mst replace-next-line import { useInitialRootStore } from "src/models" import { useFonts } from "@expo-google-fonts/space-grotesk" import { customFontsToLoad } from "src/theme" @@ -17,9 +18,11 @@ if (__DEV__) { export { ErrorBoundary } from "src/components/ErrorBoundary/ErrorBoundary" export default function Root() { + // @mst remove-block-start // Wait for stores to load and render our layout inside of it so we have access // to auth info etc const { rehydrated } = useInitialRootStore() + // @mst remove-block-end const [fontsLoaded, fontError] = useFonts(customFontsToLoad) diff --git a/boilerplate/src/app/index.tsx b/boilerplate/src/app/index.tsx index 441faf865..29f0eceaf 100644 --- a/boilerplate/src/app/index.tsx +++ b/boilerplate/src/app/index.tsx @@ -1,3 +1,4 @@ +// @mst replace-next-line import { observer } from "mobx-react-lite" import React from "react" import { Image, ImageStyle, TextStyle, View, ViewStyle } from "react-native" @@ -9,6 +10,7 @@ import { useSafeAreaInsetsStyle } from "../utils/useSafeAreaInsetsStyle" const welcomeLogo = require("../../assets/images/logo.png") const welcomeFace = require("../../assets/images/welcome-face.png") +// @mst replace-next-line export default function WelcomeScreen() { export default observer(function WelcomeScreen() { const $bottomContainerInsets = useSafeAreaInsetsStyle(["bottom"]) @@ -31,6 +33,7 @@ export default observer(function WelcomeScreen() { ) +// @mst replace-next-line { }) const $container: ViewStyle = { From ec793535493e31f8b288a44676931b4fe1739b6a Mon Sep 17 00:00:00 2001 From: Frank Calise Date: Tue, 6 Aug 2024 19:01:56 -0400 Subject: [PATCH 06/14] fix(cli): updates for mst markup --- boilerplate/src/app/_layout.tsx | 1 + boilerplate/src/app/index.tsx | 2 +- src/commands/new.ts | 83 +++++++++++++++++-------------- src/commands/remove-mst-markup.ts | 3 +- 4 files changed, 51 insertions(+), 38 deletions(-) diff --git a/boilerplate/src/app/_layout.tsx b/boilerplate/src/app/_layout.tsx index ef0b46cca..cf4c64523 100644 --- a/boilerplate/src/app/_layout.tsx +++ b/boilerplate/src/app/_layout.tsx @@ -1,4 +1,5 @@ import React from "react" +import { ViewStyle } from "react-native" import { Slot, SplashScreen } from "expo-router" import { GestureHandlerRootView } from "react-native-gesture-handler" // @mst replace-next-line diff --git a/boilerplate/src/app/index.tsx b/boilerplate/src/app/index.tsx index 29f0eceaf..3e5604017 100644 --- a/boilerplate/src/app/index.tsx +++ b/boilerplate/src/app/index.tsx @@ -33,7 +33,7 @@ export default observer(function WelcomeScreen() { ) -// @mst replace-next-line { +// @mst replace-next-line } }) const $container: ViewStyle = { diff --git a/src/commands/new.ts b/src/commands/new.ts index db694a5a7..eb8477e57 100644 --- a/src/commands/new.ts +++ b/src/commands/new.ts @@ -370,7 +370,6 @@ module.exports = { p(yellow(`Setting includeMST to true.`)) includeMST = true } - // #endregion // #region Packager @@ -814,41 +813,6 @@ module.exports = { stopSpinner(` Removing fancy demo ${removeDemoPart}`, "🛠️") // #endregion - // #region Remove MST code - if (includeMST) { - // remove MST markup only - startSpinner(`Removing MobX-State-Tree markup`) - try { - const IGNITE = "node " + filesystem.path(__dirname, "..", "..", "bin", "ignite") - log(`Ignite bin path: ${IGNITE}`) - await system.run(`${IGNITE} remove-mst-markup "${targetPath}"`, { - onProgress: log, - }) - } catch (e) { - log(e) - p(yellow(`Unable to remove MobX-State-Tree markup`)) - } - stopSpinner(`Removing MobX-State-Tree markup`, "🛠️") - } else { - startSpinner(`Removing MobX-State-Tree code`) - try { - const IGNITE = "node " + filesystem.path(__dirname, "..", "..", "bin", "ignite") - log(`Ignite bin path: ${IGNITE}`) - await system.run(`${IGNITE} remove-mst "${targetPath}"`, { - onProgress: log, - }) - } catch (e) { - log(e) - p( - yellow( - `Unable to remove MobX-State-Tree code. To perform updates manually, check out the recipe with full instructions: https://ignitecookbook.com/docs/recipes/RemoveMobxStateTree`, - ), - ) - } - stopSpinner(`Removing MobX-State-Tree code`, "🛠️") - } - // #endregion - // #region Expo Router edits /** * instructions mostly adapted from https://ignitecookbook.com/docs/recipes/ExpoRouter @@ -893,6 +857,11 @@ module.exports = { .replace(/navigate\(route as any\).*/g, "router.push(route)") .replace(/goBack\(\).*/g, " router.back()") + // Define the custom command to be removed using a regular expression + const customCommandToRemoveRegex = + /reactotron\.onCustomCommand\({\s*title: "Reset Navigation State",\s*description: "Resets the navigation state",\s*command: "resetNavigation",\s*handler: \(\) => {\s*Reactotron\.log\("resetting navigation state"\)\s*resetRoot\({ index: 0, routes: \[\] }\)\s*},\s*}\),?\n?/g + reactotronConfig = reactotronConfig.replace(customCommandToRemoveRegex, "") + write(reactotronConfigPath, reactotronConfig) // some clean up @@ -913,6 +882,48 @@ module.exports = { } // #endregion + // #region Remove MST code + if (includeMST) { + // remove MST markup only + startSpinner(`Removing MobX-State-Tree markup`) + try { + const IGNITE = "node " + filesystem.path(__dirname, "..", "..", "bin", "ignite") + log(`Ignite bin path: ${IGNITE}`) + await system.run( + `${IGNITE} remove-mst-markup "${targetPath}" "${ + experimentalExpoRouter ? "src" : "app" + }"`, + { + onProgress: log, + }, + ) + } catch (e) { + log(e) + p(yellow(`Unable to remove MobX-State-Tree markup`)) + } + stopSpinner(`Removing MobX-State-Tree markup`, "🌳") + } else { + startSpinner(`Removing MobX-State-Tree code`) + try { + const IGNITE = "node " + filesystem.path(__dirname, "..", "..", "bin", "ignite") + log(`Ignite bin path: ${IGNITE}`) + await system.run( + `${IGNITE} remove-mst "${targetPath}" "${experimentalExpoRouter ? "src" : "app"}"`, + { + onProgress: log, + }, + ) + } catch (e) { + log(e) + p( + yellow( + `Unable to remove MobX-State-Tree code. To perform updates manually, check out the recipe with full instructions: https://ignitecookbook.com/docs/recipes/RemoveMobxStateTree`, + ), + ) + } + stopSpinner(`Removing MobX-State-Tree code`, "🌳") + } + // #region Format generator templates EOL for Windows let warnAboutEOL = false if (isWindows) { diff --git a/src/commands/remove-mst-markup.ts b/src/commands/remove-mst-markup.ts index 33252f86d..cfc0b89ae 100644 --- a/src/commands/remove-mst-markup.ts +++ b/src/commands/remove-mst-markup.ts @@ -13,6 +13,7 @@ module.exports = { const CWD = process.cwd() const TARGET_DIR = parameters.first ?? CWD + const SRC_DIR = parameters.second ?? "app" const dryRun = boolFlag(parameters.options.dryRun) ?? false p() @@ -54,7 +55,7 @@ module.exports = { // Run prettier at the end to clean up any spacing issues if (!dryRun) { p(`Running prettier to clean up code formatting`) - await system.run(`npx prettier@2.8.8 --write "./app/**/*.{js,jsx,json,md,ts,tsx}"`, { + await system.run(`npx prettier@2.8.8 --write "./${SRC_DIR}/**/*.{js,jsx,json,md,ts,tsx}"`, { trim: true, cwd: TARGET_DIR, }) From 09e5d338da3c302bb4357830991c0abaaa94b5dd Mon Sep 17 00:00:00 2001 From: Frank Calise Date: Wed, 7 Aug 2024 07:28:02 -0400 Subject: [PATCH 07/14] fix(cli): remove-mst src dir param --- src/commands/remove-mst.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/commands/remove-mst.ts b/src/commands/remove-mst.ts index c6fdadf12..3eb4785a8 100644 --- a/src/commands/remove-mst.ts +++ b/src/commands/remove-mst.ts @@ -15,6 +15,7 @@ module.exports = { const CWD = process.cwd() const TARGET_DIR = parameters.first ?? CWD + const SRC_DIR = parameters.second ?? "app" const dryRun = boolFlag(parameters.options.dryRun) ?? false p() @@ -71,7 +72,7 @@ module.exports = { // Run prettier at the end to clean up any spacing issues if (!dryRun) { p(`Running prettier to clean up code formatting`) - await system.run(`npx prettier@2.8.8 --write "./app/**/*.{js,jsx,json,md,ts,tsx}"`, { + await system.run(`npx prettier@2.8.8 --write "./${SRC_DIR}/**/*.{js,jsx,json,md,ts,tsx}"`, { trim: true, cwd: TARGET_DIR, }) From 1d62ea265237e343c16d1834ffe62b00e27502fb Mon Sep 17 00:00:00 2001 From: Frank Calise Date: Wed, 7 Aug 2024 07:29:32 -0400 Subject: [PATCH 08/14] fix(cli): update generators for expo-router and obey --mst --- src/commands/new.ts | 56 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 46 insertions(+), 10 deletions(-) diff --git a/src/commands/new.ts b/src/commands/new.ts index eb8477e57..a9e4a44a5 100644 --- a/src/commands/new.ts +++ b/src/commands/new.ts @@ -367,7 +367,7 @@ module.exports = { if (!removeDemo && includeMST === false) { p() p(yellow(`Warning: You can't remove MobX-State-Tree code without removing demo code.`)) - p(yellow(`Setting includeMST to true.`)) + p(yellow(`Setting --mst=true`)) includeMST = true } // #endregion @@ -465,7 +465,17 @@ module.exports = { // force demo code removal for easier conversion // maybe one day convert the demo app expoRouter = true - removeDemo = true + + if (!removeDemo) { + p() + p( + yellow( + `Enabling Expo Router will currently remove the demo application. To continue with the demo app, check out the recipe with full instructions: https://ignitecookbook.com/docs/recipes/ExpoRouter`, + ), + ) + p(yellow(`Setting --remove-demo=true`)) + removeDemo = true + } } } }) @@ -814,12 +824,7 @@ module.exports = { // #endregion // #region Expo Router edits - /** - * instructions mostly adapted from https://ignitecookbook.com/docs/recipes/ExpoRouter - * TODO - * set up a proper screen generator (depends on PR #2726) - * remove the resetNavigation command from reactotronConfig - */ + // instructions mostly adapted from https://ignitecookbook.com/docs/recipes/ExpoRouter if (experimentalExpoRouter) { // mv ALL files under app/ to src/ const expoRouterMsg = " Recalibrating compass with Expo Router" @@ -834,6 +839,8 @@ module.exports = { "src/components/Toggle/Switch.tsx", "src/components/ListView.tsx", "src/screens/WelcomeScreen.tsx", + "ignite/templates/model/NAME.ts.ejs", + "ignite/templates/component/NAME.tsx.ejs", ] expoRouterFilesToFix.forEach((file) => { const filePath = path(targetPath, file) @@ -846,7 +853,7 @@ module.exports = { } }) - // work in reactotron custom commands + // update reactotron custom commands const reactotronConfigPath = path(targetPath, "src/devtools/ReactotronConfig.ts") let reactotronConfig = read(reactotronConfigPath) reactotronConfig = reactotronConfig @@ -857,19 +864,48 @@ module.exports = { .replace(/navigate\(route as any\).*/g, "router.push(route)") .replace(/goBack\(\).*/g, " router.back()") - // Define the custom command to be removed using a regular expression + // this one gets removed entirely const customCommandToRemoveRegex = /reactotron\.onCustomCommand\({\s*title: "Reset Navigation State",\s*description: "Resets the navigation state",\s*command: "resetNavigation",\s*handler: \(\) => {\s*Reactotron\.log\("resetting navigation state"\)\s*resetRoot\({ index: 0, routes: \[\] }\)\s*},\s*}\),?\n?/g reactotronConfig = reactotronConfig.replace(customCommandToRemoveRegex, "") write(reactotronConfigPath, reactotronConfig) + // rewrite the screens generator to something more useful + try { + const filePath = path(targetPath, "ignite/templates/screen/NAME.tsx.ejs") + const EXPO_ROUTER_SCREEN_TPL = `import React, { FC } from "react" +import { observer } from "mobx-react-lite" +import { ViewStyle } from "react-native" +import { Screen, Text } from "src/components" + +// @mst replace-next-line export default function <%= props.pascalCaseName %>Screen() { +export default observer(function <%= props.pascalCaseName %>Screen() { + return ( + + + + ) +// @mst replace-next-line } +}) + +const $root: ViewStyle = { + flex: 1, +} +` + write(filePath, EXPO_ROUTER_SCREEN_TPL) + } catch (e) { + log(`Unable to write screen generator template.`) + } + // some clean up await system.run( log(` \\rm src/app.tsx mkdir src/components/ErrorBoundary mv src/screens/ErrorScreen/* src/components/ErrorBoundary + rm ignite/templates/screen/NAMEScreen.tsx.ejs + rm -rf ignite/templates/navigator rm -rf src/screens rm -rf src/navigators rm -rf app From 1f6faa736934bf341d18f8bdf8fe1be0e4250ce6 Mon Sep 17 00:00:00 2001 From: Frank Calise Date: Wed, 7 Aug 2024 08:03:06 -0400 Subject: [PATCH 09/14] fix(cli): clean up cmd with helpers --- src/commands/new.ts | 99 +++++------------------------ src/tools/react-native.ts | 127 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+), 82 deletions(-) diff --git a/src/commands/new.ts b/src/commands/new.ts index a9e4a44a5..40c41228e 100644 --- a/src/commands/new.ts +++ b/src/commands/new.ts @@ -5,6 +5,10 @@ import { copyBoilerplate, renameReactNativeApp, replaceMaestroBundleIds, + createExpoRouterScreenTemplate, + refactorExpoRouterReactotronCmds, + updateExpoRouterSrcDir, + cleanupExpoRouterConversion, } from "../tools/react-native" import { packager, PackagerName } from "../tools/packager" import { @@ -824,93 +828,24 @@ module.exports = { // #endregion // #region Expo Router edits - // instructions mostly adapted from https://ignitecookbook.com/docs/recipes/ExpoRouter if (experimentalExpoRouter) { - // mv ALL files under app/ to src/ const expoRouterMsg = " Recalibrating compass with Expo Router" startSpinner(expoRouterMsg) - await system.run(log(`mv app/* src/`)) - - // replace app/ with src/ in files - const expoRouterFilesToFix = [ - "tsconfig.json", - "test/i18n.test.ts", - "src/devtools/ReactotronConfig.ts", - "src/components/Toggle/Switch.tsx", - "src/components/ListView.tsx", - "src/screens/WelcomeScreen.tsx", - "ignite/templates/model/NAME.ts.ejs", - "ignite/templates/component/NAME.tsx.ejs", - ] - expoRouterFilesToFix.forEach((file) => { - const filePath = path(targetPath, file) - let fileContents = read(filePath) - try { - fileContents = fileContents.replace(/app\//g, "src/") - write(filePath, fileContents) - } catch (e) { - log(`Unable to locate ${file}.`) - } - }) - - // update reactotron custom commands - const reactotronConfigPath = path(targetPath, "src/devtools/ReactotronConfig.ts") - let reactotronConfig = read(reactotronConfigPath) - reactotronConfig = reactotronConfig - .replace( - /import { goBack, resetRoot, navigate }.*/g, - 'import { router } from "expo-router"', - ) - .replace(/navigate\(route as any\).*/g, "router.push(route)") - .replace(/goBack\(\).*/g, " router.back()") - - // this one gets removed entirely - const customCommandToRemoveRegex = - /reactotron\.onCustomCommand\({\s*title: "Reset Navigation State",\s*description: "Resets the navigation state",\s*command: "resetNavigation",\s*handler: \(\) => {\s*Reactotron\.log\("resetting navigation state"\)\s*resetRoot\({ index: 0, routes: \[\] }\)\s*},\s*}\),?\n?/g - reactotronConfig = reactotronConfig.replace(customCommandToRemoveRegex, "") - write(reactotronConfigPath, reactotronConfig) - - // rewrite the screens generator to something more useful - try { - const filePath = path(targetPath, "ignite/templates/screen/NAME.tsx.ejs") - const EXPO_ROUTER_SCREEN_TPL = `import React, { FC } from "react" -import { observer } from "mobx-react-lite" -import { ViewStyle } from "react-native" -import { Screen, Text } from "src/components" - -// @mst replace-next-line export default function <%= props.pascalCaseName %>Screen() { -export default observer(function <%= props.pascalCaseName %>Screen() { - return ( - - - - ) -// @mst replace-next-line } -}) - -const $root: ViewStyle = { - flex: 1, -} -` - write(filePath, EXPO_ROUTER_SCREEN_TPL) - } catch (e) { - log(`Unable to write screen generator template.`) - } + /** + * Instructions mostly adapted from https://ignitecookbook.com/docs/recipes/ExpoRouter + * 1. Move all files from app/ to src/ + * 2. Update code refs to app/ with src/ + * 3. Refactor Reactotron commands to use `router` instead of refs to react navigation + * 4. Create a screen template that makes sense for Expo Router + * 5. Clean up - move ErrorBoundary to proper spot and remove unused files + */ + await system.run(log(`mv app/* src/`)) + updateExpoRouterSrcDir(toolbox) + refactorExpoRouterReactotronCmds(toolbox) + createExpoRouterScreenTemplate(toolbox) + await cleanupExpoRouterConversion(toolbox) - // some clean up - await system.run( - log(` - \\rm src/app.tsx - mkdir src/components/ErrorBoundary - mv src/screens/ErrorScreen/* src/components/ErrorBoundary - rm ignite/templates/screen/NAMEScreen.tsx.ejs - rm -rf ignite/templates/navigator - rm -rf src/screens - rm -rf src/navigators - rm -rf app - `), - ) stopSpinner(expoRouterMsg, "🧭") } else { // remove src/ dir since not using expo-router diff --git a/src/tools/react-native.ts b/src/tools/react-native.ts index 35ce62ff3..3905ebb2a 100644 --- a/src/tools/react-native.ts +++ b/src/tools/react-native.ts @@ -225,3 +225,130 @@ export async function replaceMaestroBundleIds( }), ) } + +export function createExpoRouterScreenTemplate(toolbox: GluegunToolbox) { + const { filesystem, parameters, print } = toolbox + + // debug? + const debug = boolFlag(parameters.options.debug) + const log = (m: T): T => { + debug && print.info(` ${m}`) + return m + } + + try { + const TARGET_DIR = filesystem.path(process.cwd()) + const filePath = filesystem.path(TARGET_DIR, "ignite/templates/screen/NAME.tsx.ejs") + + const EXPO_ROUTER_SCREEN_TPL = `import React, { FC } from "react" +import { observer } from "mobx-react-lite" +import { ViewStyle } from "react-native" +import { Screen, Text } from "src/components" + +// @mst replace-next-line export default function <%= props.pascalCaseName %>Screen() { +export default observer(function <%= props.pascalCaseName %>Screen() { + return ( + + + + ) +// @mst replace-next-line } +}) + +const $root: ViewStyle = { + flex: 1, +} +` + filesystem.write(filePath, EXPO_ROUTER_SCREEN_TPL) + } catch (e) { + log(`Unable to write screen generator template.`) + } +} + +export function refactorExpoRouterReactotronCmds(toolbox: GluegunToolbox) { + const { filesystem, parameters, print } = toolbox + + // debug? + const debug = boolFlag(parameters.options.debug) + const log = (m: T): T => { + debug && print.info(` ${m}`) + return m + } + + try { + const TARGET_DIR = filesystem.path(process.cwd()) + const reactotronConfigPath = filesystem.path(TARGET_DIR, "src/devtools/ReactotronConfig.ts") + + let reactotronConfig = filesystem.read(reactotronConfigPath) + reactotronConfig = reactotronConfig + .replace(/import { goBack, resetRoot, navigate }.*/g, 'import { router } from "expo-router"') + .replace(/navigate\(route as any\).*/g, "router.push(route)") + .replace(/goBack\(\).*/g, " router.back()") + + // this one gets removed entirely + const customCommandToRemoveRegex = + /reactotron\.onCustomCommand\({\s*title: "Reset Navigation State",\s*description: "Resets the navigation state",\s*command: "resetNavigation",\s*handler: \(\) => {\s*Reactotron\.log\("resetting navigation state"\)\s*resetRoot\({ index: 0, routes: \[\] }\)\s*},\s*}\),?\n?/g + reactotronConfig = reactotronConfig.replace(customCommandToRemoveRegex, "") + + filesystem.write(reactotronConfigPath, reactotronConfig) + } catch (e) { + log(`Unable to update ReactotronConfig.`) + } +} + +export function updateExpoRouterSrcDir(toolbox: GluegunToolbox) { + const { filesystem, parameters, print } = toolbox + + // debug? + const debug = boolFlag(parameters.options.debug) + const log = (m: T): T => { + debug && print.info(` ${m}`) + return m + } + + const TARGET_DIR = filesystem.path(process.cwd()) + const expoRouterFilesToFix = [ + "tsconfig.json", + "test/i18n.test.ts", + "src/devtools/ReactotronConfig.ts", + "src/components/Toggle/Switch.tsx", + "src/components/ListView.tsx", + "src/screens/WelcomeScreen.tsx", + "ignite/templates/model/NAME.ts.ejs", + "ignite/templates/component/NAME.tsx.ejs", + ] + expoRouterFilesToFix.forEach((file) => { + const filePath = filesystem.path(TARGET_DIR, file) + let fileContents = filesystem.read(filePath) + try { + fileContents = fileContents.replace(/app\//g, "src/") + filesystem.write(filePath, fileContents) + } catch (e) { + log(`Unable to locate ${file}.`) + } + }) +} + +export async function cleanupExpoRouterConversion(toolbox: GluegunToolbox) { + const { system, parameters, print } = toolbox + + // debug? + const debug = boolFlag(parameters.options.debug) + const log = (m: T): T => { + debug && print.info(` ${m}`) + return m + } + + await system.run( + log(` + \\rm src/app.tsx + mkdir src/components/ErrorBoundary + mv src/screens/ErrorScreen/* src/components/ErrorBoundary + rm ignite/templates/screen/NAMEScreen.tsx.ejs + rm -rf ignite/templates/navigator + rm -rf src/screens + rm -rf src/navigators + rm -rf app + `), + ) +} From 42960beae12510177feb24e93a84ad665f20f8b6 Mon Sep 17 00:00:00 2001 From: Frank Calise Date: Wed, 7 Aug 2024 08:15:06 -0400 Subject: [PATCH 10/14] docs: update cli docs for expo router flag --- docs/cli/Ignite-CLI.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/cli/Ignite-CLI.md b/docs/cli/Ignite-CLI.md index 1693200e9..3abb65d89 100644 --- a/docs/cli/Ignite-CLI.md +++ b/docs/cli/Ignite-CLI.md @@ -104,6 +104,7 @@ Starts the interactive prompt for generating a new Ignite project. Any options n - `--yes` accept all prompt defaults - `--workflow` string, one of `expo`, `cng` or `manual` for project initialization - `--experimental` comma separated string, indicates experimental features (which may or may not be stable) to turn on during installation. **A CNG workflow is require for these flags** `--workflow=cng` + - `expo-router` converts the project to [Expo Router](https://docs.expo.dev/router/introduction/) from React Navigation - `new-arch` enables [The New Architecture](https://reactnative.dev/docs/new-architecture-intro) - `expo-canary` uses Expo's highly experimental canary release instead of the la test stable SDK - `expo-beta` uses Expo's latest beta SDK available instead of the latest stable SDK From 6e7d318267b0c23e25c5cd800d62511ec4857cc7 Mon Sep 17 00:00:00 2001 From: Frank Calise Date: Wed, 7 Aug 2024 09:10:04 -0400 Subject: [PATCH 11/14] fix(cli): remove unnecessary file patch for router conversion --- src/tools/react-native.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/tools/react-native.ts b/src/tools/react-native.ts index 3905ebb2a..aa8df43db 100644 --- a/src/tools/react-native.ts +++ b/src/tools/react-native.ts @@ -313,7 +313,6 @@ export function updateExpoRouterSrcDir(toolbox: GluegunToolbox) { "src/devtools/ReactotronConfig.ts", "src/components/Toggle/Switch.tsx", "src/components/ListView.tsx", - "src/screens/WelcomeScreen.tsx", "ignite/templates/model/NAME.ts.ejs", "ignite/templates/component/NAME.tsx.ejs", ] From 9ce6e44aee7e6276ecbf6f99a247aad1998d1d3c Mon Sep 17 00:00:00 2001 From: Frank Calise Date: Wed, 7 Aug 2024 09:10:12 -0400 Subject: [PATCH 12/14] test(cli): add expo router tests --- test/vanilla/ignite-new-expo-router.test.ts | 145 ++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 test/vanilla/ignite-new-expo-router.test.ts diff --git a/test/vanilla/ignite-new-expo-router.test.ts b/test/vanilla/ignite-new-expo-router.test.ts new file mode 100644 index 000000000..dc8c85e53 --- /dev/null +++ b/test/vanilla/ignite-new-expo-router.test.ts @@ -0,0 +1,145 @@ +import { filesystem } from "gluegun" +import * as tempy from "tempy" +import { runIgnite } from "../_test-helpers" + +const APP_NAME = "Foo" +const originalDir = process.cwd() + +describe(`ignite new with expo-router`, () => { + describe(`ignite new ${APP_NAME} --debug --packager=bun --install-deps=false --experimental=expo-router --mst --yes`, () => { + let tempDir: string + let result: string + let appPath: string + + beforeAll(async () => { + tempDir = tempy.directory({ prefix: "ignite-" }) + result = await runIgnite( + `new ${APP_NAME} --debug --packager=bun --install-deps=false --experimental=expo-router --mst --yes`, + { + pre: `cd ${tempDir}`, + post: `cd ${originalDir}`, + }, + ) + appPath = filesystem.path(tempDir, APP_NAME) + }) + + afterAll(() => { + // console.log(tempDir) // uncomment for debugging, then run `code ` to see the generated app + filesystem.remove(tempDir) // clean up our mess + }) + + it("should convert to Expo Router with MST", async () => { + expect(result).toContain("--mst") + + // make sure src/navigators, src/screens, app/, app.tsx is gone + const dirs = filesystem.list(appPath) + expect(dirs).toContain("src") + expect(dirs).not.toContain("app") + expect(dirs).not.toContain("app.tsx") + expect(dirs).not.toContain("src/screens") + expect(dirs).not.toContain("src/navigators") + + // check the contents of ignite/templates + const templates = filesystem.list(`${appPath}/ignite/templates`) + expect(templates).toContain("component") + expect(templates).toContain("model") + expect(templates).toContain("screen") + expect(templates).not.toContain("navigator") + + // check tsconfig for path alias + const tsConfigJson = filesystem.read(`${appPath}/tsconfig.json`) + expect(tsConfigJson).toContain(`"src/*": ["./src/*"]`) + + // check entry point + const packageJson = filesystem.read(`${appPath}/package.json`) + expect(packageJson).toContain("expo-router/entry") + expect(packageJson).not.toContain("AppEntry.js") + + // check plugin in app.json + // check typedRoutes is turned on + const appJson = filesystem.read(`${appPath}/app.json`) + expect(appJson).toContain("expo-router") + expect(appJson).toContain("typedRoutes") + + // check generator templates for src/components and src/models + const componentGenerator = filesystem.read( + `${appPath}/ignite/templates/component/NAME.tsx.ejs`, + ) + expect(componentGenerator).toContain("src/components/index.ts") + expect(componentGenerator).toContain("src/theme") + expect(componentGenerator).not.toContain("app/components/index.ts") + expect(componentGenerator).not.toContain("app/theme") + const modelGenerator = filesystem.read(`${appPath}/ignite/templates/model/NAME.ts.ejs`) + expect(modelGenerator).toContain("src/models") + expect(modelGenerator).not.toContain("app/models") + + // check components for src/i18n + const listViewComponent = filesystem.read(`${appPath}/src/components/ListView.tsx`) + expect(listViewComponent).toContain("src/i18n") + expect(listViewComponent).not.toContain("app/i18n") + + const switchComponent = filesystem.read(`${appPath}/src/components/Toggle/Switch.tsx`) + expect(switchComponent).toContain("src/i18n") + expect(switchComponent).not.toContain("app/i18n") + + // check ReactotronConfig for router.back etc + const reactotronConfig = filesystem.read(`${appPath}/src/devtools/ReactotronConfig.ts`) + expect(reactotronConfig).toContain("router.back()") + expect(reactotronConfig).not.toContain("navigate(") + expect(reactotronConfig).not.toContain("react-navigation") + expect(reactotronConfig).not.toContain("reset navigation state") + + // make sure _layout sets up initial root store + const rootLayout = filesystem.read(`${appPath}/src/app/_layout.tsx`) + expect(rootLayout).toContain("useInitialRootStore") + + // make sure index has observer + const rootIndex = filesystem.read(`${appPath}/src/app/index.tsx`) + expect(rootIndex).toContain("observer") + }) + }) + + describe(`ignite new ${APP_NAME} --debug --packager=bun --install-deps=false --experimental=expo-router --mst-false --yes`, () => { + let tempDir: string + let result: string + let appPath: string + + beforeAll(async () => { + tempDir = tempy.directory({ prefix: "ignite-" }) + result = await runIgnite( + `new ${APP_NAME} --debug --packager=bun --install-deps=false --experimental=expo-router --mst=false --remove-demo --yes`, + { + pre: `cd ${tempDir}`, + post: `cd ${originalDir}`, + }, + ) + appPath = filesystem.path(tempDir, APP_NAME) + }) + + afterAll(() => { + // console.log(tempDir) // uncomment for debugging, then run `code ` to see the generated app + filesystem.remove(tempDir) // clean up our mess + }) + + it("should convert to Expo Router without MST", async () => { + expect(result).toContain("--mst=false") + expect(result).not.toContain("Setting --mst=true") + + // check the contents of ignite/templates + const templates = filesystem.list(`${appPath}/ignite/templates`) + expect(templates).toContain("component") + expect(templates).toContain("screen") + expect(templates).not.toContain("model") + expect(templates).not.toContain("navigator") + + // same as with MST but.. + // make sure _layout doesn't have mention of rehydrating root store + const rootLayout = filesystem.read(`${appPath}/src/app/_layout.tsx`) + expect(rootLayout).not.toContain("useInitialRootStore") + + // make sure index does not have observer + const rootIndex = filesystem.read(`${appPath}/src/app/index.tsx`) + expect(rootIndex).not.toContain("observer") + }) + }) +}) From 0bd617f636e394d647446317781749ab62aaa46a Mon Sep 17 00:00:00 2001 From: Frank Calise Date: Thu, 8 Aug 2024 09:43:05 -0400 Subject: [PATCH 13/14] fix(cli): router prompt grammar, tone down experimental barking --- src/commands/new.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/new.ts b/src/commands/new.ts index 40c41228e..6a6ff995d 100644 --- a/src/commands/new.ts +++ b/src/commands/new.ts @@ -494,7 +494,7 @@ module.exports = { const expoRouterResponse = await prompt.ask<{ experimentalExpoRouter: boolean }>(() => ({ type: "confirm", name: "experimentalExpoRouter", - message: "❗EXPERIMENTAL❗Would you use Expo Router for navigation?", + message: "[Experimental] Expo Router for navigation?", initial: defaultExpoRouter, format: prettyPrompt.format.boolean, prefix, @@ -514,7 +514,7 @@ module.exports = { const newArchResponse = await prompt.ask<{ experimentalNewArch: boolean }>(() => ({ type: "confirm", name: "experimentalNewArch", - message: "❗EXPERIMENTAL❗Would you like to enable the New Architecture?", + message: "[Experimental] the New Architecture?", initial: defaultNewArch, format: prettyPrompt.format.boolean, prefix, From eee948ab08f9271708837e2cee81204a035c1ed4 Mon Sep 17 00:00:00 2001 From: Frank Calise Date: Tue, 13 Aug 2024 08:13:14 -0400 Subject: [PATCH 14/14] fix(cli): reworks --mst to --state=mst (#2731 by @frankcalise) * fix(cli): reworks --mst to --state=mst * refactor(cli): tidy up MST removal --- docs/cli/Ignite-CLI.md | 4 +- docs/concept/MobX-State-Tree.md | 4 +- src/commands/new.ts | 93 ++++++++------------- test/vanilla/ignite-new-expo-router.test.ts | 14 ++-- 4 files changed, 48 insertions(+), 67 deletions(-) diff --git a/docs/cli/Ignite-CLI.md b/docs/cli/Ignite-CLI.md index 3abb65d89..c381d9e67 100644 --- a/docs/cli/Ignite-CLI.md +++ b/docs/cli/Ignite-CLI.md @@ -98,11 +98,11 @@ Starts the interactive prompt for generating a new Ignite project. Any options n - `--overwrite` overwrite the target directory if it exists - `--targetPath` string, specify a target directory where the project should be created - `--removeDemo` will remove the boilerplate demo code after project creation -- `--mst` flag to specify whether to include MobX-State-Tree in project (can only be set to `false` if `--removeDemo=true`) +- `--state` string, one of `mst` or `none` to include MobX-State-Tree in project (can only be set to `none` if `--removeDemo=true`) - `--useCache` flag specifying to use dependency cache for quicker installs - `--no-timeout` flag to disable the timeout protection (useful for slow internet connections) - `--yes` accept all prompt defaults -- `--workflow` string, one of `expo`, `cng` or `manual` for project initialization +- `--workflow` string, one of `cng` or `manual` for project initialization - `--experimental` comma separated string, indicates experimental features (which may or may not be stable) to turn on during installation. **A CNG workflow is require for these flags** `--workflow=cng` - `expo-router` converts the project to [Expo Router](https://docs.expo.dev/router/introduction/) from React Navigation - `new-arch` enables [The New Architecture](https://reactnative.dev/docs/new-architecture-intro) diff --git a/docs/concept/MobX-State-Tree.md b/docs/concept/MobX-State-Tree.md index f38a0294c..739594b01 100644 --- a/docs/concept/MobX-State-Tree.md +++ b/docs/concept/MobX-State-Tree.md @@ -41,10 +41,10 @@ We also recognize no state management solution is perfect. MST has some known do ### Remove MST Option -We understand that state management is a highly opinionated topic with various options available. To accommodate this, we've added an option in Ignite CLI to remove MobX-State-Tree if you choose so! When Igniting a new project, provide `--mst=false` to remove MobX-State-Tree code from the boilerplate. This option only works when also removing demo code. +We understand that state management is a highly opinionated topic with various options available. To accommodate this, we've added an option in Ignite CLI to remove MobX-State-Tree if you choose so! When Igniting a new project, provide `--state=none` to remove MobX-State-Tree code from the boilerplate. This option only works when also removing demo code. ``` -npx ignite-cli@latest new PizzaApp --removeDemo=true --mst=false +npx ignite-cli@latest new PizzaApp --removeDemo=true --state=none ``` ## Learning MobX-State-Tree diff --git a/src/commands/new.ts b/src/commands/new.ts index 6a6ff995d..94aad0000 100644 --- a/src/commands/new.ts +++ b/src/commands/new.ts @@ -35,6 +35,7 @@ import { findAndRemoveDependencies } from "../tools/dependencies" import { demoDependenciesToRemove } from "../tools/demo" type Workflow = "cng" | "manual" +type StateMgmt = "mst" | "none" export interface Options { /** @@ -137,6 +138,7 @@ export interface Options { * and include them in .gitignore or not * * Input Source: `prompt.ask`| `parameter.option` + * @default cng */ workflow?: Workflow /** @@ -149,9 +151,9 @@ export interface Options { * Whether or not to include MobX-State-Tree boilerplate code * * Input Source: `prompt.ask` | `parameter.option` - * @default true + * @default mst */ - mst?: boolean + state?: StateMgmt } module.exports = { @@ -287,7 +289,7 @@ module.exports = { const defaultWorkflow = "cng" let workflow = useDefault(options.workflow) ? defaultWorkflow : options.workflow if (workflow === undefined) { - const useExpoResponse = await prompt.ask<{ workflow: "cng" | "manual" }>(() => ({ + const useExpoResponse = await prompt.ask<{ workflow: Workflow }>(() => ({ type: "select", name: "workflow", message: "How do you want to manage Native code?", @@ -348,15 +350,15 @@ module.exports = { // #endregion // #region Prompt to Remove MobX-State-Tree code - const defaultMST = true - let includeMST = useDefault(options.mst) ? defaultMST : boolFlag(options.mst) + const defaultMST = "mst" + let stateMgmt = useDefault(options.state) ? defaultMST : options.state - if (includeMST === undefined) { + if (stateMgmt === undefined) { if (!removeDemo) { - includeMST = true + stateMgmt = "mst" } else { // only ask if we're removing the demo code - const includeMSTResponse = await prompt.ask<{ includeMST: boolean }>(() => ({ + const includeMSTResponse = await prompt.ask<{ includeMST: StateMgmt }>(() => ({ type: "confirm", name: "includeMST", message: "Include MobX-State-Tree code? (recommended)", @@ -364,15 +366,15 @@ module.exports = { format: prettyPrompt.format.boolean, prefix, })) - includeMST = includeMSTResponse.includeMST + stateMgmt = includeMSTResponse.includeMST } } - if (!removeDemo && includeMST === false) { + if (!removeDemo && stateMgmt === "none") { p() p(yellow(`Warning: You can't remove MobX-State-Tree code without removing demo code.`)) - p(yellow(`Setting --mst=true`)) - includeMST = true + p(yellow(`Setting --state=mst`)) + stateMgmt = "mst" } // #endregion @@ -658,7 +660,7 @@ module.exports = { packageJsonRaw = findAndRemoveDependencies(packageJsonRaw, demoDependenciesToRemove) } - if (!includeMST) { + if (stateMgmt === "none") { log(`Removing MST dependencies... ${mstDependenciesToRemove.join(", ")}`) packageJsonRaw = findAndRemoveDependencies(packageJsonRaw, mstDependenciesToRemove) } @@ -817,9 +819,7 @@ module.exports = { const CMD = removeDemo === true ? "remove-demo" : "remove-demo-markup" log(`Ignite bin path: ${IGNITE}`) - await system.run(`${IGNITE} ${CMD} "${targetPath}"`, { - onProgress: log, - }) + await system.run(`${IGNITE} ${CMD} "${targetPath}"`, { onProgress: log }) } catch (e) { log(e) p(yellow(`Unable to remove demo ${removeDemoPart}.`)) @@ -854,46 +854,27 @@ module.exports = { // #endregion // #region Remove MST code - if (includeMST) { - // remove MST markup only - startSpinner(`Removing MobX-State-Tree markup`) - try { - const IGNITE = "node " + filesystem.path(__dirname, "..", "..", "bin", "ignite") - log(`Ignite bin path: ${IGNITE}`) - await system.run( - `${IGNITE} remove-mst-markup "${targetPath}" "${ - experimentalExpoRouter ? "src" : "app" - }"`, - { - onProgress: log, - }, - ) - } catch (e) { - log(e) - p(yellow(`Unable to remove MobX-State-Tree markup`)) - } - stopSpinner(`Removing MobX-State-Tree markup`, "🌳") - } else { - startSpinner(`Removing MobX-State-Tree code`) - try { - const IGNITE = "node " + filesystem.path(__dirname, "..", "..", "bin", "ignite") - log(`Ignite bin path: ${IGNITE}`) - await system.run( - `${IGNITE} remove-mst "${targetPath}" "${experimentalExpoRouter ? "src" : "app"}"`, - { - onProgress: log, - }, - ) - } catch (e) { - log(e) - p( - yellow( - `Unable to remove MobX-State-Tree code. To perform updates manually, check out the recipe with full instructions: https://ignitecookbook.com/docs/recipes/RemoveMobxStateTree`, - ), - ) - } - stopSpinner(`Removing MobX-State-Tree code`, "🌳") + const removeMstPart = stateMgmt === "none" ? "code" : "markup" + startSpinner(`Removing MobX-State-Tree ${removeMstPart}`) + try { + const IGNITE = "node " + filesystem.path(__dirname, "..", "..", "bin", "ignite") + const CMD = stateMgmt === "none" ? "remove-mst" : "remove-mst-markup" + + log(`Ignite bin path: ${IGNITE}`) + await system.run( + `${IGNITE} ${CMD} "${targetPath}" "${experimentalExpoRouter ? "src" : "app"}"`, + { onProgress: log }, + ) + } catch (e) { + log(e) + const additionalInfo = + stateMgmt === "none" + ? ` To perform updates manually, check out the recipe with full instructions: https://ignitecookbook.com/docs/recipes/RemoveMobxStateTree` + : "" + p(yellow(`Unable to remove MobX-State-Tree ${removeMstPart}.${additionalInfo}`)) } + stopSpinner(`Removing MobX-State-Tree ${removeMstPart}`, "🌳") + // #endregion // #region Format generator templates EOL for Windows let warnAboutEOL = false @@ -985,7 +966,7 @@ module.exports = { y: yname, yes: yname, noTimeout, - mst: includeMST, + state: stateMgmt, }, projectName, toolbox, diff --git a/test/vanilla/ignite-new-expo-router.test.ts b/test/vanilla/ignite-new-expo-router.test.ts index dc8c85e53..e00056edc 100644 --- a/test/vanilla/ignite-new-expo-router.test.ts +++ b/test/vanilla/ignite-new-expo-router.test.ts @@ -6,7 +6,7 @@ const APP_NAME = "Foo" const originalDir = process.cwd() describe(`ignite new with expo-router`, () => { - describe(`ignite new ${APP_NAME} --debug --packager=bun --install-deps=false --experimental=expo-router --mst --yes`, () => { + describe(`ignite new ${APP_NAME} --debug --packager=bun --install-deps=false --experimental=expo-router --state=mst --yes`, () => { let tempDir: string let result: string let appPath: string @@ -14,7 +14,7 @@ describe(`ignite new with expo-router`, () => { beforeAll(async () => { tempDir = tempy.directory({ prefix: "ignite-" }) result = await runIgnite( - `new ${APP_NAME} --debug --packager=bun --install-deps=false --experimental=expo-router --mst --yes`, + `new ${APP_NAME} --debug --packager=bun --install-deps=false --experimental=expo-router --state=mst --yes`, { pre: `cd ${tempDir}`, post: `cd ${originalDir}`, @@ -29,7 +29,7 @@ describe(`ignite new with expo-router`, () => { }) it("should convert to Expo Router with MST", async () => { - expect(result).toContain("--mst") + expect(result).toContain("--state=mst") // make sure src/navigators, src/screens, app/, app.tsx is gone const dirs = filesystem.list(appPath) @@ -99,7 +99,7 @@ describe(`ignite new with expo-router`, () => { }) }) - describe(`ignite new ${APP_NAME} --debug --packager=bun --install-deps=false --experimental=expo-router --mst-false --yes`, () => { + describe(`ignite new ${APP_NAME} --debug --packager=bun --install-deps=false --experimental=expo-router --state-none --yes`, () => { let tempDir: string let result: string let appPath: string @@ -107,7 +107,7 @@ describe(`ignite new with expo-router`, () => { beforeAll(async () => { tempDir = tempy.directory({ prefix: "ignite-" }) result = await runIgnite( - `new ${APP_NAME} --debug --packager=bun --install-deps=false --experimental=expo-router --mst=false --remove-demo --yes`, + `new ${APP_NAME} --debug --packager=bun --install-deps=false --experimental=expo-router --state=none --remove-demo --yes`, { pre: `cd ${tempDir}`, post: `cd ${originalDir}`, @@ -122,8 +122,8 @@ describe(`ignite new with expo-router`, () => { }) it("should convert to Expo Router without MST", async () => { - expect(result).toContain("--mst=false") - expect(result).not.toContain("Setting --mst=true") + expect(result).toContain("--state=none") + expect(result).not.toContain("Setting --state=mst") // check the contents of ignite/templates const templates = filesystem.list(`${appPath}/ignite/templates`)