@scalar/workspace-store 0.63.0 → 0.65.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/README.md +123 -0
  3. package/dist/client.d.ts +28 -0
  4. package/dist/client.d.ts.map +1 -1
  5. package/dist/client.js +320 -144
  6. package/dist/entities/auth/schema.d.ts +236 -0
  7. package/dist/entities/auth/schema.d.ts.map +1 -1
  8. package/dist/entities/auth/schema.js +3 -1
  9. package/dist/helpers/chunk-index.d.ts +113 -0
  10. package/dist/helpers/chunk-index.d.ts.map +1 -0
  11. package/dist/helpers/chunk-index.js +152 -0
  12. package/dist/helpers/external-examples.d.ts +19 -0
  13. package/dist/helpers/external-examples.d.ts.map +1 -0
  14. package/dist/helpers/external-examples.js +51 -0
  15. package/dist/helpers/for-each-path-item-operation.js +4 -4
  16. package/dist/helpers/get-example-value.d.ts +15 -0
  17. package/dist/helpers/get-example-value.d.ts.map +1 -0
  18. package/dist/helpers/get-example-value.js +28 -0
  19. package/dist/helpers/get-resolved-ref.d.ts.map +1 -1
  20. package/dist/helpers/get-resolved-ref.js +12 -1
  21. package/dist/helpers/normalize-boolean-schemas.d.ts +10 -0
  22. package/dist/helpers/normalize-boolean-schemas.d.ts.map +1 -0
  23. package/dist/helpers/normalize-boolean-schemas.js +193 -0
  24. package/dist/helpers/operation-examples.d.ts +6 -0
  25. package/dist/helpers/operation-examples.d.ts.map +1 -0
  26. package/dist/helpers/operation-examples.js +48 -0
  27. package/dist/helpers/serialize-stream-example.d.ts +8 -0
  28. package/dist/helpers/serialize-stream-example.d.ts.map +1 -0
  29. package/dist/helpers/serialize-stream-example.js +52 -0
  30. package/dist/helpers/use-external-examples.d.ts +15 -0
  31. package/dist/helpers/use-external-examples.d.ts.map +1 -0
  32. package/dist/helpers/use-external-examples.js +62 -0
  33. package/dist/mutators/index.d.ts +3 -3
  34. package/dist/mutators/operation/body.d.ts.map +1 -1
  35. package/dist/mutators/operation/body.js +9 -2
  36. package/dist/mutators/operation/parameters.d.ts.map +1 -1
  37. package/dist/mutators/operation/parameters.js +28 -9
  38. package/dist/plugins/bundler/index.d.ts +6 -2
  39. package/dist/plugins/bundler/index.d.ts.map +1 -1
  40. package/dist/plugins/bundler/index.js +20 -4
  41. package/dist/plugins/bundler/openapi-document.d.ts +6 -0
  42. package/dist/plugins/bundler/openapi-document.d.ts.map +1 -0
  43. package/dist/plugins/bundler/openapi-document.js +57 -0
  44. package/dist/request-example/builder/body/build-multipart.d.ts +29 -0
  45. package/dist/request-example/builder/body/build-multipart.d.ts.map +1 -0
  46. package/dist/request-example/builder/body/build-multipart.js +191 -0
  47. package/dist/request-example/builder/body/build-request-body.d.ts +6 -1
  48. package/dist/request-example/builder/body/build-request-body.d.ts.map +1 -1
  49. package/dist/request-example/builder/body/build-request-body.js +80 -23
  50. package/dist/request-example/builder/body/encode-multipart-body.d.ts +8 -8
  51. package/dist/request-example/builder/body/encode-multipart-body.d.ts.map +1 -1
  52. package/dist/request-example/builder/body/encode-multipart-body.js +56 -18
  53. package/dist/request-example/builder/body/get-request-body-example.d.ts.map +1 -1
  54. package/dist/request-example/builder/body/get-request-body-example.js +20 -6
  55. package/dist/request-example/builder/body/multipart-limits.d.ts +3 -0
  56. package/dist/request-example/builder/body/multipart-limits.d.ts.map +1 -0
  57. package/dist/request-example/builder/body/multipart-limits.js +2 -0
  58. package/dist/request-example/builder/body/serialize-form-property.d.ts +2 -0
  59. package/dist/request-example/builder/body/serialize-form-property.d.ts.map +1 -1
  60. package/dist/request-example/builder/body/serialize-form-property.js +3 -2
  61. package/dist/request-example/builder/body/serialize-multipart-array.d.ts +1 -1
  62. package/dist/request-example/builder/build-request.js +5 -0
  63. package/dist/request-example/builder/header/build-request-parameters.d.ts.map +1 -1
  64. package/dist/request-example/builder/header/build-request-parameters.js +5 -1
  65. package/dist/request-example/builder/header/is-param-disabled.d.ts +2 -2
  66. package/dist/request-example/builder/header/is-param-disabled.d.ts.map +1 -1
  67. package/dist/request-example/builder/header/is-param-disabled.js +5 -4
  68. package/dist/request-example/builder/header/serialize-parameter.d.ts +11 -0
  69. package/dist/request-example/builder/header/serialize-parameter.d.ts.map +1 -1
  70. package/dist/request-example/builder/header/serialize-parameter.js +22 -0
  71. package/dist/request-example/builder/helpers/get-example-from-schema.d.ts.map +1 -1
  72. package/dist/request-example/builder/helpers/get-example-from-schema.js +42 -3
  73. package/dist/request-example/builder/helpers/get-example.d.ts +2 -0
  74. package/dist/request-example/builder/helpers/get-example.d.ts.map +1 -1
  75. package/dist/request-example/builder/helpers/get-example.js +14 -9
  76. package/dist/request-example/builder/index.d.ts +2 -2
  77. package/dist/request-example/builder/index.d.ts.map +1 -1
  78. package/dist/request-example/builder/index.js +1 -1
  79. package/dist/request-example/builder/security/secret-types.d.ts +4 -1
  80. package/dist/request-example/builder/security/secret-types.d.ts.map +1 -1
  81. package/dist/request-example/context/headers.d.ts.map +1 -1
  82. package/dist/request-example/context/headers.js +3 -4
  83. package/dist/request-example/context/security/extract-security-scheme-secrets.d.ts.map +1 -1
  84. package/dist/request-example/context/security/extract-security-scheme-secrets.js +15 -0
  85. package/dist/request-example/index.d.ts +5 -2
  86. package/dist/request-example/index.d.ts.map +1 -1
  87. package/dist/request-example/index.js +4 -1
  88. package/dist/request-example/xml/serialize-xml-part.d.ts +9 -0
  89. package/dist/request-example/xml/serialize-xml-part.d.ts.map +1 -0
  90. package/dist/request-example/xml/serialize-xml-part.js +12 -0
  91. package/dist/schemas/extensions.d.ts +9 -0
  92. package/dist/schemas/extensions.d.ts.map +1 -1
  93. package/dist/schemas/extensions.js +9 -0
  94. package/dist/schemas/reference-config/index.d.ts +1 -0
  95. package/dist/schemas/reference-config/index.d.ts.map +1 -1
  96. package/dist/schemas/reference-config/settings.d.ts +1 -0
  97. package/dist/schemas/reference-config/settings.d.ts.map +1 -1
  98. package/dist/schemas/v3.1/strict/oauthflows.d.ts +26 -0
  99. package/dist/schemas/v3.1/strict/oauthflows.d.ts.map +1 -1
  100. package/dist/schemas/v3.1/strict/oauthflows.js +3 -0
  101. package/dist/schemas/v3.1/strict/openapi-document.d.ts +812 -7
  102. package/dist/schemas/v3.1/strict/openapi-document.d.ts.map +1 -1
  103. package/dist/schemas/v3.2/openapi/index.d.ts +5 -0
  104. package/dist/schemas/v3.2/openapi/index.d.ts.map +1 -1
  105. package/dist/schemas/v3.2/openapi/index.js +46 -12
  106. package/dist/schemas/v3.2/strict/openapi-document.d.ts +37 -0
  107. package/dist/schemas/v3.2/strict/openapi-document.d.ts.map +1 -1
  108. package/dist/schemas/v3.2/strict/openapi-document.js +8 -0
  109. package/dist/server.d.ts +21 -1
  110. package/dist/server.d.ts.map +1 -1
  111. package/dist/server.js +60 -34
  112. package/package.json +19 -9
  113. package/dist/schemas/v3.1/openapi/index.d.ts +0 -149
  114. package/dist/schemas/v3.1/openapi/index.d.ts.map +0 -1
  115. package/dist/schemas/v3.1/openapi/index.js +0 -732
  116. package/dist/schemas/v3.1/openapi/reference.d.ts +0 -4
  117. package/dist/schemas/v3.1/openapi/reference.d.ts.map +0 -1
  118. package/dist/schemas/v3.1/openapi/reference.js +0 -29
package/dist/client.js CHANGED
@@ -16,16 +16,20 @@ import { reactive } from 'vue';
16
16
  import YAML from 'yaml';
17
17
  import { createAuthStore } from './entities/auth/index.js';
18
18
  import { createHistoryStore } from './entities/history/index.js';
19
+ import { chunkReference, expandChunkIndex } from './helpers/chunk-index.js';
19
20
  import { deepClone } from './helpers/deep-clone.js';
20
21
  import { createDetectChangesProxy } from './helpers/detect-changes-proxy.js';
21
22
  import { bumpDocumentRevision } from './helpers/document-revision.js';
23
+ import { createExternalExampleResolver } from './helpers/external-examples.js';
22
24
  import { safeAssign } from './helpers/general.js';
23
25
  import { getFetch } from './helpers/get-fetch.js';
26
+ import { getResolvedRef } from './helpers/get-resolved-ref.js';
24
27
  import { mergeObjects } from './helpers/merge-object.js';
28
+ import { normalizeBooleanSchemas } from './helpers/normalize-boolean-schemas.js';
25
29
  import { createOverridesProxy } from './helpers/overrides-proxy.js';
26
30
  import { unpackProxyObject } from './helpers/unpack-proxy.js';
27
31
  import { createNavigation, traverseAsyncApiDocument } from './navigation/index.js';
28
- import { externalValueResolver, loadingStatus, normalizeAuthSchemes, normalizeRefs, refsEverywhere, removeExtraScalarKeys, restoreOriginalRefs, syncPathParameters, } from './plugins/bundler/index.js';
32
+ import { externalValueResolver, loadingStatus, normalizeAuthSchemes, normalizeRefs, openApiDocument, refsEverywhere, removeExtraScalarKeys, resolveOpenApiDocument, restoreOriginalRefs, syncPathParameters, } from './plugins/bundler/index.js';
29
33
  import { extensions } from './schemas/extensions.js';
30
34
  import { isAsyncApiDocument, isOpenApiDocument } from './schemas/type-guards.js';
31
35
  import { generateSchema } from './schemas/v3.2/openapi/index.js';
@@ -129,8 +133,10 @@ const purgeInternalDocumentKeys = (input) => {
129
133
  // Bundler metadata fields added temporarily during document processing
130
134
  'x-ext',
131
135
  'x-ext-urls',
136
+ 'x-scalar-original-refs',
132
137
  // Scalar internal/external metadata fields
133
138
  'x-scalar-navigation',
139
+ 'x-scalar-navigation-chunk',
134
140
  'x-scalar-is-dirty',
135
141
  'x-original-oas-version',
136
142
  'x-scalar-original-document-hash',
@@ -156,7 +162,7 @@ const purgeInternalDocumentKeys = (input) => {
156
162
  * @returns An object containing methods and getters for managing the workspace
157
163
  */
158
164
  export const createWorkspaceStore = (workspaceProps) => {
159
- const { verbose = false } = workspaceProps ?? {};
165
+ const { verbose = false, reactive: isReactiveWorkspace = true } = workspaceProps ?? {};
160
166
  const withMeasurementSync = (name, fn) => (verbose ? measureSync(name, fn) : fn());
161
167
  const withMeasurementAsync = (name, fn) => verbose ? measureAsync(name, fn) : fn();
162
168
  /**
@@ -165,6 +171,11 @@ export const createWorkspaceStore = (workspaceProps) => {
165
171
  * This can include settings that can not be persisted between sessions (not JSON serializable)
166
172
  */
167
173
  const extraDocumentConfigurations = {};
174
+ const externalExampleResolvers = new WeakMap();
175
+ const fallbackExternalExamples = createExternalExampleResolver({
176
+ fetch: workspaceProps?.fetch,
177
+ fileLoader: workspaceProps?.fileLoader,
178
+ });
168
179
  /**
169
180
  * Notifies all workspace plugins of a workspace state change event.
170
181
  *
@@ -177,20 +188,9 @@ export const createWorkspaceStore = (workspaceProps) => {
177
188
  workspaceProps?.plugins?.forEach((plugin) => plugin.hooks?.onWorkspaceStateChanges?.(event));
178
189
  };
179
190
  /**
180
- * An object containing the reactive workspace state.
181
- *
182
- * Every change to the workspace, is tracked and broadcast to all registered plugins.
183
- * allowing for change tracking.
184
- *
185
- * NOTE:
186
- * The detect changes proxy is applied separately beacause the vue reactitvity proxy have to be the outer most proxy.
187
- * If the order is reversed, Vue cannot properly track mutations, leading to lost reactivity and bugs.
188
- * By wrapping the contents with the detect changes proxy first, and then passing the result to Vue's `reactive`,
189
- * we ensure that Vue manages its reactivity as expected and our change detection hooks
190
- * are also triggered reliably.
191
- * Do not reverse this order‼️
191
+ * The plain workspace state, before any observability wrappers are applied.
192
192
  */
193
- const workspace = reactive(createDetectChangesProxy({
193
+ const workspaceState = {
194
194
  ...workspaceProps?.meta,
195
195
  documents: {},
196
196
  /**
@@ -203,88 +203,105 @@ export const createWorkspaceStore = (workspaceProps) => {
203
203
  get activeDocument() {
204
204
  return workspace.documents[getActiveDocumentName()];
205
205
  },
206
- }, {
207
- hooks: {
208
- onAfterChange(path) {
209
- const type = path[0];
210
- /** Document changes */
211
- if (type === 'documents') {
212
- // We are overriding the while documents object, ignore. This should not happen
213
- if (path.length < 2) {
214
- console.log('[WARN]: Overriding entire documents object is not supported');
206
+ };
207
+ /**
208
+ * An object containing the reactive workspace state.
209
+ *
210
+ * Every change to the workspace, is tracked and broadcast to all registered plugins.
211
+ * allowing for change tracking.
212
+ *
213
+ * With `reactive: false` the state is used as-is, so reads cost nothing beyond the plain object and
214
+ * nothing observes a write. See the `reactive` option for the full list of what stops happening.
215
+ *
216
+ * NOTE:
217
+ * The detect changes proxy is applied separately beacause the vue reactitvity proxy have to be the outer most proxy.
218
+ * If the order is reversed, Vue cannot properly track mutations, leading to lost reactivity and bugs.
219
+ * By wrapping the contents with the detect changes proxy first, and then passing the result to Vue's `reactive`,
220
+ * we ensure that Vue manages its reactivity as expected and our change detection hooks
221
+ * are also triggered reliably.
222
+ * Do not reverse this order‼️
223
+ */
224
+ const workspace = !isReactiveWorkspace
225
+ ? workspaceState
226
+ : reactive(createDetectChangesProxy(workspaceState, {
227
+ hooks: {
228
+ onAfterChange(path) {
229
+ const type = path[0];
230
+ /** Document changes */
231
+ if (type === 'documents') {
232
+ // We are overriding the while documents object, ignore. This should not happen
233
+ if (path.length < 2) {
234
+ console.log('[WARN]: Overriding entire documents object is not supported');
235
+ return;
236
+ }
237
+ const documentName = path[1];
238
+ const document = workspace.documents[documentName] ?? {
239
+ openapi: '3.1.0',
240
+ info: { title: '', version: '' },
241
+ 'x-scalar-original-document-hash': '',
242
+ };
243
+ // Every write through the store passes here, which is what makes the revision a
244
+ // complete record of the document changing.
245
+ bumpDocumentRevision(document);
246
+ const event = {
247
+ type: 'documents',
248
+ documentName,
249
+ value: unpackProxyObject(document),
250
+ path: path.slice(2),
251
+ };
252
+ // Don't mark as dirty when the document is first created or when
253
+ // only metadata-only fields change. `x-scalar-registry-meta` is
254
+ // updated programmatically (commit hash, conflict cache) and
255
+ // does not represent a user edit.
256
+ if (event.path.length > 0 && !METADATA_ONLY_DOCUMENT_KEYS.has(event.path[0])) {
257
+ // The document has been modified since it was last saved
258
+ document['x-scalar-is-dirty'] = true;
259
+ }
260
+ fireWorkspaceChange(event);
215
261
  return;
216
262
  }
217
- const documentName = path[1];
218
- const document = workspace.documents[documentName] ?? {
219
- openapi: '3.1.0',
220
- info: { title: '', version: '' },
221
- 'x-scalar-original-document-hash': '',
222
- };
223
- // Every write through the store passes here, which is what makes the revision a
224
- // complete record of the document changing.
225
- bumpDocumentRevision(document);
226
- const event = {
227
- type: 'documents',
228
- documentName,
229
- value: unpackProxyObject(document),
230
- path: path.slice(2),
231
- };
232
- // Don't mark as dirty when the document is first created or when
233
- // only metadata-only fields change. `x-scalar-registry-meta` is
234
- // updated programmatically (commit hash, conflict cache) and
235
- // does not represent a user edit.
236
- if (event.path.length > 0 && !METADATA_ONLY_DOCUMENT_KEYS.has(event.path[0])) {
237
- // The document has been modified since it was last saved
238
- document['x-scalar-is-dirty'] = true;
263
+ /** Active document changes */
264
+ if (type === 'activeDocument') {
265
+ const documentName = getActiveDocumentName();
266
+ const document = workspace.documents[documentName] ?? {
267
+ openapi: '3.1.0',
268
+ info: { title: '', version: '' },
269
+ 'x-scalar-original-document-hash': '',
270
+ };
271
+ bumpDocumentRevision(document);
272
+ // Active document changed
273
+ const event = {
274
+ type: 'documents',
275
+ documentName,
276
+ value: unpackProxyObject(document),
277
+ path: path.slice(2),
278
+ };
279
+ // Don't mark as dirty when the document is first created or when
280
+ // only metadata-only fields change. `x-scalar-registry-meta` is
281
+ // updated programmatically (commit hash, conflict cache) and
282
+ // does not represent a user edit.
283
+ if (event.path.length > 0 && !METADATA_ONLY_DOCUMENT_KEYS.has(event.path[0])) {
284
+ // The document has been modified since it was last saved
285
+ document['x-scalar-is-dirty'] = true;
286
+ }
287
+ fireWorkspaceChange(event);
288
+ return;
239
289
  }
240
- fireWorkspaceChange(event);
241
- return;
242
- }
243
- /** Active document changes */
244
- if (type === 'activeDocument') {
245
- const documentName = getActiveDocumentName();
246
- const document = workspace.documents[documentName] ?? {
247
- openapi: '3.1.0',
248
- info: { title: '', version: '' },
249
- 'x-scalar-original-document-hash': '',
250
- };
251
- bumpDocumentRevision(document);
252
- // Active document changed
290
+ /** Workspace meta changes */
291
+ const { activeDocument: _a, documents: _d, ...meta } = workspace;
253
292
  const event = {
254
- type: 'documents',
255
- documentName,
256
- value: unpackProxyObject(document),
257
- path: path.slice(2),
293
+ type: 'meta',
294
+ value: unpackProxyObject(meta, { depth: 1 }),
258
295
  };
259
- // Don't mark as dirty when the document is first created or when
260
- // only metadata-only fields change. `x-scalar-registry-meta` is
261
- // updated programmatically (commit hash, conflict cache) and
262
- // does not represent a user edit.
263
- if (event.path.length > 0 && !METADATA_ONLY_DOCUMENT_KEYS.has(event.path[0])) {
264
- // The document has been modified since it was last saved
265
- document['x-scalar-is-dirty'] = true;
266
- }
267
296
  fireWorkspaceChange(event);
268
297
  return;
269
- }
270
- /** Workspace meta changes */
271
- const { activeDocument: _a, documents: _d, ...meta } = workspace;
272
- const event = {
273
- type: 'meta',
274
- value: unpackProxyObject(meta, { depth: 1 }),
275
- };
276
- fireWorkspaceChange(event);
277
- return;
298
+ },
278
299
  },
279
- },
280
- }));
300
+ }));
281
301
  /**
282
- * An object containing all the workspace state, wrapped in a detect changes proxy.
283
- *
284
- * Every change to the workspace state (documents, configs, metadata, etc.) can be detected here,
285
- * allowing for change tracking.
302
+ * The plain document snapshot maps, before the detect changes proxy is applied.
286
303
  */
287
- const { originalDocuments, intermediateDocuments, overrides } = createDetectChangesProxy({
304
+ const documentSnapshots = {
288
305
  /**
289
306
  * Holds the original, unmodified documents as they were initially loaded into the workspace.
290
307
  * These documents are stored in their raw form—prior to any reactive wrapping, dereferencing, or bundling.
@@ -316,46 +333,57 @@ export const createWorkspaceStore = (workspaceProps) => {
316
333
  * OpenAPI document representing the overridden fields.
317
334
  */
318
335
  overrides: {},
319
- }, {
320
- hooks: {
321
- onAfterChange(path) {
322
- const type = path[0];
323
- if (!type) {
324
- return;
325
- }
326
- if (path.length < 2) {
327
- return;
328
- }
329
- const documentName = path[1];
330
- if (type === 'originalDocuments') {
331
- const event = {
332
- type,
333
- documentName: documentName,
334
- value: unpackProxyObject(originalDocuments[documentName] ?? {}),
335
- path: path.splice(2),
336
- };
337
- fireWorkspaceChange(event);
338
- }
339
- if (type === 'intermediateDocuments') {
340
- const event = {
341
- type,
342
- documentName: documentName,
343
- value: unpackProxyObject(intermediateDocuments[documentName] ?? {}),
344
- path: path.splice(2),
345
- };
346
- fireWorkspaceChange(event);
347
- }
348
- if (type === 'overrides') {
349
- const event = {
350
- type,
351
- documentName: documentName,
352
- value: unpackProxyObject(overrides[documentName] ?? {}),
353
- };
354
- fireWorkspaceChange(event);
355
- }
336
+ };
337
+ /**
338
+ * An object containing all the workspace state, wrapped in a detect changes proxy.
339
+ *
340
+ * Every change to the workspace state (documents, configs, metadata, etc.) can be detected here,
341
+ * allowing for change tracking.
342
+ *
343
+ * With `reactive: false` the maps are used as-is and a write to them notifies nobody.
344
+ */
345
+ const { originalDocuments, intermediateDocuments, overrides } = !isReactiveWorkspace
346
+ ? documentSnapshots
347
+ : createDetectChangesProxy(documentSnapshots, {
348
+ hooks: {
349
+ onAfterChange(path) {
350
+ const type = path[0];
351
+ if (!type) {
352
+ return;
353
+ }
354
+ if (path.length < 2) {
355
+ return;
356
+ }
357
+ const documentName = path[1];
358
+ if (type === 'originalDocuments') {
359
+ const event = {
360
+ type,
361
+ documentName: documentName,
362
+ value: unpackProxyObject(originalDocuments[documentName] ?? {}),
363
+ path: path.splice(2),
364
+ };
365
+ fireWorkspaceChange(event);
366
+ }
367
+ if (type === 'intermediateDocuments') {
368
+ const event = {
369
+ type,
370
+ documentName: documentName,
371
+ value: unpackProxyObject(intermediateDocuments[documentName] ?? {}),
372
+ path: path.splice(2),
373
+ };
374
+ fireWorkspaceChange(event);
375
+ }
376
+ if (type === 'overrides') {
377
+ const event = {
378
+ type,
379
+ documentName: documentName,
380
+ value: unpackProxyObject(overrides[documentName] ?? {}),
381
+ };
382
+ fireWorkspaceChange(event);
383
+ }
384
+ },
356
385
  },
357
- },
358
- });
386
+ });
359
387
  /**
360
388
  * This store is used to track the history of requests and responses for documents and operations.
361
389
  */
@@ -387,6 +415,17 @@ export const createWorkspaceStore = (workspaceProps) => {
387
415
  },
388
416
  },
389
417
  });
418
+ /**
419
+ * Whether a document needs to be wrapped in the overrides proxy.
420
+ *
421
+ * A reactive workspace always wraps, so the default mode keeps every document the same shape it has
422
+ * always had. A non-reactive workspace wraps only a document that actually has overrides: with an
423
+ * empty override map the proxy resolves to the target on every read and write anyway, so all it adds
424
+ * is a proxy hop on every nested read. Nothing outside this file reads the proxy's identity, and the
425
+ * store rebuilds the document from the current override map whenever the map changes, so a document
426
+ * that gains overrides later gains the proxy along with them.
427
+ */
428
+ const needsOverridesProxy = (documentOverrides) => isReactiveWorkspace || (isObject(documentOverrides) && Object.keys(documentOverrides).length > 0);
390
429
  /**
391
430
  * Returns the name of the currently active document in the workspace.
392
431
  * The active document is determined by the 'x-scalar-active-document' metadata field,
@@ -440,6 +479,10 @@ export const createWorkspaceStore = (workspaceProps) => {
440
479
  const { name } = input;
441
480
  const meta = deepClone(input.meta);
442
481
  const clonedRawInputDocument = withMeasurementSync('deepClone', () => deepClone(input.document));
482
+ // A compact sparse document is expanded back into per-node chunk references before anything
483
+ // else reads it, so every step below — including the check for a server-generated navigation —
484
+ // sees the document a non-compact server store would have sent.
485
+ withMeasurementSync('expandChunkIndex', () => expandChunkIndex(clonedRawInputDocument));
443
486
  withMeasurementSync('initialize', () => {
444
487
  if (input.initialize !== false) {
445
488
  // Store the original document in the originalDocuments map
@@ -502,9 +545,10 @@ export const createWorkspaceStore = (workspaceProps) => {
502
545
  const navigation = traverseAsyncApiDocument(name, asyncApiDocument, navigationOptions);
503
546
  asyncApiDocument[extensions.document.navigation] = navigation;
504
547
  }
505
- workspace.documents[name] = createOverridesProxy(asyncApiDocument, {
506
- overrides: unpackProxyObject(overrides[name]),
507
- });
548
+ const asyncApiOverrides = unpackProxyObject(overrides[name]);
549
+ workspace.documents[name] = needsOverridesProxy(asyncApiOverrides)
550
+ ? createOverridesProxy(asyncApiDocument, { overrides: asyncApiOverrides })
551
+ : asyncApiDocument;
508
552
  return;
509
553
  }
510
554
  const inputDocument = withMeasurementSync('upgrade', () => upgrade(deepClone(clonedRawInputDocument), '3.1'));
@@ -514,7 +558,7 @@ export const createWorkspaceStore = (workspaceProps) => {
514
558
  'x-original-oas-version': originalDocuments[name]?.openapi ?? originalDocuments[name]?.swagger,
515
559
  'x-scalar-original-document-hash': input.documentHash,
516
560
  'x-scalar-original-source-url': input.documentSource,
517
- }, { showInternal: true });
561
+ }, { showInternal: true, documentUri: resolveOpenApiDocument(inputDocument, input.documentSource ?? '/')?.baseUri });
518
562
  // If the document navigation is not already present, bundle the entire document to resolve all references.
519
563
  // This typically applies when the document is not preprocessed by the server and needs local reference resolution.
520
564
  // We need to bundle document first before we validate, so we can also validate the external references
@@ -523,8 +567,9 @@ export const createWorkspaceStore = (workspaceProps) => {
523
567
  treeShake: false,
524
568
  plugins: [
525
569
  ...loaders,
570
+ openApiDocument(),
526
571
  normalizeRefs(),
527
- externalValueResolver(),
572
+ externalValueResolver({ lazy: true }),
528
573
  refsEverywhere(),
529
574
  normalizeAuthSchemes(),
530
575
  syncPathParameters(),
@@ -533,7 +578,7 @@ export const createWorkspaceStore = (workspaceProps) => {
533
578
  origin: input.documentSource, // use the document origin (if provided) as the base URL for resolution
534
579
  }));
535
580
  // We coerce the values only when the document is not preprocessed by the server-side-store
536
- const coerced = withMeasurementSync('coerceValue', () => coerce(openapiSchema, deepClone(strictDocument)));
581
+ const coerced = withMeasurementSync('coerceValue', () => coerce(openapiSchema, normalizeBooleanSchemas(deepClone(strictDocument))));
537
582
  withMeasurementSync('mergeObjects', () => mergeObjects(strictDocument, coerced));
538
583
  }
539
584
  const isValid = Value.Check(OpenAPIDocumentSchemaStrict, strictDocument);
@@ -555,9 +600,13 @@ export const createWorkspaceStore = (workspaceProps) => {
555
600
  // Create a proxied document with magic proxy and apply any overrides, then store it in the workspace documents map
556
601
  // We create a new proxy here in order to hide internal properties after validation and processing
557
602
  // This ensures that the workspace document only exposes the intended OpenAPI properties and extensions
558
- workspace.documents[name] = createOverridesProxy(createMagicProxy(getRaw(strictDocument)), {
559
- overrides: unpackProxyObject(overrides[name]),
603
+ const documentOverrides = unpackProxyObject(overrides[name]);
604
+ const magicDocument = createMagicProxy(getRaw(strictDocument), {
605
+ documentUri: resolveOpenApiDocument(getRaw(strictDocument), input.documentSource ?? '/')?.baseUri,
560
606
  });
607
+ workspace.documents[name] = needsOverridesProxy(documentOverrides)
608
+ ? createOverridesProxy(magicDocument, { overrides: documentOverrides })
609
+ : magicDocument;
561
610
  }
562
611
  // Asynchronously adds a new document to the workspace by loading and validating the input.
563
612
  // If loading fails, a placeholder error document is added instead.
@@ -650,9 +699,11 @@ export const createWorkspaceStore = (workspaceProps) => {
650
699
  // If the document does not exist, return null
651
700
  return null;
652
701
  }
653
- // Reverse all external references and restore original $refs
702
+ // This is the shared cleanup boundary for editing and saving. Both JSON and YAML
703
+ // exports read the cleaned saved baseline, so serializers need no marker filtering.
704
+ // Reverse all external references and restore original $refs.
654
705
  const original = (await bundle(deepClone(rawDocument), {
655
- plugins: [restoreOriginalRefs(), removeExtraScalarKeys()],
706
+ plugins: [openApiDocument(), restoreOriginalRefs(), removeExtraScalarKeys()],
656
707
  treeShake: false,
657
708
  urlMap: true,
658
709
  }));
@@ -688,10 +739,106 @@ export const createWorkspaceStore = (workspaceProps) => {
688
739
  document[extensions.document.navigation] = navigation;
689
740
  return true;
690
741
  };
742
+ /**
743
+ * Fetches the chunk a compact document keeps its navigation children in.
744
+ *
745
+ * The reference is bundled on an object of its own rather than in the document, so nothing of it
746
+ * is left behind: the chunk lands in that object's `x-ext` and is dropped along with it, and the
747
+ * navigation is never a reference, not even while the request is in flight. Navigation has one
748
+ * owner, so the children go on the document as they are and there is nothing for a shared `x-ext`
749
+ * entry to save.
750
+ *
751
+ * `depth: 0` stops the bundler at the reference itself, which is as far as a navigation chunk
752
+ * goes: its entries address the document through their own `ref` strings and carry no `$ref`.
753
+ *
754
+ * The chunk is handed back unwrapped: what it holds goes on the document, and a magic proxy there
755
+ * would enumerate a virtual `$ref-value` and stop the document being structured-cloned into
756
+ * storage. Unwrapping one level is enough, because the proxy wraps lazily.
757
+ *
758
+ * @returns The navigation the chunk holds, or `undefined` when it could not be loaded — the
759
+ * bundler reports that failure the same way it reports an unreachable component chunk.
760
+ */
761
+ const fetchNavigationChunk = async (documentName, ref, origin) => {
762
+ const holder = createMagicProxy(chunkReference(ref));
763
+ await bundle(getRaw(holder), {
764
+ plugins: [
765
+ fetchUrls({
766
+ fetch: extraDocumentConfigurations[documentName]?.fetch ?? workspaceProps?.fetch,
767
+ limit: EXTERNAL_FETCH_CONCURRENCY_LIMIT,
768
+ }),
769
+ ...(workspaceProps?.fileLoader ? [workspaceProps.fileLoader] : []),
770
+ ],
771
+ treeShake: false,
772
+ origin,
773
+ depth: 0,
774
+ urlMap: true,
775
+ });
776
+ return getRaw(getResolvedRef(holder));
777
+ };
778
+ /** Share concurrent requests only within the same document instance, including after a workspace reload. */
779
+ const navigationChildrenLoads = new WeakMap();
780
+ /**
781
+ * Loads a compact document's navigation children and assigns them onto its navigation in place.
782
+ *
783
+ * Only a compact document carries `x-scalar-navigation-chunk`, and only until its children are
784
+ * loaded, so the key answers both "is there anything to load" and "has it already happened". It
785
+ * travels with the document, which is what lets a workspace exported before the children were
786
+ * loaded still load them once it has been imported into another store. A failed load leaves the
787
+ * key in place, so asking again retries.
788
+ */
789
+ const loadNavigationChildren = async (documentName) => {
790
+ const document = workspace.documents[documentName];
791
+ if (!isOpenApiDocument(document)) {
792
+ return;
793
+ }
794
+ const ref = document[extensions.document.navigationChunk];
795
+ const navigation = document[extensions.document.navigation];
796
+ if (ref === undefined || navigation === undefined) {
797
+ return;
798
+ }
799
+ const pending = navigationChildrenLoads.get(document);
800
+ if (pending) {
801
+ return pending;
802
+ }
803
+ const load = (async () => {
804
+ const chunk = await fetchNavigationChunk(documentName, ref, document['x-scalar-original-source-url']);
805
+ // A replaced or deleted document must not publish changes from a stale request.
806
+ if (chunk === undefined || workspace.documents[documentName] !== document) {
807
+ return;
808
+ }
809
+ // Assigned through the store's document, so the write is observed: a Vue effect reading the
810
+ // children re-runs, and the workspace plugins see the document change.
811
+ navigation.children = chunk.children ?? [];
812
+ delete document[extensions.document.navigationChunk];
813
+ })();
814
+ navigationChildrenLoads.set(document, load);
815
+ try {
816
+ await load;
817
+ }
818
+ finally {
819
+ navigationChildrenLoads.delete(document);
820
+ }
821
+ };
691
822
  // Cache to track visited nodes during reference resolution to prevent bundling the same subtree multiple times
692
823
  // This is needed because we are doing partial bundle operations
693
824
  const visitedNodesCache = new Set();
694
825
  return {
826
+ externalExamples: (documentName) => {
827
+ const name = documentName ?? getActiveDocumentName();
828
+ const document = workspace.documents[name];
829
+ if (!document)
830
+ return fallbackExternalExamples;
831
+ const existing = externalExampleResolvers.get(document);
832
+ if (existing)
833
+ return existing;
834
+ const resolver = createExternalExampleResolver({
835
+ origin: document['x-scalar-original-source-url'],
836
+ fileLoader: workspaceProps?.fileLoader,
837
+ fetch: extraDocumentConfigurations[name]?.fetch ?? workspaceProps?.fetch,
838
+ });
839
+ externalExampleResolvers.set(document, resolver);
840
+ return resolver;
841
+ },
695
842
  get workspace() {
696
843
  return workspace;
697
844
  },
@@ -743,8 +890,15 @@ export const createWorkspaceStore = (workspaceProps) => {
743
890
  initialize: false,
744
891
  });
745
892
  },
746
- resolve: (path) => {
893
+ resolve: async (path) => {
747
894
  const activeDocument = workspace.activeDocument;
895
+ // The navigation is never a reference: a compact document externalizes its children alone, and
896
+ // loading them is all there is to resolve here. Everything a navigation entry points at, it
897
+ // points at with a `ref` string of its own, so the bundler has nothing left to follow.
898
+ if (path[0] === extensions.document.navigation) {
899
+ await loadNavigationChildren(getActiveDocumentName());
900
+ return getValueAtPath(activeDocument, path);
901
+ }
748
902
  const target = getValueAtPath(activeDocument, path);
749
903
  if (!isObject(target)) {
750
904
  console.error(`Invalid path provided for resolution. Path: [${path.join(', ')}]. Found value of type: ${typeof target}. Expected an object.`);
@@ -761,7 +915,16 @@ export const createWorkspaceStore = (workspaceProps) => {
761
915
  root: activeDocument,
762
916
  origin: activeDocument?.['x-scalar-original-source-url'],
763
917
  treeShake: false,
764
- plugins: [fetchUrls(), loadingStatus(), externalValueResolver()],
918
+ plugins: [
919
+ openApiDocument(),
920
+ fetchUrls({
921
+ fetch: extraDocumentConfigurations[getActiveDocumentName()]?.fetch ?? workspaceProps?.fetch,
922
+ limit: EXTERNAL_FETCH_CONCURRENCY_LIMIT,
923
+ }),
924
+ ...(workspaceProps?.fileLoader ? [workspaceProps.fileLoader] : []),
925
+ loadingStatus(),
926
+ externalValueResolver({ lazy: true }),
927
+ ],
765
928
  urlMap: true,
766
929
  visitedNodes: visitedNodesCache,
767
930
  });
@@ -855,12 +1018,25 @@ export const createWorkspaceStore = (workspaceProps) => {
855
1018
  };
856
1019
  },
857
1020
  loadWorkspace(input) {
858
- safeAssign(workspace.documents, Object.fromEntries(Object.entries(input.documents).map(([name, doc]) => [
859
- name,
860
- createOverridesProxy(createMagicProxy(doc), {
861
- overrides: input.overrides[name],
862
- }),
863
- ])));
1021
+ // A workspace from a compact server store carries an index rather than per-node chunk
1022
+ // references; expanded here for the same reason `addInMemoryDocument` expands.
1023
+ for (const document of Object.values(input.documents)) {
1024
+ expandChunkIndex(document);
1025
+ }
1026
+ safeAssign(workspace.documents, Object.fromEntries(Object.entries(input.documents).map(([name, doc]) => {
1027
+ // Hydration only rewraps: an exported document has already been upgraded, bundled, coerced
1028
+ // and given its navigation, so nothing here re-processes it.
1029
+ const magicDocument = createMagicProxy(doc, {
1030
+ documentUri: resolveOpenApiDocument(doc, doc['x-scalar-original-source-url'] ?? '/')?.baseUri,
1031
+ });
1032
+ const documentOverrides = input.overrides[name];
1033
+ return [
1034
+ name,
1035
+ needsOverridesProxy(documentOverrides)
1036
+ ? createOverridesProxy(magicDocument, { overrides: documentOverrides })
1037
+ : magicDocument,
1038
+ ];
1039
+ })));
864
1040
  safeAssign(originalDocuments, input.originalDocuments);
865
1041
  safeAssign(intermediateDocuments, input.intermediateDocuments);
866
1042
  safeAssign(overrides, input.overrides);