@teardown/cli 2.0.73 → 2.0.75

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.73",
3
+ "version": "2.0.75",
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.73",
79
+ "@teardown/tsconfig": "2.0.75",
80
80
  "@types/bun": "1.3.5",
81
81
  "@types/ejs": "^3.1.5",
82
82
  "typescript": "5.9.3"
@@ -2,11 +2,12 @@
2
2
  * Dev command - starts the Metro bundler
3
3
  */
4
4
 
5
- import { spawn } from "node:child_process";
6
5
  import { existsSync } from "node:fs";
7
- import { join } from "node:path";
6
+ import { join, resolve } from "node:path";
8
7
  import chalk from "chalk";
9
8
  import { Command } from "commander";
9
+ import { attachBundlerToForeground, startBundlerBackground } from "../../utils/bundler";
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,39 +49,60 @@ export function createDevCommand(): Command {
48
49
  console.log(chalk.gray("Tip: Add @teardown/metro-config for 36x faster builds\n"));
49
50
  }
50
51
 
51
- console.log(chalk.blue("Starting Metro bundler...\n"));
52
+ // Pre-generate routes if navigation-metro is installed
53
+ const navigationMetroPath = join(projectRoot, "node_modules/@teardown/navigation-metro");
54
+ const navConfig = getNavigationConfig(projectRoot);
52
55
 
53
- const args = ["react-native", "start", "--port", options.port];
56
+ if (existsSync(navigationMetroPath) && navConfig) {
57
+ const routesDir = resolve(projectRoot, navConfig.routesDir);
58
+ const generatedDir = resolve(projectRoot, navConfig.generatedDir);
54
59
 
55
- if (options.resetCache) {
56
- args.push("--reset-cache");
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
+ }
57
85
  }
58
86
 
59
- if (options.verbose) {
60
- args.push("--verbose");
61
- }
87
+ console.log(chalk.blue("Starting Metro bundler...\n"));
62
88
 
63
- const proc = spawn("npx", args, {
64
- stdio: "inherit",
65
- shell: true,
89
+ const metroProcess = startBundlerBackground({
90
+ port: options.port,
91
+ resetCache: options.resetCache,
92
+ verbose: options.verbose,
66
93
  cwd: projectRoot,
67
- env: {
68
- ...process.env,
69
- // Ensure proper encoding for CocoaPods compatibility
70
- LANG: "en_US.UTF-8",
71
- },
72
94
  });
73
95
 
74
- proc.on("error", (error) => {
75
- console.error(chalk.red(`Failed to start Metro: ${error.message}`));
76
- process.exit(1);
77
- });
96
+ // Handle cleanup on SIGINT/SIGTERM
97
+ const cleanup = () => {
98
+ metroProcess.kill("SIGTERM");
99
+ process.exit(0);
100
+ };
101
+ process.on("SIGINT", cleanup);
102
+ process.on("SIGTERM", cleanup);
78
103
 
79
- proc.on("close", (code) => {
80
- if (code !== 0 && code !== null) {
81
- process.exit(code);
82
- }
83
- });
104
+ // Attach to foreground with interactive input (r, j, d, etc.)
105
+ attachBundlerToForeground(metroProcess);
84
106
  });
85
107
 
86
108
  return dev;
@@ -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
 
@@ -358,6 +358,11 @@ export default defineConfig({
358
358
  backgroundColor: '#FFFFFF',
359
359
  },
360
360
 
361
+ navigation: {
362
+ routesDir: './src/_routes',
363
+ generatedDir: './.teardown',
364
+ },
365
+
361
366
  plugins: [
362
367
  // Add your plugins here
363
368
  ],
@@ -108,6 +108,20 @@ export const PluginEntrySchema = z.union([
108
108
  z.tuple([z.any()]),
109
109
  ]);
110
110
 
111
+ /**
112
+ * Navigation configuration schema
113
+ */
114
+ export const NavigationConfigSchema = z.object({
115
+ /** Path to routes directory relative to project root */
116
+ routesDir: z.string().optional().default("./src/_routes"),
117
+
118
+ /** Path for generated type files */
119
+ generatedDir: z.string().optional().default("./.teardown"),
120
+
121
+ /** Auto-populate new route files with template content */
122
+ autoTemplate: z.boolean().optional().default(true),
123
+ });
124
+
111
125
  /**
112
126
  * Main Teardown configuration schema
113
127
  */
@@ -150,6 +164,9 @@ export const TeardownConfigSchema = z.object({
150
164
 
151
165
  /** Environment variables to inject */
152
166
  env: z.record(z.string(), z.string()).optional(),
167
+
168
+ /** Navigation configuration */
169
+ navigation: NavigationConfigSchema.optional(),
153
170
  });
154
171
 
155
172
  /**
@@ -241,3 +258,4 @@ export type TeardownConfig = z.infer<typeof TeardownConfigSchema>;
241
258
  export type iOSConfig = z.infer<typeof iOSConfigSchema>;
242
259
  export type AndroidConfig = z.infer<typeof AndroidConfigSchema>;
243
260
  export type SplashConfig = z.infer<typeof SplashConfigSchema>;
261
+ export type NavigationConfig = z.infer<typeof NavigationConfigSchema>;
@@ -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,155 @@
1
+ /**
2
+ * Metro Config Parser
3
+ *
4
+ * Extracts navigation configuration from teardown.config.ts or 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 config files
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
+ * Config file names to search for teardown config (in order of priority)
28
+ */
29
+ const TEARDOWN_CONFIG_FILES = [
30
+ "teardown.config.ts",
31
+ "teardown.config.js",
32
+ "teardown.config.mjs",
33
+ "launchpad.config.ts",
34
+ ];
35
+
36
+ /**
37
+ * Finds metro.config.js in the project
38
+ */
39
+ function findMetroConfig(projectRoot: string): string | null {
40
+ const configNames = ["metro.config.js", "metro.config.cjs", "metro.config.mjs"];
41
+
42
+ for (const name of configNames) {
43
+ const configPath = join(projectRoot, name);
44
+ if (existsSync(configPath)) {
45
+ return configPath;
46
+ }
47
+ }
48
+
49
+ return null;
50
+ }
51
+
52
+ /**
53
+ * Parses metro.config.js to extract withTeardownNavigation options
54
+ *
55
+ * This uses simple regex parsing since we can't safely evaluate the JS file.
56
+ * It looks for patterns like:
57
+ * withTeardownNavigation(config, { routesDir: "./src/_routes", generatedDir: "./.teardown" })
58
+ */
59
+ export function parseNavigationConfig(projectRoot: string): NavigationConfig | null {
60
+ const configPath = findMetroConfig(projectRoot);
61
+ if (!configPath) {
62
+ return null;
63
+ }
64
+
65
+ try {
66
+ const content = readFileSync(configPath, "utf-8");
67
+
68
+ // Check if withTeardownNavigation is used
69
+ if (!content.includes("withTeardownNavigation")) {
70
+ return null;
71
+ }
72
+
73
+ // Extract routesDir using regex
74
+ // Matches: routesDir: "./src/_routes" or routesDir: './src/_routes'
75
+ const routesDirMatch = content.match(/routesDir:\s*['"]([^'"]+)['"]/);
76
+ const routesDir = routesDirMatch ? routesDirMatch[1] : DEFAULT_CONFIG.routesDir;
77
+
78
+ // Extract generatedDir using regex
79
+ const generatedDirMatch = content.match(/generatedDir:\s*['"]([^'"]+)['"]/);
80
+ const generatedDir = generatedDirMatch ? generatedDirMatch[1] : DEFAULT_CONFIG.generatedDir;
81
+
82
+ return {
83
+ routesDir,
84
+ generatedDir,
85
+ };
86
+ } catch {
87
+ return null;
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Parses teardown.config.ts to extract navigation options
93
+ *
94
+ * This uses simple regex parsing since we can't safely evaluate the TS file at runtime.
95
+ */
96
+ function parseTeardownConfig(projectRoot: string): NavigationConfig | null {
97
+ for (const fileName of TEARDOWN_CONFIG_FILES) {
98
+ const configPath = join(projectRoot, fileName);
99
+ if (existsSync(configPath)) {
100
+ try {
101
+ const content = readFileSync(configPath, "utf-8");
102
+
103
+ // Check if navigation config exists
104
+ if (!content.includes("navigation:") && !content.includes("navigation :")) {
105
+ continue;
106
+ }
107
+
108
+ // Extract routesDir using regex
109
+ const routesDirMatch = content.match(/routesDir:\s*['"]([^'"]+)['"]/);
110
+ const generatedDirMatch = content.match(/generatedDir:\s*['"]([^'"]+)['"]/);
111
+
112
+ // Only return if we found at least one navigation property
113
+ if (routesDirMatch || generatedDirMatch) {
114
+ return {
115
+ routesDir: routesDirMatch?.[1] ?? DEFAULT_CONFIG.routesDir,
116
+ generatedDir: generatedDirMatch?.[1] ?? DEFAULT_CONFIG.generatedDir,
117
+ };
118
+ }
119
+ } catch {
120
+ // Ignore read errors and try next config file
121
+ }
122
+ }
123
+ }
124
+ return null;
125
+ }
126
+
127
+ /**
128
+ * Gets navigation config for a project, with fallbacks
129
+ *
130
+ * Priority:
131
+ * 1. teardown.config.ts navigation section
132
+ * 2. metro.config.js withTeardownNavigation options
133
+ * 3. Default config if routes directory exists
134
+ */
135
+ export function getNavigationConfig(projectRoot: string): NavigationConfig | null {
136
+ // Try to parse from teardown.config.ts first (single source of truth)
137
+ const teardownConfig = parseTeardownConfig(projectRoot);
138
+ if (teardownConfig) {
139
+ return teardownConfig;
140
+ }
141
+
142
+ // Fallback: parse from metro.config.js (backwards compatibility)
143
+ const metroConfig = parseNavigationConfig(projectRoot);
144
+ if (metroConfig) {
145
+ return metroConfig;
146
+ }
147
+
148
+ // Fallback: check if default routes dir exists
149
+ const defaultRoutesPath = join(projectRoot, DEFAULT_CONFIG.routesDir);
150
+ if (existsSync(defaultRoutesPath)) {
151
+ return DEFAULT_CONFIG;
152
+ }
153
+
154
+ return null;
155
+ }
@@ -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";
@@ -11,12 +11,11 @@ const { getDefaultConfig, mergeConfig } = require("@react-native/metro-config");
11
11
  * - Monorepo/workspace detection and watch folder configuration
12
12
  * - Bun-specific handling (.bun directory blocking)
13
13
  *
14
- *
15
14
  * withTeardownNavigation() automatically handles:
16
15
  * - Type-safe route generation from file-based routes
17
16
  * - Deep linking configuration generation
18
17
  * - Hot-reload support via file watching in development
19
- *
18
+ * - Reads routesDir/generatedDir from teardown.config.ts navigation section
20
19
  *
21
20
  * withUniwindConfig() enables:
22
21
  * - Tailwind CSS 4 support for React Native
@@ -29,11 +28,8 @@ const config = {};
29
28
 
30
29
  const teardownConfig = withTeardown(mergeConfig(getDefaultConfig(__dirname), config));
31
30
 
32
- const navigationConfig = withTeardownNavigation(teardownConfig, {
33
- routesDir: "./src/_routes",
34
- generatedDir: "./.teardown",
35
- verbose: false,
36
- });
31
+ // Navigation config is read from teardown.config.ts navigation section
32
+ const navigationConfig = withTeardownNavigation(teardownConfig);
37
33
 
38
34
  module.exports = withUniwindConfig(navigationConfig, {
39
35
  cssEntryFile: "./src/global.css",
@@ -51,7 +51,7 @@ function HomeScreen() {
51
51
  <Card.Title>Getting Started</Card.Title>
52
52
  <Card.Description>
53
53
  1. Explore the components in src/components/ui/{"\n"}
54
- 2. Add new routes in src/routes/{"\n"}
54
+ 2. Add new routes in src/_routes/{"\n"}
55
55
  3. Customize the theme in global.css{"\n"}
56
56
  4. Build your app!
57
57
  </Card.Description>
@@ -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
  };