@teardown/cli 2.0.73 → 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 +2 -2
- package/src/cli/commands/dev.ts +37 -1
- package/src/cli/commands/run.ts +36 -1
- package/src/utils/index.ts +2 -0
- package/src/utils/metro-config.ts +107 -0
- package/templates/.teardown/linking.generated.ts +11 -0
- package/templates/.teardown/manifest.json +18 -0
- package/templates/.teardown/register.d.ts +9 -0
- package/templates/.teardown/routeTree.generated.ts +42 -0
- package/templates/.teardown/routes.generated.ts +19 -0
- package/templates/src/components/ui/text.tsx +2 -6
- package/templates/src/routes/home.tsx +94 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teardown/cli",
|
|
3
|
-
"version": "2.0.
|
|
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.
|
|
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"
|
package/src/cli/commands/dev.ts
CHANGED
|
@@ -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];
|
package/src/cli/commands/run.ts
CHANGED
|
@@ -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
|
|
package/src/utils/index.ts
CHANGED
|
@@ -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,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";
|
|
@@ -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
|
};
|
|
@@ -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
|
+
});
|