@nullix/zod-mongoose-studio 3.3.0 → 3.3.2

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 (119) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/_fonts/57NSSoFy1VLVs2gqly8Ls9awBnZMFyXGrefpmqvdqmc-zJfbBtpgM4cDmcXBsqZNW79_kFnlpPd62b48glgdydA.woff2 +0 -0
  3. package/.output/public/_fonts/8VR2wSMN-3U4NbWAVYXlkRV6hA0jFBXP-0RtL3X7fko-x2gYI4qfmkRdxyQQUPaBZdZdgl1TeVrquF_TxHeM4lM.woff2 +0 -0
  4. package/.output/public/_fonts/GsKUclqeNLJ96g5AU593ug6yanivOiwjW_7zESNPChw-jHA4tBeM1bjF7LATGUpfBuSTyomIFrWBTzjF7txVYfg.woff2 +0 -0
  5. package/.output/public/_fonts/Ld1FnTo3yTIwDyGfTQ5-Fws9AWsCbKfMvgxduXr7JcY-W25bL8NF1fjpLRSOgJb7RoZPHqGQNwMTM7S9tHVoxx8.woff2 +0 -0
  6. package/.output/public/_fonts/NdzqRASp2bovDUhQT1IRE_EMqKJ2KYQdTCfFcBvL8yw-KhwZiS86o3fErOe5GGMExHUemmI_dBfaEFxjISZrBd0.woff2 +0 -0
  7. package/.output/public/_fonts/iTkrULNFJJkTvihIg1Vqi5IODRH_9btXCioVF5l98I8-AndUyau2HR2felA_ra8V2mutQgschhasE5FD1dXGJX8.woff2 +0 -0
  8. package/.output/public/_nuxt/B6lqezyA.js +27 -0
  9. package/.output/public/_nuxt/{IxYCG8hB.js → B7ScFmDB.js} +1 -1
  10. package/.output/public/_nuxt/B8gqXD2U.js +1 -0
  11. package/.output/public/_nuxt/{ikxafTHA.js → BC9tFNFa.js} +180 -184
  12. package/.output/public/_nuxt/{qwxNOyg7.js → Bbi9UBvi.js} +1 -1
  13. package/.output/public/_nuxt/{CNZorjDf.js → BcYnCaqL.js} +1 -1
  14. package/.output/public/_nuxt/{BzhSDy30.js → BlZ0x31M.js} +1 -1
  15. package/.output/public/_nuxt/Bw82IGM8.js +1 -0
  16. package/.output/public/_nuxt/{qQyvCkbV.js → C8xmwXC2.js} +1 -1
  17. package/.output/public/_nuxt/{Ce-T_1aD.js → C919AVYR.js} +1 -1
  18. package/.output/public/_nuxt/{nDZOJ7rN.js → CsQL5F-l.js} +2 -2
  19. package/.output/public/_nuxt/Czp4_2o-.js +33 -0
  20. package/.output/public/_nuxt/{CC3n3Aga.js → DJa7_7Hz.js} +1 -1
  21. package/.output/public/_nuxt/DKK0Mrrg.js +1 -0
  22. package/.output/public/_nuxt/DbUZZZZu.js +6 -0
  23. package/.output/public/_nuxt/{B7P0wQ35.js → DbqvVYbv.js} +1 -1
  24. package/.output/public/_nuxt/{Ick_23I4.js → JkiDMdYG.js} +1 -1
  25. package/.output/public/_nuxt/{81vqh61j.js → OVQ3C2Ge.js} +1 -1
  26. package/.output/public/_nuxt/{Cs8GXJCB.js → PXBhK7S_.js} +1 -1
  27. package/.output/public/_nuxt/{CAd-qHfI.js → YC-RLggm.js} +1 -1
  28. package/.output/public/_nuxt/builds/latest.json +1 -1
  29. package/.output/public/_nuxt/builds/meta/46174b5e-cab6-4a73-95f8-66a1c9b1be2b.json +1 -0
  30. package/.output/public/_nuxt/{tzj4nvDE.js → d1NBz903.js} +2 -2
  31. package/.output/public/_nuxt/{entry.J_CbC0oY.css → entry.DaqrxXuH.css} +1 -1
  32. package/.output/public/_nuxt/error-404.CTSXlFYa.css +1 -0
  33. package/.output/public/_nuxt/error-500.DqEfQnVZ.css +1 -0
  34. package/.output/public/_nuxt/{Bd32GOUy.js → iGzUozbF.js} +1 -1
  35. package/.output/public/_nuxt/{DjPT-NCE.js → iKW_mt0p.js} +1 -1
  36. package/.output/server/chunks/_/error-500.mjs.map +1 -1
  37. package/.output/server/chunks/build/client.precomputed.mjs +1 -1
  38. package/.output/server/chunks/build/{error-404-BN-7rNE4.mjs → error-404-KRkA0DPI.mjs} +10 -4
  39. package/.output/server/chunks/build/error-404-KRkA0DPI.mjs.map +1 -0
  40. package/.output/server/chunks/build/error-404-styles.I0NYw-Bn.mjs +8 -0
  41. package/.output/server/chunks/build/error-404-styles.I0NYw-Bn.mjs.map +1 -0
  42. package/.output/server/chunks/build/{error-500-DhIQ-2yt.mjs → error-500-DIIFf55p.mjs} +10 -4
  43. package/.output/server/chunks/build/error-500-DIIFf55p.mjs.map +1 -0
  44. package/.output/server/chunks/build/error-500-styles.D1cs9hQy.mjs +8 -0
  45. package/.output/server/chunks/build/error-500-styles.D1cs9hQy.mjs.map +1 -0
  46. package/.output/server/chunks/build/index-CB1N4gax.mjs +557 -0
  47. package/.output/server/chunks/build/index-CB1N4gax.mjs.map +1 -0
  48. package/.output/server/chunks/build/server.mjs +326 -248
  49. package/.output/server/chunks/build/server.mjs.map +1 -1
  50. package/.output/server/chunks/build/styles.mjs +4 -4
  51. package/.output/server/chunks/nitro/nitro.mjs +2319 -852
  52. package/.output/server/chunks/nitro/nitro.mjs.map +1 -1
  53. package/.output/server/chunks/routes/api/editor-types.get.mjs +113 -0
  54. package/.output/server/chunks/routes/api/editor-types.get.mjs.map +1 -0
  55. package/.output/server/chunks/routes/api/parse.post.mjs +26 -1448
  56. package/.output/server/chunks/routes/api/parse.post.mjs.map +1 -1
  57. package/.output/server/chunks/routes/api/resolve.get.mjs +6 -0
  58. package/.output/server/chunks/routes/api/resolve.get.mjs.map +1 -1
  59. package/.output/server/chunks/routes/api/studio/run.post.mjs +40 -0
  60. package/.output/server/chunks/routes/api/studio/run.post.mjs.map +1 -0
  61. package/.output/server/chunks/routes/renderer.mjs +1 -1
  62. package/.output/server/chunks/routes/renderer.mjs.map +1 -1
  63. package/.output/server/index.mjs +7 -1
  64. package/.output/server/index.mjs.map +1 -1
  65. package/.output/server/node_modules/acorn/dist/acorn.mjs +6266 -0
  66. package/.output/server/node_modules/acorn/package.json +50 -0
  67. package/.output/server/node_modules/ip-address/dist/common.js +112 -4
  68. package/.output/server/node_modules/ip-address/dist/ipv4.js +322 -87
  69. package/.output/server/node_modules/ip-address/dist/ipv6.js +654 -199
  70. package/.output/server/node_modules/ip-address/dist/v4/constants.js +45 -2
  71. package/.output/server/node_modules/ip-address/dist/v6/constants.js +55 -3
  72. package/.output/server/node_modules/ip-address/dist/v6/helpers.js +12 -3
  73. package/.output/server/node_modules/ip-address/package.json +49 -37
  74. package/.output/server/node_modules/ufo/dist/index.mjs +2 -2
  75. package/.output/server/node_modules/ufo/package.json +9 -9
  76. package/.output/server/package.json +4 -3
  77. package/README.md +65 -20
  78. package/app/assets/css/main.css +1 -0
  79. package/app/assets/css/studio.css +131 -0
  80. package/app/components/StudioConversion.vue +41 -0
  81. package/app/components/StudioDataPanel.vue +21 -0
  82. package/app/components/StudioEditor.vue +78 -102
  83. package/app/components/StudioExpandButton.vue +11 -0
  84. package/app/components/StudioOutput.vue +7 -1
  85. package/app/components/StudioSourcePanel.vue +16 -0
  86. package/app/components/StudioValidation.vue +47 -0
  87. package/app/components/ZodMongooseStudio.vue +216 -198
  88. package/app/composables/useStudioRunner.ts +142 -0
  89. package/app/pages/index.vue +1 -1
  90. package/app/plugins/monaco.client.ts +2 -0
  91. package/bin/index.mjs +5 -0
  92. package/nuxt.config.ts +4 -3
  93. package/package.json +6 -3
  94. package/server/api/editor-types.get.ts +7 -0
  95. package/server/api/parse.post.ts +17 -175
  96. package/server/api/studio/run.post.ts +29 -0
  97. package/server/middleware/security.ts +29 -7
  98. package/server/utils/editor-types.json +1 -0
  99. package/server/utils/example-data.ts +80 -0
  100. package/server/utils/parse-request.ts +28 -0
  101. package/server/utils/runner-client.ts +40 -0
  102. package/server/utils/studio-execution.ts +176 -0
  103. package/.output/public/_nuxt/BD40YYqZ.js +0 -33
  104. package/.output/public/_nuxt/BlQmLjV4.js +0 -1
  105. package/.output/public/_nuxt/CJ4Cv9fY.js +0 -61
  106. package/.output/public/_nuxt/builds/meta/495a8ebb-444d-4414-9b98-c181aa84d817.json +0 -1
  107. package/.output/public/_nuxt/error-404.64pZEiDn.css +0 -1
  108. package/.output/public/_nuxt/error-500.DHOpI0Ir.css +0 -1
  109. package/.output/public/_nuxt/rH_O4qXT.js +0 -1
  110. package/.output/server/chunks/build/error-404-BN-7rNE4.mjs.map +0 -1
  111. package/.output/server/chunks/build/error-404-styles.Byx46oLR.mjs +0 -8
  112. package/.output/server/chunks/build/error-404-styles.Byx46oLR.mjs.map +0 -1
  113. package/.output/server/chunks/build/error-500-DhIQ-2yt.mjs.map +0 -1
  114. package/.output/server/chunks/build/error-500-styles.BL3UT0Wa.mjs +0 -8
  115. package/.output/server/chunks/build/error-500-styles.BL3UT0Wa.mjs.map +0 -1
  116. package/.output/server/chunks/build/index-B4T6zHp0.mjs +0 -1904
  117. package/.output/server/chunks/build/index-B4T6zHp0.mjs.map +0 -1
  118. package/.output/server/node_modules/zod/index.js +0 -4
  119. /package/.output/public/_nuxt/{editor.CdBKkuhR.css → monaco.CdBKkuhR.css} +0 -0
@@ -1,225 +1,243 @@
1
1
  <script setup lang="ts">
2
- import { ref, watch } from "vue";
3
- // Use lodash/debounce or vueuse in a real app, but setTimeout works for now
4
- let debounceTimer: any;
2
+ import { nextTick, onBeforeUnmount, onMounted, ref, shallowRef } from "vue";
3
+ import { useStudioRunner } from "../composables/useStudioRunner";
5
4
 
6
5
  const config = useRuntimeConfig();
7
- const isLocalMode = ref(config.public.isLocalMode);
8
- const isDocsMode = ref(config.public.isDocsMode);
6
+ const root = ref<HTMLElement | null>(null);
7
+ const mobileView = shallowRef<"schema" | "data" | "result" | "mongoose">(
8
+ "schema",
9
+ );
10
+ type Panel = "schema" | "data" | "result" | "mongoose";
11
+ const expandedPanel = shallowRef<Panel | null>(null);
12
+ let transitioning = false;
13
+ const mobileViews = [
14
+ { key: "schema", label: "Schema" },
15
+ { key: "data", label: "Data" },
16
+ { key: "result", label: "Result" },
17
+ { key: "mongoose", label: "Mongoose" },
18
+ ] as const;
9
19
 
10
- const sourceCode = ref(`import { z } from 'zod/v4';
11
- import { extractMongooseDef, toMongooseSchema } from '@nullix/zod-mongoose';
20
+ const {
21
+ sourceCode,
22
+ inputData,
23
+ schemaNames,
24
+ selectedSchema,
25
+ validation,
26
+ conversion,
27
+ isRunning,
28
+ isDirty,
29
+ hasRun,
30
+ error,
31
+ exampleError,
32
+ run,
33
+ selectSchema,
34
+ } = useStudioRunner();
12
35
 
13
- const UserSchema = z.object({
14
- name: z.string(),
15
- age: z.number().optional()
16
- });
17
-
18
- // Export your schema so the Studio can transform it
19
- export default UserSchema;`);
20
-
21
- const outputDef = ref("// Definition output...");
22
- const outputSchema = ref("// Mongoose Schema output...");
23
- const outputTree = ref("// Mongoose Schema tree output...");
24
- const outputPaths = ref("// Mongoose Schema paths output...");
25
- const activeSchemaView = ref<"obj" | "tree" | "paths">("obj");
26
- const activeSchemaViewOutput = computed(() => {
27
- switch (activeSchemaView.value) {
28
- case "obj":
29
- return outputSchema;
30
- case "tree":
31
- return outputTree;
32
- case "paths":
33
- return outputPaths;
36
+ function onKeydown(event: KeyboardEvent) {
37
+ if (event.key === "Escape" && expandedPanel.value) {
38
+ togglePanel(expandedPanel.value);
39
+ return;
34
40
  }
35
- });
36
- const isDefOpen = ref(true);
37
- const isSchemaOpen = ref(true);
38
-
39
- const isCompiling = ref(false);
41
+ if (!root.value?.contains(document.activeElement)) return;
42
+ if (
43
+ !(event.metaKey || event.ctrlKey) ||
44
+ event.altKey ||
45
+ event.shiftKey ||
46
+ event.repeat
47
+ )
48
+ return;
49
+ if (event.key.toLowerCase() !== "s" && event.key.toLowerCase() !== "r")
50
+ return;
51
+ event.preventDefault();
52
+ event.stopPropagation();
53
+ void run();
54
+ }
40
55
 
41
- const handleCompile = async () => {
42
- isCompiling.value = true;
43
- try {
44
- const response = await $fetch("/api/parse", {
45
- method: "POST",
46
- body: { sourceCode: sourceCode.value },
56
+ function togglePanel(panel: Panel) {
57
+ if (transitioning) return;
58
+ const card = root.value?.querySelector<HTMLElement>(`.studio-${panel}`);
59
+ const change = () => {
60
+ expandedPanel.value = expandedPanel.value === panel ? null : panel;
61
+ };
62
+ if (card && "startViewTransition" in document) {
63
+ transitioning = true;
64
+ card.style.viewTransitionName = "studio-expanded-panel";
65
+ const transition = document.startViewTransition(() => {
66
+ change();
67
+ return nextTick();
47
68
  });
48
- outputDef.value = `// Result of extractMongooseDef()\nconst definition = ${response.definition};`;
49
- outputSchema.value = `// Result of toMongooseSchema().obj\nconst schemaObj = ${response.schemaObj};`;
50
- outputTree.value = `// Result of toMongooseSchema().tree\nconst schemaTree = ${response.schemaTree};`;
51
- outputPaths.value = `// Result of toMongooseSchema().paths\nconst schemaPaths = ${response.schemaPaths};`;
52
- } catch (error: any) {
53
- const errText = `// Error\n${error.data?.message || error.message}`;
54
- outputDef.value = errText;
55
- outputSchema.value = errText;
56
- outputTree.value = errText;
57
- outputPaths.value = errText;
58
- } finally {
59
- isCompiling.value = false;
60
- }
61
- };
62
-
63
- // Auto-compile with a slight delay as they type
64
- watch(sourceCode, () => {
65
- if (isDocsMode.value) return;
66
- clearTimeout(debounceTimer);
67
- debounceTimer = setTimeout(handleCompile, 600);
68
- });
69
-
70
- // Initial compile for Docs Mode
71
- if (isDocsMode.value) {
72
- handleCompile();
69
+ void transition.finished.finally(() => {
70
+ card.style.viewTransitionName = "";
71
+ transitioning = false;
72
+ });
73
+ } else change();
73
74
  }
75
+
76
+ onMounted(() => document.addEventListener("keydown", onKeydown, true));
77
+ onBeforeUnmount(() => document.removeEventListener("keydown", onKeydown, true));
74
78
  </script>
75
79
 
76
80
  <template>
77
81
  <div
78
- class="flex flex-col bg-gray-50 dark:bg-gray-950 overflow-hidden relative"
79
- :class="[
80
- isDocsMode
81
- ? 'h-[calc(100vh-var(--ui-header-height,64px))] sticky top-[var(--ui-header-height,64px)]'
82
- : 'h-screen',
83
- ]"
82
+ ref="root"
83
+ class="studio-shell"
84
+ :class="{
85
+ 'studio-shell-docs': config.public.isDocsMode,
86
+ 'is-focused': expandedPanel,
87
+ }"
84
88
  >
85
- <header
86
- class="bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800 py-3 px-4 flex justify-between items-center shrink-0 z-20"
87
- >
88
- <div class="flex items-center gap-4">
89
- <h1 class="text-xl font-bold text-primary">
90
- @nullix/zod-mongoose Studio
91
- </h1>
92
- <UBadge v-if="isLocalMode" color="success" variant="soft"
93
- >Local Mode</UBadge
94
- >
95
- <UBadge v-else color="info" variant="soft">Docs Mode</UBadge>
89
+ <header class="studio-header">
90
+ <div class="studio-brand">
91
+ <div>
92
+ <p class="studio-eyebrow">@nullix/zod-mongoose</p>
93
+ <h1>Studio</h1>
94
+ </div>
96
95
  </div>
97
- <div class="flex items-center gap-2">
98
- <UButton
99
- v-if="isDocsMode"
100
- icon="i-heroicons-play-solid"
101
- color="primary"
102
- :loading="isCompiling"
103
- @click="handleCompile"
96
+ <div class="studio-header-actions">
97
+ <span class="studio-mode">{{
98
+ config.public.isDocsMode ? "Playground" : "Local mode"
99
+ }}</span>
100
+ <span class="studio-shortcut" aria-hidden="true"
101
+ >⌘ / Ctrl + S or R</span
104
102
  >
105
- Run
106
- </UButton>
107
- <span v-else-if="isCompiling" class="text-sm text-gray-500"
108
- >Transforming...</span
103
+ <button
104
+ type="button"
105
+ class="studio-run"
106
+ :disabled="isRunning || !config.public.isExecutionEnabled"
107
+ @click="run()"
109
108
  >
109
+ {{ isRunning ? "Running…" : "Run" }}
110
+ </button>
110
111
  </div>
111
112
  </header>
112
113
 
113
- <UDashboardGroup
114
- class="flex-1 min-h-0 !relative !inset-auto"
115
- storage-key="studio-layout"
116
- >
117
- <!-- Left: Editor -->
118
- <UDashboardPanel id="editor" collapsible resizable :min-size="20">
119
- <div class="h-full relative bg-white dark:bg-gray-950">
120
- <ClientOnly>
121
- <StudioEditor v-model="sourceCode" />
122
- </ClientOnly>
123
- </div>
124
- </UDashboardPanel>
114
+ <div class="studio-subbar">
115
+ <div class="studio-subbar-copy">
116
+ <span
117
+ class="studio-pulse"
118
+ :class="{ 'is-busy': isRunning, error: error }"
119
+ />
120
+ <span>{{
121
+ isRunning
122
+ ? "Working through your schema…"
123
+ : error
124
+ ? "Run failed"
125
+ : !hasRun
126
+ ? "Waiting for first run"
127
+ : isDirty
128
+ ? "Changes ready to run"
129
+ : "Schema and data in sync"
130
+ }}</span>
131
+ </div>
132
+ <label class="studio-schema-picker">
133
+ <span>ACTIVE SCHEMA</span>
134
+ <select
135
+ :value="selectedSchema"
136
+ :disabled="schemaNames.length < 2"
137
+ aria-label="Active schema"
138
+ @change="selectSchema(($event.target as HTMLSelectElement).value)"
139
+ >
140
+ <option v-if="!schemaNames.length" value="">
141
+ Run to find schemas
142
+ </option>
143
+ <option v-for="name in schemaNames" :key="name" :value="name">
144
+ {{ name }}
145
+ </option>
146
+ </select>
147
+ </label>
148
+ </div>
125
149
 
126
- <!-- Right: Split Outputs -->
127
- <UDashboardPanel
128
- id="outputs"
129
- resizable
130
- :min-size="20"
131
- class="flex flex-col h-full bg-gray-200 dark:bg-gray-800 overflow-hidden"
150
+ <nav class="studio-mobile-nav" aria-label="Studio sections">
151
+ <button
152
+ v-for="view in mobileViews"
153
+ :key="view.key"
154
+ type="button"
155
+ :class="{ 'is-active': mobileView === view.key }"
156
+ @click="mobileView = view.key"
132
157
  >
133
- <!-- Top Right: Definition -->
134
- <UCollapsible
135
- v-model:open="isDefOpen"
136
- :class="[isDefOpen ? 'flex-1' : 'flex-none']"
137
- :ui="{ content: 'flex-1 flex flex-col min-h-0' }"
138
- class="bg-white dark:bg-gray-950 flex flex-col min-h-0"
158
+ {{ view.label }}
159
+ </button>
160
+ </nav>
161
+
162
+ <div v-if="error" class="studio-error-banner" role="alert">
163
+ <strong>Run failed</strong><span>{{ error }}</span>
164
+ </div>
165
+ <div
166
+ v-if="exampleError"
167
+ class="studio-error-banner studio-example-banner"
168
+ role="status"
169
+ >
170
+ <strong>Example unavailable</strong><span>{{ exampleError }}</span>
171
+ </div>
172
+
173
+ <main class="studio-workspace">
174
+ <div class="studio-column studio-input-column">
175
+ <StudioSourcePanel
176
+ v-model="sourceCode"
177
+ :class="{
178
+ 'is-mobile-active': mobileView === 'schema',
179
+ 'is-expanded': expandedPanel === 'schema',
180
+ }"
181
+ >
182
+ <template #actions
183
+ ><StudioExpandButton
184
+ label="schema editor"
185
+ :expanded="expandedPanel === 'schema'"
186
+ @toggle="togglePanel('schema')"
187
+ /></template>
188
+ </StudioSourcePanel>
189
+ <StudioDataPanel
190
+ v-model="inputData"
191
+ :is-running="isRunning"
192
+ :can-run="config.public.isExecutionEnabled"
193
+ :class="{
194
+ 'is-mobile-active': mobileView === 'data',
195
+ 'is-expanded': expandedPanel === 'data',
196
+ }"
197
+ @load-example="run('example')"
139
198
  >
140
- <template #default="{ open }">
141
- <div
142
- class="px-3 py-1 bg-gray-100 dark:bg-gray-900 text-xs font-semibold text-gray-600 dark:text-gray-400 border-b border-gray-200 dark:border-gray-800 z-10 flex justify-between items-center cursor-pointer select-none"
143
- >
144
- <span>extractMongooseDef()</span>
145
- <UIcon
146
- :name="
147
- open
148
- ? 'i-heroicons-chevron-down'
149
- : 'i-heroicons-chevron-right'
150
- "
151
- />
152
- </div>
153
- </template>
154
- <template #content>
155
- <div class="flex-1 relative">
156
- <ClientOnly>
157
- <StudioOutput :model-value="outputDef" />
158
- </ClientOnly>
159
- </div>
160
- </template>
161
- </UCollapsible>
199
+ <template #actions
200
+ ><StudioExpandButton
201
+ label="data editor"
202
+ :expanded="expandedPanel === 'data'"
203
+ @toggle="togglePanel('data')"
204
+ /></template>
205
+ </StudioDataPanel>
206
+ </div>
162
207
 
163
- <!-- Bottom Right: Schema -->
164
- <UCollapsible
165
- v-model:open="isSchemaOpen"
166
- :class="[isSchemaOpen ? 'flex-1' : 'flex-none']"
167
- :ui="{ content: 'flex-1 flex flex-col min-h-0' }"
168
- class="bg-white dark:bg-gray-950 flex flex-col min-h-0"
208
+ <div class="studio-column studio-output-column">
209
+ <StudioValidation
210
+ class="studio-result"
211
+ :class="{
212
+ 'is-mobile-active': mobileView === 'result',
213
+ 'is-expanded': expandedPanel === 'result',
214
+ }"
215
+ :result="validation"
216
+ :stale="isDirty"
169
217
  >
170
- <template #default="{ open }">
171
- <div
172
- class="px-3 py-1 bg-gray-100 dark:bg-gray-900 text-xs font-semibold text-gray-600 dark:text-gray-400 border-b border-gray-200 dark:border-gray-800 z-10 flex justify-between items-center cursor-pointer select-none"
173
- >
174
- <div class="flex items-center gap-2">
175
- <span>toMongooseSchema()</span>
176
- <div class="flex gap-1" @click.stop>
177
- <UButton
178
- size="xs"
179
- variant="ghost"
180
- :color="activeSchemaView === 'obj' ? 'primary' : 'neutral'"
181
- @click="activeSchemaView = 'obj'"
182
- >
183
- .obj
184
- </UButton>
185
- <UButton
186
- size="xs"
187
- variant="ghost"
188
- :color="activeSchemaView === 'tree' ? 'primary' : 'neutral'"
189
- @click="activeSchemaView = 'tree'"
190
- >
191
- .tree
192
- </UButton>
193
- <UButton
194
- size="xs"
195
- variant="ghost"
196
- :color="
197
- activeSchemaView === 'paths' ? 'primary' : 'neutral'
198
- "
199
- @click="activeSchemaView = 'paths'"
200
- >
201
- .paths
202
- </UButton>
203
- </div>
204
- </div>
205
- <UIcon
206
- :name="
207
- open
208
- ? 'i-heroicons-chevron-down'
209
- : 'i-heroicons-chevron-right'
210
- "
211
- />
212
- </div>
213
- </template>
214
- <template #content>
215
- <div class="flex-1 relative">
216
- <ClientOnly>
217
- <StudioOutput :model-value="activeSchemaViewOutput.value" />
218
- </ClientOnly>
219
- </div>
220
- </template>
221
- </UCollapsible>
222
- </UDashboardPanel>
223
- </UDashboardGroup>
218
+ <template #actions
219
+ ><StudioExpandButton
220
+ label="validation result"
221
+ :expanded="expandedPanel === 'result'"
222
+ @toggle="togglePanel('result')"
223
+ /></template>
224
+ </StudioValidation>
225
+ <StudioConversion
226
+ class="studio-mongoose"
227
+ :class="{
228
+ 'is-mobile-active': mobileView === 'mongoose',
229
+ 'is-expanded': expandedPanel === 'mongoose',
230
+ }"
231
+ :result="conversion"
232
+ >
233
+ <template #actions
234
+ ><StudioExpandButton
235
+ label="Mongoose output"
236
+ :expanded="expandedPanel === 'mongoose'"
237
+ @toggle="togglePanel('mongoose')"
238
+ /></template>
239
+ </StudioConversion>
240
+ </div>
241
+ </main>
224
242
  </div>
225
243
  </template>
@@ -0,0 +1,142 @@
1
+ import { onBeforeUnmount, onMounted, shallowRef, watch } from "vue";
2
+
3
+ export type ValidationResult = {
4
+ status: "valid" | "invalid" | "json-error" | "parse-error" | "empty";
5
+ parsed?: string;
6
+ message?: string;
7
+ issues?: { path: string; message: string; code: string }[];
8
+ };
9
+
10
+ export type ConversionResult = {
11
+ definition?: string;
12
+ schemaObj?: string;
13
+ schemaTree?: string;
14
+ schemaPaths?: string;
15
+ error?: string;
16
+ };
17
+
18
+ type RunResponse = {
19
+ schemaNames: string[];
20
+ selectedSchema: string;
21
+ validation: ValidationResult;
22
+ conversion: ConversionResult;
23
+ example?: string;
24
+ exampleError?: string;
25
+ };
26
+
27
+ const STORAGE_KEY = "zod-mongoose-studio:v1";
28
+
29
+ export function useStudioRunner() {
30
+ const config = useRuntimeConfig();
31
+ const sourceCode = shallowRef(`// z and every @nullix/zod-mongoose helper are ready to use.
32
+ const UserSchema = z.object({
33
+ name: z.string().trim().min(2),
34
+ email: z.email(),
35
+ age: z.number().int().min(18).optional(),
36
+ role: z.enum(['reader', 'admin']).default('reader'),
37
+ });
38
+
39
+ const PostSchema = z.object({
40
+ title: z.string().min(1),
41
+ published: z.boolean().default(false),
42
+ });`);
43
+ const inputData = shallowRef(`{
44
+ "name": " Ada Lovelace ",
45
+ "email": "ada@example.com",
46
+ "age": 28
47
+ }`);
48
+ const schemaNames = shallowRef<string[]>([]);
49
+ const selectedSchema = shallowRef("");
50
+ const validation = shallowRef<ValidationResult>({ status: "empty", message: "Run the studio to see parsed data." });
51
+ const conversion = shallowRef<ConversionResult>({});
52
+ const isRunning = shallowRef(false);
53
+ const isDirty = shallowRef(false);
54
+ const hasRun = shallowRef(false);
55
+ const error = shallowRef("");
56
+ const exampleError = shallowRef("");
57
+ let debounceTimer: ReturnType<typeof setTimeout> | undefined;
58
+ let runId = 0;
59
+
60
+ async function run(action: "run" | "example" = "run") {
61
+ if (!config.public.isExecutionEnabled) return;
62
+ if (debounceTimer) clearTimeout(debounceTimer);
63
+ const currentRun = ++runId;
64
+ const submittedSource = sourceCode.value;
65
+ const submittedInput = inputData.value;
66
+ isRunning.value = true;
67
+ error.value = "";
68
+ exampleError.value = "";
69
+ try {
70
+ const response = await $fetch<RunResponse>(config.public.isDocsMode ? "/api/studio/run" : "/api/parse", {
71
+ method: "POST",
72
+ body: {
73
+ sourceCode: submittedSource,
74
+ inputData: submittedInput,
75
+ selectedSchema: selectedSchema.value || undefined,
76
+ action,
77
+ },
78
+ });
79
+ if (currentRun !== runId) return;
80
+ schemaNames.value = response.schemaNames;
81
+ hasRun.value = true;
82
+ selectedSchema.value = response.selectedSchema;
83
+ validation.value = response.validation;
84
+ conversion.value = response.conversion;
85
+ exampleError.value = response.exampleError || "";
86
+ if (response.example && sourceCode.value === submittedSource && inputData.value === submittedInput) inputData.value = response.example;
87
+ isDirty.value = sourceCode.value !== submittedSource || (inputData.value !== submittedInput && inputData.value !== response.example);
88
+ } catch (caught: any) {
89
+ if (currentRun !== runId) return;
90
+ error.value = caught.data?.message || caught.message || "The studio could not run this schema.";
91
+ validation.value = { status: "empty", message: "Run the studio to see parsed data." };
92
+ conversion.value = {};
93
+ } finally {
94
+ if (currentRun === runId) isRunning.value = false;
95
+ }
96
+ }
97
+
98
+ function selectSchema(name: string) {
99
+ selectedSchema.value = name;
100
+ isDirty.value = true;
101
+ if (config.public.isExecutionEnabled) void run();
102
+ }
103
+
104
+ function saveSession() {
105
+ if (!import.meta.client) return;
106
+ try {
107
+ sessionStorage.setItem(STORAGE_KEY, JSON.stringify({
108
+ sourceCode: sourceCode.value,
109
+ inputData: inputData.value,
110
+ selectedSchema: selectedSchema.value,
111
+ }));
112
+ } catch { /* Storage can be unavailable or full; the editor remains usable. */ }
113
+ }
114
+
115
+ watch(sourceCode, () => {
116
+ isDirty.value = true;
117
+ if (!config.public.isDocsMode) {
118
+ if (debounceTimer) clearTimeout(debounceTimer);
119
+ debounceTimer = setTimeout(() => void run(), 650);
120
+ }
121
+ });
122
+ watch(inputData, () => { isDirty.value = true; }, { flush: "sync" });
123
+ watch([sourceCode, inputData, selectedSchema], saveSession, { flush: "sync" });
124
+
125
+ onMounted(() => {
126
+ try {
127
+ const saved = JSON.parse(sessionStorage.getItem(STORAGE_KEY) || "null");
128
+ if (saved && typeof saved === "object") {
129
+ if (typeof saved.sourceCode === "string" && saved.sourceCode.length <= 50_000) sourceCode.value = saved.sourceCode;
130
+ if (typeof saved.inputData === "string" && saved.inputData.length <= 100_000) inputData.value = saved.inputData;
131
+ if (typeof saved.selectedSchema === "string") selectedSchema.value = saved.selectedSchema;
132
+ }
133
+ } catch { /* Ignore invalid browser storage. */ }
134
+ if (config.public.isExecutionEnabled) void run();
135
+ });
136
+ onBeforeUnmount(() => { if (debounceTimer) clearTimeout(debounceTimer); });
137
+
138
+ return {
139
+ sourceCode, inputData, schemaNames, selectedSchema, validation, conversion,
140
+ isRunning, isDirty, hasRun, error, exampleError, run, selectSchema,
141
+ };
142
+ }
@@ -1,3 +1,3 @@
1
1
  <template>
2
- <ZodMongooseStudio />
2
+ <ZodMongooseStudio class="studio-standalone" />
3
3
  </template>
@@ -1,5 +1,6 @@
1
1
  import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
2
2
  import tsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker";
3
+ import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker";
3
4
 
4
5
  export default defineNuxtPlugin(() => {
5
6
  // Tell Monaco how to load its Web Workers via Vite
@@ -8,6 +9,7 @@ export default defineNuxtPlugin(() => {
8
9
  if (label === "typescript" || label === "javascript") {
9
10
  return new tsWorker();
10
11
  }
12
+ if (label === "json") return new jsonWorker();
11
13
  return new editorWorker();
12
14
  },
13
15
  };
package/bin/index.mjs CHANGED
@@ -34,8 +34,13 @@ async function startStudio() {
34
34
  const env = {
35
35
  ...process.env,
36
36
  PORT: port.toString(),
37
+ NITRO_HOST: "127.0.0.1",
38
+ HOST: "127.0.0.1",
37
39
  DOCS_MODE: "false", // CLI is NEVER docs mode
40
+ NUXT_PUBLIC_IS_DOCS_MODE: "false",
41
+ NUXT_PUBLIC_IS_EXECUTION_ENABLED: "true",
38
42
  LOCAL_MODE: enableFs ? "true" : "false", // Enable FS unless opted out
43
+ NUXT_PUBLIC_IS_LOCAL_MODE: enableFs ? "true" : "false",
39
44
  NODE_ENV: "production",
40
45
  };
41
46
 
package/nuxt.config.ts CHANGED
@@ -3,13 +3,14 @@ export default defineNuxtConfig({
3
3
 
4
4
  runtimeConfig: {
5
5
  public: {
6
- // By default, assume we are in a safe, sandboxed documentation environment.
7
- // The CLI script will explicitly pass DOCS_MODE="false" to disable this.
8
- isDocsMode: process.env.DOCS_MODE !== "false",
6
+ // Standalone Studio development uses the loopback-only local endpoint.
7
+ // Embedders must explicitly opt into docs mode.
8
+ isDocsMode: process.env.DOCS_MODE === "true",
9
9
 
10
10
  // By default, local file system access is OFF.
11
11
  // The CLI script will explicitly pass LOCAL_MODE="true" to enable it.
12
12
  isLocalMode: process.env.LOCAL_MODE === "true",
13
+ isExecutionEnabled: true,
13
14
  },
14
15
  },
15
16
  css: ["~/assets/css/main.css"],