@idlesummer/pen 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,6 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 idlesummer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation...
package/README.md ADDED
@@ -0,0 +1,304 @@
1
+ # @idlesummer/pen
2
+
3
+ File-based routing framework for React Ink terminal applications (inspired by Next.js).
4
+
5
+ > **Heads up:** This is a personal learning project and my first attempt at building a routing framework. The API will change, there are bugs, and I'm figuring things out as I go. Use it for experiments and side projects, but probably not in production.
6
+
7
+ ## What is this?
8
+
9
+ Pen brings Next.js-style file-based routing to terminal applications built with [Ink](https://github.com/vadimdemedes/ink). Instead of manually wiring up routes and navigation, just organize your files in a specific structure and the routing happens automatically.
10
+
11
+ ```
12
+ src/app/
13
+ ├── layout.tsx → Root layout for all routes
14
+ ├── screen.tsx → Home route (/)
15
+ ├── about/
16
+ │ └── screen.tsx → About page (/about)
17
+ └── settings/
18
+ ├── layout.tsx → Settings layout
19
+ ├── screen.tsx → Settings home (/settings)
20
+ └── profile/
21
+ └── screen.tsx → Profile page (/settings/profile)
22
+ ```
23
+
24
+ That's it. No router config, no manual route definitions. The directory structure **is** your routing table.
25
+
26
+ ## Why?
27
+
28
+ I wanted to build something complex enough to learn from but small enough to actually finish. Terminal apps need navigation too, and doing it manually gets tedious fast. This scratches both itches.
29
+
30
+ Also, Ink is awesome and deserves better tooling.
31
+
32
+ ## Features
33
+
34
+ - **File-based routing** - Organize files, get routes for free
35
+ - **Nested layouts** - Wrap child routes with parent layouts automatically
36
+ - **Error boundaries** - Catch errors at any level of your route tree
37
+ - **Not-found handling** - Custom 404 screens per route
38
+ - **Type-safe navigation** - Full TypeScript support throughout
39
+ - **Simple CLI** - Three commands: `init`, `build`, `start`
40
+
41
+ ## What's missing?
42
+
43
+ A lot, honestly:
44
+
45
+ - No dev mode or watch mode (you have to rebuild manually)
46
+ - No dynamic routes like `/users/[id]`
47
+ - No loading states
48
+ - No route middleware
49
+ - No lazy loading
50
+ - Not optimized for large apps
51
+
52
+ I'll get to these eventually. Or maybe you will! PRs welcome.
53
+
54
+ ## Installation
55
+
56
+ ```bash
57
+ npm install github:idlesummer/pen
58
+ ```
59
+
60
+ Once published to npm, it'll be:
61
+ ```bash
62
+ npm install @idlesummer/pen
63
+ ```
64
+
65
+ ## Quick Start
66
+
67
+ ### 1. Initialize your project
68
+
69
+ ```bash
70
+ npx pen init
71
+ ```
72
+
73
+ This creates a basic file structure:
74
+ ```
75
+ src/app/
76
+ ├── layout.tsx
77
+ └── screen.tsx
78
+ ```
79
+
80
+ ### 2. Create some routes
81
+
82
+ ```bash
83
+ mkdir src/app/about
84
+ echo "export default () => 'About page'" > src/app/about/screen.tsx
85
+ ```
86
+
87
+ ### 3. Build and run
88
+
89
+ ```bash
90
+ npx pen build
91
+ npx pen start
92
+ ```
93
+
94
+ Your app starts at the initial route defined in your entry point.
95
+
96
+ ## File Conventions
97
+
98
+ ### `screen.tsx` (required)
99
+
100
+ The actual content for a route. Every route needs one.
101
+
102
+ ```tsx
103
+ export default function HomeScreen() {
104
+ return <Text>Welcome home</Text>;
105
+ }
106
+ ```
107
+
108
+ ### `layout.tsx` (optional)
109
+
110
+ Wraps child routes. Layouts inherit from parent to child.
111
+
112
+ ```tsx
113
+ import { Box, Text } from 'ink';
114
+
115
+ export default function SettingsLayout({ children }) {
116
+ return (
117
+ <Box flexDirection="column">
118
+ <Text bold>Settings</Text>
119
+ {children}
120
+ </Box>
121
+ );
122
+ }
123
+ ```
124
+
125
+ ### `error.tsx` (optional)
126
+
127
+ Error boundary fallback for this route and its children.
128
+
129
+ ```tsx
130
+ export default function SettingsError({ error, reset }) {
131
+ return (
132
+ <Box>
133
+ <Text color="red">Something broke: {error.message}</Text>
134
+ <Text dimColor>Press R to reset</Text>
135
+ </Box>
136
+ );
137
+ }
138
+ ```
139
+
140
+ ### `not-found.tsx` (optional)
141
+
142
+ Custom 404 screen when a child route doesn't exist.
143
+
144
+ ```tsx
145
+ export default function NotFound({ url }) {
146
+ return <Text>Route not found: {url}</Text>;
147
+ }
148
+ ```
149
+
150
+ ### Special Folders
151
+
152
+ - `_private/` - Ignored by the router (for utilities, shared components, etc.)
153
+ - `(group)/` - Groups routes without adding a URL segment
154
+
155
+ ## Navigation
156
+
157
+ Use the provided hooks to navigate around your app:
158
+
159
+ ```tsx
160
+ import { useNavigate, useUrl, useHistory } from '@idlesummer/pen';
161
+
162
+ function MyComponent() {
163
+ const navigate = useNavigate();
164
+ const url = useUrl();
165
+ const { back, forward, position, history } = useHistory();
166
+
167
+ return (
168
+ <Box>
169
+ <Text>Current URL: {url}</Text>
170
+ <Text onPress={() => navigate('/about')}>Go to About</Text>
171
+ <Text onPress={() => back()}>Go back</Text>
172
+ </Box>
173
+ );
174
+ }
175
+ ```
176
+
177
+ Available hooks:
178
+ - `useNavigate()` - Returns `push`, `replace` functions
179
+ - `useUrl()` - Current URL string
180
+ - `useHistory()` - History stack and navigation
181
+ - `useRouteData()` - Data passed during navigation
182
+ - `useRouter()` - Full router context (all of the above)
183
+
184
+ ## CLI Commands
185
+
186
+ ### `pen init`
187
+
188
+ Creates initial app structure and config file.
189
+
190
+ ```bash
191
+ npx pen init
192
+ ```
193
+
194
+ ### `pen build`
195
+
196
+ Scans your `src/app` directory, generates routing code, and bundles everything.
197
+
198
+ ```bash
199
+ npx pen build
200
+ ```
201
+
202
+ ### `pen start [url]`
203
+
204
+ Runs your compiled app. Optionally pass an initial URL.
205
+
206
+ ```bash
207
+ npx pen start
208
+ npx pen start /settings/profile
209
+ ```
210
+
211
+ ## Configuration
212
+
213
+ Create a `pen.config.ts` file in your project root:
214
+
215
+ ```typescript
216
+ import { defineConfig } from '@idlesummer/pen';
217
+
218
+ export default defineConfig({
219
+ appDir: './src/app', // Where your routes live
220
+ outDir: './.pen', // Build output directory
221
+ });
222
+ ```
223
+
224
+ ## How It Works
225
+
226
+ The build process has three phases:
227
+
228
+ 1. **Scan** - Walks your `src/app` directory and builds a file tree
229
+ 2. **Generate** - Creates TypeScript files with route manifests and component imports
230
+ 3. **Compile** - Bundles everything with Rolldown into a single executable
231
+
232
+ At runtime, the router:
233
+ - Matches URLs in O(1) time using a flat manifest
234
+ - Composes route trees from inside-out (screen → layout → layout...)
235
+ - Manages navigation history and state
236
+
237
+ For a deep dive, check out [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
238
+
239
+ ## Examples
240
+
241
+ The `examples/` directory has working demos you can run:
242
+
243
+ ```bash
244
+ cd examples/basic-app
245
+ npx pen build
246
+ npx pen start
247
+ ```
248
+
249
+ ## Development
250
+
251
+ ```bash
252
+ # Install dependencies
253
+ npm install
254
+
255
+ # Build the library
256
+ npm run build
257
+
258
+ # Run tests
259
+ npm test
260
+
261
+ # Lint
262
+ npm run lint
263
+ ```
264
+
265
+ ## Roadmap
266
+
267
+ Things I want to add (no timeline, just a wishlist):
268
+
269
+ - [ ] Dev mode with watch/rebuild
270
+ - [ ] Dynamic routes and catch-all routes (`[id]`, `[...slug]`)
271
+ - [ ] Integration tests
272
+ - [ ] Loading states
273
+ - [ ] Route middleware
274
+ - [ ] Better error messages
275
+ - [ ] Plugin system
276
+ - [ ] TypeScript route type generation
277
+ - [ ] Parallel routes (`@modal`, `@sidebar`)
278
+
279
+ ## Inspiration
280
+
281
+ This project wouldn't exist without:
282
+
283
+ - [Next.js](https://nextjs.org) - For showing that file-based routing can be elegant
284
+ - [Ink](https://github.com/vadimdemedes/ink) - For making terminal UIs actually enjoyable to build
285
+ - [TanStack Router](https://tanstack.com/router) - For ideas around type-safe routing
286
+
287
+ ## Contributing
288
+
289
+ I'm learning as I build this, so the codebase is probably messy in places. That said, if you want to contribute:
290
+
291
+ - Open an issue first to discuss big changes
292
+ - Keep it simple - I'm trying to learn, not build the next industry standard
293
+ - Tests are appreciated but not required
294
+ - Don't expect fast reviews (this is a side project)
295
+
296
+ ## License
297
+
298
+ MIT - See [LICENSE](LICENSE) file
299
+
300
+ ## Questions?
301
+
302
+ Open an issue! I can't promise quick responses, but I'll do my best.
303
+
304
+ Happy hacking! 🚀
package/dist/bin.d.mts ADDED
@@ -0,0 +1 @@
1
+ export { };
package/dist/bin.mjs ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ import{f as e,i as t,n,r,t as i}from"./component-map-CLP6MqF8.mjs";import{join as a,resolve as o}from"path";import{pathToFileURL as s}from"url";import{existsSync as c}from"fs";import{Command as l}from"commander";import{duration as u,fileList as d,pipe as f}from"@idlesummer/tasker";import{mkdir as p,writeFile as m}from"fs/promises";import{build as h}from"rolldown";import g from"rollup-plugin-node-externals";var _=Object.create,v=Object.defineProperty,y=Object.getOwnPropertyDescriptor,b=Object.getOwnPropertyNames,x=Object.getPrototypeOf,S=Object.prototype.hasOwnProperty,C=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),w=(e,t,n,r)=>{if(t&&typeof t==`object`||typeof t==`function`)for(var i=b(t),a=0,o=i.length,s;a<o;a++)s=i[a],!S.call(e,s)&&s!==n&&v(e,s,{get:(e=>t[e]).bind(null,s),enumerable:!(r=y(t,s))||r.enumerable});return e},T=(e,t,n)=>(n=e==null?{}:_(x(e)),w(t||!e||!e.__esModule?v(n,`default`,{value:e,enumerable:!0}):n,e));const E=`@idlesummer/pen`,D=`0.1.0`,O=`@idlesummer/pen`.split(`/`)?.[1]??`@idlesummer/pen`;var k=T(C(((e,t)=>{let n=process||{},r=n.argv||[],i=n.env||{},a=!(i.NO_COLOR||r.includes(`--no-color`))&&(!!i.FORCE_COLOR||r.includes(`--color`)||n.platform===`win32`||(n.stdout||{}).isTTY&&i.TERM!==`dumb`||!!i.CI),o=(e,t,n=e)=>r=>{let i=``+r,a=i.indexOf(t,e.length);return~a?e+s(i,t,n,a)+t:e+i+t},s=(e,t,n,r)=>{let i=``,a=0;do i+=e.substring(a,r)+n,a=r+t.length,r=e.indexOf(t,a);while(~r);return i+e.substring(a)},c=(e=a)=>{let t=e?o:()=>String;return{isColorSupported:e,reset:t(`\x1B[0m`,`\x1B[0m`),bold:t(`\x1B[1m`,`\x1B[22m`,`\x1B[22m\x1B[1m`),dim:t(`\x1B[2m`,`\x1B[22m`,`\x1B[22m\x1B[2m`),italic:t(`\x1B[3m`,`\x1B[23m`),underline:t(`\x1B[4m`,`\x1B[24m`),inverse:t(`\x1B[7m`,`\x1B[27m`),hidden:t(`\x1B[8m`,`\x1B[28m`),strikethrough:t(`\x1B[9m`,`\x1B[29m`),black:t(`\x1B[30m`,`\x1B[39m`),red:t(`\x1B[31m`,`\x1B[39m`),green:t(`\x1B[32m`,`\x1B[39m`),yellow:t(`\x1B[33m`,`\x1B[39m`),blue:t(`\x1B[34m`,`\x1B[39m`),magenta:t(`\x1B[35m`,`\x1B[39m`),cyan:t(`\x1B[36m`,`\x1B[39m`),white:t(`\x1B[37m`,`\x1B[39m`),gray:t(`\x1B[90m`,`\x1B[39m`),bgBlack:t(`\x1B[40m`,`\x1B[49m`),bgRed:t(`\x1B[41m`,`\x1B[49m`),bgGreen:t(`\x1B[42m`,`\x1B[49m`),bgYellow:t(`\x1B[43m`,`\x1B[49m`),bgBlue:t(`\x1B[44m`,`\x1B[49m`),bgMagenta:t(`\x1B[45m`,`\x1B[49m`),bgCyan:t(`\x1B[46m`,`\x1B[49m`),bgWhite:t(`\x1B[47m`,`\x1B[49m`),blackBright:t(`\x1B[90m`,`\x1B[39m`),redBright:t(`\x1B[91m`,`\x1B[39m`),greenBright:t(`\x1B[92m`,`\x1B[39m`),yellowBright:t(`\x1B[93m`,`\x1B[39m`),blueBright:t(`\x1B[94m`,`\x1B[39m`),magentaBright:t(`\x1B[95m`,`\x1B[39m`),cyanBright:t(`\x1B[96m`,`\x1B[39m`),whiteBright:t(`\x1B[97m`,`\x1B[39m`),bgBlackBright:t(`\x1B[100m`,`\x1B[49m`),bgRedBright:t(`\x1B[101m`,`\x1B[49m`),bgGreenBright:t(`\x1B[102m`,`\x1B[49m`),bgYellowBright:t(`\x1B[103m`,`\x1B[49m`),bgBlueBright:t(`\x1B[104m`,`\x1B[49m`),bgMagentaBright:t(`\x1B[105m`,`\x1B[49m`),bgCyanBright:t(`\x1B[106m`,`\x1B[49m`),bgWhiteBright:t(`\x1B[107m`,`\x1B[49m`)}};t.exports=c(),t.exports.createColors=c}))(),1);const A=[{name:`Scanning filesystem...`,onSuccess:(e,t)=>`Scanned filesystem (${u(t)})`,run:async e=>({fileTree:t(e.appDir)})},{name:`Building segment tree...`,onSuccess:(e,t)=>`Built segment tree (${u(t)})`,run:async e=>({segmentTree:r(e.fileTree)})},{name:`Generating manifest`,onSuccess:(e,t)=>`Generated manifest (${u(t)})`,run:async e=>({manifest:n(e.segmentTree)})}],j=[{name:`Writing manifest.ts`,onSuccess:(e,t)=>`Saved manifest.ts (${u(t)})`,run:async e=>{let t=a(e.outDir,`generated`),n=a(t,`manifest.ts`);await p(t,{recursive:!0}),await m(n,[`// Auto-generated by ${E}`,`// Do not manually edit this file`,``,`import { RouteManifest } from '${E}'`,``,`export const manifest: RouteManifest = ${JSON.stringify(e.manifest,null,2)} as const`,``].join(`
3
+ `),`utf-8`)}},{name:`Writing components.ts`,onSuccess:(e,t)=>`Saved components.ts (${u(t)})`,run:async e=>{let t=a(e.outDir,`generated`),n=a(t,`components.ts`);await p(t,{recursive:!0});let r=i(e.manifest,e.outDir),o=Object.entries(r).sort(([e],[t])=>e.localeCompare(t)),s=o.map(([e,t],n)=>`import Component${n} from '${t}'`).join(`
4
+ `),c=o.map(([e],t)=>` '${e.replace(/\\/g,`\\\\`)}': Component${t},`).join(`
5
+ `);await m(n,[`// Auto-generated by ${E}`,`// Do not manually edit this file`,``,`import { ComponentMap } from '${E}'`,``,s,``,`export const components: ComponentMap = {`,c,`} as const`,``].join(`
6
+ `),`utf-8`)}},{name:`Writing entry.ts`,onSuccess:(e,t)=>`Saved entry.ts (${u(t)})`,run:async e=>{let t=a(e.outDir,`generated`),n=a(t,`entry.ts`);await p(t,{recursive:!0}),await m(n,[`// Auto-generated by ${E}`,`// Do not manually edit this file`,``,`import { createElement } from 'react'`,`import { render } from 'ink'`,`import { App } from '${E}'`,``,`import { manifest } from './manifest'`,`import { components } from './components'`,``,`export async function run(initialUrl: string) {`,` const element = createElement(App, { initialUrl, manifest, components })`,` const { waitUntilExit } = render(element)`,` await waitUntilExit()`,`}`,``].join(`
7
+ `),`utf-8`)}}],M={name:`Compiling application`,onSuccess:(e,t)=>`Compiled application (${u(t)})`,onError:e=>`Compilation failed: ${e.message}`,run:async e=>{await h({input:o(a(e.outDir,`generated`,`entry.ts`)),platform:`node`,resolve:{extensions:[`.ts`,`.tsx`,`.js`,`.jsx`]},plugins:[g()],output:{dir:o(a(e.outDir,`dist`)),format:`esm`,sourcemap:!0,minify:!0}})}},N={name:`build`,desc:`Build the route manifest and compile application`,action:async()=>{try{let{appDir:t,outDir:n}=await e();console.log(k.default.cyan(` Starting production build...
8
+ `)),console.log(k.default.bold(` ✎ ${O} v${D}\n`)),console.log(k.default.dim(` entry: ${t}`)),console.log(k.default.dim(` target: node24`)),console.log(k.default.dim(` output: ${n}`)),console.log();let{duration:r}=await f([...A,...j,M]).run({appDir:t,outDir:n});console.log(),console.log(d(n,`**/*`)),console.log(),console.log(`${k.default.green(`✓`)} Built in ${k.default.bold(u(r))}`),console.log()}catch(e){console.error(`${k.default.red(`✗`)} Build failed`),console.log();let t=e instanceof Error?e.message:String(e);console.error(k.default.red(t)),console.log(),process.exit(1)}}},P={name:`init`,desc:`Initialize a new Pen project`,action:async()=>{if(console.log(k.default.cyan(`\n Initializing ${O} project...\n`)),c(`pen.config.ts`)){console.error(k.default.yellow(`⚠`)+` Project already initialized`),console.error(k.default.dim(` pen.config.ts already exists`));return}await m(`pen.config.ts`,[`import { defineConfig } from '${E}'`,``,`export default defineConfig({`,` appDir: './src/app',`,` outDir: './.pen',`,`})`,``].join(`
9
+ `),`utf-8`),console.log(k.default.green(`✓`)+` Created pen.config.ts`);let e=`./src/app`;await p(e,{recursive:!0});let t=[`import { Box, Text } from 'ink'`,`import type { ReactNode } from 'react'`,``,`export default function Layout({ children }: { children?: ReactNode }) {`,` return (`,` <Box flexDirection="column" padding={1}>`,` <Box marginBottom={1} borderStyle="round" borderColor="cyan" paddingX={2}>`,` <Text bold color="cyan">Welcome to Pen</Text>`,` </Box>`,` {children}`,` </Box>`,` )`,`}`,``].join(`
10
+ `);await m(a(e,`layout.tsx`),t,`utf-8`),console.log(k.default.green(`✓`)+` Created src/app/layout.tsx`);let n=[`import { useState } from 'react'`,`import { Box, Text, useInput } from 'ink'`,``,`export default function Screen() {`,` const [count, setCount] = useState(0)`,``,` useInput((input) => {`,` if (input === ' ') setCount(c => c + 1)`,` })`,``,` return (`,` <Box flexDirection="column" gap={1}>`,` <Box>`,` <Text>Count: <Text bold color="green">{count}</Text></Text>`,` </Box>`,` <Box>`,` <Text dimColor>Press <Text bold>SPACE</Text> to increment</Text>`,` </Box>`,` </Box>`,` )`,`}`,``].join(`
11
+ `);await m(a(e,`screen.tsx`),n,`utf-8`),console.log(k.default.green(`✓`)+` Created src/app/screen.tsx`),console.log(),console.log(k.default.green(`✓`)+` Project initialized!`),console.log(),console.log(k.default.bold(` Next steps:`)),console.log(),console.log(k.default.dim(` 1. Add scripts to package.json:`)),console.log(),console.log(` {`),console.log(` "scripts": {`),console.log(` "build": "${O} build",`),console.log(` "start": "${O} start"`),console.log(` }`),console.log(` }`),console.log(),console.log(k.default.dim(` 2. Run "npm run build" to build your app`)),console.log(k.default.dim(` 3. Run "npm run start" to start your app`)),console.log()}},F={name:`start`,desc:`Start the application`,action:async()=>{let{outDir:t}=await e(),n=o(t,`dist`,`entry.js`);if(!c(n))throw console.error(k.default.red(`✗`)+` Build not found`),console.error(k.default.dim(` Run "${O} build" first`)),Error(`Build not found`);let{run:r}=await import(s(n).href);await r(`/`)}};function I(e){e.name(O).description(`File-based routing for React Ink apps (experimental)`).version(D);let t=[N,P,F];for(let{name:n,desc:r,action:i}of t)e.command(n).description(r).action(i);return e}async function L(e){let t=I(new l);try{return await t.parseAsync(e),0}catch(e){let t=e instanceof Error?e.message:`Unknown error`;return console.error(t),1}}process.exit(await L(process.argv));export{};
12
+ //# sourceMappingURL=bin.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bin.mjs","names":["rolldownBuild","build","pc","pc","pc","run","build"],"sources":["../src/core/constants.ts","../node_modules/picocolors/picocolors.js","../src/cli/commands/build/tasks/scan.ts","../src/cli/commands/build/tasks/generate.ts","../src/cli/commands/build/tasks/compile.ts","../src/cli/commands/build/index.ts","../src/cli/commands/init/index.ts","../src/cli/commands/start/index.ts","../src/cli/index.ts","../src/bin.ts"],"sourcesContent":["// Build-time globals injected by tsdown via `define`.\r\n// tsdown replaces these placeholders with actual values\r\n// from package.json at compile time.\r\n\r\ndeclare const __DESCRIPTION__: string\r\ndeclare const __PACKAGE_NAME__: string\r\ndeclare const __VERSION__: string\r\n\r\n// Package metadata\r\nexport const DESCRIPTION = __DESCRIPTION__\r\nexport const PACKAGE_NAME = __PACKAGE_NAME__\r\nexport const VERSION = __VERSION__\r\n\r\n// Framework metadata\r\nexport const CLI_NAME = __PACKAGE_NAME__.split('/')?.[1] ?? __PACKAGE_NAME__\r\n","let p = process || {}, argv = p.argv || [], env = p.env || {}\nlet isColorSupported =\n\t!(!!env.NO_COLOR || argv.includes(\"--no-color\")) &&\n\t(!!env.FORCE_COLOR || argv.includes(\"--color\") || p.platform === \"win32\" || ((p.stdout || {}).isTTY && env.TERM !== \"dumb\") || !!env.CI)\n\nlet formatter = (open, close, replace = open) =>\n\tinput => {\n\t\tlet string = \"\" + input, index = string.indexOf(close, open.length)\n\t\treturn ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close\n\t}\n\nlet replaceClose = (string, close, replace, index) => {\n\tlet result = \"\", cursor = 0\n\tdo {\n\t\tresult += string.substring(cursor, index) + replace\n\t\tcursor = index + close.length\n\t\tindex = string.indexOf(close, cursor)\n\t} while (~index)\n\treturn result + string.substring(cursor)\n}\n\nlet createColors = (enabled = isColorSupported) => {\n\tlet f = enabled ? formatter : () => String\n\treturn {\n\t\tisColorSupported: enabled,\n\t\treset: f(\"\\x1b[0m\", \"\\x1b[0m\"),\n\t\tbold: f(\"\\x1b[1m\", \"\\x1b[22m\", \"\\x1b[22m\\x1b[1m\"),\n\t\tdim: f(\"\\x1b[2m\", \"\\x1b[22m\", \"\\x1b[22m\\x1b[2m\"),\n\t\titalic: f(\"\\x1b[3m\", \"\\x1b[23m\"),\n\t\tunderline: f(\"\\x1b[4m\", \"\\x1b[24m\"),\n\t\tinverse: f(\"\\x1b[7m\", \"\\x1b[27m\"),\n\t\thidden: f(\"\\x1b[8m\", \"\\x1b[28m\"),\n\t\tstrikethrough: f(\"\\x1b[9m\", \"\\x1b[29m\"),\n\n\t\tblack: f(\"\\x1b[30m\", \"\\x1b[39m\"),\n\t\tred: f(\"\\x1b[31m\", \"\\x1b[39m\"),\n\t\tgreen: f(\"\\x1b[32m\", \"\\x1b[39m\"),\n\t\tyellow: f(\"\\x1b[33m\", \"\\x1b[39m\"),\n\t\tblue: f(\"\\x1b[34m\", \"\\x1b[39m\"),\n\t\tmagenta: f(\"\\x1b[35m\", \"\\x1b[39m\"),\n\t\tcyan: f(\"\\x1b[36m\", \"\\x1b[39m\"),\n\t\twhite: f(\"\\x1b[37m\", \"\\x1b[39m\"),\n\t\tgray: f(\"\\x1b[90m\", \"\\x1b[39m\"),\n\n\t\tbgBlack: f(\"\\x1b[40m\", \"\\x1b[49m\"),\n\t\tbgRed: f(\"\\x1b[41m\", \"\\x1b[49m\"),\n\t\tbgGreen: f(\"\\x1b[42m\", \"\\x1b[49m\"),\n\t\tbgYellow: f(\"\\x1b[43m\", \"\\x1b[49m\"),\n\t\tbgBlue: f(\"\\x1b[44m\", \"\\x1b[49m\"),\n\t\tbgMagenta: f(\"\\x1b[45m\", \"\\x1b[49m\"),\n\t\tbgCyan: f(\"\\x1b[46m\", \"\\x1b[49m\"),\n\t\tbgWhite: f(\"\\x1b[47m\", \"\\x1b[49m\"),\n\n\t\tblackBright: f(\"\\x1b[90m\", \"\\x1b[39m\"),\n\t\tredBright: f(\"\\x1b[91m\", \"\\x1b[39m\"),\n\t\tgreenBright: f(\"\\x1b[92m\", \"\\x1b[39m\"),\n\t\tyellowBright: f(\"\\x1b[93m\", \"\\x1b[39m\"),\n\t\tblueBright: f(\"\\x1b[94m\", \"\\x1b[39m\"),\n\t\tmagentaBright: f(\"\\x1b[95m\", \"\\x1b[39m\"),\n\t\tcyanBright: f(\"\\x1b[96m\", \"\\x1b[39m\"),\n\t\twhiteBright: f(\"\\x1b[97m\", \"\\x1b[39m\"),\n\n\t\tbgBlackBright: f(\"\\x1b[100m\", \"\\x1b[49m\"),\n\t\tbgRedBright: f(\"\\x1b[101m\", \"\\x1b[49m\"),\n\t\tbgGreenBright: f(\"\\x1b[102m\", \"\\x1b[49m\"),\n\t\tbgYellowBright: f(\"\\x1b[103m\", \"\\x1b[49m\"),\n\t\tbgBlueBright: f(\"\\x1b[104m\", \"\\x1b[49m\"),\n\t\tbgMagentaBright: f(\"\\x1b[105m\", \"\\x1b[49m\"),\n\t\tbgCyanBright: f(\"\\x1b[106m\", \"\\x1b[49m\"),\n\t\tbgWhiteBright: f(\"\\x1b[107m\", \"\\x1b[49m\"),\n\t}\n}\n\nmodule.exports = createColors()\nmodule.exports.createColors = createColors\n","import { duration } from '@idlesummer/tasker'\r\nimport { buildFileTree } from '@/core/route-builder'\r\nimport { buildSegmentTree } from '@/core/route-builder'\r\nimport { buildRouteManifest } from '@/core/route-builder'\r\nimport type { Task } from '@idlesummer/tasker'\r\nimport type { BuildContext } from '../types'\r\n\r\nexport const scanTasks: Task<BuildContext>[] = [\r\n {\r\n name: 'Scanning filesystem...',\r\n onSuccess: (_, dur) => `Scanned filesystem (${duration(dur)})`,\r\n run: async (ctx) => ({ fileTree: buildFileTree(ctx.appDir) }),\r\n },\r\n {\r\n name: 'Building segment tree...',\r\n onSuccess: (_, dur) => `Built segment tree (${duration(dur)})`,\r\n run: async (ctx) => ({ segmentTree: buildSegmentTree(ctx.fileTree!) }),\r\n },\r\n {\r\n name: 'Generating manifest',\r\n onSuccess: (_, dur) => `Generated manifest (${duration(dur)})`,\r\n run: async (ctx) => {\r\n // await delay(500) // Simulate work\r\n const manifest = buildRouteManifest(ctx.segmentTree!) // Safe: set by previous task\r\n return { manifest }\r\n },\r\n },\r\n]\r\n","import { mkdir, writeFile } from 'fs/promises'\r\nimport { join } from 'path'\r\nimport { duration } from '@idlesummer/tasker'\r\nimport { PACKAGE_NAME } from '@/core/constants'\r\nimport { buildComponentMap } from '@/core/route-builder'\r\nimport type { Task } from '@idlesummer/tasker'\r\nimport type { BuildContext } from '../types'\r\n\r\nexport const generateTasks: Task<BuildContext>[] = [\r\n {\r\n name: 'Writing manifest.ts',\r\n onSuccess: (_, dur) => `Saved manifest.ts (${duration(dur)})`,\r\n run: async (ctx) => {\r\n const genDir = join(ctx.outDir, 'generated')\r\n const manifestPath = join(genDir, 'manifest.ts')\r\n await mkdir(genDir, { recursive: true })\r\n\r\n const code = [\r\n `// Auto-generated by ${PACKAGE_NAME}`,\r\n '// Do not manually edit this file',\r\n '',\r\n `import { RouteManifest } from '${PACKAGE_NAME}'`,\r\n '',\r\n `export const manifest: RouteManifest = ${JSON.stringify(ctx.manifest!, null, 2)} as const`,\r\n '',\r\n ].join('\\n')\r\n\r\n await writeFile(manifestPath, code, 'utf-8')\r\n },\r\n },\r\n {\r\n name: 'Writing components.ts',\r\n onSuccess: (_, dur) => `Saved components.ts (${duration(dur)})`,\r\n run: async (ctx) => {\r\n const genDir = join(ctx.outDir, 'generated')\r\n const componentsPath = join(genDir, 'components.ts')\r\n await mkdir(genDir, { recursive: true })\r\n\r\n const componentMap = buildComponentMap(ctx.manifest!, ctx.outDir)\r\n const entries = Object.entries(componentMap).sort(([a], [b]) => a.localeCompare(b))\r\n\r\n const imports = entries\r\n .map(([_, importPath], i) => `import Component${i} from '${importPath}'`)\r\n .join('\\n')\r\n\r\n const exports = entries\r\n .map(([absPath], i) => ` '${absPath.replace(/\\\\/g, '\\\\\\\\')}': Component${i},`)\r\n .join('\\n')\r\n\r\n const code = [\r\n `// Auto-generated by ${PACKAGE_NAME}`,\r\n '// Do not manually edit this file',\r\n '',\r\n `import { ComponentMap } from '${PACKAGE_NAME}'`,\r\n '',\r\n imports,\r\n '',\r\n 'export const components: ComponentMap = {',\r\n exports,\r\n '} as const',\r\n '',\r\n ].join('\\n')\r\n\r\n await writeFile(componentsPath, code, 'utf-8')\r\n },\r\n },\r\n {\r\n name: 'Writing entry.ts',\r\n onSuccess: (_, dur) => `Saved entry.ts (${duration(dur)})`,\r\n run: async (ctx) => {\r\n const genDir = join(ctx.outDir, 'generated')\r\n const entryPath = join(genDir, 'entry.ts')\r\n await mkdir(genDir, { recursive: true })\r\n\r\n const code = [\r\n `// Auto-generated by ${PACKAGE_NAME}`,\r\n '// Do not manually edit this file',\r\n '',\r\n 'import { createElement } from \\'react\\'',\r\n 'import { render } from \\'ink\\'',\r\n `import { App } from '${PACKAGE_NAME}'`,\r\n '',\r\n 'import { manifest } from \\'./manifest\\'',\r\n 'import { components } from \\'./components\\'',\r\n '',\r\n 'export async function run(initialUrl: string) {',\r\n ' const element = createElement(App, { initialUrl, manifest, components })',\r\n ' const { waitUntilExit } = render(element)',\r\n ' await waitUntilExit()',\r\n '}',\r\n '',\r\n ].join('\\n')\r\n\r\n await writeFile(entryPath, code, 'utf-8')\r\n },\r\n },\r\n]\r\n","import { join, resolve } from 'path'\r\nimport { build as rolldownBuild } from 'rolldown'\r\nimport nodeExternals from 'rollup-plugin-node-externals'\r\nimport { duration } from '@idlesummer/tasker'\r\nimport type { Task } from '@idlesummer/tasker'\r\nimport type { BuildContext } from '../types'\r\n\r\nexport const compileTask: Task<BuildContext> = {\r\n name: 'Compiling application',\r\n onSuccess: (_, dur) => `Compiled application (${duration(dur)})`,\r\n onError: (err) => `Compilation failed: ${err.message}`,\r\n run: async (ctx) => {\r\n const entryPoint = resolve(join(ctx.outDir, 'generated', 'entry.ts'))\r\n\r\n await rolldownBuild({\r\n input: entryPoint,\r\n platform: 'node',\r\n resolve: {\r\n extensions: ['.ts', '.tsx', '.js', '.jsx'],\r\n },\r\n plugins: [nodeExternals()],\r\n output: {\r\n dir: resolve(join(ctx.outDir, 'dist')), // Also resolve output\r\n format: 'esm',\r\n sourcemap: true,\r\n minify: true,\r\n },\r\n })\r\n },\r\n}\r\n","import pc from 'picocolors'\r\nimport { pipe, duration, fileList } from '@idlesummer/tasker'\r\nimport { loadConfig } from '@/core/config'\r\nimport { CLI_NAME, VERSION } from '@/core/constants'\r\nimport { scanTasks } from './tasks/scan'\r\nimport { generateTasks } from './tasks/generate'\r\nimport { compileTask } from './tasks/compile'\r\nimport type { CLICommand } from '../../types'\r\n\r\nexport const build: CLICommand = {\r\n name: 'build',\r\n desc: 'Build the route manifest and compile application',\r\n action: async () => {\r\n try {\r\n const { appDir, outDir } = await loadConfig()\r\n console.log(pc.cyan(' Starting production build...\\n'))\r\n console.log(pc.bold(` ✎ ${CLI_NAME} v${VERSION}\\n`))\r\n console.log(pc.dim( ` entry: ${appDir}`))\r\n console.log(pc.dim( ' target: node24'))\r\n console.log(pc.dim( ` output: ${outDir}`))\r\n console.log()\r\n\r\n const tasks = [...scanTasks, ...generateTasks, compileTask]\r\n const pipeline = pipe(tasks)\r\n\r\n const { duration: dur } = await pipeline.run({ appDir, outDir })\r\n console.log()\r\n console.log(fileList(outDir, '**/*'))\r\n console.log()\r\n console.log(`${pc.green('✓')} Built in ${pc.bold(duration(dur))}`)\r\n console.log()\r\n }\r\n\r\n catch (err) {\r\n console.error(`${pc.red('✗')} Build failed`)\r\n console.log()\r\n\r\n const message = err instanceof Error ? err.message : String(err)\r\n console.error(pc.red(message))\r\n console.log()\r\n process.exit(1)\r\n }\r\n },\r\n}\r\n","import { existsSync } from 'fs'\r\nimport { mkdir, writeFile } from 'fs/promises'\r\nimport { join } from 'path'\r\nimport pc from 'picocolors'\r\n\r\nimport { CLI_NAME, PACKAGE_NAME } from '@/core/constants'\r\nimport type { CLICommand } from '../../types'\r\n\r\nexport const init: CLICommand = {\r\n name: 'init',\r\n desc: 'Initialize a new Pen project',\r\n action: async () => {\r\n console.log(pc.cyan(`\\n Initializing ${CLI_NAME} project...\\n`))\r\n\r\n // Check if already initialized\r\n if (existsSync('pen.config.ts')) {\r\n console.error(pc.yellow('⚠') + ' Project already initialized')\r\n console.error(pc.dim(' pen.config.ts already exists'))\r\n return\r\n }\r\n\r\n // Create pen.config.ts\r\n const configContent = [\r\n `import { defineConfig } from '${PACKAGE_NAME}'`,\r\n '',\r\n 'export default defineConfig({',\r\n ' appDir: \\'./src/app\\',',\r\n ' outDir: \\'./.pen\\',',\r\n '})',\r\n '',\r\n ].join('\\n')\r\n\r\n await writeFile('pen.config.ts', configContent, 'utf-8')\r\n console.log(pc.green('✓') + ' Created pen.config.ts')\r\n\r\n // Create src/app directory\r\n const appDir = './src/app'\r\n await mkdir(appDir, { recursive: true })\r\n\r\n // Create layout.tsx\r\n const layoutContent = [\r\n 'import { Box, Text } from \\'ink\\'',\r\n 'import type { ReactNode } from \\'react\\'',\r\n '',\r\n 'export default function Layout({ children }: { children?: ReactNode }) {',\r\n ' return (',\r\n ' <Box flexDirection=\"column\" padding={1}>',\r\n ' <Box marginBottom={1} borderStyle=\"round\" borderColor=\"cyan\" paddingX={2}>',\r\n ' <Text bold color=\"cyan\">Welcome to Pen</Text>',\r\n ' </Box>',\r\n ' {children}',\r\n ' </Box>',\r\n ' )',\r\n '}',\r\n '',\r\n ].join('\\n')\r\n\r\n await writeFile(join(appDir, 'layout.tsx'), layoutContent, 'utf-8')\r\n console.log(pc.green('✓') + ' Created src/app/layout.tsx')\r\n\r\n // Create screen.tsx\r\n const screenContent = [\r\n 'import { useState } from \\'react\\'',\r\n 'import { Box, Text, useInput } from \\'ink\\'',\r\n '',\r\n 'export default function Screen() {',\r\n ' const [count, setCount] = useState(0)',\r\n '',\r\n ' useInput((input) => {',\r\n ' if (input === \\' \\') setCount(c => c + 1)',\r\n ' })',\r\n '',\r\n ' return (',\r\n ' <Box flexDirection=\"column\" gap={1}>',\r\n ' <Box>',\r\n ' <Text>Count: <Text bold color=\"green\">{count}</Text></Text>',\r\n ' </Box>',\r\n ' <Box>',\r\n ' <Text dimColor>Press <Text bold>SPACE</Text> to increment</Text>',\r\n ' </Box>',\r\n ' </Box>',\r\n ' )',\r\n '}',\r\n '',\r\n ].join('\\n')\r\n\r\n await writeFile(join(appDir, 'screen.tsx'), screenContent, 'utf-8')\r\n console.log(pc.green('✓') + ' Created src/app/screen.tsx')\r\n\r\n // Success message with instructions\r\n console.log()\r\n console.log(pc.green('✓') + ' Project initialized!')\r\n console.log()\r\n console.log(pc.bold(' Next steps:'))\r\n console.log()\r\n console.log(pc.dim(' 1. Add scripts to package.json:'))\r\n console.log()\r\n console.log(' {')\r\n console.log(' \"scripts\": {')\r\n console.log(` \"build\": \"${CLI_NAME} build\",`)\r\n console.log(` \"start\": \"${CLI_NAME} start\"`)\r\n console.log(' }')\r\n console.log(' }')\r\n console.log()\r\n console.log(pc.dim(' 2. Run \"npm run build\" to build your app'))\r\n console.log(pc.dim(' 3. Run \"npm run start\" to start your app'))\r\n console.log()\r\n },\r\n}\r\n","import { existsSync } from 'fs'\r\nimport { resolve } from 'path'\r\nimport { pathToFileURL } from 'url'\r\nimport pc from 'picocolors'\r\n\r\nimport { loadConfig } from '@/core/config'\r\nimport { CLI_NAME } from '@/core/constants'\r\nimport type { CLICommand } from '../../types'\r\n\r\nexport const start: CLICommand = {\r\n name: 'start',\r\n desc: 'Start the application',\r\n action: async () => {\r\n const { outDir } = await loadConfig()\r\n const initialUrl = '/'\r\n const entryPath = resolve(outDir, 'dist', 'entry.js')\r\n\r\n // Check if build exists\r\n if (!existsSync(entryPath)) {\r\n console.error(pc.red('✗') + ' Build not found')\r\n console.error(pc.dim(` Run \"${CLI_NAME} build\" first`))\r\n throw new Error('Build not found') // Let Commander handle exit\r\n }\r\n\r\n // Import and run the bundled entry\r\n const entryUrl = pathToFileURL(entryPath).href\r\n const { run } = await import(entryUrl)\r\n\r\n // Run with initial URL\r\n await run(initialUrl)\r\n },\r\n}\r\n","import { Command } from 'commander'\r\nimport { CLI_NAME, DESCRIPTION, VERSION } from '@/core/constants'\r\nimport { build } from './commands/build'\r\nimport { init } from './commands/init'\r\nimport { start } from './commands/start'\r\n\r\nfunction configureProgram(program: Command) {\r\n program\r\n .name(CLI_NAME)\r\n .description(DESCRIPTION)\r\n .version(VERSION)\r\n\r\n const commands = [build, init, start] as const\r\n for (const { name, desc, action } of commands) {\r\n program\r\n .command(name)\r\n .description(desc)\r\n .action(action)\r\n }\r\n return program\r\n}\r\n\r\nexport async function run(argv: string[]) {\r\n const command = new Command()\r\n const program = configureProgram(command)\r\n\r\n try {\r\n await program.parseAsync(argv)\r\n return 0\r\n }\r\n catch (err) {\r\n const message = err instanceof Error ? err.message : 'Unknown error'\r\n console.error(message)\r\n return 1\r\n }\r\n}\r\n","#!/usr/bin/env node\r\n\r\nimport { run } from './cli'\r\n\r\nprocess.exit(await run(process.argv))\r\n"],"x_google_ignoreList":[1],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import{join as e,parse as t,posix as n,relative as r,resolve as i}from"path";import{pathToFileURL as a}from"url";import{readdirSync as o,statSync as s}from"fs";function c(e){return e}const l={appDir:`./src/app`,outDir:`./.pen`};async function u(){try{let e=(await import(a(i(process.cwd(),`pen.config.ts`)).href)).default||{};return{...l,...e}}catch{return l}}function d(e){let{root:t,visit:n,expand:r,attach:i,filter:a}=e,o=[t];for(let e of o){n?.(e);let t=r?.(e);if(t)for(let n of t)i?.(n,e),(!a||a(n))&&o.push(n)}return t}function f(e){let{root:t,visit:n,expand:r,attach:i,filter:a}=e,o=[t];for(;o.length;){let e=o.pop();n?.(e);let t=r?.(e);if(!t)continue;let s=t.length;for(let n=s-1;n>=0;n--){let r=t[n];i?.(r,e),(!a||a(r))&&o.push(r)}}return t}function p(e,t,n){let r=[],i=e;for(;i;)r.push(n(i)),i=t(i);return r}var m=class extends Error{constructor(e){super(e),this.name=`FileRouterError`}},h=class extends m{constructor(e){super(`Directory not found: "${e}"`),this.path=e,this.name=`DirectoryNotFoundError`}},g=class extends m{constructor(e){super(`Path is not a directory: "${e}"`),this.path=e,this.name=`NotADirectoryError`}},_=class extends m{constructor(e){super(`Root path is a file, not a directory: "${e}"\n\nThe app directory must be a directory, not a file.`),this.path=e,this.name=`RootIsFileError`}},v=class extends m{constructor(e,t){super(`Conflicting screen routes found at "${e}":\n`+t.map(e=>` - ${e}`).join(`
2
+ `)+`
3
+
4
+ Each URL can only have one screen file.
5
+ Move one screen to a different directory or rename the route segment.`),this.url=e,this.files=t,this.name=`DuplicateScreenError`}};function y(t){let r=i(t),a=s(r,{throwIfNoEntry:!1});if(!a)throw new h(r);if(!a.isDirectory())throw new g(r);let c={name:`app`,relPath:`/app`,absPath:r,children:[]};function l(t){let r=o(t.absPath,{withFileTypes:!0}),i=[];for(let a of r){if(!a.isFile()&&!a.isDirectory())continue;let r=n.join(t.relPath,a.name),o=e(t.absPath,a.name),s=a.isDirectory()?{name:a.name,relPath:r,absPath:o,children:[]}:{name:a.name,relPath:r,absPath:o};i.push(s)}return i.sort((e,t)=>e.name.localeCompare(t.name))}return d({root:c,expand:l,attach:(e,t)=>t.children.push(e),filter:e=>e.children!==void 0})}function b(e){if(e.children===void 0)throw new _(e.absPath);let r={segment:``,url:`/`,type:`page`,roles:{},children:[],file:e};if(!e.children.length)return r;let i={};function a(e){let n=e.file;if(!n.children?.length)return;for(let r of n.children){let{name:n,ext:i}=t(r.name);if(i===`.tsx`)switch(n){case`screen`:e.roles.screen=r.absPath;break;case`not-found`:e.roles[`not-found`]=r.absPath;break;case`error`:e.roles.error=r.absPath;break;case`layout`:e.roles.layout=r.absPath;break}}if(!e.roles.screen)return;let r=i[e.url];if(r)throw new v(e.url,[r,n.absPath]);i[e.url]=n.absPath}function o(e){let t=e.file;if(!t.children)return;let r=[];for(let i of t.children){if(!i.children||i.name.startsWith(`_`))continue;let t=i.name.startsWith(`(`)&&i.name.endsWith(`)`);r.push({segment:i.name,url:t?e.url:`${n.join(e.url,i.name)}/`,type:t?`group`:`page`,roles:{},parent:e,children:[],file:i})}return r.sort((e,t)=>e.segment.localeCompare(t.segment))}return f({root:r,visit:a,expand:o,attach:(e,t)=>t.children.push(e)})}function x(n){let r=t(n);return e(r.dir,r.name)}function S(e){let t={};function n(e){if(!e.roles.screen)return;let n=p(e,e=>e.parent,t=>{let n={},r=Object.entries(t.roles);for(let[i,a]of r)(i!==`screen`||t===e)&&(n[i]=x(a));return n});t[e.url]={url:e.url,chain:n}}return f({root:e,visit:n,expand:e=>e.children}),t}function C(e,t){let n=new Set;for(let t of Object.values(e))for(let e of t.chain)e.screen&&n.add(e.screen),e[`not-found`]&&n.add(e[`not-found`]),e.error&&n.add(e.error),e.layout&&n.add(e.layout);let i={},a=`${t}/generated`;for(let e of n)i[e]=r(a,e).replace(/\\/g,`/`)+`.js`;return i}export{h as a,g as c,c as d,u as f,y as i,_ as l,S as n,v as o,b as r,m as s,C as t,l as u};
6
+ //# sourceMappingURL=component-map-CLP6MqF8.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"component-map-CLP6MqF8.mjs","names":[],"sources":["../src/core/config.ts","../src/lib/tree-utils.ts","../src/core/route-builder/errors.ts","../src/core/route-builder/builders/file-tree.ts","../src/core/route-builder/builders/segment-tree.ts","../src/lib/path-utils.ts","../src/core/route-builder/builders/route-manifest.ts","../src/core/route-builder/builders/component-map.ts"],"sourcesContent":["import { resolve } from 'path'\r\nimport { pathToFileURL } from 'url'\r\n\r\nexport interface PenConfig {\r\n /**\r\n * Directory containing your app routes.\r\n * @default './src/app'\r\n */\r\n appDir: string\r\n\r\n /**\r\n * Output directory for generated files and build artifacts.\r\n * @default './.pen'\r\n */\r\n outDir: string\r\n}\r\n\r\nexport function defineConfig(config: Partial<PenConfig>): Partial<PenConfig> {\r\n return config\r\n}\r\n\r\nexport const defaultConfig: PenConfig = {\r\n appDir: './src/app',\r\n outDir: './.pen',\r\n}\r\n\r\n/**\r\n * Loads pen.config.ts from the current directory.\r\n * Falls back to defaults if config file doesn't exist.\r\n */\r\nexport async function loadConfig(): Promise<PenConfig> {\r\n try {\r\n const configPath = resolve(process.cwd(), 'pen.config.ts')\r\n const configUrl = pathToFileURL(configPath).href\r\n\r\n const module = await import(configUrl) as { default?: Partial<PenConfig> }\r\n const userConfig = module.default || {}\r\n\r\n // Merge with defaults\r\n return { ...defaultConfig, ...userConfig }\r\n }\r\n catch {\r\n // Config file doesn't exist or failed to load - use defaults\r\n return defaultConfig\r\n }\r\n}\r\n","export type TraversalOptions<TNode> = {\r\n root: TNode\r\n visit?: (node: TNode) => void // Inspect/process current node\r\n expand?: (node: TNode) => TNode[] | undefined // Get/create child nodes\r\n attach?: (child: TNode, parent: TNode) => void // Link child to parent\r\n filter?: (child: TNode) => boolean // Control child traversal\r\n}\r\n\r\n/**\r\n * Traverse tree breadth-first (level by level).\r\n * Processes all siblings before any children.\r\n */\r\nexport function traverseBreadthFirst<TNode>(options: TraversalOptions<TNode>) {\r\n const { root, visit, expand, attach, filter } = options\r\n const queue = [root]\r\n\r\n for (const node of queue) {\r\n visit?.(node)\r\n\r\n const children = expand?.(node)\r\n if (!children) continue\r\n\r\n for (const child of children) {\r\n attach?.(child, node)\r\n\r\n if (!filter || filter(child))\r\n queue.push(child)\r\n }\r\n }\r\n return root\r\n}\r\n\r\n/**\r\n * Traverse tree depth-first with preorder traversal.\r\n * Processes parent before children, going deep before wide.\r\n */\r\nexport function traverseDepthFirst<TNode>(options: TraversalOptions<TNode>) {\r\n const { root, visit, expand, attach, filter } = options\r\n const stack = [root]\r\n\r\n while (stack.length) {\r\n const node = stack.pop()!\r\n visit?.(node)\r\n\r\n const children = expand?.(node)\r\n if (!children) continue\r\n\r\n // Process in reverse to maintain left-to-right\r\n // order when popping from stack\r\n const len = children.length\r\n for (let i = len-1; i >= 0; i--) {\r\n const child = children[i]!\r\n attach?.(child, node)\r\n\r\n if (!filter || filter(child))\r\n stack.push(child)\r\n }\r\n }\r\n return root\r\n}\r\n\r\nexport function collectAncestors<T, R>(\r\n node: T,\r\n parent: (node: T) => T | undefined,\r\n mapper: (node: T) => R,\r\n): R[] {\r\n const results: R[] = []\r\n let currentNode: T | undefined = node\r\n\r\n while (currentNode) {\r\n results.push(mapper(currentNode))\r\n currentNode = parent(currentNode)\r\n }\r\n\r\n return results\r\n}\r\n","/**\r\n * Base error for all route-builder build errors\r\n */\r\nexport class FileRouterError extends Error {\r\n constructor(message: string) {\r\n super(message)\r\n this.name = 'FileRouterError'\r\n }\r\n}\r\n\r\n// ============================================================================\r\n// File Tree Errors\r\n// ============================================================================\r\n\r\nexport class DirectoryNotFoundError extends FileRouterError {\r\n constructor(public path: string) {\r\n super(`Directory not found: \"${path}\"`)\r\n this.name = 'DirectoryNotFoundError'\r\n }\r\n}\r\n\r\nexport class NotADirectoryError extends FileRouterError {\r\n constructor(public path: string) {\r\n super(`Path is not a directory: \"${path}\"`)\r\n this.name = 'NotADirectoryError'\r\n }\r\n}\r\n\r\n// ============================================================================\r\n// Route Tree Errors\r\n// ============================================================================\r\n\r\nexport class RootIsFileError extends FileRouterError {\r\n constructor(public path: string) {\r\n super(\r\n `Root path is a file, not a directory: \"${path}\"\\n\\n` +\r\n 'The app directory must be a directory, not a file.',\r\n )\r\n this.name = 'RootIsFileError'\r\n }\r\n}\r\n\r\nexport class DuplicateScreenError extends FileRouterError {\r\n constructor(\r\n public url: string,\r\n public files: string[],\r\n ) {\r\n super(\r\n `Conflicting screen routes found at \"${url}\":\\n` +\r\n files.map(f => ` - ${f}`).join('\\n') + '\\n\\n' +\r\n 'Each URL can only have one screen file.\\n' +\r\n 'Move one screen to a different directory or rename the route segment.',\r\n )\r\n this.name = 'DuplicateScreenError'\r\n }\r\n}\r\n","import { readdirSync, statSync } from 'fs'\r\nimport { resolve, join, posix } from 'path'\r\nimport { traverseBreadthFirst } from '@/lib/tree-utils'\r\nimport { DirectoryNotFoundError, NotADirectoryError } from '../errors'\r\n\r\nexport type FileNode = {\r\n name: string // dirent name\r\n relPath: string\r\n absPath: string\r\n children?: FileNode[] // present only for directories\r\n}\r\n\r\n/**\r\n * Builds a file tree from a directory path.\r\n *\r\n * @param appPath - Path to the app directory\r\n * @returns File tree structure\r\n * @throws {DirectoryNotFoundError} If the directory doesn't exist\r\n * @throws {NotADirectoryError} If the path is not a directory\r\n */\r\nexport function buildFileTree(appPath: string): FileNode {\r\n const rootPath = resolve(appPath)\r\n const stat = statSync(rootPath, { throwIfNoEntry: false })\r\n\r\n // Validation\r\n if (!stat)\r\n throw new DirectoryNotFoundError(rootPath)\r\n\r\n if (!stat.isDirectory())\r\n throw new NotADirectoryError(rootPath)\r\n\r\n // Root node\r\n const root: FileNode = {\r\n name: 'app',\r\n relPath: '/app',\r\n absPath: rootPath,\r\n children: [],\r\n }\r\n\r\n function expand(parentFile: FileNode) {\r\n const dirents = readdirSync(parentFile.absPath, { withFileTypes: true })\r\n const children: FileNode[] = []\r\n\r\n for (const dirent of dirents) {\r\n if (!dirent.isFile() && !dirent.isDirectory()) // Traverse only dirs and files\r\n continue\r\n\r\n const relPath = posix.join(parentFile.relPath, dirent.name)\r\n const absPath = join(parentFile.absPath, dirent.name)\r\n\r\n const child: FileNode = dirent.isDirectory()\r\n ? { name: dirent.name, relPath, absPath, children: [] }\r\n : { name: dirent.name, relPath, absPath }\r\n children.push(child)\r\n }\r\n return children.sort((a, b) => a.name.localeCompare(b.name))\r\n }\r\n\r\n return traverseBreadthFirst({\r\n root,\r\n expand,\r\n attach: (child, parent) => parent.children!.push(child),\r\n filter: (file) => file.children !== undefined,\r\n })\r\n}\r\n","import { parse, posix } from 'path'\r\nimport { traverseDepthFirst } from '@/lib/tree-utils'\r\nimport { RootIsFileError, DuplicateScreenError } from '../errors'\r\nimport type { FileNode } from './file-tree'\r\n\r\nexport type SegmentRoles = {\r\n 'layout'?: string\r\n 'screen'?: string\r\n 'error'?: string\r\n 'not-found'?: string\r\n}\r\n\r\nexport type SegmentNode = {\r\n segment: string // directory name\r\n url: string\r\n type: 'page' | 'group'\r\n roles: SegmentRoles\r\n parent?: SegmentNode\r\n children?: SegmentNode[]\r\n file: FileNode\r\n}\r\n\r\n/**\r\n * Builds a segment tree from a file system tree.\r\n *\r\n * @param fileTree - File system tree\r\n * @returns Segment tree with computed URLs and validated structure\r\n * @throws {RootIsFileError} If the root is a file instead of a directory\r\n * @throws {DuplicateScreenError} If multiple screens map to the same URL\r\n */\r\nexport function buildSegmentTree(fileTree: FileNode): SegmentNode {\r\n if (fileTree.children === undefined)\r\n throw new RootIsFileError(fileTree.absPath)\r\n\r\n // Special case: Root has \"/\" as url instead of \"/app\",\r\n // and empty segment instead of \"app\"\r\n const root: SegmentNode = {\r\n segment: '',\r\n url: '/',\r\n type: 'page',\r\n roles: {},\r\n children: [],\r\n file: fileTree,\r\n }\r\n\r\n // Early return if root has no children\r\n if (!fileTree.children.length)\r\n return root\r\n\r\n // For tracking duplicate screens\r\n const screenSegments: Record<string, string> = {}\r\n\r\n function visit(parentSegment: SegmentNode) {\r\n const parentFile = parentSegment.file\r\n if (!parentFile.children?.length) return\r\n\r\n // Step 1: Assign file to segment role\r\n for (const file of parentFile.children) {\r\n const { name, ext } = parse(file.name)\r\n if (ext !== '.tsx') continue\r\n\r\n switch (name) {\r\n case 'screen': parentSegment.roles['screen'] = file.absPath; break\r\n case 'not-found': parentSegment.roles['not-found'] = file.absPath; break\r\n case 'error': parentSegment.roles['error'] = file.absPath; break\r\n case 'layout': parentSegment.roles['layout'] = file.absPath; break\r\n }\r\n }\r\n\r\n // Step 2: Validate that screen URLs are unique across the entire tree\r\n if (!parentSegment.roles.screen)\r\n return // No screen to validate\r\n\r\n // Check if another screen already claimed this URL\r\n const existingFilePath = screenSegments[parentSegment.url]\r\n if (existingFilePath)\r\n throw new DuplicateScreenError(parentSegment.url, [existingFilePath, parentFile.absPath])\r\n\r\n screenSegments[parentSegment.url] = parentFile.absPath\r\n }\r\n\r\n function expand(parentSegment: SegmentNode) {\r\n const parentFile = parentSegment.file\r\n if (!parentFile.children) return // Skip expand if route has no children\r\n\r\n // Create container for route children\r\n const segments: SegmentNode[] = []\r\n\r\n for (const file of parentFile.children) {\r\n if (!file.children) continue // Skip if file (only dirs pass through)\r\n if (file.name.startsWith('_')) continue // Skip if private directory\r\n\r\n const isGroup = file.name.startsWith('(') && file.name.endsWith(')')\r\n segments.push({\r\n segment: file.name,\r\n url: isGroup ? parentSegment.url : `${posix.join(parentSegment.url, file.name)}/`,\r\n type: isGroup ? 'group' : 'page',\r\n roles: {},\r\n parent: parentSegment,\r\n children: [],\r\n file,\r\n })\r\n }\r\n\r\n return segments.sort((a, b) => a.segment.localeCompare(b.segment))\r\n }\r\n\r\n return traverseDepthFirst({\r\n root,\r\n visit,\r\n expand,\r\n attach: (child, parent) => parent.children!.push(child),\r\n })\r\n}\r\n","import { parse, join } from 'path'\r\n\r\nexport function removeExtension(filePath: string) {\r\n const parsed = parse(filePath)\r\n return join(parsed.dir, parsed.name) // Use posix for forward slashes\r\n}\r\n","import { removeExtension } from '@/lib/path-utils'\r\nimport { collectAncestors, traverseDepthFirst } from '@/lib/tree-utils'\r\nimport type { SegmentNode, SegmentRoles } from './segment-tree'\r\n\r\nexport type RouteManifest = Record<string, Route>\r\nexport type Route = {\r\n url: string\r\n chain: SegmentRoles[]\r\n}\r\n\r\n/**\r\n * Builds a route manifest from a segment tree.\r\n *\r\n * Flattens the tree into a dictionary mapping URLs to route metadata.\r\n * Only includes routes that have screens.\r\n *\r\n * @param segmentTree - Segment tree with parent pointers\r\n * @returns Flat manifest ready for runtime composition\r\n */\r\nexport function buildRouteManifest(segmentTree: SegmentNode): RouteManifest {\r\n const manifest: RouteManifest = {}\r\n\r\n function visit(segment: SegmentNode) {\r\n // Only create manifest entry if this node has a screen\r\n if (!segment.roles.screen) return\r\n\r\n // Build ancestor chain (leaf → root order)\r\n const segmentChain = collectAncestors(\r\n segment,\r\n node => node.parent,\r\n ancestorSegment => {\r\n const roles: SegmentRoles = {}\r\n const entries = Object.entries(ancestorSegment.roles) as [keyof SegmentRoles, string][]\r\n\r\n for (const [roleName, path] of entries)\r\n if (roleName !== 'screen' || ancestorSegment === segment) // Skip ancestor screens\r\n roles[roleName] = removeExtension(path)\r\n\r\n return roles\r\n },\r\n )\r\n\r\n manifest[segment.url] = {\r\n url: segment.url,\r\n chain: segmentChain,\r\n }\r\n }\r\n\r\n traverseDepthFirst({\r\n root: segmentTree,\r\n visit,\r\n expand: parentSegment => parentSegment.children,\r\n })\r\n\r\n return manifest\r\n}\r\n","import { relative } from 'path'\r\nimport type { RouteManifest } from './route-manifest'\r\n\r\nexport type ComponentImportMap = Record<string, string>\r\n\r\n/**\r\n * Builds a component map from a route manifest.\r\n * Maps absolute component paths to relative import paths.\r\n */\r\nexport function buildComponentMap(manifest: RouteManifest, outDir: string): ComponentImportMap {\r\n const segmentPaths = new Set<string>()\r\n\r\n // Collect all unique absolute paths from manifest\r\n for (const route of Object.values(manifest)) {\r\n for (const segment of route.chain) {\r\n if (segment['screen']) segmentPaths.add(segment['screen'])\r\n if (segment['not-found']) segmentPaths.add(segment['not-found'])\r\n if (segment['error']) segmentPaths.add(segment['error'])\r\n if (segment['layout']) segmentPaths.add(segment['layout'])\r\n }\r\n }\r\n\r\n // Build map: absolute path → relative import path\r\n const componentMap: ComponentImportMap = {}\r\n const genDir = `${outDir}/generated`\r\n\r\n for (const absPath of segmentPaths) {\r\n // Calculate relative path and normalize to forward slashes for ES modules\r\n const relPath = relative(genDir, absPath).replace(/\\\\/g, '/')\r\n const importPath = relPath + '.js'\r\n componentMap[absPath] = importPath\r\n }\r\n\r\n return componentMap\r\n}\r\n"],"mappings":"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"}
@@ -0,0 +1,337 @@
1
+ import * as react0 from "react";
2
+ import { Component, ComponentType, PropsWithChildren, ReactElement } from "react";
3
+ import * as react_jsx_runtime0 from "react/jsx-runtime";
4
+
5
+ //#region src/core/config.d.ts
6
+ interface PenConfig {
7
+ /**
8
+ * Directory containing your app routes.
9
+ * @default './src/app'
10
+ */
11
+ appDir: string;
12
+ /**
13
+ * Output directory for generated files and build artifacts.
14
+ * @default './.pen'
15
+ */
16
+ outDir: string;
17
+ }
18
+ declare function defineConfig(config: Partial<PenConfig>): Partial<PenConfig>;
19
+ declare const defaultConfig: PenConfig;
20
+ /**
21
+ * Loads pen.config.ts from the current directory.
22
+ * Falls back to defaults if config file doesn't exist.
23
+ */
24
+ declare function loadConfig(): Promise<PenConfig>;
25
+ //#endregion
26
+ //#region src/core/route-builder/builders/file-tree.d.ts
27
+ type FileNode = {
28
+ name: string;
29
+ relPath: string;
30
+ absPath: string;
31
+ children?: FileNode[];
32
+ };
33
+ /**
34
+ * Builds a file tree from a directory path.
35
+ *
36
+ * @param appPath - Path to the app directory
37
+ * @returns File tree structure
38
+ * @throws {DirectoryNotFoundError} If the directory doesn't exist
39
+ * @throws {NotADirectoryError} If the path is not a directory
40
+ */
41
+ declare function buildFileTree(appPath: string): FileNode;
42
+ //#endregion
43
+ //#region src/core/route-builder/builders/segment-tree.d.ts
44
+ type SegmentRoles = {
45
+ 'layout'?: string;
46
+ 'screen'?: string;
47
+ 'error'?: string;
48
+ 'not-found'?: string;
49
+ };
50
+ type SegmentNode = {
51
+ segment: string;
52
+ url: string;
53
+ type: 'page' | 'group';
54
+ roles: SegmentRoles;
55
+ parent?: SegmentNode;
56
+ children?: SegmentNode[];
57
+ file: FileNode;
58
+ };
59
+ /**
60
+ * Builds a segment tree from a file system tree.
61
+ *
62
+ * @param fileTree - File system tree
63
+ * @returns Segment tree with computed URLs and validated structure
64
+ * @throws {RootIsFileError} If the root is a file instead of a directory
65
+ * @throws {DuplicateScreenError} If multiple screens map to the same URL
66
+ */
67
+ declare function buildSegmentTree(fileTree: FileNode): SegmentNode;
68
+ //#endregion
69
+ //#region src/core/route-builder/builders/route-manifest.d.ts
70
+ type RouteManifest = Record<string, Route>;
71
+ type Route = {
72
+ url: string;
73
+ chain: SegmentRoles[];
74
+ };
75
+ /**
76
+ * Builds a route manifest from a segment tree.
77
+ *
78
+ * Flattens the tree into a dictionary mapping URLs to route metadata.
79
+ * Only includes routes that have screens.
80
+ *
81
+ * @param segmentTree - Segment tree with parent pointers
82
+ * @returns Flat manifest ready for runtime composition
83
+ */
84
+ declare function buildRouteManifest(segmentTree: SegmentNode): RouteManifest;
85
+ //#endregion
86
+ //#region src/core/route-builder/builders/component-map.d.ts
87
+ type ComponentImportMap = Record<string, string>;
88
+ /**
89
+ * Builds a component map from a route manifest.
90
+ * Maps absolute component paths to relative import paths.
91
+ */
92
+ declare function buildComponentMap(manifest: RouteManifest, outDir: string): ComponentImportMap;
93
+ //#endregion
94
+ //#region src/core/route-builder/errors.d.ts
95
+ /**
96
+ * Base error for all route-builder build errors
97
+ */
98
+ declare class FileRouterError extends Error {
99
+ constructor(message: string);
100
+ }
101
+ declare class DirectoryNotFoundError extends FileRouterError {
102
+ path: string;
103
+ constructor(path: string);
104
+ }
105
+ declare class NotADirectoryError extends FileRouterError {
106
+ path: string;
107
+ constructor(path: string);
108
+ }
109
+ declare class RootIsFileError extends FileRouterError {
110
+ path: string;
111
+ constructor(path: string);
112
+ }
113
+ declare class DuplicateScreenError extends FileRouterError {
114
+ url: string;
115
+ files: string[];
116
+ constructor(url: string, files: string[]);
117
+ }
118
+ //#endregion
119
+ //#region src/core/router/RouterProvider.d.ts
120
+ interface RouterContextValue {
121
+ url: string;
122
+ data: unknown | undefined;
123
+ history: readonly string[];
124
+ position: number;
125
+ push: (url: string, data?: unknown) => void;
126
+ replace: (url: string) => void;
127
+ back: () => void;
128
+ forward: () => void;
129
+ }
130
+ interface RouterProviderProps extends PropsWithChildren {
131
+ initialUrl: string;
132
+ }
133
+ declare const RouterContext: react0.Context<RouterContextValue | null>;
134
+ declare function RouterProvider({
135
+ initialUrl,
136
+ children
137
+ }: RouterProviderProps): react_jsx_runtime0.JSX.Element;
138
+ //#endregion
139
+ //#region src/core/router/hooks/use-history.d.ts
140
+ /**
141
+ * Get the navigation history (URLs only)
142
+ *
143
+ * @returns readonly array of visited URLs
144
+ *
145
+ * @example
146
+ * ```tsx
147
+ * const history = useHistory()
148
+ * return (
149
+ * <Box>
150
+ * <Text>Visited pages:</Text>
151
+ * {history.map((url, i) => (
152
+ * <Text key={i}>{url}</Text>
153
+ * ))}
154
+ * </Box>
155
+ * )
156
+ * ```
157
+ */
158
+ declare function useHistory(): readonly string[];
159
+ //#endregion
160
+ //#region src/core/router/hooks/use-navigate.d.ts
161
+ /**
162
+ * Get navigation functions without url/data
163
+ */
164
+ declare function useNavigate(): {
165
+ push: (url: string, data?: unknown) => void;
166
+ replace: (url: string) => void;
167
+ back: () => void;
168
+ forward: () => void;
169
+ };
170
+ //#endregion
171
+ //#region src/core/router/hooks/use-route-data.d.ts
172
+ /**
173
+ * Access route data passed via router.push()
174
+ *
175
+ * @example
176
+ * ```tsx
177
+ * const user = useRouteData<User>()
178
+ * if (!user) return <Text>No user</Text>
179
+ * return <Text>{user.name}</Text>
180
+ * ```
181
+ */
182
+ declare function useRouteData<T = unknown>(): T | undefined;
183
+ //#endregion
184
+ //#region src/core/router/hooks/use-router.d.ts
185
+ declare function useRouter(): RouterContextValue;
186
+ //#endregion
187
+ //#region src/core/router/hooks/use-url.d.ts
188
+ /**
189
+ * Get the current URL
190
+ */
191
+ declare function useUrl(): string;
192
+ //#endregion
193
+ //#region src/core/runtime/types.d.ts
194
+ type ComponentMap = Record<string, ComponentType>;
195
+ //#endregion
196
+ //#region src/core/runtime/App.d.ts
197
+ interface AppProps {
198
+ initialUrl: string;
199
+ manifest: RouteManifest;
200
+ components: ComponentMap;
201
+ }
202
+ declare function App({
203
+ initialUrl,
204
+ manifest,
205
+ components
206
+ }: AppProps): react_jsx_runtime0.JSX.Element;
207
+ //#endregion
208
+ //#region src/core/runtime/boundaries/ErrorBoundary.d.ts
209
+ /** Props passed to error.tsx components */
210
+ interface ErrorComponentProps {
211
+ error: Error;
212
+ reset: () => void;
213
+ }
214
+ interface ErrorBoundaryProps extends PropsWithChildren {
215
+ fallback: ComponentType<ErrorComponentProps>;
216
+ }
217
+ interface ErrorBoundaryState {
218
+ error: Error | null;
219
+ }
220
+ /**
221
+ * React Error Boundary that catches errors in child components.
222
+ * Renders the provided ErrorComponent when an error occurs.
223
+ *
224
+ * Catches:
225
+ * - Errors during rendering
226
+ * - Errors in lifecycle methods
227
+ * - Errors in constructors
228
+ *
229
+ * Does NOT catch:
230
+ * - Event handlers (use try-catch)
231
+ * - Async code (use try-catch)
232
+ * - Errors in the error boundary itself
233
+ */
234
+ declare class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
235
+ state: ErrorBoundaryState;
236
+ constructor(props: ErrorBoundaryProps);
237
+ /** Called when a child component throws during render. */
238
+ static getDerivedStateFromError(error: Error): ErrorBoundaryState;
239
+ /** Called after error is caught. */
240
+ componentDidCatch(): void;
241
+ /** Resets error state and attempts to re-render children. */
242
+ reset(): void;
243
+ render(): string | number | bigint | boolean | react_jsx_runtime0.JSX.Element | Iterable<react0.ReactNode> | Promise<string | number | bigint | boolean | react0.ReactPortal | react0.ReactElement<unknown, string | react0.JSXElementConstructor<any>> | Iterable<react0.ReactNode> | null | undefined> | null | undefined;
244
+ }
245
+ //#endregion
246
+ //#region src/core/runtime/boundaries/NotFoundBoundary.d.ts
247
+ /** Props passed to not-found.tsx components */
248
+ interface NotFoundComponentProps {
249
+ url: string;
250
+ }
251
+ interface NotFoundBoundaryProps extends PropsWithChildren {
252
+ fallback: ComponentType<NotFoundComponentProps>;
253
+ }
254
+ /**
255
+ * Router-aware wrapper for NotFoundErrorBoundary.
256
+ * Injects the current URL so the boundary can reset on navigation.
257
+ */
258
+ declare function NotFoundBoundary({
259
+ fallback,
260
+ children
261
+ }: NotFoundBoundaryProps): react_jsx_runtime0.JSX.Element;
262
+ //#endregion
263
+ //#region src/core/runtime/screens/ErrorScreen.d.ts
264
+ /** Global error fallback that always wraps the app. */
265
+ declare function ErrorScreen({
266
+ error,
267
+ reset
268
+ }: ErrorComponentProps): react_jsx_runtime0.JSX.Element;
269
+ //#endregion
270
+ //#region src/core/runtime/screens/NotFoundScreen.d.ts
271
+ /**
272
+ * Not found error screen displayed when a route is not found.
273
+ * Shows the URL that was attempted.
274
+ */
275
+ declare function NotFoundScreen({
276
+ url
277
+ }: NotFoundComponentProps): react_jsx_runtime0.JSX.Element;
278
+ //#endregion
279
+ //#region src/core/runtime/FileRouter.d.ts
280
+ /**
281
+ * Props for the FileRouter component.
282
+ * Defines the URL to render, the route manifest, and component map.
283
+ */
284
+ interface FileRouterProps {
285
+ manifest: RouteManifest;
286
+ components: ComponentMap;
287
+ }
288
+ /**
289
+ * Router component that orchestrates route matching and composition.
290
+ * Returns the composed route element or throws NotFoundError.
291
+ */
292
+ declare function FileRouter({
293
+ manifest,
294
+ components
295
+ }: FileRouterProps): ReactElement;
296
+ //#endregion
297
+ //#region src/core/runtime/routing/composer.d.ts
298
+ /**
299
+ * Composes a route element by processing each segment in the chain.
300
+ *
301
+ * Composition order per segment (inside to outside):
302
+ * 1. Screen component (only in leaf segment)
303
+ * 2. Not-found boundary (wraps screen if present)
304
+ * 3. Layout (wraps content)
305
+ * 4. Error boundary (wraps layout + all descendants)
306
+ *
307
+ * This ensures error boundaries catch errors at the right level,
308
+ * and only layouts above the error remain visible.
309
+ */
310
+ declare function composeRoute(route: Route, components: ComponentMap): ReactElement;
311
+ //#endregion
312
+ //#region src/core/runtime/routing/matcher.d.ts
313
+ /**
314
+ * Matches a URL against the route manifest.
315
+ * Returns the matched route metadata or null if no match found.
316
+ */
317
+ declare function matchRoute(url: string, manifest: RouteManifest): Route | null;
318
+ //#endregion
319
+ //#region src/core/runtime/errors.d.ts
320
+ /** Special error thrown to trigger not-found.tsx. */
321
+ declare class NotFoundError extends Error {
322
+ readonly url: string;
323
+ constructor(url: string, message?: string);
324
+ }
325
+ /** Error thrown when a route has an empty chain. */
326
+ declare class EmptyChainError extends Error {
327
+ url: string;
328
+ constructor(url: string);
329
+ }
330
+ /** Error thrown when a component is missing from the component map. */
331
+ declare class ComponentNotFoundError extends Error {
332
+ componentPath: string;
333
+ constructor(componentPath: string);
334
+ }
335
+ //#endregion
336
+ export { App, type AppProps, ComponentImportMap, type ComponentMap, ComponentNotFoundError, DirectoryNotFoundError, DuplicateScreenError, EmptyChainError, ErrorBoundary, type ErrorComponentProps, ErrorScreen, FileNode, FileRouter, FileRouterError, type FileRouterProps, NotADirectoryError, NotFoundBoundary, type NotFoundComponentProps, NotFoundError, NotFoundScreen, PenConfig, RootIsFileError, Route, RouteManifest, RouterContext, type RouterContextValue, RouterProvider, type RouterProviderProps, SegmentNode, SegmentRoles, buildComponentMap, buildFileTree, buildRouteManifest, buildSegmentTree, composeRoute, defaultConfig, defineConfig, loadConfig, matchRoute, useHistory, useNavigate, useRouteData, useRouter, useUrl };
337
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/core/config.ts","../src/core/route-builder/builders/file-tree.ts","../src/core/route-builder/builders/segment-tree.ts","../src/core/route-builder/builders/route-manifest.ts","../src/core/route-builder/builders/component-map.ts","../src/core/route-builder/errors.ts","../src/core/router/RouterProvider.tsx","../src/core/router/hooks/use-history.ts","../src/core/router/hooks/use-navigate.ts","../src/core/router/hooks/use-route-data.ts","../src/core/router/hooks/use-router.ts","../src/core/router/hooks/use-url.ts","../src/core/runtime/types.ts","../src/core/runtime/App.tsx","../src/core/runtime/boundaries/ErrorBoundary.tsx","../src/core/runtime/boundaries/NotFoundBoundary.tsx","../src/core/runtime/screens/ErrorScreen.tsx","../src/core/runtime/screens/NotFoundScreen.tsx","../src/core/runtime/FileRouter.tsx","../src/core/runtime/routing/composer.ts","../src/core/runtime/routing/matcher.ts","../src/core/runtime/errors.ts"],"sourcesContent":[],"mappings":"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"}
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import{a as e,c as t,d as n,f as r,i,l as a,n as o,o as s,r as c,s as l,t as u,u as d}from"./component-map-CLP6MqF8.mjs";import{Component as f,createContext as p,createElement as m,useCallback as h,useContext as g,useState as _}from"react";import{jsx as v,jsxs as y}from"react/jsx-runtime";import{Box as b,Text as x,useInput as S}from"ink";function C(e,t,n){return{stack:[...e.stack.slice(0,e.position+1),{url:t,data:n}],position:e.position+1}}function w(e,t){let n=[...e.stack];return n[e.position]={url:t},{...e,stack:n}}function T(e){return e.position>0?{...e,position:e.position-1}:e}function E(e){return e.position<e.stack.length-1?{...e,position:e.position+1}:e}const D=p(null);function O({initialUrl:e,children:t}){let[n,r]=_({stack:[{url:e}],position:0}),{url:i,data:a}=n.stack[n.position]??{url:e},o=h((e,t)=>{r(n=>C(n,e,t))},[]),s=h(e=>{r(t=>w(t,e))},[]),c=h(()=>{r(e=>T(e))},[]),l=h(()=>{r(e=>E(e))},[]);return v(D.Provider,{value:{url:i,data:a,history:n.stack.map(e=>e.url),position:n.position,push:o,replace:s,back:c,forward:l},children:t})}function k(){let e=g(D);if(!e)throw Error(`useRouter must be used within a RouterProvider`);return e}function A(){let{history:e}=k();return e}function j(){let{push:e,replace:t,back:n,forward:r}=k();return{push:e,replace:t,back:n,forward:r}}function M(){let{data:e}=k();return e}function N(){let{url:e}=k();return e}var P=class extends f{state;constructor(e){super(e),this.state={error:null},this.reset=this.reset.bind(this)}static getDerivedStateFromError(e){return{error:e}}componentDidCatch(){}reset(){this.setState({error:null})}render(){if(this.state.error){let e=this.props.fallback;return v(e,{error:this.state.error,reset:this.reset})}return this.props.children}},F=class extends Error{url;constructor(e,t=`Not Found`){super(t),this.name=`NotFoundError`,this.url=e}},I=class extends Error{constructor(e){super(`Route ${e} has an empty chain. This indicates a bug in manifest generation - routes must have at least one segment.`),this.url=e,this.name=`EmptyChainError`}},L=class extends Error{constructor(e){super(`Component not found: ${e}. This indicates the component map is out of sync with the manifest. Try running 'pen build' again.`),this.componentPath=e,this.name=`ComponentNotFoundError`}},R=class extends f{state;constructor(e){super(e),this.state={url:null}}static getDerivedStateFromError(e){return e instanceof F?{url:e.url}:null}componentDidCatch(e){if(!(e instanceof F))throw e}componentDidUpdate(e){e.url!==this.props.url&&this.state.url&&this.setState({url:null})}render(){if(this.state.url){let e=this.props.fallback;return v(e,{url:this.state.url})}return this.props.children}};function z({fallback:e,children:t}){let{url:n}=k();return v(R,{fallback:e,url:n,children:t})}function B({error:e,reset:t}){let[n,r]=_(!1);return S(e=>{e===`r`&&t(),e===`s`&&r(e=>!e),e===`q`&&process.exit(1)}),y(b,{flexDirection:`column`,padding:2,borderStyle:`double`,borderColor:`red`,children:[v(b,{marginBottom:1,children:v(x,{bold:!0,color:`red`,children:`💥 Critical Application Error`})}),v(b,{marginBottom:1,children:v(x,{color:`red`,children:`The application encountered a fatal error and cannot continue.`})}),y(b,{flexDirection:`column`,padding:1,borderStyle:`round`,borderColor:`yellow`,marginBottom:1,children:[v(x,{bold:!0,color:`yellow`,children:`Error Details:`}),v(x,{children:e.message})]}),n&&e.stack&&y(b,{flexDirection:`column`,padding:1,borderStyle:`single`,borderColor:`gray`,marginBottom:1,children:[v(x,{bold:!0,dimColor:!0,children:`Stack Trace:`}),v(x,{dimColor:!0,children:e.stack})]}),y(b,{flexDirection:`column`,marginTop:1,children:[v(x,{bold:!0,children:`Options:`}),v(x,{dimColor:!0,children:` [r] Retry [s] Toggle Stack Trace [q] Quit`})]}),v(b,{marginTop:1,children:v(x,{dimColor:!0,children:`If this error persists, please report it.`})})]})}function V({url:e}){let t=k();return S(e=>{e===`b`&&t.back(),e===`q`&&process.exit(0)}),y(b,{flexDirection:`column`,padding:2,borderStyle:`round`,borderColor:`red`,children:[y(b,{flexDirection:`column`,marginBottom:1,children:[v(x,{bold:!0,color:`red`,children:`Route Not Found`}),v(x,{dimColor:!0,children:`The router couldn't match the requested URL.`})]}),v(b,{flexDirection:`column`,paddingLeft:1,marginBottom:1,children:y(x,{children:[`Requested:`,` `,v(x,{color:`yellow`,bold:!0,children:e})]})}),y(b,{flexDirection:`column`,padding:1,borderStyle:`single`,borderColor:`gray`,marginBottom:1,children:[v(x,{bold:!0,children:`Actions`}),v(x,{dimColor:!0,children:` [b] Go back`}),v(x,{dimColor:!0,children:` [q] Quit`})]}),v(x,{dimColor:!0,children:`Tip: Check the route segment name or add a screen.tsx for this path.`})]})}function H(e,t){let n=e[t];if(!n)throw new L(t);return n}function U(e,t){if(!e.chain.length)throw new I(e.url);let n=e.chain[0].screen,r=m(H(t,n),{key:n});for(let n of e.chain){if(n[`not-found`]){let e=n[`not-found`];r=m(z,{key:e,fallback:H(t,e)},r)}if(n.error){let e=n.error;r=m(P,{key:e,fallback:H(t,e)},r)}if(n.layout){let e=n.layout;r=m(H(t,e),{key:e},r)}}return r}function W(e,t){return t[e.endsWith(`/`)?e:`${e}/`]??null}function G({manifest:e,components:t}){let{url:n}=k(),r=W(n,e);if(!r)throw new F(n);return U(r,t)}function K({initialUrl:e,manifest:t,components:n}){return v(P,{fallback:B,children:v(O,{initialUrl:e,children:v(z,{fallback:V,children:v(G,{manifest:t,components:n})})})})}export{K as App,L as ComponentNotFoundError,e as DirectoryNotFoundError,s as DuplicateScreenError,I as EmptyChainError,P as ErrorBoundary,B as ErrorScreen,G as FileRouter,l as FileRouterError,t as NotADirectoryError,z as NotFoundBoundary,F as NotFoundError,V as NotFoundScreen,a as RootIsFileError,D as RouterContext,O as RouterProvider,u as buildComponentMap,i as buildFileTree,o as buildRouteManifest,c as buildSegmentTree,U as composeRoute,d as defaultConfig,n as defineConfig,r as loadConfig,W as matchRoute,A as useHistory,j as useNavigate,M as useRouteData,k as useRouter,N as useUrl};
2
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":["push","actions.push","replace","actions.replace","back","actions.back","forward","actions.forward"],"sources":["../src/core/router/actions.ts","../src/core/router/RouterProvider.tsx","../src/core/router/hooks/use-router.ts","../src/core/router/hooks/use-history.ts","../src/core/router/hooks/use-navigate.ts","../src/core/router/hooks/use-route-data.ts","../src/core/router/hooks/use-url.ts","../src/core/runtime/boundaries/ErrorBoundary.tsx","../src/core/runtime/errors.ts","../src/core/runtime/boundaries/NotFoundBoundary.tsx","../src/core/runtime/screens/ErrorScreen.tsx","../src/core/runtime/screens/NotFoundScreen.tsx","../src/core/runtime/routing/composer.ts","../src/core/runtime/routing/matcher.ts","../src/core/runtime/FileRouter.tsx","../src/core/runtime/App.tsx"],"sourcesContent":["import type { NavigationHistory } from './types'\r\n\r\nexport function push(prev: NavigationHistory, newUrl: string, newData?: unknown): NavigationHistory {\r\n return {\r\n stack: [...prev.stack.slice(0, prev.position + 1), { url: newUrl, data: newData }],\r\n position: prev.position + 1,\r\n }\r\n}\r\n\r\nexport function replace(prev: NavigationHistory, newUrl: string): NavigationHistory {\r\n const newStack = [...prev.stack]\r\n newStack[prev.position] = { url: newUrl }\r\n return { ...prev, stack: newStack }\r\n}\r\n\r\nexport function back(prev: NavigationHistory): NavigationHistory {\r\n return prev.position > 0\r\n ? { ...prev, position: prev.position - 1 }\r\n : prev\r\n}\r\n\r\nexport function forward(prev: NavigationHistory): NavigationHistory {\r\n return prev.position < prev.stack.length - 1\r\n ? { ...prev, position: prev.position + 1 }\r\n : prev\r\n}\r\n","import { createContext, useState, useCallback, type PropsWithChildren } from 'react'\r\nimport * as actions from './actions'\r\nimport type { NavigationHistory } from './types'\r\n\r\nexport interface RouterContextValue {\r\n url: string\r\n data: unknown | undefined\r\n history: readonly string[] // Expose as readonly\r\n position: number // Expose current position\r\n push: (url: string, data?: unknown) => void\r\n replace: (url: string) => void\r\n back: () => void\r\n forward: () => void\r\n}\r\n\r\nexport interface RouterProviderProps extends PropsWithChildren {\r\n initialUrl: string\r\n}\r\n\r\n// Step 1: Define what data exists on the channel\r\nexport const RouterContext = createContext<RouterContextValue | null>(null)\r\n\r\n// Step 2: Broadcast the data\r\nexport function RouterProvider({ initialUrl, children }: RouterProviderProps) {\r\n const [history, setHistory] = useState<NavigationHistory>({\r\n stack: [{ url: initialUrl }],\r\n position: 0,\r\n })\r\n\r\n // Grab the current url and data\r\n const { url, data } = history.stack[history.position] ?? { url: initialUrl }\r\n\r\n // Push new URL and data to history\r\n const push = useCallback((newUrl: string, newData?: unknown) => {\r\n setHistory(prev => actions.push(prev, newUrl, newData))\r\n }, [])\r\n\r\n // Replace current URL without adding to history\r\n const replace = useCallback((newUrl: string) => {\r\n setHistory(prev => actions.replace(prev, newUrl))\r\n }, [])\r\n\r\n // Navigate backwards\r\n const back = useCallback(() => {\r\n setHistory(prev => actions.back(prev))\r\n }, [])\r\n\r\n // Navigate forwards\r\n const forward = useCallback(() => {\r\n setHistory(prev => actions.forward(prev))\r\n }, [])\r\n\r\n return (\r\n <RouterContext.Provider\r\n value={{\r\n url,\r\n data,\r\n history: history.stack.map(entry => entry.url),\r\n position: history.position,\r\n push,\r\n replace,\r\n back,\r\n forward,\r\n }}\r\n >\r\n {children}\r\n </RouterContext.Provider>\r\n )\r\n}\r\n","import { useContext } from 'react'\r\nimport { RouterContext } from '../RouterProvider'\r\n\r\n// Step 3: Tune into the channel and receive the broadcast\r\nexport function useRouter() {\r\n const context = useContext(RouterContext)\r\n if (!context)\r\n throw new Error('useRouter must be used within a RouterProvider')\r\n return context\r\n}\r\n","import { useRouter } from './use-router'\r\n\r\n/**\r\n * Get the navigation history (URLs only)\r\n *\r\n * @returns readonly array of visited URLs\r\n *\r\n * @example\r\n * ```tsx\r\n * const history = useHistory()\r\n * return (\r\n * <Box>\r\n * <Text>Visited pages:</Text>\r\n * {history.map((url, i) => (\r\n * <Text key={i}>{url}</Text>\r\n * ))}\r\n * </Box>\r\n * )\r\n * ```\r\n */\r\nexport function useHistory(): readonly string[] {\r\n const { history } = useRouter()\r\n return history\r\n}\r\n","import { useRouter } from './use-router'\r\n\r\n/**\r\n * Get navigation functions without url/data\r\n */\r\nexport function useNavigate() {\r\n const { push, replace, back, forward } = useRouter()\r\n return { push, replace, back, forward }\r\n}\r\n","import { useRouter } from './use-router'\r\n\r\n/**\r\n * Access route data passed via router.push()\r\n *\r\n * @example\r\n * ```tsx\r\n * const user = useRouteData<User>()\r\n * if (!user) return <Text>No user</Text>\r\n * return <Text>{user.name}</Text>\r\n * ```\r\n */\r\nexport function useRouteData<T = unknown>() {\r\n const { data } = useRouter()\r\n return data as T | undefined\r\n}\r\n","import { useRouter } from './use-router'\r\n\r\n/**\r\n * Get the current URL\r\n */\r\nexport function useUrl(): string {\r\n const { url } = useRouter()\r\n return url\r\n}\r\n","import { Component, type ComponentType, type PropsWithChildren } from 'react'\r\n\r\n/** Props passed to error.tsx components */\r\nexport interface ErrorComponentProps {\r\n error: Error\r\n reset: () => void\r\n}\r\n\r\ninterface ErrorBoundaryProps extends PropsWithChildren {\r\n fallback: ComponentType<ErrorComponentProps>\r\n}\r\n\r\ninterface ErrorBoundaryState {\r\n error: Error | null\r\n}\r\n\r\n/**\r\n * React Error Boundary that catches errors in child components.\r\n * Renders the provided ErrorComponent when an error occurs.\r\n *\r\n * Catches:\r\n * - Errors during rendering\r\n * - Errors in lifecycle methods\r\n * - Errors in constructors\r\n *\r\n * Does NOT catch:\r\n * - Event handlers (use try-catch)\r\n * - Async code (use try-catch)\r\n * - Errors in the error boundary itself\r\n */\r\nexport class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {\r\n state: ErrorBoundaryState\r\n\r\n constructor(props: ErrorBoundaryProps) {\r\n super(props)\r\n this.state = { error: null }\r\n this.reset = this.reset.bind(this) // Bind all methods that will be passed as callbacks\r\n }\r\n\r\n /** Called when a child component throws during render. */\r\n static getDerivedStateFromError(error: Error): ErrorBoundaryState {\r\n return { error }\r\n }\r\n\r\n /** Called after error is caught. */\r\n componentDidCatch() {\r\n // Error is passed to error.tsx component via props\r\n }\r\n\r\n /** Resets error state and attempts to re-render children. */\r\n reset() {\r\n this.setState({ error: null })\r\n }\r\n\r\n render() {\r\n if (this.state.error) {\r\n const ErrorComponent = this.props.fallback\r\n return <ErrorComponent error={this.state.error} reset={this.reset} />\r\n }\r\n return this.props.children\r\n }\r\n}\r\n","/** Special error thrown to trigger not-found.tsx. */\r\nexport class NotFoundError extends Error {\r\n public readonly url: string\r\n\r\n constructor(url: string, message: string = 'Not Found') {\r\n super(message)\r\n this.name = 'NotFoundError'\r\n this.url = url\r\n }\r\n}\r\n\r\n/** Error thrown when a route has an empty chain. */\r\nexport class EmptyChainError extends Error {\r\n constructor(public url: string) {\r\n super(\r\n `Route ${url} has an empty chain. ` +\r\n 'This indicates a bug in manifest generation - ' +\r\n 'routes must have at least one segment.',\r\n )\r\n this.name = 'EmptyChainError'\r\n }\r\n}\r\n\r\n/** Error thrown when a component is missing from the component map. */\r\nexport class ComponentNotFoundError extends Error {\r\n constructor(public componentPath: string) {\r\n super(\r\n `Component not found: ${componentPath}. ` +\r\n 'This indicates the component map is out of sync with the manifest. ' +\r\n 'Try running \\'pen build\\' again.',\r\n )\r\n this.name = 'ComponentNotFoundError'\r\n }\r\n}\r\n","import { Component, type ComponentType, type PropsWithChildren } from 'react'\r\nimport { useRouter } from '@/core/router'\r\nimport { NotFoundError } from '../errors'\r\n\r\n/** Props passed to not-found.tsx components */\r\nexport interface NotFoundComponentProps {\r\n url: string\r\n}\r\n\r\ninterface NotFoundErrorBoundaryProps extends PropsWithChildren {\r\n fallback: ComponentType<NotFoundComponentProps>\r\n url: string\r\n}\r\n\r\ninterface NotFoundBoundaryState {\r\n url: string | null\r\n}\r\n\r\n/**\r\n * Catches NotFoundError thrown by notFound() function.\r\n * Renders fallback component when caught.\r\n *\r\n * Does NOT catch:\r\n * - Non-NotFoundError errors (they bubble)\r\n */\r\nclass NotFoundErrorBoundary extends Component<NotFoundErrorBoundaryProps, NotFoundBoundaryState> {\r\n state: NotFoundBoundaryState\r\n\r\n constructor(props: NotFoundErrorBoundaryProps) {\r\n super(props)\r\n this.state = { url: null }\r\n }\r\n\r\n /** Called when a child component throws during render. */\r\n static getDerivedStateFromError(error: Error) {\r\n // Only catch NotFoundError, other errors bubble\r\n return error instanceof NotFoundError ? { url: error.url } : null\r\n }\r\n\r\n /** Called after error is caught. */\r\n componentDidCatch(error: Error) {\r\n if (!(error instanceof NotFoundError)) // Only handle NotFoundError\r\n throw error // Re-throw other errors\r\n }\r\n\r\n /** Called after updates. Clears the not-found state when the URL prop changes. */\r\n componentDidUpdate(prevProps: NotFoundErrorBoundaryProps) {\r\n if (prevProps.url !== this.props.url && this.state.url) // URL changed → clear not-found state\r\n this.setState({ url: null })\r\n }\r\n\r\n render() {\r\n if (this.state.url) {\r\n const NotFoundComponent = this.props.fallback\r\n return <NotFoundComponent url={this.state.url} />\r\n }\r\n return this.props.children\r\n }\r\n}\r\n\r\nexport interface NotFoundBoundaryProps extends PropsWithChildren {\r\n fallback: ComponentType<NotFoundComponentProps>\r\n}\r\n\r\n/**\r\n * Router-aware wrapper for NotFoundErrorBoundary.\r\n * Injects the current URL so the boundary can reset on navigation.\r\n */\r\nexport function NotFoundBoundary({ fallback, children }: NotFoundBoundaryProps) {\r\n const { url } = useRouter()\r\n return (\r\n <NotFoundErrorBoundary fallback={fallback} url={url}>\r\n {children}\r\n </NotFoundErrorBoundary>\r\n )\r\n}\r\n","// src/core/router/components/GlobalErrorFallback.tsx\r\nimport { Box, Text, useInput } from 'ink'\r\nimport { useState } from 'react'\r\nimport { type ErrorComponentProps } from '../boundaries/ErrorBoundary'\r\n\r\n/** Global error fallback that always wraps the app. */\r\nexport function ErrorScreen({ error, reset }: ErrorComponentProps) {\r\n const [showStack, setShowStack] = useState(false)\r\n\r\n useInput((input) => {\r\n if (input === 'r') reset()\r\n if (input === 's') setShowStack(prev => !prev)\r\n if (input === 'q') process.exit(1)\r\n })\r\n\r\n return (\r\n <Box\r\n flexDirection=\"column\"\r\n padding={2}\r\n borderStyle=\"double\"\r\n borderColor=\"red\"\r\n >\r\n <Box marginBottom={1}>\r\n <Text bold color=\"red\">💥 Critical Application Error</Text>\r\n </Box>\r\n\r\n <Box marginBottom={1}>\r\n <Text color=\"red\">The application encountered a fatal error and cannot continue.</Text>\r\n </Box>\r\n\r\n <Box\r\n flexDirection=\"column\"\r\n padding={1}\r\n borderStyle=\"round\"\r\n borderColor=\"yellow\"\r\n marginBottom={1}\r\n >\r\n <Text bold color=\"yellow\">Error Details:</Text>\r\n <Text>{error.message}</Text>\r\n </Box>\r\n\r\n {showStack && error.stack && (\r\n <Box\r\n flexDirection=\"column\"\r\n padding={1}\r\n borderStyle=\"single\"\r\n borderColor=\"gray\"\r\n marginBottom={1}\r\n >\r\n <Text bold dimColor>Stack Trace:</Text>\r\n <Text dimColor>{error.stack}</Text>\r\n </Box>\r\n )}\r\n\r\n <Box flexDirection=\"column\" marginTop={1}>\r\n <Text bold>Options:</Text>\r\n <Text dimColor> [r] Retry [s] Toggle Stack Trace [q] Quit</Text>\r\n </Box>\r\n\r\n <Box marginTop={1}>\r\n <Text dimColor>\r\n If this error persists, please report it.\r\n </Text>\r\n </Box>\r\n </Box>\r\n )\r\n}\r\n","import { Box, Text, useInput } from 'ink'\r\nimport { useRouter } from '@/core/router'\r\nimport { type NotFoundComponentProps } from '../boundaries/NotFoundBoundary'\r\n\r\n/**\r\n * Not found error screen displayed when a route is not found.\r\n * Shows the URL that was attempted.\r\n */\r\nexport function NotFoundScreen({ url }: NotFoundComponentProps) {\r\n const router = useRouter()\r\n\r\n useInput((input) => {\r\n if (input === 'b') router.back()\r\n if (input === 'q') process.exit(0)\r\n })\r\n\r\n return (\r\n <Box flexDirection=\"column\" padding={2} borderStyle=\"round\" borderColor=\"red\">\r\n <Box flexDirection=\"column\" marginBottom={1}>\r\n <Text bold color=\"red\">Route Not Found</Text>\r\n <Text dimColor>{'The router couldn\\'t match the requested URL.'}</Text>\r\n </Box>\r\n\r\n <Box flexDirection=\"column\" paddingLeft={1} marginBottom={1}>\r\n <Text>\r\n Requested:{' '}\r\n <Text color=\"yellow\" bold>\r\n {url}\r\n </Text>\r\n </Text>\r\n </Box>\r\n\r\n <Box\r\n flexDirection=\"column\"\r\n padding={1}\r\n borderStyle=\"single\"\r\n borderColor=\"gray\"\r\n marginBottom={1}\r\n >\r\n <Text bold>Actions</Text>\r\n <Text dimColor> [b] Go back</Text>\r\n <Text dimColor> [q] Quit</Text>\r\n </Box>\r\n\r\n <Text dimColor>\r\n Tip: Check the route segment name or add a screen.tsx for this path.\r\n </Text>\r\n </Box>\r\n )\r\n}\r\n","import { createElement } from 'react'\r\n\r\nimport { ErrorBoundary } from '../boundaries/ErrorBoundary'\r\nimport { NotFoundBoundary } from '../boundaries/NotFoundBoundary'\r\nimport { ComponentNotFoundError, EmptyChainError } from '../errors'\r\n\r\nimport type { ComponentType, ReactElement } from 'react'\r\nimport type { Route } from '@/core/route-builder'\r\nimport type { ErrorComponentProps } from '../boundaries/ErrorBoundary'\r\nimport type { NotFoundComponentProps } from '../boundaries/NotFoundBoundary'\r\nimport type { ComponentMap } from '../types'\r\n\r\n/**\r\n * Gets a component from the component map or throws.\r\n */\r\nfunction getComponent(components: ComponentMap, path: string): ComponentType {\r\n const component = components[path]\r\n if (!component) throw new ComponentNotFoundError(path)\r\n return component\r\n}\r\n\r\n/**\r\n * Composes a route element by processing each segment in the chain.\r\n *\r\n * Composition order per segment (inside to outside):\r\n * 1. Screen component (only in leaf segment)\r\n * 2. Not-found boundary (wraps screen if present)\r\n * 3. Layout (wraps content)\r\n * 4. Error boundary (wraps layout + all descendants)\r\n *\r\n * This ensures error boundaries catch errors at the right level,\r\n * and only layouts above the error remain visible.\r\n */\r\nexport function composeRoute(route: Route, components: ComponentMap): ReactElement {\r\n if (!route.chain.length)\r\n throw new EmptyChainError(route.url)\r\n\r\n // Start with the screen (guaranteed in first segment)\r\n const leafSegment = route.chain[0]!\r\n const screenPath = leafSegment['screen']!\r\n let element = createElement(getComponent(components, screenPath), { key: screenPath })\r\n\r\n // Process segments from leaf → root\r\n for (const segment of route.chain) {\r\n // Not-found boundary\r\n if (segment['not-found']) {\r\n const path = segment['not-found']\r\n const fallback = getComponent(components, path) as ComponentType<NotFoundComponentProps>\r\n element = createElement(NotFoundBoundary, { key: path, fallback }, element)\r\n }\r\n\r\n // Error boundary\r\n if (segment['error']) {\r\n const path = segment['error']\r\n const fallback = getComponent(components, path) as ComponentType<ErrorComponentProps>\r\n element = createElement(ErrorBoundary, { key: path, fallback }, element)\r\n }\r\n\r\n // Layout\r\n if (segment['layout']) {\r\n const path = segment['layout']\r\n element = createElement(getComponent(components, path), { key: path }, element)\r\n }\r\n }\r\n\r\n return element\r\n}\r\n","import type { Route, RouteManifest } from '@/core/route-builder'\r\n\r\n/**\r\n * Matches a URL against the route manifest.\r\n * Returns the matched route metadata or null if no match found.\r\n */\r\nexport function matchRoute(url: string, manifest: RouteManifest): Route | null {\r\n // Normalize URL (ensure trailing slash)\r\n const normalizedUrl = url.endsWith('/') ? url : `${url}/`\r\n\r\n // Direct lookup\r\n return manifest[normalizedUrl] ?? null\r\n}\r\n","import { useRouter } from '@/core/router'\r\nimport { composeRoute } from './routing/composer'\r\nimport { matchRoute } from './routing/matcher'\r\nimport { NotFoundError } from './errors'\r\n\r\nimport type { ReactElement } from 'react'\r\nimport type { RouteManifest } from '@/core/route-builder'\r\nimport type { ComponentMap } from './types'\r\n\r\n/**\r\n * Props for the FileRouter component.\r\n * Defines the URL to render, the route manifest, and component map.\r\n */\r\nexport interface FileRouterProps {\r\n manifest: RouteManifest\r\n components: ComponentMap\r\n}\r\n\r\n/**\r\n * Router component that orchestrates route matching and composition.\r\n * Returns the composed route element or throws NotFoundError.\r\n */\r\nexport function FileRouter({ manifest, components }: FileRouterProps): ReactElement {\r\n const { url } = useRouter()\r\n const route = matchRoute(url, manifest)\r\n\r\n if (!route)\r\n throw new NotFoundError(url)\r\n\r\n return composeRoute(route, components)\r\n}\r\n","import { RouterProvider } from '@/core/router'\r\nimport { ErrorBoundary } from './boundaries/ErrorBoundary'\r\nimport { NotFoundBoundary } from './boundaries/NotFoundBoundary'\r\nimport { ErrorScreen } from './screens/ErrorScreen'\r\nimport { NotFoundScreen } from './screens/NotFoundScreen'\r\nimport { FileRouter } from './FileRouter'\r\n\r\nimport type { RouteManifest } from '@/core/route-builder'\r\nimport type { ComponentMap } from './types'\r\n\r\nexport interface AppProps {\r\n initialUrl: string\r\n manifest: RouteManifest\r\n components: ComponentMap\r\n}\r\n\r\nexport function App({ initialUrl, manifest, components }: AppProps) {\r\n return (\r\n <ErrorBoundary fallback={ErrorScreen}>\r\n <RouterProvider initialUrl={initialUrl}>\r\n <NotFoundBoundary fallback={NotFoundScreen}>\r\n <FileRouter manifest={manifest} components={components} />\r\n </NotFoundBoundary>\r\n </RouterProvider>\r\n </ErrorBoundary>\r\n )\r\n}\r\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,81 @@
1
+ {
2
+ "name": "@idlesummer/pen",
3
+ "version": "0.1.0",
4
+ "description": "File-based routing for React Ink apps (experimental)",
5
+ "keywords": [
6
+ "cli",
7
+ "ink",
8
+ "routing",
9
+ "react",
10
+ "terminal",
11
+ "experimental",
12
+ "learning-project"
13
+ ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/idlesummer/pen.git"
17
+ },
18
+ "license": "MIT",
19
+ "author": "idlesummer <09louisthebob@gmail.com> (https://github.com/idlesummer)",
20
+ "type": "module",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.mts",
24
+ "default": "./dist/index.mjs"
25
+ }
26
+ },
27
+ "bin": {
28
+ "pen": "./dist/bin.mjs"
29
+ },
30
+ "files": [
31
+ "dist"
32
+ ],
33
+ "workspaces": [
34
+ "examples/*"
35
+ ],
36
+ "scripts": {
37
+ "build": "tsdown",
38
+ "lint": "eslint",
39
+ "prepare": "npm run build",
40
+ "test": "vitest",
41
+ "test:coverage": "vitest run --coverage",
42
+ "test:run": "vitest run",
43
+ "test:ui": "vitest --ui"
44
+ },
45
+ "dependencies": {
46
+ "@idlesummer/tasker": "^0.1.2",
47
+ "commander": "^14.0.2",
48
+ "ora": "^9.0.0",
49
+ "picomatch": "^4.0.3",
50
+ "pretty-bytes": "^7.1.0",
51
+ "pretty-ms": "^9.3.0",
52
+ "rolldown": "^1.0.0-beta.59",
53
+ "rollup-plugin-node-externals": "^8.1.2"
54
+ },
55
+ "devDependencies": {
56
+ "@eslint/js": "^9.39.2",
57
+ "@testing-library/react": "^16.3.1",
58
+ "@types/node": "^25.0.3",
59
+ "@types/picomatch": "^4.0.2",
60
+ "@types/react": "^19.2.8",
61
+ "@vitest/ui": "^4.0.17",
62
+ "eslint": "^9.39.2",
63
+ "eslint-plugin-react": "^7.37.5",
64
+ "eslint-plugin-react-hooks": "^7.0.1",
65
+ "globals": "^16.5.0",
66
+ "happy-dom": "^20.3.1",
67
+ "ink": "^6.5.1",
68
+ "react": "^19.2.3",
69
+ "tsdown": "^0.19.0-beta.5",
70
+ "typescript": "^5.9.3",
71
+ "typescript-eslint": "^8.51.0",
72
+ "vitest": "^4.0.17"
73
+ },
74
+ "peerDependencies": {
75
+ "ink": "^6.5.1",
76
+ "react": "^19.2.3"
77
+ },
78
+ "engines": {
79
+ "node": ">=24"
80
+ }
81
+ }