@scalar/workspace-store 0.63.0 → 0.64.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 (36) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +88 -0
  3. package/dist/client.d.ts +28 -0
  4. package/dist/client.d.ts.map +1 -1
  5. package/dist/client.js +216 -139
  6. package/dist/helpers/chunk-index.d.ts +95 -0
  7. package/dist/helpers/chunk-index.d.ts.map +1 -0
  8. package/dist/helpers/chunk-index.js +139 -0
  9. package/dist/helpers/external-examples.d.ts +17 -0
  10. package/dist/helpers/external-examples.d.ts.map +1 -0
  11. package/dist/helpers/external-examples.js +50 -0
  12. package/dist/helpers/operation-examples.d.ts +6 -0
  13. package/dist/helpers/operation-examples.d.ts.map +1 -0
  14. package/dist/helpers/operation-examples.js +45 -0
  15. package/dist/helpers/use-external-examples.d.ts +15 -0
  16. package/dist/helpers/use-external-examples.d.ts.map +1 -0
  17. package/dist/helpers/use-external-examples.js +55 -0
  18. package/dist/mutators/operation/parameters.d.ts.map +1 -1
  19. package/dist/mutators/operation/parameters.js +28 -9
  20. package/dist/plugins/bundler/index.d.ts +5 -2
  21. package/dist/plugins/bundler/index.d.ts.map +1 -1
  22. package/dist/plugins/bundler/index.js +12 -3
  23. package/dist/request-example/builder/header/is-param-disabled.d.ts +2 -2
  24. package/dist/request-example/builder/header/is-param-disabled.d.ts.map +1 -1
  25. package/dist/request-example/builder/header/is-param-disabled.js +5 -4
  26. package/dist/request-example/builder/helpers/get-example.d.ts +2 -0
  27. package/dist/request-example/builder/helpers/get-example.d.ts.map +1 -1
  28. package/dist/request-example/builder/helpers/get-example.js +9 -7
  29. package/dist/request-example/context/headers.d.ts.map +1 -1
  30. package/dist/request-example/context/headers.js +3 -4
  31. package/dist/schemas/v3.2/strict/openapi-document.d.ts.map +1 -1
  32. package/dist/schemas/v3.2/strict/openapi-document.js +17 -1
  33. package/dist/server.d.ts +16 -0
  34. package/dist/server.d.ts.map +1 -1
  35. package/dist/server.js +34 -9
  36. package/package.json +3 -3
@@ -0,0 +1,139 @@
1
+ import { isHttpMethod } from '@scalar/helpers/http/is-http-method';
2
+ import { isObject } from '@scalar/helpers/object/is-object';
3
+ import { escapeJsonPointer } from '@scalar/json-magic/helpers/escape-json-pointer';
4
+ import { encodeChunkName } from '../helpers/encode-chunk-name.js';
5
+ /**
6
+ * The extension key a compact sparse document carries its chunk index under.
7
+ *
8
+ * It exists on the wire only: the client expands the index back into per-node references while the
9
+ * document is ingested and removes the key, so nothing downstream ever sees it.
10
+ */
11
+ export const CHUNK_INDEX_KEY = 'x-scalar-chunk-index';
12
+ /** Stands in for an externalized operation on a compact path item. */
13
+ const OPERATION_PLACEHOLDER = 0;
14
+ /** Matches a `{slot}` in a template, or a doubled brace standing for a literal one. */
15
+ const TEMPLATE_SLOT = /\{\{|\}\}|\{(\w+)\}/g;
16
+ /**
17
+ * Fills the `{slot}`s of a chunk-reference template.
18
+ *
19
+ * `{{` and `}}` stand for literal braces, so text the writer inlined into a template — a document
20
+ * name, a base URL — may contain braces without being read back as a slot. An unknown slot is left
21
+ * alone rather than blanked, so a template from a newer writer fails visibly instead of resolving
22
+ * to the wrong chunk.
23
+ */
24
+ export const fillChunkRef = (template, values = {}) => template.replace(TEMPLATE_SLOT, (match, slot) => slot === undefined ? match.charAt(0) : (values[slot] ?? match));
25
+ /** Escapes text inlined into a template verbatim, so its braces are not read back as slots. */
26
+ const escapeTemplateLiteral = (text) => text.replaceAll('{', '{{').replaceAll('}', '}}');
27
+ const identity = (value) => value;
28
+ /**
29
+ * How each template slot is spelled, per mode.
30
+ *
31
+ * Static references name files, so their variable segments go through `encodeChunkName` — the same
32
+ * call `generateWorkspaceChunks` makes when it writes those files. SSR references name pointers
33
+ * into the store's in-memory assets, which `get()` looks up with JSON Pointer segments, so only the
34
+ * path is escaped. A method is never encoded in either mode: it is written as it was read, and
35
+ * `isHttpMethod` keeps it to letters.
36
+ *
37
+ * Both sides read the table from here. A reader that encoded a segment differently would build a
38
+ * reference to a chunk that does not exist.
39
+ */
40
+ const SLOT_ENCODERS = {
41
+ static: { type: encodeChunkName, name: encodeChunkName, path: encodeChunkName, method: identity },
42
+ ssr: { type: identity, name: identity, path: escapeJsonPointer, method: identity },
43
+ };
44
+ /**
45
+ * Builds the reference templates for one document.
46
+ *
47
+ * Filled, these produce exactly the references `externalizeComponentReferences` and
48
+ * `externalizePathReferences` write, so a client that expands the index lands on the same chunk.
49
+ */
50
+ export const chunkRefTemplates = (meta) => {
51
+ if (meta.mode === 'ssr') {
52
+ const base = `${escapeTemplateLiteral(meta.baseUrl)}/${escapeTemplateLiteral(meta.name)}`;
53
+ return {
54
+ components: `${base}/components/{type}/{name}#`,
55
+ operations: `${base}/operations/{path}/{method}#`,
56
+ navigation: `${base}/navigation#`,
57
+ };
58
+ }
59
+ const base = `./chunks/${escapeTemplateLiteral(encodeChunkName(meta.name))}`;
60
+ return {
61
+ components: `${base}/components/{type}/{name}.json#`,
62
+ operations: `${base}/operations/{path}/{method}.json#`,
63
+ navigation: `${base}/navigation.json#`,
64
+ };
65
+ };
66
+ /** The reference a lazily loaded chunk is reached through. */
67
+ export const chunkReference = (ref) => ({ '$ref': ref, $global: true });
68
+ /**
69
+ * Compacts a sparse document's `components` and `paths` into an index.
70
+ *
71
+ * Takes the sections the externalizers produced rather than the document itself, so the index is
72
+ * built from the very references it replaces and the two cannot come to describe different sets of
73
+ * chunks.
74
+ */
75
+ export const buildChunkIndex = ({ mode, refs, components, paths, }) => ({
76
+ mode,
77
+ refs,
78
+ components: Object.fromEntries(Object.entries(components).map(([type, names]) => [type, Object.keys(names)])),
79
+ paths: Object.fromEntries(Object.entries(paths).map(([path, pathItem]) => [
80
+ path,
81
+ Object.fromEntries(Object.entries(pathItem).map(([key, value]) => [key, isHttpMethod(key) ? OPERATION_PLACEHOLDER : value])),
82
+ ])),
83
+ });
84
+ /** Whether a value is shaped like an index this build knows how to expand. */
85
+ const isChunkIndex = (value) => isObject(value) &&
86
+ (value['mode'] === 'static' || value['mode'] === 'ssr') &&
87
+ isObject(value['refs']) &&
88
+ isObject(value['components']) &&
89
+ isObject(value['paths']);
90
+ /**
91
+ * Expands a compact sparse document into the one a non-compact server store would have sent.
92
+ *
93
+ * Mutates the document in place and drops the index key, so what the rest of the store sees is an
94
+ * ordinary sparse document: `resolve()`, the bundler and anything enumerating `paths` or
95
+ * `components` are looking at the shape they always have.
96
+ *
97
+ * A document without an index is left alone, which is every document a non-compact server store or
98
+ * an author produces.
99
+ *
100
+ * @param document - The document to expand, mutated in place
101
+ * @returns Whether an index was found and expanded
102
+ */
103
+ export const expandChunkIndex = (document) => {
104
+ if (!isObject(document) || document[CHUNK_INDEX_KEY] === undefined) {
105
+ return false;
106
+ }
107
+ const index = document[CHUNK_INDEX_KEY];
108
+ if (!isChunkIndex(index)) {
109
+ // The key is dropped rather than passed on: it is not part of any document a consumer should
110
+ // see, and the sections it stands for are missing either way, so there is nothing a partial
111
+ // expansion could recover.
112
+ delete document[CHUNK_INDEX_KEY];
113
+ console.warn(`Ignoring an unreadable "${CHUNK_INDEX_KEY}"; this document's chunks cannot be resolved.`);
114
+ return false;
115
+ }
116
+ const encoders = SLOT_ENCODERS[index.mode];
117
+ // Define own properties so document keys such as `__proto__` remain data, without invoking
118
+ // inherited setters or changing the prototype of any expanded object.
119
+ const components = Object.fromEntries(Object.entries(index.components).map(([type, names]) => [
120
+ type,
121
+ Object.fromEntries(names.map((name) => [
122
+ name,
123
+ chunkReference(fillChunkRef(index.refs.components, { type: encoders.type(type), name: encoders.name(name) })),
124
+ ])),
125
+ ]));
126
+ const paths = Object.fromEntries(Object.entries(index.paths).map(([path, pathItem]) => [
127
+ path,
128
+ Object.fromEntries(Object.entries(pathItem).map(([key, value]) => [
129
+ key,
130
+ isHttpMethod(key)
131
+ ? chunkReference(fillChunkRef(index.refs.operations, { path: encoders.path(path), method: encoders.method(key) }))
132
+ : value,
133
+ ])),
134
+ ]));
135
+ document['components'] = components;
136
+ document['paths'] = paths;
137
+ delete document[CHUNK_INDEX_KEY];
138
+ return true;
139
+ };
@@ -0,0 +1,17 @@
1
+ import { type LoaderPlugin } from '@scalar/json-magic/bundle';
2
+ import { fetchUrls } from '@scalar/json-magic/bundle/plugins/browser';
3
+ import type { ExampleObject } from '../schemas/v3.2/strict/openapi-document.js';
4
+ /** Download state lives outside the document so reading an example never creates an edit. */
5
+ export type ExternalExampleState = {
6
+ status: 'idle' | 'loading' | 'loaded' | 'error';
7
+ value?: unknown;
8
+ load: () => Promise<void>;
9
+ };
10
+ /** A resolver belongs to one document revision and its configured transport. */
11
+ export type ExternalExampleResolver = (example: ExampleObject) => ExternalExampleState;
12
+ /** Cache external payloads and in-flight requests without changing authored examples. */
13
+ export declare const createExternalExampleResolver: (options?: Parameters<typeof fetchUrls>[0] & {
14
+ origin?: string;
15
+ fileLoader?: LoaderPlugin;
16
+ }) => ExternalExampleResolver;
17
+ //# sourceMappingURL=external-examples.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"external-examples.d.ts","sourceRoot":"","sources":["../../src/helpers/external-examples.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,YAAY,EAAwB,MAAM,2BAA2B,CAAA;AACnF,OAAO,EAAE,SAAS,EAAE,MAAM,2CAA2C,CAAA;AAGrE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wCAAwC,CAAA;AAE3E,6FAA6F;AAC7F,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,QAAQ,GAAG,OAAO,CAAA;IAC/C,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC1B,CAAA;AAED,gFAAgF;AAChF,MAAM,MAAM,uBAAuB,GAAG,CAAC,OAAO,EAAE,aAAa,KAAK,oBAAoB,CAAA;AAEtF,yFAAyF;AACzF,eAAO,MAAM,6BAA6B,GACxC,UAAS,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC,CAAC,CAAC,GAAG;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,YAAY,CAAA;CAAO,KAC7F,uBAuCF,CAAA"}
@@ -0,0 +1,50 @@
1
+ import { resolveReferencePath } from '@scalar/json-magic/bundle';
2
+ import { fetchUrls } from '@scalar/json-magic/bundle/plugins/browser';
3
+ import { shallowReactive } from 'vue';
4
+ /** Cache external payloads and in-flight requests without changing authored examples. */
5
+ export const createExternalExampleResolver = (options = {}) => {
6
+ const loader = fetchUrls({ ...options, limit: 10 });
7
+ const cache = new Map();
8
+ return (example) => {
9
+ if (example.value !== undefined || !example.externalValue) {
10
+ return { status: 'loaded', value: example.value, load: () => Promise.resolve() };
11
+ }
12
+ const url = resolveReferencePath(options.origin ?? '', example.externalValue) ?? example.externalValue;
13
+ const cached = cache.get(url);
14
+ if (cached)
15
+ return cached;
16
+ let pending;
17
+ const state = shallowReactive({
18
+ status: 'idle',
19
+ load: () => {
20
+ if (pending)
21
+ return pending;
22
+ if (state.status === 'loaded')
23
+ return Promise.resolve();
24
+ state.status = 'loading';
25
+ pending = (async () => {
26
+ try {
27
+ const selectedLoader = [loader, options.fileLoader].find((candidate) => candidate?.validate(url));
28
+ const result = await selectedLoader?.exec(url);
29
+ if (result?.ok) {
30
+ state.value = result.data === undefined ? result.raw : result.data;
31
+ state.status = 'loaded';
32
+ }
33
+ else {
34
+ state.status = 'error';
35
+ }
36
+ }
37
+ catch {
38
+ state.status = 'error';
39
+ }
40
+ finally {
41
+ pending = undefined;
42
+ }
43
+ })();
44
+ return pending;
45
+ },
46
+ });
47
+ cache.set(url, state);
48
+ return state;
49
+ };
50
+ };
@@ -0,0 +1,6 @@
1
+ import type { ExampleObject, OperationObject } from '../schemas/v3.2/strict/openapi-document.js';
2
+ /** The request body and parameter examples needed by a single request, never its responses. */
3
+ export declare const getOperationExamples: (operation: OperationObject, key: string, contentType?: string) => (ExampleObject | undefined)[];
4
+ /** Overlay fetched values for request consumers without persisting them as document edits. */
5
+ export declare const resolveOperationExamples: (operation: OperationObject, key: string, contentType: string | undefined, resolve: (example: ExampleObject | undefined) => ExampleObject | undefined) => OperationObject;
6
+ //# sourceMappingURL=operation-examples.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"operation-examples.d.ts","sourceRoot":"","sources":["../../src/helpers/operation-examples.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAmB,eAAe,EAAE,MAAM,wCAAwC,CAAA;AAE7G,+FAA+F;AAC/F,eAAO,MAAM,oBAAoB,GAC/B,WAAW,eAAe,EAC1B,KAAK,MAAM,EACX,cAAc,MAAM,KACnB,CAAC,aAAa,GAAG,SAAS,CAAC,EAS7B,CAAA;AAED,8FAA8F;AAC9F,eAAO,MAAM,wBAAwB,GACnC,WAAW,eAAe,EAC1B,KAAK,MAAM,EACX,aAAa,MAAM,GAAG,SAAS,EAC/B,SAAS,CAAC,OAAO,EAAE,aAAa,GAAG,SAAS,KAAK,aAAa,GAAG,SAAS,KACzE,eA6BF,CAAA"}
@@ -0,0 +1,45 @@
1
+ import { getResolvedRef } from '../helpers/get-resolved-ref.js';
2
+ import { getExample } from '../request-example/builder/helpers/get-example.js';
3
+ /** The request body and parameter examples needed by a single request, never its responses. */
4
+ export const getOperationExamples = (operation, key, contentType) => {
5
+ const body = getResolvedRef(operation.requestBody);
6
+ return [
7
+ body ? getExample(body, key, contentType) : undefined,
8
+ ...(operation.parameters ?? []).map((parameter) => {
9
+ const resolved = getResolvedRef(parameter);
10
+ return resolved ? getExample(resolved, key, undefined) : undefined;
11
+ }),
12
+ ];
13
+ };
14
+ /** Overlay fetched values for request consumers without persisting them as document edits. */
15
+ export const resolveOperationExamples = (operation, key, contentType, resolve) => {
16
+ const mapMedia = (media) => {
17
+ const selected = key || Object.keys(media.examples ?? {})[0];
18
+ const example = selected ? getResolvedRef(media.examples?.[selected]) : undefined;
19
+ const resolved = resolve(example);
20
+ return selected && resolved && resolved !== example
21
+ ? { ...media, examples: { ...media.examples, [selected]: resolved } }
22
+ : media;
23
+ };
24
+ const body = getResolvedRef(operation.requestBody);
25
+ const type = contentType ?? Object.keys(body?.content ?? {})[0];
26
+ return {
27
+ ...operation,
28
+ ...(body && type && body.content[type]
29
+ ? {
30
+ requestBody: { ...body, content: { ...body.content, [type]: mapMedia(body.content[type]) } },
31
+ }
32
+ : {}),
33
+ parameters: operation.parameters?.map((parameter) => {
34
+ const resolved = getResolvedRef(parameter);
35
+ if (!resolved)
36
+ return parameter;
37
+ const content = 'content' in resolved ? resolved.content : undefined;
38
+ const mediaType = Object.keys(content ?? {})[0];
39
+ if (mediaType && content?.[mediaType]) {
40
+ return { ...resolved, content: { ...content, [mediaType]: mapMedia(content[mediaType]) } };
41
+ }
42
+ return { ...resolved, ...mapMedia(resolved) };
43
+ }),
44
+ };
45
+ };
@@ -0,0 +1,15 @@
1
+ import { type ComponentPublicInstance, type ComputedRef, type InjectionKey, type Ref } from 'vue';
2
+ import { type ExternalExampleResolver } from '../helpers/external-examples.js';
3
+ import type { ExampleObject } from '../schemas/v3.2/strict/openapi-document.js';
4
+ /** Supply the current document's resolver so consumers share downloads and invalidation. */
5
+ export declare const EXTERNAL_EXAMPLES: InjectionKey<() => ExternalExampleResolver>;
6
+ /** Observe current visibility, rather than mounting, to avoid fetching every operation on a long page. */
7
+ export declare const useExampleVisibility: (target: Ref<Element | ComponentPublicInstance | null>) => Ref<boolean>;
8
+ /** Resolve only the selected examples and expose immutable views of their downloaded values. */
9
+ export declare const useExternalExamples: (examples: () => (ExampleObject | undefined)[], enabled?: () => boolean, resolver?: () => ExternalExampleResolver) => {
10
+ pending: ComputedRef<boolean>;
11
+ failed: ComputedRef<boolean>;
12
+ resolve: (example: ExampleObject | undefined) => ExampleObject | undefined;
13
+ retry: () => Promise<void>;
14
+ };
15
+ //# sourceMappingURL=use-external-examples.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-external-examples.d.ts","sourceRoot":"","sources":["../../src/helpers/use-external-examples.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,uBAAuB,EAC5B,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,GAAG,EAOT,MAAM,KAAK,CAAA;AAEZ,OAAO,EAAE,KAAK,uBAAuB,EAAiC,MAAM,6BAA6B,CAAA;AACzG,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wCAAwC,CAAA;AAE3E,4FAA4F;AAC5F,eAAO,MAAM,iBAAiB,EAAE,YAAY,CAAC,MAAM,uBAAuB,CAA+B,CAAA;AAEzG,0GAA0G;AAC1G,eAAO,MAAM,oBAAoB,GAAI,QAAQ,GAAG,CAAC,OAAO,GAAG,uBAAuB,GAAG,IAAI,CAAC,KAAG,GAAG,CAAC,OAAO,CAuBvG,CAAA;AAED,gGAAgG;AAChG,eAAO,MAAM,mBAAmB,GAC9B,UAAU,MAAM,CAAC,aAAa,GAAG,SAAS,CAAC,EAAE,EAC7C,UAAS,MAAM,OAAoB,EACnC,WAAW,MAAM,uBAAuB,KACvC;IACD,OAAO,EAAE,WAAW,CAAC,OAAO,CAAC,CAAA;IAC7B,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,CAAA;IAC5B,OAAO,EAAE,CAAC,OAAO,EAAE,aAAa,GAAG,SAAS,KAAK,aAAa,GAAG,SAAS,CAAA;IAC1E,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAgC3B,CAAA"}
@@ -0,0 +1,55 @@
1
+ import { computed, inject, onScopeDispose, ref, watch, watchEffect, } from 'vue';
2
+ import { createExternalExampleResolver } from '../helpers/external-examples.js';
3
+ /** Supply the current document's resolver so consumers share downloads and invalidation. */
4
+ export const EXTERNAL_EXAMPLES = Symbol('external-examples');
5
+ /** Observe current visibility, rather than mounting, to avoid fetching every operation on a long page. */
6
+ export const useExampleVisibility = (target) => {
7
+ const visible = ref(false);
8
+ let observer;
9
+ watch(target, (element) => {
10
+ observer?.disconnect();
11
+ visible.value = false;
12
+ const node = element && ('$el' in element ? element.$el : element);
13
+ if (typeof Element === 'undefined' || !(node instanceof Element))
14
+ return;
15
+ if (typeof IntersectionObserver === 'undefined') {
16
+ visible.value = true;
17
+ return;
18
+ }
19
+ observer = new IntersectionObserver(([entry]) => {
20
+ visible.value = entry?.isIntersecting ?? false;
21
+ });
22
+ observer.observe(node);
23
+ }, { flush: 'post' });
24
+ onScopeDispose(() => observer?.disconnect());
25
+ return visible;
26
+ };
27
+ /** Resolve only the selected examples and expose immutable views of their downloaded values. */
28
+ export const useExternalExamples = (examples, enabled = () => true, resolver) => {
29
+ const fallback = createExternalExampleResolver();
30
+ const getResolver = resolver ?? inject(EXTERNAL_EXAMPLES, () => fallback);
31
+ const states = computed(() => examples()
32
+ .filter((example) => example !== undefined && example.value === undefined && !!example.externalValue)
33
+ .map((example) => getResolver()(example)));
34
+ watchEffect(() => {
35
+ if (!enabled())
36
+ return;
37
+ for (const state of states.value) {
38
+ if (state.status === 'idle')
39
+ void state.load();
40
+ }
41
+ });
42
+ return {
43
+ pending: computed(() => states.value.some((state) => state.status !== 'loaded')),
44
+ failed: computed(() => states.value.some((state) => state.status === 'error')),
45
+ resolve: (example) => {
46
+ if (!example || example.value !== undefined || !example.externalValue)
47
+ return example;
48
+ const state = getResolver()(example);
49
+ return state.status === 'loaded' ? { ...example, value: state.value } : example;
50
+ },
51
+ retry: async () => {
52
+ await Promise.all(states.value.map((state) => state.load()));
53
+ },
54
+ };
55
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"parameters.d.ts","sourceRoot":"","sources":["../../../src/mutators/operation/parameters.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAA;AAIrE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAA;AAmClD;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,wBAAwB,GACnC,UAAU,iBAAiB,GAAG,IAAI,EAClC,4CAA4C,eAAe,CAAC,4BAA4B,CAAC,SAuD1F,CAAA;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,8BAA8B,GACzC,UAAU,iBAAiB,GAAG,IAAI,EAClC,uCAAuC,eAAe,CAAC,mCAAmC,CAAC,SA8C5F,CAAA;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,wBAAwB,GACnC,UAAU,iBAAiB,GAAG,IAAI,EAClC,6BAA6B,eAAe,CAAC,4BAA4B,CAAC,SAkC3E,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,4BAA4B,GACvC,UAAU,iBAAiB,GAAG,IAAI,EAClC,gBAAgB,eAAe,CAAC,iCAAiC,CAAC,SAanE,CAAA"}
1
+ {"version":3,"file":"parameters.d.ts","sourceRoot":"","sources":["../../../src/mutators/operation/parameters.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAA;AAIrE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAA;AAmClD;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,wBAAwB,GACnC,UAAU,iBAAiB,GAAG,IAAI,EAClC,4CAA4C,eAAe,CAAC,4BAA4B,CAAC,SAuE1F,CAAA;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,8BAA8B,GACzC,UAAU,iBAAiB,GAAG,IAAI,EAClC,uCAAuC,eAAe,CAAC,mCAAmC,CAAC,SA8C5F,CAAA;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,wBAAwB,GACnC,UAAU,iBAAiB,GAAG,IAAI,EAClC,6BAA6B,eAAe,CAAC,4BAA4B,CAAC,SAkC3E,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,4BAA4B,GACvC,UAAU,iBAAiB,GAAG,IAAI,EAClC,gBAAgB,eAAe,CAAC,iCAAiC,CAAC,SAanE,CAAA"}
@@ -2,6 +2,7 @@ import { getPathItemOperation } from '../../helpers/for-each-path-item-operation
2
2
  import { getResolvedRef } from '../../helpers/get-resolved-ref.js';
3
3
  import { unpackProxyObject } from '../../helpers/unpack-proxy.js';
4
4
  import { isOpenApiDocument } from '../../schemas/type-guards.js';
5
+ import { isContentTypeParameterObject } from '../../schemas/v3.2/strict/type-guards.js';
5
6
  const getPathItemsForParameterMutation = (pathItemRef) => {
6
7
  if (!pathItemRef || typeof pathItemRef !== 'object') {
7
8
  return [];
@@ -40,23 +41,41 @@ const getPathItemsForParameterMutation = (pathItemRef) => {
40
41
  export const upsertOperationParameter = (document, { meta, type, payload, originalParameter }) => {
41
42
  // We are editing an existing parameter
42
43
  if (originalParameter) {
43
- // To support content-type parameters in the API client, we just assume an
44
- // examples property can be set.
45
44
  const param = originalParameter;
45
+ const target = isContentTypeParameterObject(param) ? Object.values(param.content ?? {})[0] : param;
46
+ if (!target) {
47
+ return;
48
+ }
49
+ if (target !== param) {
50
+ // Keep authored examples and migrate edits saved by older clients. Those edits
51
+ // already take precedence when reading, so they must also win during migration.
52
+ target.examples = {
53
+ ...(target.example !== undefined ? { default: { value: target.example } } : {}),
54
+ ...target.examples,
55
+ ...('example' in param && param.example !== undefined ? { default: { value: param.example } } : {}),
56
+ ...('examples' in param ? param.examples : {}),
57
+ };
58
+ // A media type cannot carry both the singular example and the examples map.
59
+ delete target.example;
60
+ if ('examples' in param) {
61
+ delete param.examples;
62
+ }
63
+ if ('example' in param) {
64
+ delete param.example;
65
+ }
66
+ }
46
67
  // Only update the name when the payload carries a non-empty value — an
47
68
  // empty name in the payload means the key input blurred before rendering
48
69
  // its initial value and should not overwrite the existing parameter name.
49
70
  if (payload.name || !param.name) {
50
71
  param.name = payload.name;
51
72
  }
52
- if (!param.examples) {
53
- param.examples = {};
54
- }
55
- // Create the example if it doesn't exist
56
- if (!param.examples[meta.exampleKey]) {
57
- param.examples[meta.exampleKey] = {};
73
+ target.examples ??= {};
74
+ target.examples[meta.exampleKey] ??= {};
75
+ const example = getResolvedRef(target.examples[meta.exampleKey]);
76
+ if (!example) {
77
+ return;
58
78
  }
59
- const example = getResolvedRef(param.examples[meta.exampleKey]);
60
79
  // Update the example value and disabled state
61
80
  example.value = payload.value;
62
81
  example['x-disabled'] = payload.isDisabled;
@@ -20,9 +20,12 @@ export declare const loadingStatus: () => LifecyclePlugin;
20
20
  *
21
21
  * This is useful for inlining external content (like examples or schemas) into the OpenAPI document during bundling.
22
22
  *
23
- * @param node - The node being processed, which may contain an 'externalValue' property.
23
+ * In lazy mode, preserve the absolute URL for on-demand client resolution without fetching a payload.
24
+ * The default eager mode remains available to existing bundler consumers.
24
25
  */
25
- export declare const externalValueResolver: () => LifecyclePlugin;
26
+ export declare const externalValueResolver: (options?: {
27
+ lazy?: boolean;
28
+ }) => LifecyclePlugin;
26
29
  /**
27
30
  * Lifecycle plugin to resolve $ref on any object, including non-standard locations like the info object.
28
31
  *
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/plugins/bundler/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,EAAE,KAAK,eAAe,EAAwB,MAAM,2BAA2B,CAAA;AAStF;;;;;GAKG;AACH,eAAO,MAAM,aAAa,QAAO,eAahC,CAAA;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,qBAAqB,QAAO,eAmCxC,CAAA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,QAAO,eAwCjC,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,mBAAmB,QAAO,eAuBtC,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,oBAAoB,QAAO,eAuBvC,CAAA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa,QAAO,eAyBhC,CAAA;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,kBAAkB,QAAO,eAkFrC,CAAA;AAKD;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,QAAO,eAaxC,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/plugins/bundler/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,EAAE,KAAK,eAAe,EAAwB,MAAM,2BAA2B,CAAA;AAStF;;;;;GAKG;AACH,eAAO,MAAM,aAAa,QAAO,eAahC,CAAA;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,qBAAqB,GAAI,UAAU;IAAE,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,KAAG,eA2CpE,CAAA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,QAAO,eAwCjC,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,mBAAmB,QAAO,eAuBtC,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,oBAAoB,QAAO,eAuBvC,CAAA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa,QAAO,eAyBhC,CAAA;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,kBAAkB,QAAO,eAkFrC,CAAA;AAKD;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,QAAO,eAaxC,CAAA"}
@@ -39,21 +39,30 @@ export const loadingStatus = () => {
39
39
  *
40
40
  * This is useful for inlining external content (like examples or schemas) into the OpenAPI document during bundling.
41
41
  *
42
- * @param node - The node being processed, which may contain an 'externalValue' property.
42
+ * In lazy mode, preserve the absolute URL for on-demand client resolution without fetching a payload.
43
+ * The default eager mode remains available to existing bundler consumers.
43
44
  */
44
- export const externalValueResolver = () => {
45
+ export const externalValueResolver = (options) => {
45
46
  return {
46
47
  type: 'lifecycle',
47
48
  onAfterNodeProcess: async (node, context) => {
48
49
  const externalValue = node['externalValue'];
49
50
  const cache = context.resolutionCache;
50
51
  // Only process if 'externalValue' is a string
51
- if (typeof externalValue !== 'string') {
52
+ if (typeof externalValue !== 'string' || node['value'] !== undefined) {
52
53
  return;
53
54
  }
54
55
  // `externalValue` may be relative (for example `/examples/pet.json`). Resolve it against the
55
56
  // origin of the document it lives in so it becomes an absolute URL a loader can fetch.
56
57
  const resolvedValue = resolveReferencePath(context.origin, externalValue);
58
+ if (options?.lazy) {
59
+ const path = context.path.at(-2) === 'examples' ? context.path : (context.referencedFromPath ?? context.path);
60
+ if (path.at(-2) !== 'examples' || isSchemaPath(path))
61
+ return;
62
+ // Preserve the referenced document origin before bundling loses that context.
63
+ node['externalValue'] = resolvedValue;
64
+ return;
65
+ }
57
66
  const loader = context.loaders.find((it) => it.validate(resolvedValue));
58
67
  // We can not process the external value
59
68
  if (!loader) {
@@ -3,11 +3,11 @@ import type { ExampleObject, ParameterObject } from '@scalar/workspace-store/sch
3
3
  * Determines if a parameter is disabled
4
4
  *
5
5
  * First we explicitly check if its been disabled via the `x-disabled` extension.
6
- * Then we check if its an optional parameter and not a path parameter.
6
+ * Populated examples are enabled unless explicitly disabled. Empty optional parameters stay disabled.
7
7
  *
8
8
  * @param param - The parameter to check.
9
9
  * @param example - The example to check.
10
- * @param defaultDisabled - When true (default), optional parameters are treated as disabled unless explicitly enabled. When false, only parameters explicitly marked `x-disabled: true` are disabled.
10
+ * @param defaultDisabled - When true (default), empty optional parameters are treated as disabled unless explicitly enabled. When false, only parameters explicitly marked `x-disabled: true` are disabled.
11
11
  * @returns true if the parameter is disabled, false otherwise.
12
12
  */
13
13
  export declare const isParamDisabled: (param: ParameterObject, example: ExampleObject | undefined, defaultDisabled?: boolean) => boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"is-param-disabled.d.ts","sourceRoot":"","sources":["../../../../src/request-example/builder/header/is-param-disabled.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,8DAA8D,CAAA;AAElH;;;;;;;;;;GAUG;AACH,eAAO,MAAM,eAAe,GAC1B,OAAO,eAAe,EACtB,SAAS,aAAa,GAAG,SAAS,EAClC,kBAAiB,OAAc,YAgBhC,CAAA"}
1
+ {"version":3,"file":"is-param-disabled.d.ts","sourceRoot":"","sources":["../../../../src/request-example/builder/header/is-param-disabled.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,8DAA8D,CAAA;AAElH;;;;;;;;;;GAUG;AACH,eAAO,MAAM,eAAe,GAC1B,OAAO,eAAe,EACtB,SAAS,aAAa,GAAG,SAAS,EAClC,kBAAiB,OAAc,KAC9B,OAgBF,CAAA"}
@@ -2,11 +2,11 @@
2
2
  * Determines if a parameter is disabled
3
3
  *
4
4
  * First we explicitly check if its been disabled via the `x-disabled` extension.
5
- * Then we check if its an optional parameter and not a path parameter.
5
+ * Populated examples are enabled unless explicitly disabled. Empty optional parameters stay disabled.
6
6
  *
7
7
  * @param param - The parameter to check.
8
8
  * @param example - The example to check.
9
- * @param defaultDisabled - When true (default), optional parameters are treated as disabled unless explicitly enabled. When false, only parameters explicitly marked `x-disabled: true` are disabled.
9
+ * @param defaultDisabled - When true (default), empty optional parameters are treated as disabled unless explicitly enabled. When false, only parameters explicitly marked `x-disabled: true` are disabled.
10
10
  * @returns true if the parameter is disabled, false otherwise.
11
11
  */
12
12
  export const isParamDisabled = (param, example, defaultDisabled = true) => {
@@ -15,8 +15,9 @@ export const isParamDisabled = (param, example, defaultDisabled = true) => {
15
15
  if (typeof xDisabled === 'boolean') {
16
16
  return xDisabled;
17
17
  }
18
- // If the parameter is not disabled by default, return false
19
- if (!defaultDisabled) {
18
+ // Keep the editor, generated snippets, and outgoing requests aligned for pre-populated values.
19
+ const hasValue = example?.value !== undefined && example.value !== '' && example.value !== null;
20
+ if (!defaultDisabled || hasValue) {
20
21
  return false;
21
22
  }
22
23
  // Otherwise, disable optional parameters (except path parameters which are always required)
@@ -4,6 +4,8 @@ import type { ExampleObject, MediaTypeObject, ParameterObject, RequestBodyObject
4
4
  * Or the [deprecated] `example` field.
5
5
  * If no exampleKey is provided it will fallback to the first example in the examples object then the [deprecated]
6
6
  * `example` field.
7
+ * When the parameter carries both its own `examples`/`example` and a `content` object, the parameter-level value
8
+ * takes priority to preserve edits saved by older clients before they are migrated into the media type.
7
9
  * Used both for send-request and generating code snippets.
8
10
  */
9
11
  export declare const getExample: (param: ParameterObject | RequestBodyObject | MediaTypeObject, exampleName: string | undefined, contentType: string | undefined) => ExampleObject | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"get-example.d.ts","sourceRoot":"","sources":["../../../../src/request-example/builder/helpers/get-example.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,aAAa,EACb,eAAe,EACf,eAAe,EACf,iBAAiB,EAClB,MAAM,8DAA8D,CAAA;AAgCrE;;;;;;GAMG;AACH,eAAO,MAAM,UAAU,GACrB,OAAO,eAAe,GAAG,iBAAiB,GAAG,eAAe,EAC5D,aAAa,MAAM,GAAG,SAAS,EAC/B,aAAa,MAAM,GAAG,SAAS,KAC9B,aAAa,GAAG,SA6ClB,CAAA"}
1
+ {"version":3,"file":"get-example.d.ts","sourceRoot":"","sources":["../../../../src/request-example/builder/helpers/get-example.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,aAAa,EACb,eAAe,EACf,eAAe,EACf,iBAAiB,EAClB,MAAM,8DAA8D,CAAA;AAgCrE;;;;;;;;GAQG;AACH,eAAO,MAAM,UAAU,GACrB,OAAO,eAAe,GAAG,iBAAiB,GAAG,eAAe,EAC5D,aAAa,MAAM,GAAG,SAAS,EAC/B,aAAa,MAAM,GAAG,SAAS,KAC9B,aAAa,GAAG,SA6ClB,CAAA"}
@@ -23,20 +23,22 @@ const getExampleFromExamples = (examples, exampleField, exampleName) => {
23
23
  * Or the [deprecated] `example` field.
24
24
  * If no exampleKey is provided it will fallback to the first example in the examples object then the [deprecated]
25
25
  * `example` field.
26
+ * When the parameter carries both its own `examples`/`example` and a `content` object, the parameter-level value
27
+ * takes priority to preserve edits saved by older clients before they are migrated into the media type.
26
28
  * Used both for send-request and generating code snippets.
27
29
  */
28
30
  export const getExample = (param, exampleName, contentType) => {
29
- // Content based parameters
30
- if ('content' in param) {
31
- const content = param.content?.[contentType ?? Object.keys(param.content)[0] ?? ''];
32
- const result = getExampleFromExamples(content?.examples, content?.example, exampleName);
31
+ // Schema-based parameters and content-based parameter edits saved by older clients.
32
+ if ('examples' in param || 'example' in param) {
33
+ const result = getExampleFromExamples(param.examples, param.example, exampleName);
33
34
  if (result !== undefined) {
34
35
  return result;
35
36
  }
36
37
  }
37
- // Schema based parameters
38
- if ('examples' in param || 'example' in param) {
39
- const result = getExampleFromExamples(param.examples, param.example, exampleName);
38
+ // Content based parameters
39
+ if ('content' in param) {
40
+ const content = param.content?.[contentType ?? Object.keys(param.content)[0] ?? ''];
41
+ const result = getExampleFromExamples(content?.examples, content?.example, exampleName);
40
42
  if (result !== undefined) {
41
43
  return result;
42
44
  }
@@ -1 +1 @@
1
- {"version":3,"file":"headers.d.ts","sourceRoot":"","sources":["../../../src/request-example/context/headers.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mCAAmC,CAAA;AAEnE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8DAA8D,CAAA;AAanG;;;;;GAKG;AACH,eAAO,MAAM,6BAA6B,GAAI,YAAY,MAAM,KAAG,MACQ,CAAA;AAE3E;;GAEG;AACH,eAAO,MAAM,qCAAqC,GAAI,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CACK,CAAA;AAkDlH;;;;;;GAMG;AACH,eAAO,MAAM,4BAA4B,GACvC,WAAW,eAAe,EAC1B,aAAa,MAAM,EACnB,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAC9B,MAAM,CAAC,MAAM,EAAE,MAAM,CAIpB,CAAA;AAEJ;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,iBAAiB,GAAI,0FAU/B;IACD,MAAM,EAAE,UAAU,CAAA;IAClB,SAAS,EAAE,eAAe,CAAA;IAC1B,WAAW,EAAE,MAAM,CAAA;IACnB,mBAAmB,CAAC,EAAE,OAAO,CAAA;IAC7B,qBAAqB,CAAC,EAAE,OAAO,CAAA;IAC/B,OAAO,CAAC,EAAE;QACR,UAAU,EAAE,MAAM,CAAA;QAClB,UAAU,EAAE,OAAO,CAAA;KACpB,CAAA;CACF,KAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAuCxB,CAAA"}
1
+ {"version":3,"file":"headers.d.ts","sourceRoot":"","sources":["../../../src/request-example/context/headers.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mCAAmC,CAAA;AAEnE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8DAA8D,CAAA;AAcnG;;;;;GAKG;AACH,eAAO,MAAM,6BAA6B,GAAI,YAAY,MAAM,KAAG,MACQ,CAAA;AAE3E;;GAEG;AACH,eAAO,MAAM,qCAAqC,GAAI,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CACK,CAAA;AAgDlH;;;;;;GAMG;AACH,eAAO,MAAM,4BAA4B,GACvC,WAAW,eAAe,EAC1B,aAAa,MAAM,EACnB,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAC9B,MAAM,CAAC,MAAM,EAAE,MAAM,CAIpB,CAAA;AAEJ;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,iBAAiB,GAAI,0FAU/B;IACD,MAAM,EAAE,UAAU,CAAA;IAClB,SAAS,EAAE,eAAe,CAAA;IAC1B,WAAW,EAAE,MAAM,CAAA;IACnB,mBAAmB,CAAC,EAAE,OAAO,CAAA;IAC7B,qBAAqB,CAAC,EAAE,OAAO,CAAA;IAC/B,OAAO,CAAC,EAAE;QACR,UAAU,EAAE,MAAM,CAAA;QAClB,UAAU,EAAE,OAAO,CAAA;KACpB,CAAA;CACF,KAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAuCxB,CAAA"}
@@ -1,6 +1,7 @@
1
1
  import { canMethodHaveBody } from '@scalar/helpers/http/can-method-have-body';
2
2
  import { getResolvedRef } from '@scalar/workspace-store/helpers/get-resolved-ref';
3
3
  import { isParamDisabled } from '../../request-example/builder/header/is-param-disabled.js';
4
+ import { getExample } from '../../request-example/builder/helpers/get-example.js';
4
5
  /** Default Accept header value to accept all response types. */
5
6
  const DEFAULT_ACCEPT = '*/*';
6
7
  const CONVENTIONAL_DEFAULT_HEADER_NAMES = {
@@ -21,8 +22,7 @@ export const restoreConventionalHeaderName = (headerName) => CONVENTIONAL_DEFAUL
21
22
  export const restoreConventionalDefaultHeaderNames = (headers) => Object.fromEntries(Object.entries(headers).map(([name, value]) => [restoreConventionalHeaderName(name), value]));
22
23
  /**
23
24
  * Lowercase names of **enabled** operation parameters with `in: header` for the given example.
24
- * Uses the same rules as the request builder (`isParamDisabled`): optional parameters are treated
25
- * as disabled unless `examples[exampleName]['x-disabled']` is explicitly `false`.
25
+ * Uses the same example selection and enablement rules as the request builder, including schema defaults.
26
26
  */
27
27
  const getEnabledOperationHeaderParameterNames = (operation, exampleName) => {
28
28
  const names = new Set();
@@ -31,8 +31,7 @@ const getEnabledOperationHeaderParameterNames = (operation, exampleName) => {
31
31
  if (!param || param.in !== 'header') {
32
32
  continue;
33
33
  }
34
- const rawExample = 'examples' in param && param.examples?.[exampleName] ? param.examples[exampleName] : undefined;
35
- const example = rawExample ? getResolvedRef(rawExample) : undefined;
34
+ const example = getExample(param, exampleName, undefined);
36
35
  if (!isParamDisabled(param, example)) {
37
36
  names.add(param.name.toLowerCase());
38
37
  }