@teardown/cli 2.0.72 → 2.0.74

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teardown/cli",
3
- "version": "2.0.72",
3
+ "version": "2.0.74",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -76,7 +76,7 @@
76
76
  },
77
77
  "devDependencies": {
78
78
  "@biomejs/biome": "2.3.11",
79
- "@teardown/tsconfig": "2.0.72",
79
+ "@teardown/tsconfig": "2.0.74",
80
80
  "@types/bun": "1.3.5",
81
81
  "@types/ejs": "^3.1.5",
82
82
  "typescript": "5.9.3"
@@ -4,9 +4,10 @@
4
4
 
5
5
  import { spawn } from "node:child_process";
6
6
  import { existsSync } from "node:fs";
7
- import { join } from "node:path";
7
+ import { join, resolve } from "node:path";
8
8
  import chalk from "chalk";
9
9
  import { Command } from "commander";
10
+ import { getNavigationConfig } from "../../utils/metro-config";
10
11
 
11
12
  /**
12
13
  * Check if a metro.config.js file exists in the project
@@ -48,6 +49,41 @@ export function createDevCommand(): Command {
48
49
  console.log(chalk.gray("Tip: Add @teardown/metro-config for 36x faster builds\n"));
49
50
  }
50
51
 
52
+ // Pre-generate routes if navigation-metro is installed
53
+ const navigationMetroPath = join(projectRoot, "node_modules/@teardown/navigation-metro");
54
+ const navConfig = getNavigationConfig(projectRoot);
55
+
56
+ if (existsSync(navigationMetroPath) && navConfig) {
57
+ const routesDir = resolve(projectRoot, navConfig.routesDir);
58
+ const generatedDir = resolve(projectRoot, navConfig.generatedDir);
59
+
60
+ if (existsSync(routesDir)) {
61
+ try {
62
+ // Dynamic require from user's node_modules
63
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
64
+ const { generateAllRouteFiles } = require(navigationMetroPath) as {
65
+ generateAllRouteFiles: (opts: {
66
+ routesDir: string;
67
+ generatedDir: string;
68
+ prefixes: string[];
69
+ verbose: boolean;
70
+ }) => void;
71
+ };
72
+ generateAllRouteFiles({
73
+ routesDir,
74
+ generatedDir,
75
+ prefixes: [],
76
+ verbose: options.verbose,
77
+ });
78
+ if (options.verbose) {
79
+ console.log(chalk.gray("Generated route types\n"));
80
+ }
81
+ } catch {
82
+ // Ignore - Metro will regenerate
83
+ }
84
+ }
85
+ }
86
+
51
87
  console.log(chalk.blue("Starting Metro bundler...\n"));
52
88
 
53
89
  const args = ["react-native", "start", "--port", options.port];
@@ -12,7 +12,7 @@
12
12
 
13
13
  import { type ChildProcess, exec, spawn } from "node:child_process";
14
14
  import { existsSync } from "node:fs";
15
- import { join } from "node:path";
15
+ import { join, resolve } from "node:path";
16
16
  import { createInterface } from "node:readline";
17
17
  import { promisify } from "node:util";
18
18
  import chalk from "chalk";
@@ -20,6 +20,7 @@ import { Command } from "commander";
20
20
  import ora from "ora";
21
21
  import { attachBundlerToForeground, startBundlerBackground, waitForBundlerReady } from "../../utils/bundler";
22
22
  import { getAppScheme, launchAppWithBundler } from "../../utils/deep-link";
23
+ import { getNavigationConfig } from "../../utils/metro-config";
23
24
 
24
25
  const execAsync = promisify(exec);
25
26
 
@@ -336,6 +337,40 @@ export function createRunCommand(): Command {
336
337
 
337
338
  // Start Metro bundler FIRST (unless --no-bundler or --release)
338
339
  if (options.bundler && !options.release) {
340
+ // Pre-generate routes if navigation-metro is installed
341
+ const projectRoot = process.cwd();
342
+ const navigationMetroPath = join(projectRoot, "node_modules/@teardown/navigation-metro");
343
+ const navConfig = getNavigationConfig(projectRoot);
344
+
345
+ if (existsSync(navigationMetroPath) && navConfig) {
346
+ const routesDir = resolve(projectRoot, navConfig.routesDir);
347
+ const generatedDir = resolve(projectRoot, navConfig.generatedDir);
348
+
349
+ if (existsSync(routesDir)) {
350
+ try {
351
+ // Dynamic require from user's node_modules
352
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
353
+ const { generateAllRouteFiles } = require(navigationMetroPath) as {
354
+ generateAllRouteFiles: (opts: {
355
+ routesDir: string;
356
+ generatedDir: string;
357
+ prefixes: string[];
358
+ verbose: boolean;
359
+ }) => void;
360
+ };
361
+ const slug = getAppScheme(projectRoot) || "app";
362
+ generateAllRouteFiles({
363
+ routesDir,
364
+ generatedDir,
365
+ prefixes: [`${slug}://`],
366
+ verbose: false,
367
+ });
368
+ } catch {
369
+ // Ignore - Metro will regenerate
370
+ }
371
+ }
372
+ }
373
+
339
374
  metroProcess = await startMetroAndWait(options.port, platform);
340
375
  }
341
376
 
@@ -85,7 +85,7 @@ export function startBundlerBackground(options: StartBundlerOptions = {}): Child
85
85
  const proc = spawn("npx", args, {
86
86
  cwd,
87
87
  shell: true,
88
- stdio: ["ignore", "pipe", "pipe"],
88
+ stdio: ["pipe", "pipe", "pipe"],
89
89
  detached: false,
90
90
  env: {
91
91
  ...process.env,
@@ -239,16 +239,41 @@ export async function killBundler(proc: ChildProcess, timeoutMs = 5000): Promise
239
239
  }
240
240
 
241
241
  /**
242
- * Attach bundler process to foreground (pipe stdout/stderr)
242
+ * Attach bundler process to foreground (pipe stdin/stdout/stderr)
243
+ *
244
+ * Enables keyboard shortcuts like 'r' for reload and 'j' for debugger.
243
245
  *
244
246
  * @param proc - Metro process
245
247
  */
246
248
  export function attachBundlerToForeground(proc: ChildProcess): void {
249
+ // Pipe output
247
250
  proc.stdout?.pipe(process.stdout);
248
251
  proc.stderr?.pipe(process.stderr);
249
252
 
253
+ // Connect stdin for keyboard shortcuts (r, j, d, etc.)
254
+ if (proc.stdin && process.stdin.isTTY) {
255
+ // Set raw mode to capture single keypresses
256
+ process.stdin.setRawMode(true);
257
+ process.stdin.resume();
258
+ process.stdin.pipe(proc.stdin);
259
+
260
+ // Handle Ctrl+C to exit gracefully
261
+ process.stdin.on("data", (data) => {
262
+ // Check for Ctrl+C (0x03)
263
+ if (data[0] === 0x03) {
264
+ proc.kill("SIGTERM");
265
+ process.exit(0);
266
+ }
267
+ });
268
+ }
269
+
250
270
  // Handle process exit
251
271
  proc.on("close", (code) => {
272
+ // Restore terminal settings
273
+ if (process.stdin.isTTY) {
274
+ process.stdin.setRawMode(false);
275
+ }
276
+
252
277
  if (code !== 0 && code !== null) {
253
278
  console.error(chalk.red(`Metro bundler exited with code ${code}`));
254
279
  }
@@ -41,5 +41,7 @@ export {
41
41
  formatKeyValue,
42
42
  formatList,
43
43
  } from "./logger";
44
+ export type { NavigationConfig } from "./metro-config";
45
+ export { getNavigationConfig, parseNavigationConfig } from "./metro-config";
44
46
  export type { ReporterStep, StepStatus, TerminalReporterConfig } from "./terminal-reporter";
45
47
  export { createReporter, TerminalReporter } from "./terminal-reporter";
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Metro Config Parser
3
+ *
4
+ * Extracts navigation configuration from metro.config.js
5
+ */
6
+
7
+ import { existsSync, readFileSync } from "node:fs";
8
+ import { join } from "node:path";
9
+
10
+ /**
11
+ * Navigation options extracted from metro.config.js
12
+ */
13
+ export interface NavigationConfig {
14
+ routesDir: string;
15
+ generatedDir: string;
16
+ }
17
+
18
+ /**
19
+ * Default navigation configuration
20
+ */
21
+ const DEFAULT_CONFIG: NavigationConfig = {
22
+ routesDir: "./src/routes",
23
+ generatedDir: "./.teardown",
24
+ };
25
+
26
+ /**
27
+ * Finds metro.config.js in the project
28
+ */
29
+ function findMetroConfig(projectRoot: string): string | null {
30
+ const configNames = ["metro.config.js", "metro.config.cjs", "metro.config.mjs"];
31
+
32
+ for (const name of configNames) {
33
+ const configPath = join(projectRoot, name);
34
+ if (existsSync(configPath)) {
35
+ return configPath;
36
+ }
37
+ }
38
+
39
+ return null;
40
+ }
41
+
42
+ /**
43
+ * Parses metro.config.js to extract withTeardownNavigation options
44
+ *
45
+ * This uses simple regex parsing since we can't safely evaluate the JS file.
46
+ * It looks for patterns like:
47
+ * withTeardownNavigation(config, { routesDir: "./src/_routes", generatedDir: "./.teardown" })
48
+ */
49
+ export function parseNavigationConfig(projectRoot: string): NavigationConfig | null {
50
+ const configPath = findMetroConfig(projectRoot);
51
+ if (!configPath) {
52
+ return null;
53
+ }
54
+
55
+ try {
56
+ const content = readFileSync(configPath, "utf-8");
57
+
58
+ // Check if withTeardownNavigation is used
59
+ if (!content.includes("withTeardownNavigation")) {
60
+ return null;
61
+ }
62
+
63
+ // Extract routesDir using regex
64
+ // Matches: routesDir: "./src/_routes" or routesDir: './src/_routes'
65
+ const routesDirMatch = content.match(/routesDir:\s*['"]([^'"]+)['"]/);
66
+ const routesDir = routesDirMatch ? routesDirMatch[1] : DEFAULT_CONFIG.routesDir;
67
+
68
+ // Extract generatedDir using regex
69
+ const generatedDirMatch = content.match(/generatedDir:\s*['"]([^'"]+)['"]/);
70
+ const generatedDir = generatedDirMatch ? generatedDirMatch[1] : DEFAULT_CONFIG.generatedDir;
71
+
72
+ return {
73
+ routesDir,
74
+ generatedDir,
75
+ };
76
+ } catch {
77
+ return null;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Gets navigation config for a project, with fallbacks
83
+ */
84
+ export function getNavigationConfig(projectRoot: string): NavigationConfig | null {
85
+ // Try to parse from metro.config.js first
86
+ const parsedConfig = parseNavigationConfig(projectRoot);
87
+ if (parsedConfig) {
88
+ return parsedConfig;
89
+ }
90
+
91
+ // Fallback: check if default routes dir exists
92
+ const defaultRoutesPath = join(projectRoot, DEFAULT_CONFIG.routesDir);
93
+ if (existsSync(defaultRoutesPath)) {
94
+ return DEFAULT_CONFIG;
95
+ }
96
+
97
+ // Try alternate common location
98
+ const altRoutesPath = join(projectRoot, "src/_routes");
99
+ if (existsSync(altRoutesPath)) {
100
+ return {
101
+ routesDir: "./src/_routes",
102
+ generatedDir: "./.teardown",
103
+ };
104
+ }
105
+
106
+ return null;
107
+ }
@@ -0,0 +1,11 @@
1
+ // Auto-generated by @teardown/navigation
2
+ import type { LinkingOptions } from "@react-navigation/native";
3
+ import type { RouteParams } from "./routes.generated";
4
+
5
+ export const generatedLinkingConfig: LinkingOptions<RouteParams>["config"] = {
6
+ screens: {
7
+ "/home": "home",
8
+ },
9
+ };
10
+
11
+ export const defaultPrefixes: string[] = ["app://"];
@@ -0,0 +1,18 @@
1
+ {
2
+ "generatedAt": "2026-01-31T19:48:52.201Z",
3
+ "routeCount": 1,
4
+ "routes": [
5
+ {
6
+ "path": "",
7
+ "file": "_layout.tsx",
8
+ "params": [],
9
+ "layoutType": "stack"
10
+ },
11
+ {
12
+ "path": "/home",
13
+ "file": "home.tsx",
14
+ "params": [],
15
+ "layoutType": "none"
16
+ }
17
+ ]
18
+ }
@@ -0,0 +1,9 @@
1
+ // Auto-generated by @teardown/navigation
2
+ import type { RouteParams, RoutePath } from "./routes.generated";
3
+
4
+ declare module '@teardown/navigation' {
5
+ interface Register {
6
+ routeParams: RouteParams;
7
+ routePath: RoutePath;
8
+ }
9
+ }
@@ -0,0 +1,42 @@
1
+ /* eslint-disable */
2
+ // @ts-nocheck
3
+
4
+ // Auto-generated by @teardown/navigation
5
+ // Do not edit this file directly
6
+ // Generated at: 2026-01-31T19:48:52.202Z
7
+
8
+ import type { NavigatorNode } from "@teardown/navigation";
9
+
10
+ import layout from "../src/_routes/_layout";
11
+ import home from "../src/_routes/home";
12
+
13
+ export const routeTree: NavigatorNode =
14
+ {
15
+ type: "stack",
16
+ layout: layout,
17
+ screens: {
18
+ "home": {
19
+ screen: home,
20
+ path: "/home",
21
+ },
22
+ },
23
+ };
24
+
25
+ // Flat screens export for backwards compatibility
26
+ export const screens = {
27
+ "/home": home,
28
+ } as const;
29
+
30
+ export const layouts = {
31
+ "src__routes": layout,
32
+ } as const;
33
+
34
+ export type Screens = typeof screens;
35
+ export type Layouts = typeof layouts;
36
+ export type RouteTreeType = typeof routeTree;
37
+
38
+ export const routePaths = [
39
+ "/home",
40
+ ] as const;
41
+
42
+ export type RoutePath = (typeof routePaths)[number];
@@ -0,0 +1,19 @@
1
+ // Auto-generated by @teardown/navigation
2
+ // Do not edit this file directly
3
+ // Generated at: 2026-01-31T19:48:52.201Z
4
+
5
+ export interface RouteParams {
6
+ "/home": undefined;
7
+ }
8
+
9
+ export type RoutePath = keyof RouteParams;
10
+
11
+ export type ParamsFor<T extends RoutePath> = RouteParams[T];
12
+
13
+ export type RouteWithParams = {
14
+ [K in RoutePath]: RouteParams[K] extends undefined
15
+ ? { path: K }
16
+ : { path: K; params: RouteParams[K] };
17
+ }[RoutePath];
18
+
19
+ export type NavigatorType = "stack" | "tabs" | "drawer";
@@ -5,13 +5,12 @@
5
5
  */
6
6
 
7
7
  import { defineScreen } from "@teardown/navigation/primitives";
8
- import type React from "react";
9
8
  import { ScrollView, Text, View } from "react-native";
10
9
  import { Button } from "@/components/ui/button";
11
10
  import { Card } from "@/components/ui/card";
12
11
  import { APP_NAME } from "@/core/constants";
13
12
 
14
- function HomeScreen(): React.JSX.Element {
13
+ function HomeScreen() {
15
14
  return (
16
15
  <ScrollView className="flex-1 bg-background" contentContainerClassName="p-6 gap-6">
17
16
  {/* Hero Section */}
@@ -20,12 +19,8 @@ function HomeScreen(): React.JSX.Element {
20
19
  <Text className="text-3xl font-bold text-primary-foreground">T</Text>
21
20
  </View>
22
21
  <View className="items-center gap-2">
23
- <Text className="text-2xl font-bold text-foreground text-center">
24
- Welcome to {APP_NAME}
25
- </Text>
26
- <Text className="text-base text-muted-foreground text-center">
27
- Your React Native app is ready to go
28
- </Text>
22
+ <Text className="text-2xl font-bold text-foreground text-center">Welcome to {APP_NAME}</Text>
23
+ <Text className="text-base text-muted-foreground text-center">Your React Native app is ready to go</Text>
29
24
  </View>
30
25
  </View>
31
26
 
@@ -45,21 +40,9 @@ function HomeScreen(): React.JSX.Element {
45
40
  {/* Features */}
46
41
  <View className="gap-4">
47
42
  <Text className="text-lg font-semibold text-foreground">Features</Text>
48
- <FeatureCard
49
- icon="🔒"
50
- title="Type-Safe Navigation"
51
- description="Routes are fully typed with TypeScript"
52
- />
53
- <FeatureCard
54
- icon="📁"
55
- title="File-Based Routing"
56
- description="Routes generated from file structure"
57
- />
58
- <FeatureCard
59
- icon="🎨"
60
- title="HeroUI Components"
61
- description="Beautiful pre-built UI components"
62
- />
43
+ <FeatureCard icon="🔒" title="Type-Safe Navigation" description="Routes are fully typed with TypeScript" />
44
+ <FeatureCard icon="📁" title="File-Based Routing" description="Routes generated from file structure" />
45
+ <FeatureCard icon="🎨" title="HeroUI Components" description="Beautiful pre-built UI components" />
63
46
  </View>
64
47
 
65
48
  {/* Getting Started */}
@@ -78,15 +61,7 @@ function HomeScreen(): React.JSX.Element {
78
61
  );
79
62
  }
80
63
 
81
- function FeatureCard({
82
- icon,
83
- title,
84
- description,
85
- }: {
86
- icon: string;
87
- title: string;
88
- description: string;
89
- }): React.JSX.Element {
64
+ function FeatureCard({ icon, title, description }: { icon: string; title: string; description: string }) {
90
65
  return (
91
66
  <Card>
92
67
  <Card.Body className="flex-row items-center gap-4">
@@ -10,13 +10,11 @@
10
10
 
11
11
  import "../global.css";
12
12
 
13
- import type React from "react";
14
- import { StatusBar, View } from "react-native";
15
- import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";
13
+ import { SafeAreaProvider } from "react-native-safe-area-context";
16
14
  import { Router } from "@/navigation";
17
15
  import { AppProviders } from "@/providers";
18
16
 
19
- export function App(): React.JSX.Element {
17
+ export function App() {
20
18
  return (
21
19
  <SafeAreaProvider>
22
20
  <AppProviders>
@@ -97,17 +97,13 @@ const Text = (props: TextProps) => {
97
97
  invert: invert,
98
98
  }),
99
99
  textClassName,
100
- className,
100
+ className
101
101
  );
102
102
  }, [variant, color, invert, textClassName, className]);
103
103
 
104
104
  return (
105
105
  <TextClassContext.Provider value={classNames}>
106
- <Animated.Text
107
- {...otherProps}
108
- className={classNames}
109
- allowFontScaling={false}
110
- />
106
+ <Animated.Text {...otherProps} className={classNames} allowFontScaling={false} />
111
107
  </TextClassContext.Provider>
112
108
  );
113
109
  };
@@ -6,14 +6,14 @@
6
6
  */
7
7
 
8
8
  import { HeroUINativeProvider } from "heroui-native";
9
- import type React from "react";
9
+ import type { ReactNode } from "react";
10
10
  import { GestureHandlerRootView } from "react-native-gesture-handler";
11
11
 
12
12
  interface AppProvidersProps {
13
- children: React.ReactNode;
13
+ children: ReactNode;
14
14
  }
15
15
 
16
- export function AppProviders({ children }: AppProvidersProps): React.JSX.Element {
16
+ export function AppProviders({ children }: AppProvidersProps) {
17
17
  return (
18
18
  <GestureHandlerRootView style={{ flex: 1 }}>
19
19
  <HeroUINativeProvider>{children}</HeroUINativeProvider>
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Home Screen
3
+ *
4
+ * The main landing screen of the app.
5
+ */
6
+
7
+ import { defineScreen } from "@teardown/navigation/primitives";
8
+ import type React from "react";
9
+ import { ScrollView, Text, View } from "react-native";
10
+ import { Button } from "@/components/ui/button";
11
+ import { Card } from "@/components/ui/card";
12
+ import { APP_NAME } from "@/core/constants";
13
+
14
+ function HomeScreen(): React.JSX.Element {
15
+ return (
16
+ <ScrollView className="flex-1 bg-background" contentContainerClassName="p-6 gap-6">
17
+ {/* Hero Section */}
18
+ <View className="items-center gap-4 py-8">
19
+ <View className="h-20 w-20 items-center justify-center rounded-2xl bg-primary">
20
+ <Text className="text-3xl font-bold text-primary-foreground">T</Text>
21
+ </View>
22
+ <View className="items-center gap-2">
23
+ <Text className="text-2xl font-bold text-foreground text-center">Welcome to {APP_NAME}</Text>
24
+ <Text className="text-base text-muted-foreground text-center">Your React Native app is ready to go</Text>
25
+ </View>
26
+ </View>
27
+
28
+ {/* Quick Actions */}
29
+ <Card>
30
+ <Card.Body className="gap-3">
31
+ <Card.Title>Quick Actions</Card.Title>
32
+ <Button onPress={() => console.log("Primary action")} fullWidth>
33
+ Get Started
34
+ </Button>
35
+ <Button variant="bordered" onPress={() => console.log("Secondary action")} fullWidth>
36
+ Learn More
37
+ </Button>
38
+ </Card.Body>
39
+ </Card>
40
+
41
+ {/* Features */}
42
+ <View className="gap-4">
43
+ <Text className="text-lg font-semibold text-foreground">Features</Text>
44
+ <FeatureCard icon="🔒" title="Type-Safe Navigation" description="Routes are fully typed with TypeScript" />
45
+ <FeatureCard icon="📁" title="File-Based Routing" description="Routes generated from file structure" />
46
+ <FeatureCard icon="🎨" title="HeroUI Components" description="Beautiful pre-built UI components" />
47
+ </View>
48
+
49
+ {/* Getting Started */}
50
+ <Card className="bg-primary/5 border-primary/20">
51
+ <Card.Body className="py-4">
52
+ <Card.Title>Getting Started</Card.Title>
53
+ <Card.Description>
54
+ 1. Explore the components in src/components/ui/{"\n"}
55
+ 2. Add new routes in src/routes/{"\n"}
56
+ 3. Customize the theme in global.css{"\n"}
57
+ 4. Build your app!
58
+ </Card.Description>
59
+ </Card.Body>
60
+ </Card>
61
+ </ScrollView>
62
+ );
63
+ }
64
+
65
+ function FeatureCard({
66
+ icon,
67
+ title,
68
+ description,
69
+ }: {
70
+ icon: string;
71
+ title: string;
72
+ description: string;
73
+ }): React.JSX.Element {
74
+ return (
75
+ <Card>
76
+ <Card.Body className="flex-row items-center gap-4">
77
+ <View className="h-12 w-12 items-center justify-center rounded-xl bg-muted">
78
+ <Text className="text-2xl">{icon}</Text>
79
+ </View>
80
+ <View className="flex-1">
81
+ <Card.Title>{title}</Card.Title>
82
+ <Card.Description>{description}</Card.Description>
83
+ </View>
84
+ </Card.Body>
85
+ </Card>
86
+ );
87
+ }
88
+
89
+ export default defineScreen({
90
+ component: HomeScreen,
91
+ options: {
92
+ title: "Home",
93
+ },
94
+ });