@holo-js/cli 0.1.6 → 0.1.8

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.
@@ -30,7 +30,8 @@ import {
30
30
  SUPPORTED_CONFIG_EXTENSIONS,
31
31
  importProjectModule,
32
32
  isRecord,
33
- makeProjectRelativePath
33
+ makeProjectRelativePath,
34
+ normalizeScaffoldOptionalPackages
34
35
  } from "./chunk-R6BWRY3E.mjs";
35
36
 
36
37
  // src/templates.ts
@@ -253,15 +254,1071 @@ function resolveArtifactPath(root, subdir, directory, fileName) {
253
254
  return resolve(root, subdir, directory, fileName);
254
255
  }
255
256
 
257
+ // src/project/scaffold/framework-renderers.ts
258
+ var HOSTED_AUTH_PROVIDERS = {
259
+ workos: {
260
+ provider: "workos",
261
+ packageName: "@holo-js/auth-workos",
262
+ loginFunction: "loginWithWorkos",
263
+ registerFunction: "registerWithWorkos",
264
+ callbackFunction: "completeWorkosAuth",
265
+ logoutFunction: "logoutWithWorkos"
266
+ },
267
+ clerk: {
268
+ provider: "clerk",
269
+ packageName: "@holo-js/auth-clerk",
270
+ loginFunction: "loginWithClerk",
271
+ registerFunction: "registerWithClerk",
272
+ callbackFunction: "completeClerkAuth",
273
+ logoutFunction: "logoutWithClerk"
274
+ }
275
+ };
276
+ function getRequestedHostedAuthProviders(features) {
277
+ return [
278
+ ...features.workos ? ["workos"] : [],
279
+ ...features.clerk ? ["clerk"] : []
280
+ ];
281
+ }
282
+ function renderNuxtAppVue(projectName) {
283
+ return [
284
+ "<template>",
285
+ ' <main class="shell">',
286
+ " <h1>{{ appName }}</h1>",
287
+ " <p>Nuxt renders the UI. Holo owns the backend runtime and canonical server directories.</p>",
288
+ " </main>",
289
+ "</template>",
290
+ "",
291
+ '<script setup lang="ts">',
292
+ `const appName = ${JSON.stringify(projectName)}`,
293
+ "</script>",
294
+ "",
295
+ "<style scoped>",
296
+ ".shell {",
297
+ " min-height: 100vh;",
298
+ " display: grid;",
299
+ " place-content: center;",
300
+ " gap: 1rem;",
301
+ " padding: 3rem;",
302
+ " font-family: sans-serif;",
303
+ "}",
304
+ "h1 {",
305
+ " margin: 0;",
306
+ " font-size: clamp(2.5rem, 6vw, 4rem);",
307
+ "}",
308
+ "p {",
309
+ " margin: 0;",
310
+ " max-width: 40rem;",
311
+ " line-height: 1.6;",
312
+ "}",
313
+ "</style>",
314
+ ""
315
+ ].join("\n");
316
+ }
317
+ function renderNuxtConfig() {
318
+ return [
319
+ "export default defineNuxtConfig({",
320
+ " modules: ['@holo-js/adapter-nuxt'],",
321
+ " sourcemap: {",
322
+ " client: false,",
323
+ " server: false,",
324
+ " },",
325
+ " vite: {",
326
+ " build: {",
327
+ " rollupOptions: {",
328
+ " onwarn(warning, defaultHandler) {",
329
+ " if (",
330
+ " warning.message.includes('nuxt:module-preload-polyfill')",
331
+ " && warning.message.includes('didn\\'t generate a sourcemap')",
332
+ " ) {",
333
+ " return",
334
+ " }",
335
+ "",
336
+ " defaultHandler(warning)",
337
+ " },",
338
+ " },",
339
+ " },",
340
+ " },",
341
+ "})",
342
+ ""
343
+ ].join("\n");
344
+ }
345
+ function renderNuxtCurrentAuthRoute() {
346
+ return [
347
+ "import auth, { check, isAuthError, provider, user } from '@holo-js/auth'",
348
+ "import { setResponseStatus } from 'h3'",
349
+ "",
350
+ "export default defineEventHandler(async (event) => {",
351
+ " const query = getQuery(event)",
352
+ " const guard = typeof query.guard === 'string' ? query.guard : undefined",
353
+ " try {",
354
+ " const guardAuth = guard ? auth.guard(guard) : undefined",
355
+ "",
356
+ " return {",
357
+ " authenticated: guardAuth ? await guardAuth.check() : await check(),",
358
+ " guard: guard ?? 'web',",
359
+ " provider: guardAuth ? await guardAuth.provider() : await provider(),",
360
+ " user: guardAuth ? await guardAuth.user() : await user(),",
361
+ " }",
362
+ " } catch (error) {",
363
+ " if (isAuthError(error) && error.code === 'guard_not_configured') {",
364
+ " setResponseStatus(event, 400)",
365
+ "",
366
+ " return {",
367
+ " authenticated: false,",
368
+ " guard: guard ?? 'web',",
369
+ " provider: null,",
370
+ " user: null,",
371
+ " }",
372
+ " }",
373
+ "",
374
+ " throw error",
375
+ " }",
376
+ "})",
377
+ ""
378
+ ].join("\n");
379
+ }
380
+ function renderNuxtHostedAuthLoginRoute(spec) {
381
+ return [
382
+ `import { ${spec.loginFunction} } from '${spec.packageName}'`,
383
+ "",
384
+ "export default defineEventHandler(async (event) => {",
385
+ ` return await ${spec.loginFunction}(event)`,
386
+ "})",
387
+ ""
388
+ ].join("\n");
389
+ }
390
+ function renderNuxtHostedAuthRegisterRoute(spec) {
391
+ return [
392
+ `import { ${spec.registerFunction} } from '${spec.packageName}'`,
393
+ "",
394
+ "export default defineEventHandler(async (event) => {",
395
+ ` return await ${spec.registerFunction}(event)`,
396
+ "})",
397
+ ""
398
+ ].join("\n");
399
+ }
400
+ function renderNuxtHostedAuthCallbackRoute(spec) {
401
+ return [
402
+ `import { ${spec.callbackFunction} } from '${spec.packageName}'`,
403
+ "import { sendRedirect } from 'h3'",
404
+ "",
405
+ "export default defineEventHandler(async (event) => {",
406
+ ` const { error } = await ${spec.callbackFunction}(event)`,
407
+ " if (error) {",
408
+ " return await sendRedirect(event, `/login?error=${encodeURIComponent(error.code)}`, 303)",
409
+ " }",
410
+ "",
411
+ " return await sendRedirect(event, '/', 303)",
412
+ "})",
413
+ ""
414
+ ].join("\n");
415
+ }
416
+ function renderNuxtHostedAuthLogoutRoute(spec) {
417
+ return [
418
+ "import { provider } from '@holo-js/auth'",
419
+ `import { ${spec.logoutFunction} } from '${spec.packageName}'`,
420
+ "import { createError, sendRedirect } from 'h3'",
421
+ "",
422
+ "export default defineEventHandler(async (event) => {",
423
+ " let currentProvider: string | null",
424
+ " try {",
425
+ " currentProvider = await provider()",
426
+ " } catch {",
427
+ " return await sendRedirect(event, '/', 303)",
428
+ " }",
429
+ "",
430
+ ` if (currentProvider !== '${spec.provider}') {`,
431
+ " return await sendRedirect(event, '/', 303)",
432
+ " }",
433
+ "",
434
+ ` const { data, error } = await ${spec.logoutFunction}(event)`,
435
+ " if (error) {",
436
+ " throw createError({",
437
+ " statusCode: error.status,",
438
+ " statusMessage: error.message,",
439
+ " })",
440
+ " }",
441
+ "",
442
+ " return await sendRedirect(event, data.url, 303)",
443
+ "})",
444
+ ""
445
+ ].join("\n");
446
+ }
447
+ function renderNuxtHostedAuthRouteFiles(provider) {
448
+ const spec = HOSTED_AUTH_PROVIDERS[provider];
449
+ return [
450
+ { path: `server/api/auth/${provider}/login.get.ts`, contents: renderNuxtHostedAuthLoginRoute(spec) },
451
+ { path: `server/api/auth/${provider}/register.get.ts`, contents: renderNuxtHostedAuthRegisterRoute(spec) },
452
+ { path: `server/api/auth/${provider}/callback.get.ts`, contents: renderNuxtHostedAuthCallbackRoute(spec) },
453
+ { path: `server/api/auth/${provider}/logout.post.ts`, contents: renderNuxtHostedAuthLogoutRoute(spec) }
454
+ ];
455
+ }
456
+ function renderNextConfig() {
457
+ return [
458
+ "import type { NextConfig } from 'next'",
459
+ "import { withHolo } from '@holo-js/adapter-next/config'",
460
+ "",
461
+ "const nextConfig: NextConfig = withHolo({",
462
+ " /* config options here */",
463
+ "})",
464
+ "",
465
+ "export default nextConfig",
466
+ ""
467
+ ].join("\n");
468
+ }
469
+ function renderNextLayout(projectName) {
470
+ return [
471
+ "import type { ReactNode } from 'react'",
472
+ "",
473
+ "export const metadata = {",
474
+ ` title: ${JSON.stringify(projectName)},`,
475
+ " description: 'Holo on Next.js',",
476
+ "}",
477
+ "",
478
+ "export default function RootLayout({ children }: { children: ReactNode }) {",
479
+ " return (",
480
+ ' <html lang="en">',
481
+ " <body>{children}</body>",
482
+ " </html>",
483
+ " )",
484
+ "}",
485
+ ""
486
+ ].join("\n");
487
+ }
488
+ function escapeHtml(value) {
489
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;").replaceAll("{", "&#123;").replaceAll("}", "&#125;");
490
+ }
491
+ function renderNextPage(projectName) {
492
+ const escapedProjectName = escapeHtml(projectName);
493
+ return [
494
+ "export default function HomePage() {",
495
+ " return (",
496
+ " <main style={{ padding: '3rem', fontFamily: 'sans-serif' }}>",
497
+ ` <h1>${escapedProjectName}</h1>`,
498
+ " <p>Next.js handles rendering. Holo powers the backend runtime and discovered server resources.</p>",
499
+ " </main>",
500
+ " )",
501
+ "}",
502
+ ""
503
+ ].join("\n");
504
+ }
505
+ function renderNextEnvDts() {
506
+ return [
507
+ '/// <reference types="next" />',
508
+ '/// <reference types="next/image-types/global" />',
509
+ "",
510
+ "// Generated by Holo. Do not edit.",
511
+ ""
512
+ ].join("\n");
513
+ }
514
+ function renderNextRouteBridge(modulePath, methods) {
515
+ return [
516
+ `export { ${methods.join(", ")} } from '${modulePath}'`,
517
+ ""
518
+ ].join("\n");
519
+ }
520
+ function renderNextHoloHelper() {
521
+ return [
522
+ "import { dirname, resolve } from 'node:path'",
523
+ "import { fileURLToPath } from 'node:url'",
524
+ "import { createNextHoloHelpers } from '@holo-js/adapter-next/runtime'",
525
+ "",
526
+ "const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../..')",
527
+ "",
528
+ "export const holo = createNextHoloHelpers({ projectRoot })",
529
+ ""
530
+ ].join("\n");
531
+ }
532
+ function renderNextCurrentAuthRoute() {
533
+ return renderNextRouteBridge("../../../../.holo-js/generated/next/auth-user-route", ["GET"]);
534
+ }
535
+ function renderNextGeneratedCurrentAuthRoute() {
536
+ return [
537
+ "import auth, { check, isAuthError, provider, user } from '@holo-js/auth'",
538
+ "import { holo } from './holo'",
539
+ "",
540
+ "export async function GET(request: Request) {",
541
+ " await holo.getApp()",
542
+ "",
543
+ " const guard = new URL(request.url).searchParams.get('guard') ?? undefined",
544
+ " try {",
545
+ " const guardAuth = guard ? auth.guard(guard) : undefined",
546
+ "",
547
+ " return Response.json({",
548
+ " authenticated: guardAuth ? await guardAuth.check() : await check(),",
549
+ " guard: guard ?? 'web',",
550
+ " provider: guardAuth ? await guardAuth.provider() : await provider(),",
551
+ " user: guardAuth ? await guardAuth.user() : await user(),",
552
+ " })",
553
+ " } catch (error) {",
554
+ " if (isAuthError(error) && error.code === 'guard_not_configured') {",
555
+ " return Response.json({",
556
+ " authenticated: false,",
557
+ " guard: guard ?? 'web',",
558
+ " provider: null,",
559
+ " user: null,",
560
+ " }, { status: 400 })",
561
+ " }",
562
+ "",
563
+ " throw error",
564
+ " }",
565
+ "}",
566
+ ""
567
+ ].join("\n");
568
+ }
569
+ function renderNextHostedAuthLoginRoute(spec) {
570
+ return renderNextRouteBridge(`../../../../../.holo-js/generated/next/auth-${spec.provider}-login-route`, ["GET"]);
571
+ }
572
+ function renderNextGeneratedHostedAuthLoginRoute(spec) {
573
+ return [
574
+ `import { ${spec.loginFunction} } from '${spec.packageName}'`,
575
+ "import { holo } from './holo'",
576
+ "",
577
+ "export async function GET(request: Request) {",
578
+ " await holo.getApp()",
579
+ "",
580
+ ` return await ${spec.loginFunction}(request)`,
581
+ "}",
582
+ ""
583
+ ].join("\n");
584
+ }
585
+ function renderNextHostedAuthRegisterRoute(spec) {
586
+ return renderNextRouteBridge(`../../../../../.holo-js/generated/next/auth-${spec.provider}-register-route`, ["GET"]);
587
+ }
588
+ function renderNextGeneratedHostedAuthRegisterRoute(spec) {
589
+ return [
590
+ `import { ${spec.registerFunction} } from '${spec.packageName}'`,
591
+ "import { holo } from './holo'",
592
+ "",
593
+ "export async function GET(request: Request) {",
594
+ " await holo.getApp()",
595
+ "",
596
+ ` return await ${spec.registerFunction}(request)`,
597
+ "}",
598
+ ""
599
+ ].join("\n");
600
+ }
601
+ function renderNextHostedAuthCallbackRoute(spec) {
602
+ return renderNextRouteBridge(`../../../../../.holo-js/generated/next/auth-${spec.provider}-callback-route`, ["GET"]);
603
+ }
604
+ function renderNextGeneratedHostedAuthCallbackRoute(spec) {
605
+ return [
606
+ `import { ${spec.callbackFunction} } from '${spec.packageName}'`,
607
+ "import { holo } from './holo'",
608
+ "",
609
+ "export async function GET(request: Request) {",
610
+ " await holo.getApp()",
611
+ "",
612
+ ` const { error } = await ${spec.callbackFunction}(request)`,
613
+ " if (error) {",
614
+ " return Response.redirect(new URL(`/login?error=${encodeURIComponent(error.code)}`, request.url))",
615
+ " }",
616
+ "",
617
+ " return Response.redirect(new URL('/', request.url))",
618
+ "}",
619
+ ""
620
+ ].join("\n");
621
+ }
622
+ function renderNextHostedAuthLogoutRoute(spec) {
623
+ return renderNextRouteBridge(`../../../../../.holo-js/generated/next/auth-${spec.provider}-logout-route`, ["POST"]);
624
+ }
625
+ function renderNextGeneratedHostedAuthLogoutRoute(spec) {
626
+ return [
627
+ "import { provider } from '@holo-js/auth'",
628
+ `import { ${spec.logoutFunction} } from '${spec.packageName}'`,
629
+ "import { holo } from './holo'",
630
+ "",
631
+ "export async function POST(request: Request) {",
632
+ " await holo.getApp()",
633
+ "",
634
+ " let currentProvider: string | null",
635
+ " try {",
636
+ " currentProvider = await provider()",
637
+ " } catch {",
638
+ " return Response.redirect(new URL('/', request.url), 303)",
639
+ " }",
640
+ "",
641
+ ` if (currentProvider !== '${spec.provider}') {`,
642
+ " return Response.redirect(new URL('/', request.url), 303)",
643
+ " }",
644
+ "",
645
+ ` const { data, error } = await ${spec.logoutFunction}(request)`,
646
+ " if (error) {",
647
+ " return Response.json({ data, error }, { status: error.status })",
648
+ " }",
649
+ "",
650
+ " return Response.redirect(data.url, 303)",
651
+ "}",
652
+ ""
653
+ ].join("\n");
654
+ }
655
+ function renderNextHostedAuthRouteFiles(provider) {
656
+ const spec = HOSTED_AUTH_PROVIDERS[provider];
657
+ return [
658
+ { path: `app/api/auth/${provider}/login/route.ts`, contents: renderNextHostedAuthLoginRoute(spec) },
659
+ { path: `app/api/auth/${provider}/register/route.ts`, contents: renderNextHostedAuthRegisterRoute(spec) },
660
+ { path: `app/api/auth/${provider}/callback/route.ts`, contents: renderNextHostedAuthCallbackRoute(spec) },
661
+ { path: `app/api/auth/${provider}/logout/route.ts`, contents: renderNextHostedAuthLogoutRoute(spec) }
662
+ ];
663
+ }
664
+ function renderNextStorageRoute() {
665
+ return renderNextRouteBridge("../../../.holo-js/generated/next/storage-route", ["GET"]);
666
+ }
667
+ function renderNextGeneratedStorageRoute() {
668
+ return [
669
+ "import { createPublicStorageResponse } from '@holo-js/storage'",
670
+ "import { holo } from './holo'",
671
+ "",
672
+ "export async function GET(request: Request) {",
673
+ " const app = await holo.getApp()",
674
+ " return createPublicStorageResponse(app.projectRoot, app.config.storage, request)",
675
+ "}",
676
+ ""
677
+ ].join("\n");
678
+ }
679
+ function renderNextBroadcastAuthRoute() {
680
+ return renderNextRouteBridge("../../../.holo-js/generated/next/broadcast-auth-route", ["POST"]);
681
+ }
682
+ function renderNextGeneratedBroadcastAuthRoute() {
683
+ return [
684
+ "import { renderBroadcastAuthResponse } from '@holo-js/broadcast/auth'",
685
+ "import { holo } from './holo'",
686
+ "",
687
+ "export async function POST(request: Request) {",
688
+ " const app = await holo.getApp()",
689
+ " const auth = await holo.getAuth()",
690
+ "",
691
+ " return await renderBroadcastAuthResponse(request, {",
692
+ " resolveUser: async () => await auth?.user(),",
693
+ " channelAuth: {",
694
+ " registry: {",
695
+ " projectRoot: app.projectRoot,",
696
+ " channels: app.registry?.channels ?? [],",
697
+ " },",
698
+ " },",
699
+ " })",
700
+ "}",
701
+ ""
702
+ ].join("\n");
703
+ }
704
+ function renderNextManagedRouteFiles(options = {}) {
705
+ return [
706
+ { path: ".holo-js/generated/next/holo.ts", contents: renderNextHoloHelper() },
707
+ ...options.authEnabled ? [{ path: ".holo-js/generated/next/auth-user-route.ts", contents: renderNextGeneratedCurrentAuthRoute() }] : [],
708
+ ...options.storageEnabled ? [{ path: ".holo-js/generated/next/storage-route.ts", contents: renderNextGeneratedStorageRoute() }] : [],
709
+ ...options.broadcastAuthEnabled ? [{ path: ".holo-js/generated/next/broadcast-auth-route.ts", contents: renderNextGeneratedBroadcastAuthRoute() }] : []
710
+ ];
711
+ }
712
+ function renderNextManagedHostedAuthRouteFiles(features) {
713
+ return getRequestedHostedAuthProviders(features).flatMap((provider) => {
714
+ const spec = HOSTED_AUTH_PROVIDERS[provider];
715
+ return [
716
+ { path: ".holo-js/generated/next/holo.ts", contents: renderNextHoloHelper() },
717
+ { path: `.holo-js/generated/next/auth-${provider}-login-route.ts`, contents: renderNextGeneratedHostedAuthLoginRoute(spec) },
718
+ { path: `.holo-js/generated/next/auth-${provider}-register-route.ts`, contents: renderNextGeneratedHostedAuthRegisterRoute(spec) },
719
+ { path: `.holo-js/generated/next/auth-${provider}-callback-route.ts`, contents: renderNextGeneratedHostedAuthCallbackRoute(spec) },
720
+ { path: `.holo-js/generated/next/auth-${provider}-logout-route.ts`, contents: renderNextGeneratedHostedAuthLogoutRoute(spec) }
721
+ ];
722
+ });
723
+ }
724
+ function renderSvelteManagedRuntimeFiles() {
725
+ return [
726
+ { path: ".holo-js/generated/sveltekit/holo.ts", contents: renderSvelteHoloHelper() }
727
+ ];
728
+ }
729
+ function renderSvelteConfig() {
730
+ return [
731
+ "import adapter from '@sveltejs/adapter-node'",
732
+ "import { withHoloSvelteKit } from '@holo-js/adapter-sveltekit/config'",
733
+ "import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'",
734
+ "",
735
+ "/** @type {import('@sveltejs/kit').Config} */",
736
+ "const config = withHoloSvelteKit({",
737
+ " preprocess: vitePreprocess(),",
738
+ " kit: {",
739
+ " adapter: adapter(),",
740
+ " },",
741
+ "})",
742
+ "",
743
+ "export default config",
744
+ ""
745
+ ].join("\n");
746
+ }
747
+ function renderSvelteUserHooks() {
748
+ return [
749
+ "export {}",
750
+ ""
751
+ ].join("\n");
752
+ }
753
+ function renderSvelteServerUserHooks() {
754
+ return [
755
+ "export {}",
756
+ ""
757
+ ].join("\n");
758
+ }
759
+ function renderSvelteViteConfig(_storageEnabled) {
760
+ const externals = [
761
+ " '@holo-js/adapter-sveltekit',",
762
+ " '@holo-js/auth',",
763
+ " '@holo-js/auth-clerk',",
764
+ " '@holo-js/auth-social',",
765
+ " '@holo-js/auth-workos',",
766
+ " '@holo-js/authorization',",
767
+ " '@holo-js/broadcast',",
768
+ " '@holo-js/cache',",
769
+ " '@holo-js/cache-db',",
770
+ " '@holo-js/cache-redis',",
771
+ " '@holo-js/config',",
772
+ " '@holo-js/core',",
773
+ " '@holo-js/db',",
774
+ " '@holo-js/db-mysql',",
775
+ " '@holo-js/db-postgres',",
776
+ " '@holo-js/db-sqlite',",
777
+ " '@holo-js/events',",
778
+ " '@holo-js/flux',",
779
+ " '@holo-js/flux-svelte',",
780
+ " '@holo-js/forms',",
781
+ " '@holo-js/mail',",
782
+ " '@holo-js/media',",
783
+ " '@holo-js/notifications',",
784
+ " '@holo-js/queue',",
785
+ " '@holo-js/queue-db',",
786
+ " '@holo-js/queue-redis',",
787
+ " '@holo-js/security',",
788
+ " '@holo-js/session',",
789
+ " '@holo-js/storage',",
790
+ " '@holo-js/storage/runtime',",
791
+ " '@holo-js/storage-s3',",
792
+ " '@holo-js/validation',",
793
+ " 'better-sqlite3',",
794
+ " 'ioredis',",
795
+ " 'mysql2',",
796
+ " 'pg',"
797
+ ];
798
+ return [
799
+ "import { sveltekit } from '@sveltejs/kit/vite'",
800
+ "import { defineConfig } from 'vite'",
801
+ "",
802
+ "export default defineConfig({",
803
+ " plugins: [sveltekit()],",
804
+ " server: {",
805
+ " fs: {",
806
+ " allow: ['.holo-js/generated'],",
807
+ " },",
808
+ " },",
809
+ " ssr: {",
810
+ " external: [",
811
+ ...externals,
812
+ " ],",
813
+ " },",
814
+ "})",
815
+ ""
816
+ ].join("\n");
817
+ }
818
+ function renderSvelteAppHtml() {
819
+ return [
820
+ "<!doctype html>",
821
+ '<html lang="en">',
822
+ " <head>",
823
+ ' <meta charset="utf-8" />',
824
+ ' <meta name="viewport" content="width=device-width, initial-scale=1" />',
825
+ " %sveltekit.head%",
826
+ " </head>",
827
+ ' <body data-sveltekit-preload-data="hover">',
828
+ ' <div style="display: contents">%sveltekit.body%</div>',
829
+ " </body>",
830
+ "</html>",
831
+ ""
832
+ ].join("\n");
833
+ }
834
+ function renderSveltePage(projectName) {
835
+ const escapedProjectName = escapeHtml(projectName);
836
+ return [
837
+ `<svelte:head><title>${escapedProjectName}</title></svelte:head>`,
838
+ "",
839
+ '<script lang="ts">',
840
+ ` const projectName = ${JSON.stringify(projectName)}`,
841
+ "</script>",
842
+ "",
843
+ '<main class="shell">',
844
+ " <h1>{projectName}</h1>",
845
+ " <p>SvelteKit owns rendering. Holo owns config, discovery, and backend runtime services.</p>",
846
+ "</main>",
847
+ "",
848
+ "<style>",
849
+ " .shell {",
850
+ " min-height: 100vh;",
851
+ " display: grid;",
852
+ " place-content: center;",
853
+ " gap: 1rem;",
854
+ " padding: 3rem;",
855
+ " font-family: sans-serif;",
856
+ " }",
857
+ " h1 {",
858
+ " margin: 0;",
859
+ " font-size: clamp(2.5rem, 6vw, 4rem);",
860
+ " }",
861
+ " p {",
862
+ " margin: 0;",
863
+ " max-width: 40rem;",
864
+ " line-height: 1.6;",
865
+ " }",
866
+ "</style>",
867
+ ""
868
+ ].join("\n");
869
+ }
870
+ function renderSvelteHoloHelper() {
871
+ return [
872
+ "import { dirname, resolve } from 'node:path'",
873
+ "import { fileURLToPath } from 'node:url'",
874
+ "import { createSvelteKitHoloHelpers } from '@holo-js/adapter-sveltekit'",
875
+ "",
876
+ "const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../..')",
877
+ "",
878
+ "export const holo = createSvelteKitHoloHelpers({ projectRoot })",
879
+ ""
880
+ ].join("\n");
881
+ }
882
+ function renderSvelteHostedAuthLoginRoute(spec) {
883
+ return [
884
+ `import { ${spec.loginFunction} } from '${spec.packageName}'`,
885
+ "import type { RequestEvent } from '@sveltejs/kit'",
886
+ "",
887
+ "export async function GET(event: RequestEvent) {",
888
+ ` return await ${spec.loginFunction}(event)`,
889
+ "}",
890
+ ""
891
+ ].join("\n");
892
+ }
893
+ function renderSvelteHostedAuthRegisterRoute(spec) {
894
+ return [
895
+ `import { ${spec.registerFunction} } from '${spec.packageName}'`,
896
+ "import type { RequestEvent } from '@sveltejs/kit'",
897
+ "",
898
+ "export async function GET(event: RequestEvent) {",
899
+ ` return await ${spec.registerFunction}(event)`,
900
+ "}",
901
+ ""
902
+ ].join("\n");
903
+ }
904
+ function renderSvelteHostedAuthCallbackRoute(spec) {
905
+ return [
906
+ "import { redirect, type RequestEvent } from '@sveltejs/kit'",
907
+ `import { ${spec.callbackFunction} } from '${spec.packageName}'`,
908
+ "",
909
+ "export async function GET(event: RequestEvent) {",
910
+ ` const { error } = await ${spec.callbackFunction}(event)`,
911
+ " if (error) {",
912
+ " throw redirect(303, `/login?error=${encodeURIComponent(error.code)}`)",
913
+ " }",
914
+ "",
915
+ " throw redirect(303, '/')",
916
+ "}",
917
+ ""
918
+ ].join("\n");
919
+ }
920
+ function renderSvelteHostedAuthLogoutRoute(spec) {
921
+ return [
922
+ "import { redirect, type RequestEvent } from '@sveltejs/kit'",
923
+ "import { provider } from '@holo-js/auth'",
924
+ `import { ${spec.logoutFunction} } from '${spec.packageName}'`,
925
+ "",
926
+ "export async function POST(event: RequestEvent) {",
927
+ " let currentProvider: string | null",
928
+ " try {",
929
+ " currentProvider = await provider()",
930
+ " } catch {",
931
+ " throw redirect(303, '/')",
932
+ " }",
933
+ "",
934
+ ` if (currentProvider !== '${spec.provider}') {`,
935
+ " throw redirect(303, '/')",
936
+ " }",
937
+ "",
938
+ ` const { data, error } = await ${spec.logoutFunction}(event)`,
939
+ " if (error) {",
940
+ " return Response.json({ data, error }, { status: error.status })",
941
+ " }",
942
+ "",
943
+ " throw redirect(303, data.url)",
944
+ "}",
945
+ ""
946
+ ].join("\n");
947
+ }
948
+ function renderSvelteHostedAuthRouteFiles(provider) {
949
+ const spec = HOSTED_AUTH_PROVIDERS[provider];
950
+ return [
951
+ { path: `src/routes/api/auth/${provider}/login/+server.ts`, contents: renderSvelteHostedAuthLoginRoute(spec) },
952
+ { path: `src/routes/api/auth/${provider}/register/+server.ts`, contents: renderSvelteHostedAuthRegisterRoute(spec) },
953
+ { path: `src/routes/api/auth/${provider}/callback/+server.ts`, contents: renderSvelteHostedAuthCallbackRoute(spec) },
954
+ { path: `src/routes/api/auth/${provider}/logout/+server.ts`, contents: renderSvelteHostedAuthLogoutRoute(spec) },
955
+ ...renderSvelteManagedRuntimeFiles()
956
+ ];
957
+ }
958
+ function renderAuthProviderRouteFiles(framework, features) {
959
+ return getRequestedHostedAuthProviders(features).flatMap((provider) => {
960
+ if (framework === "nuxt") {
961
+ return renderNuxtHostedAuthRouteFiles(provider);
962
+ }
963
+ if (framework === "next") {
964
+ return renderNextHostedAuthRouteFiles(provider);
965
+ }
966
+ return renderSvelteHostedAuthRouteFiles(provider);
967
+ });
968
+ }
969
+ function renderAuthRouteFiles(framework) {
970
+ if (framework === "next") {
971
+ return [
972
+ { path: "app/api/auth/user/route.ts", contents: renderNextCurrentAuthRoute() },
973
+ { path: ".holo-js/generated/next/holo.ts", contents: renderNextHoloHelper() },
974
+ { path: ".holo-js/generated/next/auth-user-route.ts", contents: renderNextGeneratedCurrentAuthRoute() }
975
+ ];
976
+ }
977
+ if (framework === "nuxt") {
978
+ return [
979
+ { path: "server/api/auth/user.get.ts", contents: renderNuxtCurrentAuthRoute() }
980
+ ];
981
+ }
982
+ return [
983
+ ...renderSvelteManagedRuntimeFiles()
984
+ ];
985
+ }
986
+ function renderFrameworkFiles(options) {
987
+ const optionalPackages = normalizeScaffoldOptionalPackages(options.optionalPackages);
988
+ const storageEnabled = optionalPackages.includes("storage");
989
+ const authEnabled = optionalPackages.includes("auth");
990
+ if (options.framework === "nuxt") {
991
+ return [
992
+ { path: "app/app.vue", contents: renderNuxtAppVue(options.projectName) },
993
+ { path: "nuxt.config.ts", contents: renderNuxtConfig() },
994
+ { path: "shared/.gitkeep", contents: "" },
995
+ ...authEnabled ? renderAuthRouteFiles("nuxt") : []
996
+ ];
997
+ }
998
+ if (options.framework === "next") {
999
+ return [
1000
+ { path: "next.config.ts", contents: renderNextConfig() },
1001
+ { path: "next-env.d.ts", contents: renderNextEnvDts() },
1002
+ { path: "app/layout.tsx", contents: renderNextLayout(options.projectName) },
1003
+ { path: "app/page.tsx", contents: renderNextPage(options.projectName) },
1004
+ ...authEnabled ? [{ path: "app/api/auth/user/route.ts", contents: renderNextCurrentAuthRoute() }] : [],
1005
+ ...storageEnabled ? [{ path: "app/storage/[[...path]]/route.ts", contents: renderNextStorageRoute() }] : [],
1006
+ ...renderNextManagedRouteFiles({ authEnabled, storageEnabled })
1007
+ ];
1008
+ }
1009
+ return [
1010
+ { path: "svelte.config.js", contents: renderSvelteConfig() },
1011
+ { path: "vite.config.ts", contents: renderSvelteViteConfig(storageEnabled) },
1012
+ { path: "src/hooks.ts", contents: renderSvelteUserHooks() },
1013
+ { path: "src/hooks.server.ts", contents: renderSvelteServerUserHooks() },
1014
+ { path: "src/app.html", contents: renderSvelteAppHtml() },
1015
+ { path: "src/routes/+page.svelte", contents: renderSveltePage(options.projectName) },
1016
+ ...authEnabled ? renderAuthRouteFiles("sveltekit") : [],
1017
+ ...renderSvelteManagedRuntimeFiles()
1018
+ ];
1019
+ }
1020
+ function renderFrameworkRunner(options) {
1021
+ const commandName = options.framework === "nuxt" ? "nuxt" : options.framework === "next" ? "next" : "vite";
1022
+ return [
1023
+ "import { existsSync, readFileSync, readlinkSync } from 'node:fs'",
1024
+ "import { dirname, resolve } from 'node:path'",
1025
+ "import { fileURLToPath, pathToFileURL } from 'node:url'",
1026
+ "import { execFileSync, spawn } from 'node:child_process'",
1027
+ "",
1028
+ "const mode = process.argv[2]",
1029
+ "const manifestPath = fileURLToPath(new URL('./project.json', import.meta.url))",
1030
+ "const projectRoot = resolve(dirname(manifestPath), '../..')",
1031
+ "const runtimeSchemaPath = resolve(projectRoot, '.holo-js/generated/schema.mjs')",
1032
+ "const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))",
1033
+ "const framework = String(manifest.framework ?? '')",
1034
+ `const commandName = ${JSON.stringify(commandName)}`,
1035
+ "const commandArgs = mode === 'dev'",
1036
+ " ? ['dev']",
1037
+ " : mode === 'build'",
1038
+ " ? framework === 'sveltekit' ? ['build', '--logLevel', 'error'] : ['build']",
1039
+ " : undefined",
1040
+ "",
1041
+ "if (!commandArgs) {",
1042
+ " console.error(`[holo] Unknown framework runner mode: ${String(mode)}`)",
1043
+ " process.exit(1)",
1044
+ "}",
1045
+ "",
1046
+ "const binaryPath = resolve(",
1047
+ " projectRoot,",
1048
+ " 'node_modules',",
1049
+ " '.bin',",
1050
+ " process.platform === 'win32' ? `${commandName}.cmd` : commandName,",
1051
+ ")",
1052
+ "",
1053
+ "const suppressedOutput = framework === 'sveltekit'",
1054
+ " ? new Set([",
1055
+ ` '"try_get_request_store" is imported from external module "@sveltejs/kit/internal/server" but never used in ".svelte-kit/adapter-node/index.js".',`,
1056
+ " ])",
1057
+ " : new Set()",
1058
+ "",
1059
+ "function shouldSuppressOutput(line) {",
1060
+ " if (suppressedOutput.has(line)) {",
1061
+ " return true",
1062
+ " }",
1063
+ "",
1064
+ " return framework === 'sveltekit'",
1065
+ " && line.startsWith('Circular dependency: ')",
1066
+ " && line.includes('/node_modules/semver/')",
1067
+ "}",
1068
+ "",
1069
+ "function pipeOutput(stream, target, onLine) {",
1070
+ " if (!stream) {",
1071
+ " return",
1072
+ " }",
1073
+ "",
1074
+ " let buffered = ''",
1075
+ " stream.on('data', (chunk) => {",
1076
+ " buffered += chunk.toString()",
1077
+ " const lines = buffered.split(/\\r?\\n/)",
1078
+ " buffered = lines.pop() ?? ''",
1079
+ " for (const line of lines) {",
1080
+ " onLine?.(line)",
1081
+ " if (!shouldSuppressOutput(line)) {",
1082
+ " target.write(`${line}\\n`)",
1083
+ " }",
1084
+ " }",
1085
+ " })",
1086
+ "",
1087
+ " stream.on('end', () => {",
1088
+ " if (buffered.length > 0) {",
1089
+ " onLine?.(buffered)",
1090
+ " }",
1091
+ " if (buffered.length > 0 && !shouldSuppressOutput(buffered)) {",
1092
+ " target.write(buffered)",
1093
+ " }",
1094
+ " })",
1095
+ "}",
1096
+ "",
1097
+ "function extractNextConflictInfo(lines) {",
1098
+ " if (framework !== 'next' || mode !== 'dev') {",
1099
+ " return undefined",
1100
+ " }",
1101
+ "",
1102
+ " if (!lines.some(line => line.includes('Another next dev server is already running.'))) {",
1103
+ " return undefined",
1104
+ " }",
1105
+ "",
1106
+ " let pid",
1107
+ " let dir",
1108
+ "",
1109
+ " for (const line of lines) {",
1110
+ " const match = line.match(/^- PID:\\s+(\\d+)\\s*$/)",
1111
+ " if (match) {",
1112
+ " pid = Number.parseInt(match[1], 10)",
1113
+ " continue",
1114
+ " }",
1115
+ "",
1116
+ " const dirMatch = line.match(/^- Dir:\\s+(.+?)\\s*$/)",
1117
+ " if (dirMatch) {",
1118
+ " dir = dirMatch[1]",
1119
+ " }",
1120
+ " }",
1121
+ "",
1122
+ " return typeof pid === 'number' ? { pid, dir } : undefined",
1123
+ "}",
1124
+ "",
1125
+ "async function waitForProcessExit(pid, timeoutMs = 5000) {",
1126
+ " const deadline = Date.now() + timeoutMs",
1127
+ " while (Date.now() < deadline) {",
1128
+ " try {",
1129
+ " process.kill(pid, 0)",
1130
+ " } catch (error) {",
1131
+ " if (error && typeof error === 'object' && 'code' in error && error.code === 'ESRCH') {",
1132
+ " return true",
1133
+ " }",
1134
+ " throw error",
1135
+ " }",
1136
+ "",
1137
+ " await new Promise(resolve => setTimeout(resolve, 100))",
1138
+ " }",
1139
+ "",
1140
+ " return false",
1141
+ "}",
1142
+ "",
1143
+ "function inspectProcess(pid) {",
1144
+ " try {",
1145
+ " if (process.platform === 'linux' && existsSync(`/proc/${pid}`)) {",
1146
+ " return {",
1147
+ " cwd: readlinkSync(`/proc/${pid}/cwd`),",
1148
+ " args: readFileSync(`/proc/${pid}/cmdline`, 'utf8').replaceAll('\\u0000', ' ').trim(),",
1149
+ " }",
1150
+ " }",
1151
+ " } catch {",
1152
+ " // Fall through to the portable process inspection path below.",
1153
+ " }",
1154
+ "",
1155
+ " try {",
1156
+ " return {",
1157
+ " args: execFileSync('ps', ['-p', String(pid), '-o', 'args='], {",
1158
+ " encoding: 'utf8',",
1159
+ " }).trim(),",
1160
+ " }",
1161
+ " } catch {",
1162
+ " return undefined",
1163
+ " }",
1164
+ "}",
1165
+ "",
1166
+ "function isOwnedNextDevServer(pid, reportedDir) {",
1167
+ " const expectedDir = typeof reportedDir === 'string' ? resolve(reportedDir) : undefined",
1168
+ " if (expectedDir && expectedDir !== projectRoot) {",
1169
+ " return false",
1170
+ " }",
1171
+ "",
1172
+ " const details = inspectProcess(pid)",
1173
+ " if (!details) {",
1174
+ " return expectedDir === projectRoot",
1175
+ " }",
1176
+ "",
1177
+ " const argsMatch = details.args.includes('next') && details.args.includes('dev')",
1178
+ " const cwdMatches = typeof details.cwd === 'string' && resolve(details.cwd) === projectRoot",
1179
+ " const argsReferenceProject = details.args.includes(projectRoot)",
1180
+ "",
1181
+ " return argsMatch && (cwdMatches || argsReferenceProject || expectedDir === projectRoot)",
1182
+ "}",
1183
+ "",
1184
+ "async function stopStaleNextDevServer(pid, reportedDir, force = false) {",
1185
+ " if (!Number.isInteger(pid) || pid <= 0 || pid === process.pid) {",
1186
+ " return false",
1187
+ " }",
1188
+ "",
1189
+ " if (!isOwnedNextDevServer(pid, reportedDir)) {",
1190
+ " return false",
1191
+ " }",
1192
+ "",
1193
+ " if (!force) {",
1194
+ " return false",
1195
+ " }",
1196
+ "",
1197
+ " try {",
1198
+ " process.kill(pid, 'SIGTERM')",
1199
+ " } catch (error) {",
1200
+ " if (error && typeof error === 'object' && 'code' in error && error.code === 'ESRCH') {",
1201
+ " return true",
1202
+ " }",
1203
+ " return false",
1204
+ " }",
1205
+ "",
1206
+ " return waitForProcessExit(pid)",
1207
+ "}",
1208
+ "",
1209
+ "if (!existsSync(binaryPath)) {",
1210
+ ' console.error(`[holo] Missing framework binary "${commandName}" for "${framework}". Run your package manager install first.`)',
1211
+ " process.exit(1)",
1212
+ "}",
1213
+ "",
1214
+ "let child = null",
1215
+ "let forwardedSignal = null",
1216
+ "",
1217
+ "function detachSignalForwarders() {",
1218
+ " process.removeListener('SIGINT', onSigint)",
1219
+ " process.removeListener('SIGTERM', onSigterm)",
1220
+ "}",
1221
+ "",
1222
+ "function forwardSignal(signal) {",
1223
+ " if (forwardedSignal || !child || child.exitCode !== null) {",
1224
+ " return",
1225
+ " }",
1226
+ "",
1227
+ " forwardedSignal = signal",
1228
+ " child.kill(signal)",
1229
+ "}",
1230
+ "",
1231
+ "function onSigint() {",
1232
+ " detachSignalForwarders()",
1233
+ " forwardSignal('SIGINT')",
1234
+ "}",
1235
+ "",
1236
+ "function onSigterm() {",
1237
+ " detachSignalForwarders()",
1238
+ " forwardSignal('SIGTERM')",
1239
+ "}",
1240
+ "",
1241
+ "process.on('SIGINT', onSigint)",
1242
+ "process.on('SIGTERM', onSigterm)",
1243
+ "",
1244
+ "async function run() {",
1245
+ " let restartedAfterConflict = false",
1246
+ " const maxStderrLines = 200",
1247
+ "",
1248
+ " while (true) {",
1249
+ " const stderrLines = []",
1250
+ " const childEnv = { ...process.env }",
1251
+ " if (existsSync(runtimeSchemaPath)) {",
1252
+ " const preload = `--import=${pathToFileURL(runtimeSchemaPath).href}`",
1253
+ " childEnv.NODE_OPTIONS = childEnv.NODE_OPTIONS",
1254
+ " ? `${childEnv.NODE_OPTIONS} ${preload}`",
1255
+ " : preload",
1256
+ " }",
1257
+ " child = spawn(binaryPath, commandArgs, {",
1258
+ " cwd: projectRoot,",
1259
+ " env: childEnv,",
1260
+ " stdio: ['inherit', 'pipe', 'pipe'],",
1261
+ " })",
1262
+ " forwardedSignal = null",
1263
+ "",
1264
+ " pipeOutput(child.stdout, process.stdout)",
1265
+ " pipeOutput(child.stderr, process.stderr, line => {",
1266
+ " if (stderrLines.length >= maxStderrLines) {",
1267
+ " stderrLines.shift()",
1268
+ " }",
1269
+ " stderrLines.push(line)",
1270
+ " })",
1271
+ "",
1272
+ " const result = await new Promise((resolve, reject) => {",
1273
+ " child.on('error', reject)",
1274
+ " child.on('close', (code, signal) => resolve({ code, signal }))",
1275
+ " })",
1276
+ "",
1277
+ " if (result.code === 0) {",
1278
+ " process.exit(0)",
1279
+ " }",
1280
+ "",
1281
+ " const conflictInfo = extractNextConflictInfo(stderrLines)",
1282
+ " if (!restartedAfterConflict && conflictInfo) {",
1283
+ " const stopped = await stopStaleNextDevServer(conflictInfo.pid, conflictInfo.dir)",
1284
+ " if (stopped) {",
1285
+ " restartedAfterConflict = true",
1286
+ " console.error(`[holo] Stopped stale Next dev server ${conflictInfo.pid}. Restarting dev server.`)",
1287
+ " continue",
1288
+ " }",
1289
+ "",
1290
+ " // Another dev server is already running (possibly in a different directory).",
1291
+ " // Next.js already printed the conflict message with instructions to kill it.",
1292
+ " // Exit gracefully to avoid noisy npm/bun error cascades.",
1293
+ " process.exit(0)",
1294
+ " }",
1295
+ "",
1296
+ " if (result.signal) {",
1297
+ " detachSignalForwarders()",
1298
+ " process.kill(process.pid, result.signal)",
1299
+ " } else {",
1300
+ " process.exit(result.code ?? 1)",
1301
+ " }",
1302
+ " }",
1303
+ "}",
1304
+ "",
1305
+ "run().catch((error) => {",
1306
+ " console.error(error instanceof Error ? error.message : String(error))",
1307
+ " process.exit(1)",
1308
+ "})",
1309
+ ""
1310
+ ].join("\n");
1311
+ }
1312
+
256
1313
  // src/project/registry.ts
257
1314
  import { constants as fsConstants } from "fs";
258
- import { access, mkdir as mkdir2, readFile as readFile2, readdir, writeFile as writeFile2 } from "fs/promises";
1315
+ import { access, mkdir as mkdir2, readFile as readFile2, readdir as readdir2, writeFile as writeFile2 } from "fs/promises";
259
1316
  import { dirname as dirname3, extname, join, resolve as resolve3 } from "path";
260
1317
  import { loadConfigDirectory } from "@holo-js/config";
261
1318
  import { DEFAULT_HOLO_PROJECT_PATHS, renderGeneratedSchemaRuntimeModule } from "@holo-js/db";
262
1319
 
263
1320
  // src/project/registry-svelte.ts
264
- import { mkdir, readFile, unlink, writeFile } from "fs/promises";
1321
+ import { mkdir, readdir, readFile, rm, writeFile } from "fs/promises";
265
1322
  import { dirname as dirname2, resolve as resolve2 } from "path";
266
1323
  function renderManagedSvelteHooksModule() {
267
1324
  return [
@@ -308,22 +1365,151 @@ function renderManagedSvelteHooksModule() {
308
1365
  ""
309
1366
  ].join("\n");
310
1367
  }
311
- function renderManagedSvelteServerHooksModule() {
312
- return [
1368
+ function renderManagedSvelteServerHooksModule(features) {
1369
+ const imports = [
313
1370
  "// Generated by holo prepare. Do not edit.",
314
1371
  "",
315
- "import { error as svelteKitError, type Handle, type HandleFetch, type HandleServerError } from '@sveltejs/kit'",
1372
+ "import { error as svelteKitError, type Handle, type HandleFetch, type HandleServerError, type RequestEvent } from '@sveltejs/kit'",
316
1373
  "import { adapterSvelteKitInternals, runWithSvelteKitRequestEvent } from '@holo-js/adapter-sveltekit'",
317
1374
  "import { sequence } from '@sveltejs/kit/hooks'",
318
- "import { holo } from '$lib/server/holo'",
1375
+ ...features.authEnabled ? ["import auth, { check, isAuthError, provider, user } from '@holo-js/auth'"] : [],
1376
+ ...features.broadcastEnabled ? ["import { renderBroadcastAuthResponse } from '@holo-js/broadcast/auth'"] : [],
1377
+ ...features.storageEnabled ? ["import { createPublicStorageResponse } from '@holo-js/storage'"] : [],
1378
+ ...features.clerkEnabled ? ["import { completeClerkAuth, loginWithClerk, logoutWithClerk, registerWithClerk } from '@holo-js/auth-clerk'"] : [],
1379
+ ...features.workosEnabled ? ["import { completeWorkosAuth, loginWithWorkos, logoutWithWorkos, registerWithWorkos } from '@holo-js/auth-workos'"] : [],
1380
+ "import { holo } from './sveltekit/holo'",
319
1381
  "import * as userHooks from '../../src/hooks.server'",
320
- "",
1382
+ ""
1383
+ ];
1384
+ const routeHandlers = [
1385
+ ...features.authEnabled ? ["handleHoloCurrentAuthRoute"] : [],
1386
+ ...features.storageEnabled ? ["handleHoloStorageRoute"] : [],
1387
+ ...features.broadcastEnabled ? ["handleHoloBroadcastAuthRoute"] : [],
1388
+ ...features.clerkEnabled ? [
1389
+ "handleHoloClerkLoginRoute",
1390
+ "handleHoloClerkRegisterRoute",
1391
+ "handleHoloClerkCallbackRoute",
1392
+ "handleHoloClerkLogoutRoute"
1393
+ ] : [],
1394
+ ...features.workosEnabled ? [
1395
+ "handleHoloWorkosLoginRoute",
1396
+ "handleHoloWorkosRegisterRoute",
1397
+ "handleHoloWorkosCallbackRoute",
1398
+ "handleHoloWorkosLogoutRoute"
1399
+ ] : []
1400
+ ];
1401
+ return [
1402
+ ...imports,
321
1403
  "const serverHooks = userHooks as {",
322
1404
  " handle?: Handle",
323
1405
  " handleError?: HandleServerError",
324
1406
  " handleFetch?: HandleFetch",
325
1407
  "}",
326
1408
  "",
1409
+ "type HoloApp = Awaited<ReturnType<typeof holo.getApp>>",
1410
+ "type HoloRouteHandler = (event: RequestEvent, app: HoloApp) => Promise<Response | undefined>",
1411
+ "",
1412
+ "function isRoute(event: RequestEvent, method: string, paths: readonly string[]): boolean {",
1413
+ " return event.request.method.toUpperCase() === method && paths.includes(event.url.pathname)",
1414
+ "}",
1415
+ "",
1416
+ "function redirectResponse(event: RequestEvent, location: string): Response {",
1417
+ " return Response.redirect(new URL(location, event.url), 303)",
1418
+ "}",
1419
+ "",
1420
+ "function normalizeRoutePrefix(value: string | undefined, fallback: string): string {",
1421
+ " const raw = value?.trim() ?? fallback",
1422
+ " if (!raw || raw === '/') {",
1423
+ " return fallback",
1424
+ " }",
1425
+ "",
1426
+ " return `/${raw.replace(/^\\/+|\\/+$/g, '')}`",
1427
+ "}",
1428
+ "",
1429
+ "function pathMatchesPrefix(pathname: string, prefix: string): boolean {",
1430
+ " return pathname === prefix || pathname.startsWith(`${prefix}/`)",
1431
+ "}",
1432
+ "",
1433
+ ...features.authEnabled ? [
1434
+ "async function handleHoloCurrentAuthRoute(event: RequestEvent): Promise<Response | undefined> {",
1435
+ " if (!isRoute(event, 'GET', ['/api/auth/user'])) {",
1436
+ " return undefined",
1437
+ " }",
1438
+ "",
1439
+ " const guard = event.url.searchParams.get('guard') ?? undefined",
1440
+ " try {",
1441
+ " const guardAuth = guard ? auth.guard(guard) : undefined",
1442
+ "",
1443
+ " return Response.json({",
1444
+ " authenticated: guardAuth ? await guardAuth.check() : await check(),",
1445
+ " guard: guard ?? 'web',",
1446
+ " provider: guardAuth ? await guardAuth.provider() : await provider(),",
1447
+ " user: guardAuth ? await guardAuth.user() : await user(),",
1448
+ " })",
1449
+ " } catch (error) {",
1450
+ " if (isAuthError(error) && error.code === 'guard_not_configured') {",
1451
+ " return Response.json({",
1452
+ " authenticated: false,",
1453
+ " guard: guard ?? 'web',",
1454
+ " provider: null,",
1455
+ " user: null,",
1456
+ " }, { status: 400 })",
1457
+ " }",
1458
+ "",
1459
+ " throw error",
1460
+ " }",
1461
+ "}",
1462
+ ""
1463
+ ] : [],
1464
+ ...features.storageEnabled ? [
1465
+ "async function handleHoloStorageRoute(event: RequestEvent, app: HoloApp): Promise<Response | undefined> {",
1466
+ " if (event.request.method.toUpperCase() !== 'GET') {",
1467
+ " return undefined",
1468
+ " }",
1469
+ "",
1470
+ " const prefix = normalizeRoutePrefix(app.config.storage.routePrefix, '/storage')",
1471
+ " if (!pathMatchesPrefix(event.url.pathname, prefix)) {",
1472
+ " return undefined",
1473
+ " }",
1474
+ "",
1475
+ " return createPublicStorageResponse(app.projectRoot, app.config.storage, event.request)",
1476
+ "}",
1477
+ ""
1478
+ ] : [],
1479
+ ...features.broadcastEnabled ? [
1480
+ "async function handleHoloBroadcastAuthRoute(event: RequestEvent, app: HoloApp): Promise<Response | undefined> {",
1481
+ " if (!isRoute(event, 'POST', ['/broadcasting/auth'])) {",
1482
+ " return undefined",
1483
+ " }",
1484
+ "",
1485
+ " const auth = await holo.getAuth()",
1486
+ " return await renderBroadcastAuthResponse(event.request, {",
1487
+ " resolveUser: async () => await auth?.user(),",
1488
+ " channelAuth: {",
1489
+ " registry: {",
1490
+ " projectRoot: app.projectRoot,",
1491
+ " channels: app.registry?.channels ?? [],",
1492
+ " },",
1493
+ " },",
1494
+ " })",
1495
+ "}",
1496
+ ""
1497
+ ] : [],
1498
+ ...features.clerkEnabled ? renderSvelteHostedAuthHookRoutes("clerk", "Clerk") : [],
1499
+ ...features.workosEnabled ? renderSvelteHostedAuthHookRoutes("workos", "Workos") : [],
1500
+ `const holoRouteHandlers: readonly HoloRouteHandler[] = [${routeHandlers.join(", ")}]`,
1501
+ "",
1502
+ "async function handleHoloRoute(event: RequestEvent, app: HoloApp): Promise<Response | undefined> {",
1503
+ " for (const handler of holoRouteHandlers) {",
1504
+ " const response = await handler(event, app)",
1505
+ " if (response) {",
1506
+ " return response",
1507
+ " }",
1508
+ " }",
1509
+ "",
1510
+ " return undefined",
1511
+ "}",
1512
+ "",
327
1513
  "function isHoloAuthorizationHttpError(cause: unknown): cause is Error & { readonly decision: { readonly status: 403 | 404 } } {",
328
1514
  " if (!(cause instanceof Error) || cause.name !== 'AuthorizationError') {",
329
1515
  " return false",
@@ -338,9 +1524,14 @@ function renderManagedSvelteServerHooksModule() {
338
1524
  "}",
339
1525
  "",
340
1526
  "const holoHandle: Handle = ({ event, resolve }) => runWithSvelteKitRequestEvent(event, async () => {",
341
- " await holo.getApp()",
1527
+ " const app = await holo.getApp()",
342
1528
  "",
343
1529
  " try {",
1530
+ " const holoRouteResponse = await handleHoloRoute(event, app)",
1531
+ " if (holoRouteResponse) {",
1532
+ " return holoRouteResponse",
1533
+ " }",
1534
+ "",
344
1535
  " const response = await resolve(event)",
345
1536
  " return await adapterSvelteKitInternals.mapValidationActionResponse(event, response)",
346
1537
  " } catch (cause) {",
@@ -385,6 +1576,63 @@ function renderManagedSvelteServerHooksModule() {
385
1576
  ""
386
1577
  ].join("\n");
387
1578
  }
1579
+ function renderSvelteHostedAuthHookRoutes(providerName, functionSuffix) {
1580
+ return [
1581
+ `async function handleHolo${functionSuffix}LoginRoute(event: RequestEvent): Promise<Response | undefined> {`,
1582
+ ` if (!isRoute(event, 'GET', ['/api/auth/${providerName}/login'])) {`,
1583
+ " return undefined",
1584
+ " }",
1585
+ "",
1586
+ ` return await loginWith${functionSuffix}(event)`,
1587
+ "}",
1588
+ "",
1589
+ `async function handleHolo${functionSuffix}RegisterRoute(event: RequestEvent): Promise<Response | undefined> {`,
1590
+ ` if (!isRoute(event, 'GET', ['/api/auth/${providerName}/register'])) {`,
1591
+ " return undefined",
1592
+ " }",
1593
+ "",
1594
+ ` return await registerWith${functionSuffix}(event)`,
1595
+ "}",
1596
+ "",
1597
+ `async function handleHolo${functionSuffix}CallbackRoute(event: RequestEvent): Promise<Response | undefined> {`,
1598
+ ` if (!isRoute(event, 'GET', ['/api/auth/${providerName}/callback'])) {`,
1599
+ " return undefined",
1600
+ " }",
1601
+ "",
1602
+ ` const { error } = await complete${functionSuffix}Auth(event)`,
1603
+ " if (error) {",
1604
+ " return redirectResponse(event, `/login?error=${encodeURIComponent(error.code)}`)",
1605
+ " }",
1606
+ "",
1607
+ " return redirectResponse(event, '/')",
1608
+ "}",
1609
+ "",
1610
+ `async function handleHolo${functionSuffix}LogoutRoute(event: RequestEvent): Promise<Response | undefined> {`,
1611
+ ` if (!isRoute(event, 'POST', ['/api/auth/${providerName}/logout'])) {`,
1612
+ " return undefined",
1613
+ " }",
1614
+ "",
1615
+ " let currentProvider: string | null",
1616
+ " try {",
1617
+ " currentProvider = await provider()",
1618
+ " } catch {",
1619
+ " return redirectResponse(event, '/')",
1620
+ " }",
1621
+ "",
1622
+ ` if (currentProvider !== '${providerName}') {`,
1623
+ " return redirectResponse(event, '/')",
1624
+ " }",
1625
+ "",
1626
+ ` const { data, error } = await logoutWith${functionSuffix}(event)`,
1627
+ " if (error) {",
1628
+ " return Response.json({ data, error }, { status: error.status })",
1629
+ " }",
1630
+ "",
1631
+ " return redirectResponse(event, data.url)",
1632
+ "}",
1633
+ ""
1634
+ ];
1635
+ }
388
1636
  function renderSvelteDefaultHooksModule() {
389
1637
  return [
390
1638
  "export {}",
@@ -448,10 +1696,10 @@ async function readFileIfPresent(path) {
448
1696
  }
449
1697
  }
450
1698
  async function unlinkIfPresent(path) {
451
- try {
452
- await unlink(path);
453
- } catch {
454
- }
1699
+ await rm(path, { force: true });
1700
+ }
1701
+ async function pathExists(path) {
1702
+ return await readFileIfPresent(path) !== void 0;
455
1703
  }
456
1704
  var SVELTE_HOOKS_OVERRIDE_BLOCK = [
457
1705
  " files: {",
@@ -533,7 +1781,7 @@ async function writeFileIfChanged(path, contents) {
533
1781
  await mkdir(dirname2(path), { recursive: true });
534
1782
  await writeFile(path, contents, "utf8");
535
1783
  }
536
- async function ensureSvelteManagedHooks(projectRoot) {
1784
+ async function ensureSvelteManagedHooks(projectRoot, features) {
537
1785
  const hooksPath = resolve2(projectRoot, "src/hooks.ts");
538
1786
  const hooksServerPath = resolve2(projectRoot, "src/hooks.server.ts");
539
1787
  const generatedHooksPath = resolve2(projectRoot, GENERATED_SVELTE_HOOKS_PATH);
@@ -567,9 +1815,146 @@ async function ensureSvelteManagedHooks(projectRoot) {
567
1815
  await writeFileIfChanged(hooksServerPath, renderSvelteDefaultServerHooksModule());
568
1816
  }
569
1817
  await writeFileIfChanged(generatedHooksPath, renderManagedSvelteHooksModule());
570
- await writeFileIfChanged(generatedServerHooksPath, renderManagedSvelteServerHooksModule());
1818
+ await writeFileIfChanged(generatedServerHooksPath, renderManagedSvelteServerHooksModule(features));
571
1819
  await ensureSvelteConfigHooksOverride(projectRoot);
572
1820
  }
1821
+ async function removeLegacyManagedHoloHelper(path, managedContents) {
1822
+ const contents = await readFileIfPresent(path);
1823
+ const trimmedContents = contents?.trim();
1824
+ const trimmedManagedContents = managedContents.trim();
1825
+ const isLegacyManagedHelper = trimmedContents === [
1826
+ "import { createNextHoloHelpers } from '@holo-js/adapter-next'",
1827
+ "",
1828
+ "export const holo = createNextHoloHelpers()"
1829
+ ].join("\n") || trimmedContents === [
1830
+ "import { createSvelteKitHoloHelpers } from '@holo-js/adapter-sveltekit'",
1831
+ "",
1832
+ "export const holo = createSvelteKitHoloHelpers()"
1833
+ ].join("\n");
1834
+ if (trimmedContents === trimmedManagedContents || isLegacyManagedHelper) {
1835
+ await unlinkIfPresent(path);
1836
+ }
1837
+ }
1838
+ async function removeLegacyManagedRoute(path, markerGroups) {
1839
+ const contents = await readFileIfPresent(path);
1840
+ if (contents && markerGroups.some((markers) => markers.every((marker) => contents.includes(marker)))) {
1841
+ await unlinkIfPresent(path);
1842
+ }
1843
+ }
1844
+ async function directoryContainsText(path, pattern) {
1845
+ const entries = await readdir(path, { withFileTypes: true }).catch(() => []);
1846
+ for (const entry of entries) {
1847
+ const entryPath = resolve2(path, entry.name);
1848
+ if (entry.isDirectory()) {
1849
+ if (await directoryContainsText(entryPath, pattern)) {
1850
+ return true;
1851
+ }
1852
+ continue;
1853
+ }
1854
+ if (!entry.isFile() || !/\.[cm]?[jt]s$/.test(entry.name)) {
1855
+ continue;
1856
+ }
1857
+ const contents = await readFileIfPresent(entryPath);
1858
+ if (contents?.includes(pattern)) {
1859
+ return true;
1860
+ }
1861
+ }
1862
+ return false;
1863
+ }
1864
+ async function ensureNextManagedRoutes(projectRoot) {
1865
+ const authEnabled = await pathExists(resolve2(projectRoot, "app/api/auth/user/route.ts"));
1866
+ const storageEnabled = await pathExists(resolve2(projectRoot, "app/storage/[[...path]]/route.ts"));
1867
+ const broadcastAuthEnabled = await pathExists(resolve2(projectRoot, "app/broadcasting/auth/route.ts"));
1868
+ const clerkEnabled = await pathExists(resolve2(projectRoot, "app/api/auth/clerk/login/route.ts"));
1869
+ const workosEnabled = await pathExists(resolve2(projectRoot, "app/api/auth/workos/login/route.ts"));
1870
+ const files = [
1871
+ ...renderNextManagedRouteFiles({
1872
+ authEnabled,
1873
+ storageEnabled,
1874
+ broadcastAuthEnabled
1875
+ }),
1876
+ ...renderNextManagedHostedAuthRouteFiles({
1877
+ clerk: clerkEnabled,
1878
+ workos: workosEnabled
1879
+ })
1880
+ ];
1881
+ for (const file of files) {
1882
+ await writeFileIfChanged(resolve2(projectRoot, file.path), file.contents);
1883
+ }
1884
+ await removeLegacyManagedHoloHelper(resolve2(projectRoot, "server/holo.ts"), renderNextHoloHelper());
1885
+ }
1886
+ function isRecord2(value) {
1887
+ return !!value && typeof value === "object" && !Array.isArray(value);
1888
+ }
1889
+ async function readProjectDependencies(projectRoot) {
1890
+ const contents = await readFileIfPresent(resolve2(projectRoot, "package.json"));
1891
+ if (!contents) {
1892
+ return /* @__PURE__ */ new Set();
1893
+ }
1894
+ const manifest = JSON.parse(contents);
1895
+ if (!isRecord2(manifest)) {
1896
+ return /* @__PURE__ */ new Set();
1897
+ }
1898
+ const dependencies = isRecord2(manifest.dependencies) ? Object.keys(manifest.dependencies) : [];
1899
+ const devDependencies = isRecord2(manifest.devDependencies) ? Object.keys(manifest.devDependencies) : [];
1900
+ return /* @__PURE__ */ new Set([...dependencies, ...devDependencies]);
1901
+ }
1902
+ async function resolveSvelteManagedFeatures(projectRoot) {
1903
+ const dependencies = await readProjectDependencies(projectRoot);
1904
+ return {
1905
+ authEnabled: dependencies.has("@holo-js/auth"),
1906
+ broadcastEnabled: dependencies.has("@holo-js/broadcast"),
1907
+ storageEnabled: dependencies.has("@holo-js/storage"),
1908
+ clerkEnabled: dependencies.has("@holo-js/auth-clerk"),
1909
+ workosEnabled: dependencies.has("@holo-js/auth-workos")
1910
+ };
1911
+ }
1912
+ async function ensureSvelteManagedRuntime(projectRoot) {
1913
+ const files = renderSvelteManagedRuntimeFiles();
1914
+ for (const file of files) {
1915
+ await writeFileIfChanged(resolve2(projectRoot, file.path), file.contents);
1916
+ }
1917
+ }
1918
+ async function ensureSvelteManagedRoutes(projectRoot) {
1919
+ await removeLegacyManagedRoute(
1920
+ resolve2(projectRoot, "src/routes/api/holo/health/+server.ts"),
1921
+ [
1922
+ ["app.registry?.models.length ?? 0", "app.registry?.commands.length ?? 0"],
1923
+ [".holo-js/generated/sveltekit/health-route"]
1924
+ ]
1925
+ );
1926
+ await removeLegacyManagedRoute(
1927
+ resolve2(projectRoot, "src/routes/api/holo/+server.ts"),
1928
+ [
1929
+ ["app.registry?.models.length ?? 0", "app.registry?.commands.length ?? 0"],
1930
+ [".holo-js/generated/sveltekit/health-route"]
1931
+ ]
1932
+ );
1933
+ await removeLegacyManagedRoute(
1934
+ resolve2(projectRoot, "src/routes/api/auth/user/+server.ts"),
1935
+ [
1936
+ ["authenticated:", "guard:", "provider:", "user:"],
1937
+ [".holo-js/generated/sveltekit/auth-user-route"]
1938
+ ]
1939
+ );
1940
+ await removeLegacyManagedRoute(
1941
+ resolve2(projectRoot, "src/routes/storage/[...path]/+server.ts"),
1942
+ [
1943
+ ["createPublicStorageResponse", "app.config.storage"],
1944
+ [".holo-js/generated/sveltekit/storage-route"]
1945
+ ]
1946
+ );
1947
+ await removeLegacyManagedRoute(
1948
+ resolve2(projectRoot, "src/routes/broadcasting/auth/+server.ts"),
1949
+ [
1950
+ ["renderBroadcastAuthResponse", "channelAuth"],
1951
+ [".holo-js/generated/sveltekit/broadcast-auth-route"]
1952
+ ]
1953
+ );
1954
+ if (!await directoryContainsText(resolve2(projectRoot, "src"), "$lib/server/holo")) {
1955
+ await removeLegacyManagedHoloHelper(resolve2(projectRoot, "src/lib/server/holo.ts"), renderSvelteHoloHelper());
1956
+ }
1957
+ }
573
1958
  async function syncManagedFrameworkArtifacts(projectRoot) {
574
1959
  let manifest;
575
1960
  try {
@@ -579,7 +1964,13 @@ async function syncManagedFrameworkArtifacts(projectRoot) {
579
1964
  return;
580
1965
  }
581
1966
  if (manifest.framework === "sveltekit") {
582
- await ensureSvelteManagedHooks(projectRoot);
1967
+ const features = await resolveSvelteManagedFeatures(projectRoot);
1968
+ await ensureSvelteManagedRuntime(projectRoot);
1969
+ await ensureSvelteManagedHooks(projectRoot, features);
1970
+ await ensureSvelteManagedRoutes(projectRoot);
1971
+ }
1972
+ if (manifest.framework === "next") {
1973
+ await ensureNextManagedRoutes(projectRoot);
583
1974
  }
584
1975
  }
585
1976
 
@@ -1119,7 +2510,7 @@ function getConfigExtensionPriority(fileName) {
1119
2510
  }
1120
2511
  async function collectProjectConfigEntries(projectRoot) {
1121
2512
  const configDir = resolve3(projectRoot, "config");
1122
- const entries = await readdir(configDir, { withFileTypes: true }).catch(() => []);
2513
+ const entries = await readdir2(configDir, { withFileTypes: true }).catch(() => []);
1123
2514
  const selectedByName = /* @__PURE__ */ new Map();
1124
2515
  for (const entry of entries) {
1125
2516
  if (!entry.isFile()) {
@@ -1305,6 +2696,15 @@ export {
1305
2696
  renderMarkdownMailTemplate,
1306
2697
  resolveNameInfo,
1307
2698
  resolveArtifactPath,
2699
+ renderNextHoloHelper,
2700
+ renderNextBroadcastAuthRoute,
2701
+ renderNextGeneratedBroadcastAuthRoute,
2702
+ renderNextManagedHostedAuthRouteFiles,
2703
+ renderSvelteHoloHelper,
2704
+ renderAuthProviderRouteFiles,
2705
+ renderAuthRouteFiles,
2706
+ renderFrameworkFiles,
2707
+ renderFrameworkRunner,
1308
2708
  renderGeneratedModelTypes,
1309
2709
  writeGeneratedProjectRegistry,
1310
2710
  loadGeneratedProjectRegistry