orga-build 0.9.0 → 0.10.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.
Files changed (66) hide show
  1. package/README.org +282 -33
  2. package/cli.js +4 -4
  3. package/lib/__tests__/build.test.js +441 -22
  4. package/lib/__tests__/dev.test.d.ts +2 -0
  5. package/lib/__tests__/dev.test.d.ts.map +1 -0
  6. package/lib/__tests__/dev.test.js +295 -0
  7. package/lib/__tests__/fixtures.d.ts +8 -0
  8. package/lib/__tests__/fixtures.d.ts.map +1 -0
  9. package/lib/__tests__/fixtures.js +61 -0
  10. package/lib/app.jsx +23 -50
  11. package/lib/build.d.ts +4 -1
  12. package/lib/build.d.ts.map +1 -1
  13. package/lib/build.js +10 -175
  14. package/lib/config.d.ts +5 -3
  15. package/lib/config.d.ts.map +1 -1
  16. package/lib/config.js +8 -16
  17. package/lib/content.d.ts +6 -0
  18. package/lib/dev-ssr.d.ts +14 -0
  19. package/lib/dev-ssr.d.ts.map +1 -0
  20. package/lib/dev-ssr.js +137 -0
  21. package/lib/endpoint.d.ts +5 -0
  22. package/lib/endpoint.d.ts.map +1 -1
  23. package/lib/endpoint.js +1 -0
  24. package/lib/files.d.ts +8 -3
  25. package/lib/files.d.ts.map +1 -1
  26. package/lib/files.js +104 -20
  27. package/lib/fs.d.ts +0 -5
  28. package/lib/fs.d.ts.map +1 -1
  29. package/lib/fs.js +0 -18
  30. package/lib/html.d.ts +37 -0
  31. package/lib/html.d.ts.map +1 -0
  32. package/lib/html.js +101 -0
  33. package/lib/index.html +0 -1
  34. package/lib/island-client.d.ts +5 -0
  35. package/lib/island-client.d.ts.map +1 -0
  36. package/lib/island-client.js +41 -0
  37. package/lib/island.d.ts +18 -0
  38. package/lib/island.d.ts.map +1 -0
  39. package/lib/island.js +146 -0
  40. package/lib/island.jsx +108 -0
  41. package/lib/orga.d.ts +1 -1
  42. package/lib/orga.d.ts.map +1 -1
  43. package/lib/orga.js +54 -16
  44. package/lib/plugin.d.ts +20 -32
  45. package/lib/plugin.d.ts.map +1 -1
  46. package/lib/plugin.js +128 -178
  47. package/lib/prerender.d.ts +11 -0
  48. package/lib/prerender.d.ts.map +1 -0
  49. package/lib/prerender.js +184 -0
  50. package/lib/serve.d.ts.map +1 -1
  51. package/lib/serve.js +5 -23
  52. package/lib/ssr.jsx +9 -14
  53. package/lib/util.d.ts +0 -22
  54. package/lib/util.d.ts.map +1 -1
  55. package/lib/util.js +0 -62
  56. package/lib/vite.d.ts +5 -6
  57. package/lib/vite.d.ts.map +1 -1
  58. package/lib/vite.js +74 -23
  59. package/package.json +5 -8
  60. package/lib/components.d.ts +0 -2
  61. package/lib/components.d.ts.map +0 -1
  62. package/lib/components.js +0 -1
  63. package/lib/csr.jsx +0 -11
  64. package/lib/watch.d.ts +0 -10
  65. package/lib/watch.d.ts.map +0 -1
  66. package/lib/watch.js +0 -53
package/README.org CHANGED
@@ -4,7 +4,9 @@ A simple tool that builds org-mode files into a website.
4
4
 
5
5
  * Architecture
6
6
 
7
- orga-build is built on top of Vite with a Vite-native architecture. The dev server uses Vite's native =createServer().listen()= pattern (no custom Express wrapper), which ensures maximum compatibility with the Vite ecosystem, including plugins like Cloudflare Workers.
7
+ orga-build is built on top of Vite (8+) with a Vite-native architecture. The dev server uses Vite's native =createServer().listen()= pattern (no custom Express wrapper), which ensures maximum compatibility with the Vite ecosystem, including plugins like Cloudflare Workers.
8
+
9
+ The build uses Vite's Environment API: =orgaBuildPlugin= configures an =ssr= and a =client= environment, and its =buildApp= hook builds both and prerenders every page and endpoint to static HTML. Pages ship no JavaScript unless they use an [[*Interactive Components (Islands)][island]]. The CLI is a thin wrapper, so =vite build= with the plugin produces the same site.
8
10
 
9
11
  ** Key Design Principles
10
12
 
@@ -31,30 +33,26 @@ export default {
31
33
 
32
34
  Note: Do NOT add =orgaBuildPlugin()= to =vitePlugins= - it's already included by the CLI. Only add external plugins.
33
35
 
34
- For advanced users integrating directly with Vite (without the orga-build CLI), use =orgaBuildPlugin= in your =vite.config.js=:
36
+ For advanced users integrating directly with Vite (without the orga-build CLI), use =orgaBuildPlugin= in your =vite.config.js=. =vite= then serves the site with SSR and =vite build= builds the static site:
35
37
 
36
38
  #+begin_src javascript
37
39
  // vite.config.js (advanced - direct Vite integration)
38
40
  import { cloudflare } from '@cloudflare/vite-plugin'
39
- import { orgaBuildPlugin, alias } from 'orga-build'
41
+ import { orgaBuildPlugin } from 'orga-build'
40
42
 
41
43
  export default {
42
44
  plugins: [
43
45
  cloudflare(),
44
- ...orgaBuildPlugin({ root: 'pages', containerClass: [] })
45
- ],
46
- resolve: { alias }
46
+ ...orgaBuildPlugin({ root: 'pages', outDir: 'out', containerClass: [] })
47
+ ]
47
48
  }
48
49
  #+end_src
49
50
 
50
51
  ** Default HTML Template
51
52
 
52
- If your project doesn't have an =index.html= file, orga-build provides a default template that:
53
- - Sets up React rendering
54
- - Enables client-side routing
55
- - Works in both dev and production builds
53
+ If your project doesn't have an =index.html= file, orga-build provides a default template: a bare HTML document with a =<div id="root">= that pages are rendered into.
56
54
 
57
- To customize the HTML shell, create your own =index.html= in your project root.
55
+ To customize the HTML shell, create your own =index.html= in your project root. Keep =<div id="root"></div>=; Vite processes the rest like any =index.html= (bundling linked scripts and stylesheets), and =%orga.<key>%= placeholders are filled from each page's exports (e.g. =%orga.title%=).
58
56
 
59
57
  * Installation
60
58
 
@@ -62,24 +60,36 @@ To customize the HTML shell, create your own =index.html= in your project root.
62
60
  npm install orga-build
63
61
  #+end_src
64
62
 
63
+ * Usage
64
+
65
+ #+begin_src bash
66
+ orga-build dev # dev server with live reload
67
+ orga-build # build the static site into outDir
68
+ orga-build --outDir dist # override outDir (also -o)
69
+ #+end_src
70
+
65
71
  * Configuration
66
72
 
67
- orga-build uses =orga.config.js= (or =orga.config.mjs=) as the primary configuration file. This file should be placed in your project root.
73
+ orga-build uses =orga.config.js= (or =orga.config.mjs=) as the primary configuration file. This file should be placed in your project root. It is optional; a config file that fails to load fails the command.
68
74
 
69
75
  #+begin_src javascript
70
76
  // orga.config.js
71
77
  export default {
72
- // Directory containing your .org files (default: 'pages')
78
+ // Directory containing your .org files (default: '.', the project root)
73
79
  root: 'pages',
74
80
 
75
- // Output directory for production build (default: 'out')
81
+ // Output directory for production build (default: '.out')
76
82
  outDir: 'out',
77
83
 
84
+ // Absolute URL the site is served from, for canonical URLs, feeds and
85
+ // sitemaps. Include the path if the site isn't at the domain root.
86
+ site: 'https://example.com',
87
+
78
88
  // CSS class(es) to wrap rendered org content
79
89
  containerClass: ['prose', 'prose-lg'],
80
90
 
81
91
  // Global stylesheets — paths relative to orga.config.js (leading / optional).
82
- // Injected in dev SSR <head> and imported by the client entry.
92
+ // Linked from the HTML shell; Vite processes them (with HMR in dev).
83
93
  styles: ['pages/style.css'],
84
94
 
85
95
  // Extra rehype plugins appended to orga-build defaults
@@ -100,10 +110,11 @@ export default {
100
110
 
101
111
  | Option | Type | Default | Description |
102
112
  |----------------+-------------------+---------+-----------------------------------------------------------------|
103
- | =root= | =string= | ='pages'= | Directory containing content files |
104
- | =outDir= | =string= | ='out'= | Output directory for production build |
113
+ | =root= | =string= | ='.'= | Directory containing content files |
114
+ | =outDir= | =string= | ='.out'= | Output directory for production build |
115
+ | =site= | =string= | none | Absolute URL the site is served from, see [[*Site URL][Site URL]] |
105
116
  | =containerClass= | =string \vert string[]= | =[]= | CSS class(es) for content wrapper |
106
- | =styles= | =string[]= | =[]= | Stylesheets to inject/import; paths relative to =orga.config.js= |
117
+ | =styles= | =string[]= | =[]= | Stylesheets to link; paths relative to =orga.config.js= |
107
118
  | =exclude= | =string[]= | =[]= | Glob patterns (relative to =root=) excluded from content scanning |
108
119
  | =rehypePlugins= | =PluggableList= | =[]= | Extra rehype plugins appended to orga-build defaults |
109
120
  | =vitePlugins= | =PluginOption[]= | =[]= | Additional Vite plugins |
@@ -132,7 +143,7 @@ Page routes are discovered from =.org=, =.tsx=, and =.jsx= files.
132
143
 
133
144
  At build time, page routes are emitted as HTML:
134
145
 
135
- - =/about= -> =out/about/index.html=
146
+ - =/about= -> =.out/about/index.html=
136
147
 
137
148
  ** Endpoint Routes
138
149
 
@@ -152,13 +163,65 @@ export async function GET(ctx) {
152
163
  }
153
164
  #+end_src
154
165
 
166
+ =ctx= has:
167
+
168
+ - =url=: the request URL. At build time it is =site + route= (or =http://localhost= + route without =site=).
169
+ - =site=: the configured =site=, see [[*Site URL][Site URL]]
170
+ - =mode=: ='dev'= or ='build'=
171
+ - =route.route=: the route, e.g. ='/rss.xml'=
172
+
155
173
  At build time, endpoint routes are emitted to exact filenames:
156
174
 
157
- - =/rss.xml= -> =out/rss.xml=
158
- - =/api/data.json= -> =out/api/data.json=
175
+ - =/rss.xml= -> =.out/rss.xml=
176
+ - =/api/data.json= -> =.out/api/data.json=
159
177
 
160
178
  Route conflicts (same final route path) fail fast during dev/build startup.
161
179
 
180
+ ** Links
181
+
182
+ Links to =.org= files point at their pages, under Vite's =base=. Every heading gets an =id=: its =CUSTOM_ID= property when set, otherwise a slug of its text (=* Getting Started= → =#getting-started=).
183
+
184
+ | Link | Goes to |
185
+ |----------------------------------------+------------------------------|
186
+ | =[[file:guide.org]]= | =/guide= |
187
+ | =[[file:guide.org::*Getting Started]]= | =/guide#getting-started= |
188
+ | =[[file:guide.org::#install]]= | =/guide#install= |
189
+ | =[[*Getting Started]]= | heading on the same page |
190
+ | =[[#install]]= | =CUSTOM_ID= on the same page |
191
+
192
+ A =::*Heading= link to another file assumes that heading's id is the slug of its text. If the heading has a =CUSTOM_ID=, link to it with =::#id= instead. =id:= links aren't supported yet.
193
+
194
+ * Interactive Components (Islands)
195
+
196
+ Pages are static HTML. To run a component in the browser, start its module with React's ='use client'= directive, like in any React framework:
197
+
198
+ #+begin_src tsx
199
+ // _components.tsx
200
+ 'use client'
201
+ import { useState } from 'react'
202
+
203
+ export function Counter({ start }: { start: number }) {
204
+ const [count, setCount] = useState(start)
205
+ return <button onClick={() => setCount(count + 1)}>{count}</button>
206
+ }
207
+ #+end_src
208
+
209
+ #+begin_example
210
+ #+jsx: <Counter start={1} />
211
+ #+end_example
212
+
213
+ The component is still prerendered. On pages that use it, the browser imports just that module (plus a shared React chunk) and hydrates it in place; every other page ships no JavaScript. Only components become islands, recognized by their PascalCase name (or the default export); hooks and other exports of a ='use client'= module stay plain values. A page file (=.tsx=) can start with ='use client'= too, which makes the whole page an island.
214
+
215
+ Props cross to the browser as JSON, so they must be serializable: no functions and no =children=. Components rendered /inside/ an island are already client code and have no such limit.
216
+
217
+ An island is wrapped in an =<orga-island>= element, so it can't sit where HTML allows only specific children, such as directly inside =<table>=, =<tbody>=, =<tr>=, =<select>= or =<ul>=. Make the island the whole table or list instead.
218
+
219
+ Islands hydrate as soon as the page loads. Pass =client="visible"= to defer hydration until the island scrolls into view, which also defers loading React:
220
+
221
+ #+begin_example
222
+ #+jsx: <Counter start={1} client="visible" />
223
+ #+end_example
224
+
162
225
  * TypeScript Setup
163
226
 
164
227
  If you're using TypeScript and want type support for the =orga-build:content= virtual module, you need to add a reference to the type definitions.
@@ -224,9 +287,9 @@ const writing = getPages('writing')
224
287
  // Get entries in a nested path
225
288
  const posts2025 = getPages('content/writing/2025')
226
289
 
227
- // Filter out drafts
228
- const published = getPages('writing', (entry) => {
229
- return entry.data['draft'] !== 'true'
290
+ // Filter by metadata
291
+ const featured = getPages('writing', (entry) => {
292
+ return entry.data['featured'] === 'true'
230
293
  })
231
294
  #+end_src
232
295
 
@@ -268,6 +331,10 @@ const entries = getEntries([
268
331
  ])
269
332
  #+end_src
270
333
 
334
+ *** =site=
335
+
336
+ The configured =site=, without a trailing slash, or =undefined=. See [[*Site URL][Site URL]].
337
+
271
338
  *** Aliases
272
339
 
273
340
  For Astro familiarity:
@@ -285,7 +352,7 @@ interface ContentEntry {
285
352
  path: string // e.g., 'writing' or 'content/writing/2025'
286
353
  filePath: string // absolute source file path
287
354
  ext: 'org' | 'tsx' | 'jsx' // file extension
288
- data: Record<string, unknown> // metadata from org headers
355
+ data: Record<string, unknown> // org keywords, or TSX/JSX named exports
289
356
  }
290
357
  #+end_src
291
358
 
@@ -311,7 +378,48 @@ This becomes:
311
378
  }
312
379
  #+end_src
313
380
 
314
- For =.tsx= and =.jsx= files, the =data= field is currently empty in v1.
381
+ For =.tsx= and =.jsx= files, =data= holds the page's named exports whose values are literals: strings, numbers, booleans, =null=, templates without =${}=, and arrays of those. Other exports (functions, computed values) are skipped, since pages aren't run to read them.
382
+
383
+ #+begin_src tsx
384
+ export const title = 'About'
385
+ export const date = '2025-01-15'
386
+ export const tags = ['meta', 'site']
387
+
388
+ export default function About() {
389
+ return <h1>{title}</h1>
390
+ }
391
+ #+end_src
392
+
393
+ ** Drafts
394
+
395
+ Mark a page as a draft with =#+draft: t= (or =true=, =yes=), or in a TSX/JSX
396
+ page with =export const draft = true=. The dev server
397
+ serves drafts so you can preview them; the build leaves them out entirely: no
398
+ HTML, and no entry in =getPages=, so feeds, sitemaps, and index pages skip
399
+ them without filtering.
400
+
401
+ ** Site URL
402
+
403
+ Set =site= in the config to the absolute URL the site is served from. It is
404
+ exported from =orga-build:content= and passed to endpoints as =ctx.site=,
405
+ without a trailing slash, so a page's absolute URL is =site + page.slug=:
406
+
407
+ #+begin_src tsx
408
+ // _layout.tsx
409
+ import { site } from 'orga-build:content'
410
+
411
+ export default function Layout({ slug, children }) {
412
+ const share = 'https://bsky.app/intent/compose?text=' + encodeURIComponent(site + slug)
413
+ return (
414
+ <>
415
+ {children}
416
+ <a href={share}>Share</a>
417
+ </>
418
+ )
419
+ }
420
+ #+end_src
421
+
422
+ If the site is served under a path, include it: =site: 'https://example.com/blog'=.
315
423
 
316
424
  ** Path Matching Behavior
317
425
 
@@ -338,9 +446,7 @@ Path matching works hierarchically:
338
446
  import { getPages } from 'orga-build:content'
339
447
 
340
448
  export default function BlogIndex() {
341
- const posts = getPages('writing', (entry) => {
342
- return entry.data['draft'] !== 'true'
343
- }).sort((a, b) => {
449
+ const posts = getPages('writing').sort((a, b) => {
344
450
  // Sort by date descending
345
451
  return String(b.data['date']).localeCompare(String(a.data['date']))
346
452
  })
@@ -390,12 +496,155 @@ export default function Post({ slug }: { slug: string }) {
390
496
  }
391
497
  #+end_src
392
498
 
393
- * Development
499
+ * Feeds and Sitemaps
394
500
 
395
- ** TODO Items
501
+ There is no built-in feed or sitemap. Write them as [[*Endpoint Routes][endpoints]] built on =getPages=, and adapt them to your site. Both need =site= for absolute URLs. Drafts are already left out of =getPages= in the build.
396
502
 
397
- - resolve relative path in links and images
398
- - monitor file changes and cache properly
503
+ ** RSS Feed
504
+
505
+ This feed lists the pages under =writing/= that have a =#+date:=, newest first. It reads the =YYYY-MM-DD= part of the date, so both =2025-01-15= and =<2025-01-15 Wed>= work.
506
+
507
+ #+begin_src ts
508
+ // rss.xml.ts
509
+ import { getPages, site } from 'orga-build:content'
510
+
511
+ const escape = (value: unknown) =>
512
+ String(value).replace(/[<>&'"]/g, (c) => `&#${c.charCodeAt(0)};`)
513
+
514
+ export function GET() {
515
+ if (!site) throw new Error('Set "site" in orga.config.js to build the feed')
516
+
517
+ const posts = getPages('writing')
518
+ .flatMap((page) => {
519
+ const date = String(page.data['date'] ?? '').match(/\d{4}-\d{2}-\d{2}/)
520
+ return date ? [{ page, date: date[0] }] : []
521
+ })
522
+ .sort((a, b) => b.date.localeCompare(a.date))
523
+
524
+ const items = posts.map(({ page, date }) => {
525
+ const url = escape(site + page.slug)
526
+ return `
527
+ <item>
528
+ <title>${escape(page.data['title'] ?? page.id)}</title>
529
+ <link>${url}</link>
530
+ <guid>${url}</guid>
531
+ <pubDate>${new Date(date).toUTCString()}</pubDate>
532
+ </item>`
533
+ })
534
+
535
+ return new Response(
536
+ `<?xml version="1.0" encoding="UTF-8"?>
537
+ <rss version="2.0">
538
+ <channel>
539
+ <title>My Site</title>
540
+ <link>${escape(site + '/')}</link>
541
+ <description>Latest writing</description>${items.join('')}
542
+ </channel>
543
+ </rss>
544
+ `,
545
+ { headers: { 'content-type': 'application/xml; charset=utf-8' } }
546
+ )
547
+ }
548
+ #+end_src
549
+
550
+ Feed readers look for the feed in the page's =<head>=. Layouts render inside =<body>=, so add the link to the =<head>= of your own [[*Default HTML Template][index.html]] instead, with the feed's full URL (=site= + =/rss.xml=):
551
+
552
+ #+begin_src html
553
+ <link rel="alternate" type="application/rss+xml" href="https://example.com/rss.xml" />
554
+ #+end_src
555
+
556
+ ** Sitemap
557
+
558
+ #+begin_src ts
559
+ // sitemap.xml.ts
560
+ import { getPages, site } from 'orga-build:content'
561
+
562
+ const escape = (value: unknown) =>
563
+ String(value).replace(/[<>&'"]/g, (c) => `&#${c.charCodeAt(0)};`)
564
+
565
+ export function GET() {
566
+ if (!site) throw new Error('Set "site" in orga.config.js to build the sitemap')
567
+
568
+ const urls = getPages().map(
569
+ (page) => `
570
+ <url><loc>${escape(site + page.slug)}</loc></url>`
571
+ )
572
+
573
+ return new Response(
574
+ `<?xml version="1.0" encoding="UTF-8"?>
575
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${urls.join('')}
576
+ </urlset>
577
+ `,
578
+ { headers: { 'content-type': 'application/xml; charset=utf-8' } }
579
+ )
580
+ }
581
+ #+end_src
582
+
583
+ ** robots.txt
584
+
585
+ A small endpoint, which can also point crawlers at the sitemap:
586
+
587
+ #+begin_src ts
588
+ // robots.txt.ts
589
+ import { site } from 'orga-build:content'
590
+
591
+ export function GET() {
592
+ if (!site) throw new Error('Set "site" in orga.config.js to build robots.txt')
593
+ return new Response(`User-agent: *\nAllow: /\nSitemap: ${site}/sitemap.xml\n`)
594
+ }
595
+ #+end_src
596
+
597
+ * Search with Pagefind
598
+
599
+ The build output is plain static HTML, so [[https://pagefind.app][Pagefind]] can index it without any orga-build integration. Install it and run it on =outDir= (default =.out=) after each build:
600
+
601
+ #+begin_src bash
602
+ npm install -D pagefind
603
+ #+end_src
604
+
605
+ #+begin_src json
606
+ {
607
+ "scripts": {
608
+ "build": "orga-build && pagefind --site .out"
609
+ }
610
+ }
611
+ #+end_src
612
+
613
+ Pagefind writes its index and UI to =.out/pagefind/=. Pagefind already skips =<nav>= and =<footer>= elements. To also leave out headers, sidebars and other page chrome, mark the content area with =data-pagefind-body= in your root =_layout.jsx=, and Pagefind indexes only that. Then add the search UI from the same layout. It is made of web components, so it doesn't need an island:
614
+
615
+ #+begin_src jsx
616
+ // pages/_layout.jsx
617
+ export default function Layout({ children }) {
618
+ return (
619
+ <>
620
+ <header>
621
+ <pagefind-modal-trigger></pagefind-modal-trigger>
622
+ <pagefind-modal></pagefind-modal>
623
+ </header>
624
+ <main data-pagefind-body>{children}</main>
625
+ <link href="/pagefind/pagefind-component-ui.css" rel="stylesheet" />
626
+ <script src="/pagefind/pagefind-component-ui.js" type="module"></script>
627
+ </>
628
+ )
629
+ }
630
+ #+end_src
631
+
632
+ Pages without =data-pagefind-body= are left out of the index, so put it in a layout that wraps every page you want searchable.
633
+
634
+ The index only exists after a build, so search doesn't work in =orga-build dev=. To try it locally, run =npx pagefind --site .out --serve= after building.
635
+
636
+ In a =_layout.tsx=, declare the elements so TypeScript accepts them:
637
+
638
+ #+begin_src tsx
639
+ declare module 'react' {
640
+ namespace JSX {
641
+ interface IntrinsicElements {
642
+ 'pagefind-modal-trigger': React.HTMLAttributes<HTMLElement>
643
+ 'pagefind-modal': React.HTMLAttributes<HTMLElement>
644
+ }
645
+ }
646
+ }
647
+ #+end_src
399
648
 
400
649
  * License
401
650
 
package/cli.js CHANGED
@@ -1,18 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import path from 'node:path'
3
4
  import { argv } from 'node:process'
4
5
  import { parseArgs } from 'node:util'
5
6
  import { build } from './lib/build.js'
6
7
  import { loadConfig } from './lib/config.js'
7
8
  import { serve } from './lib/serve.js'
8
9
 
9
- const { positionals } = parseArgs({
10
+ const { values, positionals } = parseArgs({
10
11
  args: argv.slice(2),
11
12
  options: {
12
- watch: { type: 'boolean', short: 'w' },
13
- outDir: { type: 'string', short: 'o', default: '.out' }
13
+ outDir: { type: 'string', short: 'o' }
14
14
  },
15
- tokens: true,
16
15
  allowPositionals: true
17
16
  })
18
17
 
@@ -20,6 +19,7 @@ const { config, projectRoot } = await loadConfig(
20
19
  'orga.config.js',
21
20
  'orga.config.mjs'
22
21
  )
22
+ if (values.outDir) config.outDir = path.resolve(values.outDir)
23
23
 
24
24
  await (positionals.includes('dev')
25
25
  ? serve(config, 3000, projectRoot)