@sublime-ui/devkit 0.1.0 → 0.1.1

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.
@@ -0,0 +1,810 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/util/log.ts
4
+ import pc from "picocolors";
5
+ var log = {
6
+ info: (m) => console.log(m),
7
+ step: (m) => console.log(pc.cyan(`\u2192 ${m}`)),
8
+ success: (m) => console.log(pc.green(`\u2713 ${m}`)),
9
+ warn: (m) => console.log(pc.yellow(`! ${m}`)),
10
+ error: (m) => console.error(pc.red(`\u2717 ${m}`)),
11
+ table: (rows) => {
12
+ const width = rows.reduce((m, r) => Math.max(m, r.label.length), 0);
13
+ for (const r of rows) {
14
+ const mark = r.ok ? pc.green("\u2713") : pc.red("\u2717");
15
+ console.log(`${mark} ${r.label.padEnd(width)} ${pc.dim(r.detail)}`);
16
+ }
17
+ }
18
+ };
19
+
20
+ // src/lib/scaffold/init.ts
21
+ import { existsSync as existsSync2, readdirSync } from "fs";
22
+ import { join, basename } from "path";
23
+
24
+ // src/lib/scaffold/versions.ts
25
+ var SUBLIME_VERSIONS = {
26
+ framework: "^0.1.0",
27
+ library: "^0.1.0",
28
+ ui: "^0.1.0",
29
+ desktop: "^0.1.0"
30
+ };
31
+ var PEER_VERSIONS = {
32
+ react: "^18.3.1",
33
+ "react-dom": "^18.3.1",
34
+ "@mui/material": "^6.1.6",
35
+ "@emotion/react": "^11.13.3",
36
+ "@emotion/styled": "^11.13.0",
37
+ "react-router-dom": "^6.27.0",
38
+ "react-native": "^0.76.1",
39
+ "react-native-paper": "^5.12.5",
40
+ "react-native-safe-area-context": "^4.14.0",
41
+ "@react-navigation/native": "^6.1.18",
42
+ "@react-navigation/native-stack": "^6.11.0",
43
+ "@react-navigation/bottom-tabs": "^6.6.1",
44
+ electron: "^33.0.0",
45
+ vite: "^5.4.0",
46
+ "@vitejs/plugin-react": "^4.3.0",
47
+ typescript: "^5.6.0"
48
+ };
49
+
50
+ // src/lib/scaffold/templates/app.ts
51
+ var has = (targets, t) => targets.includes(t);
52
+ function renderAppPackageJson(name, targets) {
53
+ const deps = {
54
+ "@sublime-ui/framework": SUBLIME_VERSIONS.framework,
55
+ "@sublime-ui/library": SUBLIME_VERSIONS.library,
56
+ "@sublime-ui/ui": SUBLIME_VERSIONS.ui,
57
+ react: PEER_VERSIONS["react"]
58
+ };
59
+ const devDeps = {
60
+ "@sublime-ui/devkit": SUBLIME_VERSIONS.framework,
61
+ // shares the lockstep version
62
+ typescript: PEER_VERSIONS["typescript"],
63
+ "@types/react": "^18.3.12",
64
+ "@types/node": "^22.0.0"
65
+ };
66
+ const scripts = { "build:nav": "sublime build:nav" };
67
+ if (has(targets, "web") || has(targets, "desktop")) {
68
+ deps["react-dom"] = PEER_VERSIONS["react-dom"];
69
+ deps["react-router-dom"] = PEER_VERSIONS["react-router-dom"];
70
+ deps["@mui/material"] = PEER_VERSIONS["@mui/material"];
71
+ deps["@emotion/react"] = PEER_VERSIONS["@emotion/react"];
72
+ deps["@emotion/styled"] = PEER_VERSIONS["@emotion/styled"];
73
+ devDeps["@types/react-dom"] = "^18.3.1";
74
+ devDeps["vite"] = PEER_VERSIONS["vite"];
75
+ devDeps["@vitejs/plugin-react"] = PEER_VERSIONS["@vitejs/plugin-react"];
76
+ scripts["dev:web"] = "vite";
77
+ scripts["build:web"] = "vite build";
78
+ }
79
+ if (has(targets, "mobile")) {
80
+ deps["react-native"] = PEER_VERSIONS["react-native"];
81
+ deps["react-native-paper"] = PEER_VERSIONS["react-native-paper"];
82
+ deps["react-native-safe-area-context"] = PEER_VERSIONS["react-native-safe-area-context"];
83
+ deps["@react-navigation/native"] = PEER_VERSIONS["@react-navigation/native"];
84
+ deps["@react-navigation/native-stack"] = PEER_VERSIONS["@react-navigation/native-stack"];
85
+ deps["@react-navigation/bottom-tabs"] = PEER_VERSIONS["@react-navigation/bottom-tabs"];
86
+ scripts["dev:mobile"] = "sublime build --debug";
87
+ scripts["build:mobile"] = "sublime build";
88
+ }
89
+ if (has(targets, "desktop")) {
90
+ deps["@sublime-ui/desktop"] = SUBLIME_VERSIONS.desktop;
91
+ devDeps["electron"] = PEER_VERSIONS["electron"];
92
+ scripts["desktop:dev"] = "sublime desktop:dev";
93
+ scripts["desktop:build"] = "sublime desktop:build";
94
+ }
95
+ const pkg = {
96
+ name,
97
+ version: "0.0.0",
98
+ private: true,
99
+ type: "module",
100
+ scripts,
101
+ dependencies: deps,
102
+ devDependencies: devDeps
103
+ };
104
+ return JSON.stringify(pkg, null, 2) + "\n";
105
+ }
106
+ function renderSublimeConfig(targets) {
107
+ const cfg = {
108
+ modelsDir: "src/models",
109
+ componentsDir: "src/components",
110
+ themeDir: "src/theme",
111
+ navigationDir: "src/navigation",
112
+ importAlias: "@sublime-ui"
113
+ };
114
+ if (has(targets, "desktop")) cfg["desktop"] = { dir: "desktop" };
115
+ return JSON.stringify(cfg, null, 2) + "\n";
116
+ }
117
+ function renderTsconfig(targets = ["web", "mobile", "desktop"]) {
118
+ const wantsWeb = has(targets, "web") || has(targets, "desktop");
119
+ const types = ["react", "node"];
120
+ if (wantsWeb) types.splice(1, 0, "react-dom");
121
+ const include = ["src"];
122
+ if (wantsWeb) include.push("web");
123
+ if (has(targets, "mobile")) include.push("mobile");
124
+ if (has(targets, "desktop")) include.push("desktop/src");
125
+ return JSON.stringify(
126
+ {
127
+ compilerOptions: {
128
+ target: "ES2022",
129
+ module: "ESNext",
130
+ moduleResolution: "Bundler",
131
+ jsx: "react-jsx",
132
+ strict: true,
133
+ esModuleInterop: true,
134
+ skipLibCheck: true,
135
+ forceConsistentCasingInFileNames: true,
136
+ noUncheckedIndexedAccess: true,
137
+ resolveJsonModule: true,
138
+ types
139
+ },
140
+ include
141
+ },
142
+ null,
143
+ 2
144
+ ) + "\n";
145
+ }
146
+ function renderGitignore() {
147
+ return [
148
+ "node_modules",
149
+ "dist",
150
+ "build",
151
+ ".DS_Store",
152
+ "",
153
+ "# Generated by `sublime build:nav`",
154
+ "src/navigation/navigation.tsx",
155
+ "src/navigation/navigation.native.tsx",
156
+ "src/navigation/routes.d.ts",
157
+ "src/navigation/index.ts",
158
+ "",
159
+ "# Native build output",
160
+ "android",
161
+ "ios",
162
+ ""
163
+ ].join("\n");
164
+ }
165
+ function renderAppReadme(name, targets) {
166
+ const lines = [
167
+ `# ${name}`,
168
+ "",
169
+ "A [Sublime UI](https://sublime-ui.github.io/sublime-ui/) app \u2014 write the",
170
+ "non-UI parts once, run on mobile, web, and desktop.",
171
+ "",
172
+ "## Getting started",
173
+ "",
174
+ "```bash",
175
+ "npm install",
176
+ "npm run build:nav # compile navigation"
177
+ ];
178
+ if (has(targets, "web")) lines.push("npm run dev:web # web (Vite)");
179
+ if (has(targets, "mobile")) lines.push("npm run dev:mobile # Android (debug)");
180
+ if (has(targets, "desktop")) lines.push("npm run desktop:dev # Electron");
181
+ lines.push("```", "");
182
+ return lines.join("\n");
183
+ }
184
+
185
+ // src/lib/scaffold/templates/shared.ts
186
+ function renderTaskModel() {
187
+ return `import { Model, registerModel } from '@sublime-ui/framework';
188
+
189
+ /** A sample model. Replace with your own \u2014 see the docs on the Model layer. */
190
+ export class Task extends Model {
191
+ protected static resource = '/tasks';
192
+ declare id: number;
193
+ declare name: string;
194
+ declare done: boolean;
195
+ }
196
+ registerModel(Task);
197
+ `;
198
+ }
199
+ function renderModelsBarrel() {
200
+ return `export * from './Task.js';
201
+ `;
202
+ }
203
+ function renderThemeTokensJson() {
204
+ return JSON.stringify(
205
+ {
206
+ colors: { primary: "#4F46E5", background: "#FFFFFF", text: "#111827" },
207
+ spacing: { sm: 8, md: 16, lg: 24 },
208
+ radius: { md: 12 }
209
+ },
210
+ null,
211
+ 2
212
+ ) + "\n";
213
+ }
214
+ function renderThemeTokensTs() {
215
+ return `import tokensJson from './tokens.json' with { type: 'json' };
216
+ import type { SublimeTokens } from '@sublime-ui/library';
217
+
218
+ /** Typed app design tokens. Edit tokens.json; this stays a thin typed wrapper. */
219
+ export const tokens = tokensJson as unknown as SublimeTokens;
220
+ `;
221
+ }
222
+
223
+ // src/lib/scaffold/templates/web.ts
224
+ function renderWebTaskList() {
225
+ return `import { Screen, Stack } from '@sublime-ui/ui';
226
+ import { useNav } from '@sublime-ui/ui/navigation';
227
+ import { Task } from '../../models/Task';
228
+
229
+ export function TaskList() {
230
+ const tasks = Task.rxAll();
231
+ const nav = useNav();
232
+ return (
233
+ <Screen>
234
+ <Stack>
235
+ {tasks.map((t) => (
236
+ <button key={t.id} onClick={() => nav.turnTo('task', { id: t.id })}>
237
+ {t.name}
238
+ </button>
239
+ ))}
240
+ </Stack>
241
+ </Screen>
242
+ );
243
+ }
244
+ `;
245
+ }
246
+ function renderWebTaskDetail() {
247
+ return `import { Screen, Stack } from '@sublime-ui/ui';
248
+ import { useNav } from '@sublime-ui/ui/navigation';
249
+ import type { AppRoutes } from '../../navigation';
250
+ import { Task } from '../../models/Task';
251
+
252
+ export function TaskDetail() {
253
+ const nav = useNav<AppRoutes>();
254
+ const { id } = nav.params<'task'>();
255
+ const task = Task.rxFind(id);
256
+ return (
257
+ <Screen>
258
+ <Stack>
259
+ <h1>{task?.name ?? 'Loading\u2026'}</h1>
260
+ <button onClick={() => nav.turnBack()}>Back</button>
261
+ </Stack>
262
+ </Screen>
263
+ );
264
+ }
265
+ `;
266
+ }
267
+ function renderStorybookWeb() {
268
+ return `import { book, page } from '@sublime-ui/ui/navigation';
269
+ import { TaskList } from '../screens/web/TaskList';
270
+ import { TaskDetail } from '../screens/web/TaskDetail';
271
+
272
+ export default book({
273
+ format: 'sidebar', // web: 'sidebar' | 'stack' | 'tabs'
274
+ pages: {
275
+ tasks: page(TaskList, { title: 'Tasks', initial: true }),
276
+ task: page<{ id: number }>(TaskDetail, { title: 'Task' }),
277
+ },
278
+ });
279
+ `;
280
+ }
281
+ function renderWebScreensBarrel() {
282
+ return `export { TaskList } from '../screens/web/TaskList';
283
+ export { TaskDetail } from '../screens/web/TaskDetail';
284
+ `;
285
+ }
286
+ function renderWebIndexHtml(name) {
287
+ return `<!doctype html>
288
+ <html lang="en">
289
+ <head>
290
+ <meta charset="UTF-8" />
291
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
292
+ <title>${name}</title>
293
+ </head>
294
+ <body>
295
+ <div id="root"></div>
296
+ <script type="module" src="/web/main.tsx"></script>
297
+ </body>
298
+ </html>
299
+ `;
300
+ }
301
+ function renderWebMain() {
302
+ return `import React from 'react';
303
+ import { createRoot } from 'react-dom/client';
304
+ import { SublimeProvider } from '@sublime-ui/library';
305
+ import { Navigation } from '../src/navigation';
306
+ import { tokens } from '../src/theme/tokens';
307
+
308
+ function App() {
309
+ return (
310
+ <SublimeProvider tokens={tokens}>
311
+ <Navigation />
312
+ </SublimeProvider>
313
+ );
314
+ }
315
+
316
+ createRoot(document.getElementById('root')!).render(
317
+ <React.StrictMode>
318
+ <App />
319
+ </React.StrictMode>,
320
+ );
321
+ `;
322
+ }
323
+ function renderViteConfig() {
324
+ return `import { defineConfig } from 'vite';
325
+ import react from '@vitejs/plugin-react';
326
+
327
+ export default defineConfig({
328
+ plugins: [react()],
329
+ });
330
+ `;
331
+ }
332
+
333
+ // src/lib/scaffold/templates/mobile.ts
334
+ function renderMobileTaskList() {
335
+ return `import { Screen, Stack } from '@sublime-ui/ui';
336
+ import { useNav } from '@sublime-ui/ui/navigation';
337
+ import { Button } from 'react-native-paper';
338
+ import { Task } from '../../models/Task';
339
+
340
+ export function TaskList() {
341
+ const tasks = Task.rxAll();
342
+ const nav = useNav();
343
+ return (
344
+ <Screen>
345
+ <Stack>
346
+ {tasks.map((t) => (
347
+ <Button key={t.id} onPress={() => nav.turnTo('task', { id: t.id })}>
348
+ {t.name}
349
+ </Button>
350
+ ))}
351
+ </Stack>
352
+ </Screen>
353
+ );
354
+ }
355
+ `;
356
+ }
357
+ function renderMobileTaskDetail() {
358
+ return `import { Screen, Stack } from '@sublime-ui/ui';
359
+ import { useNav } from '@sublime-ui/ui/navigation';
360
+ import { Text, Button } from 'react-native-paper';
361
+ import type { AppRoutes } from '../../navigation';
362
+ import { Task } from '../../models/Task';
363
+
364
+ export function TaskDetail() {
365
+ const nav = useNav<AppRoutes>();
366
+ const { id } = nav.params<'task'>();
367
+ const task = Task.rxFind(id);
368
+ return (
369
+ <Screen>
370
+ <Stack>
371
+ <Text variant="headlineMedium">{task?.name ?? 'Loading\u2026'}</Text>
372
+ <Button onPress={() => nav.turnBack()}>Back</Button>
373
+ </Stack>
374
+ </Screen>
375
+ );
376
+ }
377
+ `;
378
+ }
379
+ function renderStorybookNative() {
380
+ return `import { book, page } from '@sublime-ui/ui/navigation';
381
+ import { TaskList } from '../screens/mobile/TaskList.native';
382
+ import { TaskDetail } from '../screens/mobile/TaskDetail.native';
383
+
384
+ export default book({
385
+ format: 'bottomNav', // mobile: 'drawer' | 'stack' | 'bottomNav' (<= 5 pages)
386
+ pages: {
387
+ tasks: page(TaskList, { title: 'Tasks', icon: 'format-list-bulleted', initial: true }),
388
+ task: page<{ id: number }>(TaskDetail, { title: 'Task', icon: 'note' }),
389
+ },
390
+ });
391
+ `;
392
+ }
393
+ function renderMobileScreensBarrel() {
394
+ return `export { TaskList } from '../screens/mobile/TaskList.native';
395
+ export { TaskDetail } from '../screens/mobile/TaskDetail.native';
396
+ `;
397
+ }
398
+ function renderMobileEntry() {
399
+ return `import { AppRegistry } from 'react-native';
400
+ import { App } from './App.native';
401
+ import { name as appName } from '../app.json';
402
+
403
+ AppRegistry.registerComponent(appName, () => App);
404
+ `;
405
+ }
406
+ function renderMobileApp() {
407
+ return `import { SublimeProvider } from '@sublime-ui/library';
408
+ import { Navigation } from '../src/navigation';
409
+ import { tokens } from '../src/theme/tokens';
410
+
411
+ export function App() {
412
+ return (
413
+ <SublimeProvider tokens={tokens}>
414
+ <Navigation />
415
+ </SublimeProvider>
416
+ );
417
+ }
418
+ `;
419
+ }
420
+
421
+ // src/lib/desktop/templates.ts
422
+ function renderForgeConfig() {
423
+ return `import type { ForgeConfig } from '@electron-forge/shared-types';
424
+ import { MakerSquirrel } from '@electron-forge/maker-squirrel';
425
+ import { MakerZIP } from '@electron-forge/maker-zip';
426
+ import { MakerDeb } from '@electron-forge/maker-deb';
427
+ import { MakerRpm } from '@electron-forge/maker-rpm';
428
+ import { AutoUnpackNativesPlugin } from '@electron-forge/plugin-auto-unpack-natives';
429
+ import { WebpackPlugin } from '@electron-forge/plugin-webpack';
430
+ import { FusesPlugin } from '@electron-forge/plugin-fuses';
431
+ import { FuseV1Options, FuseVersion } from '@electron/fuses';
432
+
433
+ import { mainConfig } from './webpack.main.config';
434
+ import { rendererConfig } from './webpack.renderer.config';
435
+
436
+ const config: ForgeConfig = {
437
+ packagerConfig: {
438
+ asar: true,
439
+ },
440
+ rebuildConfig: {},
441
+ makers: [
442
+ new MakerSquirrel({}),
443
+ new MakerZIP({}, ['darwin']),
444
+ new MakerRpm({}),
445
+ new MakerDeb({}),
446
+ ],
447
+ plugins: [
448
+ new AutoUnpackNativesPlugin({}),
449
+ new WebpackPlugin({
450
+ mainConfig,
451
+ renderer: {
452
+ config: rendererConfig,
453
+ entryPoints: [
454
+ {
455
+ name: 'main_window',
456
+ html: './src/renderer/index.html',
457
+ js: './src/renderer/index.ts',
458
+ preload: {
459
+ js: './src/main/preload.ts',
460
+ },
461
+ },
462
+ ],
463
+ },
464
+ }),
465
+ // Harden the packaged app at make time.
466
+ new FusesPlugin({
467
+ version: FuseVersion.V1,
468
+ [FuseV1Options.RunAsNode]: false,
469
+ [FuseV1Options.EnableCookieEncryption]: true,
470
+ [FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
471
+ [FuseV1Options.EnableNodeCliInspectArguments]: false,
472
+ [FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
473
+ [FuseV1Options.OnlyLoadAppFromAsar]: true,
474
+ }),
475
+ ],
476
+ };
477
+
478
+ export default config;
479
+ `;
480
+ }
481
+ function renderWebpackMain() {
482
+ return `import type { Configuration } from 'webpack';
483
+ import { rules } from './webpack.rules';
484
+
485
+ export const mainConfig: Configuration = {
486
+ entry: './src/main/main.ts',
487
+ module: {
488
+ rules,
489
+ },
490
+ resolve: {
491
+ extensions: ['.js', '.ts', '.jsx', '.tsx', '.css', '.json'],
492
+ },
493
+ };
494
+ `;
495
+ }
496
+ function renderWebpackRenderer() {
497
+ return `import type { Configuration } from 'webpack';
498
+ import { rules } from './webpack.rules';
499
+
500
+ // Entry points wire \`main_window\` and inject \`./src/main/preload.ts\` as its
501
+ // preload, which the Webpack plugin surfaces as MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY.
502
+ export const rendererEntryPoints = [
503
+ {
504
+ name: 'main_window',
505
+ html: './src/renderer/index.html',
506
+ js: './src/renderer/index.ts',
507
+ preload: './src/main/preload.ts',
508
+ },
509
+ ];
510
+
511
+ export const rendererConfig: Configuration = {
512
+ module: {
513
+ rules,
514
+ },
515
+ resolve: {
516
+ extensions: ['.js', '.ts', '.jsx', '.tsx', '.css'],
517
+ },
518
+ };
519
+ `;
520
+ }
521
+ function renderMainTs() {
522
+ return `import { app, ipcMain } from 'electron';
523
+ import { startDesktop } from '@sublime-ui/desktop';
524
+
525
+ // These constants are injected by the Electron Forge Webpack plugin.
526
+ declare const MAIN_WINDOW_WEBPACK_ENTRY: string;
527
+ declare const MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY: string;
528
+
529
+ startDesktop({
530
+ app,
531
+ ipcMain,
532
+ entry: MAIN_WINDOW_WEBPACK_ENTRY,
533
+ preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY,
534
+ isDev: !app.isPackaged,
535
+ });
536
+ `;
537
+ }
538
+ function renderPreloadTs() {
539
+ return `import { contextBridge, ipcRenderer } from 'electron';
540
+ import { exposeNativeBridge } from '@sublime-ui/desktop';
541
+
542
+ // Exposes exactly one function (\`window.sublimeNative.invoke\`) over the single
543
+ // \`native:invoke\` channel \u2014 nothing else crosses the isolation boundary.
544
+ exposeNativeBridge(contextBridge, ipcRenderer);
545
+ `;
546
+ }
547
+
548
+ // src/lib/scaffold/templates/desktop.ts
549
+ function renderGreeterService() {
550
+ return `import { defineNative } from '@sublime-ui/desktop';
551
+
552
+ /** A sample native service. Runs in the main process; the renderer calls it via useNative. */
553
+ export const greeter = defineNative('greeter', {
554
+ async hello(name: string): Promise<string> {
555
+ return \`Hello from the desktop main process, \${name}!\`;
556
+ },
557
+ });
558
+ `;
559
+ }
560
+ function renderDesktopPackageJson(name) {
561
+ const pkg = {
562
+ name: `${name}-desktop`,
563
+ version: "0.0.0",
564
+ private: true,
565
+ main: ".webpack/main",
566
+ scripts: {
567
+ start: "electron-forge start",
568
+ package: "electron-forge package",
569
+ make: "electron-forge make"
570
+ },
571
+ devDependencies: {
572
+ "@electron-forge/cli": "^7.5.0",
573
+ "@electron-forge/maker-deb": "^7.5.0",
574
+ "@electron-forge/maker-rpm": "^7.5.0",
575
+ "@electron-forge/maker-squirrel": "^7.5.0",
576
+ "@electron-forge/maker-zip": "^7.5.0",
577
+ "@electron-forge/plugin-auto-unpack-natives": "^7.5.0",
578
+ "@electron-forge/plugin-fuses": "^7.5.0",
579
+ "@electron-forge/plugin-webpack": "^7.5.0",
580
+ "@electron/fuses": "^1.8.0",
581
+ "@vercel/webpack-asset-relocator-loader": "^1.7.3",
582
+ "css-loader": "^7.1.2",
583
+ electron: PEER_VERSIONS["electron"],
584
+ "node-loader": "^2.0.0",
585
+ "style-loader": "^4.0.0",
586
+ "ts-loader": "^9.5.1",
587
+ typescript: PEER_VERSIONS["typescript"]
588
+ },
589
+ dependencies: {
590
+ "@sublime-ui/desktop": SUBLIME_VERSIONS.desktop
591
+ }
592
+ };
593
+ return JSON.stringify(pkg, null, 2) + "\n";
594
+ }
595
+ function renderWebpackRules() {
596
+ return `import type { ModuleOptions } from 'webpack';
597
+
598
+ export const rules: Required<ModuleOptions>['rules'] = [
599
+ { test: /native_modules[/\\\\].+\\.node$/, use: 'node-loader' },
600
+ {
601
+ test: /[/\\\\]node_modules[/\\\\].+\\.(m?js|node)$/,
602
+ parser: { amd: false },
603
+ use: {
604
+ loader: '@vercel/webpack-asset-relocator-loader',
605
+ options: { outputAssetBase: 'native_modules' },
606
+ },
607
+ },
608
+ {
609
+ test: /\\.tsx?$/,
610
+ exclude: /(node_modules|\\.webpack)/,
611
+ use: { loader: 'ts-loader', options: { transpileOnly: true } },
612
+ },
613
+ { test: /\\.css$/, use: ['style-loader', 'css-loader'] },
614
+ ];
615
+ `;
616
+ }
617
+ function renderRendererIndexHtml(name) {
618
+ return `<!doctype html>
619
+ <html lang="en">
620
+ <head>
621
+ <meta charset="UTF-8" />
622
+ <title>${name}</title>
623
+ </head>
624
+ <body>
625
+ <div id="root"></div>
626
+ </body>
627
+ </html>
628
+ `;
629
+ }
630
+ function renderRendererIndexTs() {
631
+ return `import '../../../web/main.tsx';
632
+ `;
633
+ }
634
+
635
+ // src/lib/scaffold/plan.ts
636
+ var has2 = (t, x) => t.includes(x);
637
+ function buildScaffoldPlan(opts) {
638
+ const { name, targets } = opts;
639
+ const files = [
640
+ { path: "package.json", contents: renderAppPackageJson(name, targets) },
641
+ { path: "sublime.config.json", contents: renderSublimeConfig(targets) },
642
+ { path: "tsconfig.json", contents: renderTsconfig(targets) },
643
+ { path: ".gitignore", contents: renderGitignore() },
644
+ { path: "README.md", contents: renderAppReadme(name, targets) },
645
+ { path: "src/models/Task.ts", contents: renderTaskModel() },
646
+ { path: "src/models/index.ts", contents: renderModelsBarrel() },
647
+ { path: "src/theme/tokens.json", contents: renderThemeTokensJson() },
648
+ { path: "src/theme/tokens.ts", contents: renderThemeTokensTs() }
649
+ ];
650
+ if (has2(targets, "web") || has2(targets, "desktop")) {
651
+ files.push(
652
+ { path: "src/screens/web/TaskList.tsx", contents: renderWebTaskList() },
653
+ { path: "src/screens/web/TaskDetail.tsx", contents: renderWebTaskDetail() },
654
+ { path: "src/navigation/screens.ts", contents: renderWebScreensBarrel() },
655
+ { path: "src/navigation/storybook.web.ts", contents: renderStorybookWeb() },
656
+ { path: "web/index.html", contents: renderWebIndexHtml(name) },
657
+ { path: "web/main.tsx", contents: renderWebMain() },
658
+ { path: "vite.config.ts", contents: renderViteConfig() }
659
+ );
660
+ }
661
+ if (has2(targets, "mobile")) {
662
+ files.push(
663
+ { path: "src/screens/mobile/TaskList.native.tsx", contents: renderMobileTaskList() },
664
+ { path: "src/screens/mobile/TaskDetail.native.tsx", contents: renderMobileTaskDetail() },
665
+ { path: "src/navigation/screens.native.ts", contents: renderMobileScreensBarrel() },
666
+ { path: "src/navigation/storybook.native.ts", contents: renderStorybookNative() },
667
+ { path: "mobile/index.js", contents: renderMobileEntry() },
668
+ { path: "mobile/App.native.tsx", contents: renderMobileApp() },
669
+ { path: "app.json", contents: JSON.stringify({ name }, null, 2) + "\n" }
670
+ );
671
+ }
672
+ if (has2(targets, "desktop")) {
673
+ files.push(
674
+ { path: "src/native/greeter.service.ts", contents: renderGreeterService() },
675
+ { path: "desktop/package.json", contents: renderDesktopPackageJson(name) },
676
+ { path: "desktop/forge.config.ts", contents: renderForgeConfig() },
677
+ { path: "desktop/webpack.main.config.ts", contents: renderWebpackMain() },
678
+ { path: "desktop/webpack.renderer.config.ts", contents: renderWebpackRenderer() },
679
+ { path: "desktop/webpack.rules.ts", contents: renderWebpackRules() },
680
+ { path: "desktop/src/main/main.ts", contents: renderMainTs() },
681
+ { path: "desktop/src/main/preload.ts", contents: renderPreloadTs() },
682
+ { path: "desktop/src/renderer/index.html", contents: renderRendererIndexHtml(name) },
683
+ { path: "desktop/src/renderer/index.ts", contents: renderRendererIndexTs() }
684
+ );
685
+ }
686
+ return files;
687
+ }
688
+
689
+ // src/lib/generators/write.ts
690
+ import { existsSync, mkdirSync, writeFileSync } from "fs";
691
+ import { dirname } from "path";
692
+ var FileExistsError = class _FileExistsError extends Error {
693
+ path;
694
+ constructor(path) {
695
+ super(`File already exists: ${path} (use --force to overwrite)`);
696
+ this.name = "FileExistsError";
697
+ this.path = path;
698
+ Object.setPrototypeOf(this, _FileExistsError.prototype);
699
+ }
700
+ };
701
+ function safeWrite(path, content, force) {
702
+ if (existsSync(path) && !force) throw new FileExistsError(path);
703
+ mkdirSync(dirname(path), { recursive: true });
704
+ writeFileSync(path, content);
705
+ }
706
+
707
+ // src/util/exec.ts
708
+ import { execa } from "execa";
709
+ async function run(file, args, opts = {}) {
710
+ const result = await execa(file, args, {
711
+ ...opts.cwd === void 0 ? {} : { cwd: opts.cwd },
712
+ env: { ...process.env, ...opts.env },
713
+ reject: false,
714
+ all: false
715
+ });
716
+ return {
717
+ stdout: result.stdout ?? "",
718
+ stderr: result.stderr ?? "",
719
+ exitCode: result.exitCode ?? 1
720
+ };
721
+ }
722
+ async function runInherit(file, args, opts = {}) {
723
+ const result = await execa(file, args, {
724
+ ...opts.cwd === void 0 ? {} : { cwd: opts.cwd },
725
+ env: { ...process.env, ...opts.env },
726
+ stdio: "inherit",
727
+ reject: false
728
+ });
729
+ return result.exitCode ?? 1;
730
+ }
731
+
732
+ // src/lib/scaffold/init.ts
733
+ var defaultRunner = (cmd, args, cwd) => runInherit(cmd, args, { cwd });
734
+ function isValidNpmName(s) {
735
+ return /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(s) && s.length <= 214;
736
+ }
737
+ function isEmptyDir(dir) {
738
+ if (!existsSync2(dir)) return true;
739
+ return readdirSync(dir).length === 0;
740
+ }
741
+ async function initApp(opts) {
742
+ const runner = opts.runner ?? defaultRunner;
743
+ let name = opts.name;
744
+ let targets = opts.targets;
745
+ if ((name === void 0 || targets === void 0) && !opts.yes && opts.prompt) {
746
+ const answered = await opts.prompt({ dir: opts.dir });
747
+ name ??= answered.name;
748
+ targets ??= answered.targets;
749
+ }
750
+ name ??= basename(opts.dir);
751
+ targets ??= ["web", "mobile", "desktop"];
752
+ if (!isValidNpmName(name)) {
753
+ log.error(`Invalid app name "${name}". Use a valid npm package name (lowercase, url-safe).`);
754
+ return 1;
755
+ }
756
+ if (targets.length === 0) {
757
+ log.error("Select at least one target (web, mobile, desktop).");
758
+ return 1;
759
+ }
760
+ if (targets.includes("desktop") && !targets.includes("web")) {
761
+ log.error('The desktop target renders the web UI \u2014 enable "web" alongside "desktop".');
762
+ return 1;
763
+ }
764
+ const force = opts.force ?? false;
765
+ if (!isEmptyDir(opts.dir) && !force) {
766
+ log.error(`Target directory ${opts.dir} is not empty (use --force to scaffold into it).`);
767
+ return 1;
768
+ }
769
+ const plan = buildScaffoldPlan({ name, targets });
770
+ try {
771
+ for (const file of plan) safeWrite(join(opts.dir, file.path), file.contents, force);
772
+ } catch (err) {
773
+ if (err instanceof FileExistsError) {
774
+ log.error(err.message);
775
+ return 1;
776
+ }
777
+ throw err;
778
+ }
779
+ log.success(`Scaffolded ${name} (${targets.join(", ")}) in ${opts.dir}`);
780
+ if (opts.git ?? true) {
781
+ await runner("git", ["init", "-q"], opts.dir);
782
+ }
783
+ if (opts.install ?? true) {
784
+ log.step("Installing dependencies\u2026");
785
+ const installCode = await runner("npm", ["install", "--legacy-peer-deps"], opts.dir);
786
+ if (installCode !== 0) {
787
+ log.warn("npm install failed \u2014 run it manually.");
788
+ return 0;
789
+ }
790
+ log.step("Compiling navigation (build:nav)\u2026");
791
+ await runner("npx", ["sublime", "build:nav"], opts.dir);
792
+ }
793
+ log.info("");
794
+ log.info(`Next: cd ${basename(opts.dir)}`);
795
+ if (!(opts.install ?? true)) log.info(" npm install && npm run build:nav");
796
+ if (targets.includes("web")) log.info(" npm run dev:web");
797
+ if (targets.includes("mobile")) log.info(" npm run dev:mobile");
798
+ if (targets.includes("desktop")) log.info(" npm run desktop:dev");
799
+ return 0;
800
+ }
801
+
802
+ export {
803
+ run,
804
+ runInherit,
805
+ log,
806
+ FileExistsError,
807
+ safeWrite,
808
+ isValidNpmName,
809
+ initApp
810
+ };