@sanity/cli-core 0.0.2-alpha.2 → 0.1.0-alpha.10

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 (160) hide show
  1. package/dist/SanityCommand.js +34 -3
  2. package/dist/SanityCommand.js.map +1 -1
  3. package/dist/_exports/tree.d.ts +47 -0
  4. package/dist/_exports/tree.js +3 -0
  5. package/dist/_exports/tree.js.map +1 -0
  6. package/dist/_exports/ux.d.ts +40 -0
  7. package/dist/_exports/ux.js +6 -0
  8. package/dist/_exports/ux.js.map +1 -0
  9. package/dist/config/cli/getCliConfig.worker.js +2 -1
  10. package/dist/config/cli/getCliConfig.worker.js.map +1 -1
  11. package/dist/config/cli/schemas.js +5 -2
  12. package/dist/config/cli/schemas.js.map +1 -1
  13. package/dist/config/cli/types/cliConfig.js.map +1 -1
  14. package/dist/config/findProjectRoot.js +2 -2
  15. package/dist/config/findProjectRoot.js.map +1 -1
  16. package/dist/config/studio/getStudioConfig.js +0 -3
  17. package/dist/config/studio/getStudioConfig.js.map +1 -1
  18. package/dist/config/studio/getStudioWorkspaces.js +50 -0
  19. package/dist/config/studio/getStudioWorkspaces.js.map +1 -0
  20. package/dist/config/studio/isStudioConfig.js +19 -0
  21. package/dist/config/studio/isStudioConfig.js.map +1 -0
  22. package/dist/config/studio/readStudioConfig.js +13 -9
  23. package/dist/config/studio/readStudioConfig.js.map +1 -1
  24. package/dist/config/studio/readStudioConfig.worker.js +4 -30
  25. package/dist/config/studio/readStudioConfig.worker.js.map +1 -1
  26. package/dist/config/util/findStudioConfigPath.js +24 -3
  27. package/dist/config/util/findStudioConfigPath.js.map +1 -1
  28. package/dist/config/util/recursivelyResolveProjectRoot.js.map +1 -1
  29. package/dist/index.d.ts +6027 -36
  30. package/dist/index.js +14 -3
  31. package/dist/index.js.map +1 -1
  32. package/dist/loaders/studio/studioWorkerLoader.worker.js +10 -24
  33. package/dist/loaders/studio/studioWorkerLoader.worker.js.map +1 -1
  34. package/dist/loaders/studio/studioWorkerTask.js +41 -11
  35. package/dist/loaders/studio/studioWorkerTask.js.map +1 -1
  36. package/dist/loaders/tsx/tsxWorkerTask.js +3 -0
  37. package/dist/loaders/tsx/tsxWorkerTask.js.map +1 -1
  38. package/dist/services/apiClient.js +18 -17
  39. package/dist/services/apiClient.js.map +1 -1
  40. package/dist/telemetry/cleanupOldTelemetryFiles.js +30 -0
  41. package/dist/telemetry/cleanupOldTelemetryFiles.js.map +1 -0
  42. package/dist/telemetry/createTelemetryStore.js +95 -0
  43. package/dist/telemetry/createTelemetryStore.js.map +1 -0
  44. package/dist/telemetry/createTraceId.js +10 -0
  45. package/dist/telemetry/createTraceId.js.map +1 -0
  46. package/dist/telemetry/findTelemetryFiles.js +36 -0
  47. package/dist/telemetry/findTelemetryFiles.js.map +1 -0
  48. package/dist/telemetry/flushTelemetryFiles.js +107 -0
  49. package/dist/telemetry/flushTelemetryFiles.js.map +1 -0
  50. package/dist/telemetry/generateTelemetryFilePath.js +30 -0
  51. package/dist/telemetry/generateTelemetryFilePath.js.map +1 -0
  52. package/dist/telemetry/getTelemetryBaseInfo.js +33 -0
  53. package/dist/telemetry/getTelemetryBaseInfo.js.map +1 -0
  54. package/dist/telemetry/logger.js +54 -0
  55. package/dist/telemetry/logger.js.map +1 -0
  56. package/dist/telemetry/telemetryStoreDebug.js +7 -0
  57. package/dist/telemetry/telemetryStoreDebug.js.map +1 -0
  58. package/dist/telemetry/trace.js +150 -0
  59. package/dist/telemetry/trace.js.map +1 -0
  60. package/dist/telemetry/types.js +5 -0
  61. package/dist/telemetry/types.js.map +1 -0
  62. package/dist/types.js +2 -0
  63. package/dist/types.js.map +1 -1
  64. package/dist/util/doImport.js +16 -0
  65. package/dist/util/doImport.js.map +1 -0
  66. package/dist/util/environment/getStudioEnvironmentVariables.js +2 -1
  67. package/dist/util/environment/getStudioEnvironmentVariables.js.map +1 -1
  68. package/dist/util/getCliTelemetry.js +36 -0
  69. package/dist/util/getCliTelemetry.js.map +1 -0
  70. package/dist/util/isStaging.js +10 -0
  71. package/dist/util/isStaging.js.map +1 -0
  72. package/dist/util/normalizePath.js +12 -0
  73. package/dist/util/normalizePath.js.map +1 -0
  74. package/dist/util/parseStringFlag.js +19 -0
  75. package/dist/util/parseStringFlag.js.map +1 -0
  76. package/dist/util/readNDJSON.js +18 -0
  77. package/dist/util/readNDJSON.js.map +1 -0
  78. package/dist/util/resolveLocalPackage.js +34 -0
  79. package/dist/util/resolveLocalPackage.js.map +1 -0
  80. package/dist/util/tree.js +108 -0
  81. package/dist/util/tree.js.map +1 -0
  82. package/dist/util/waitForAsync.js +5 -0
  83. package/dist/util/waitForAsync.js.map +1 -0
  84. package/dist/ux/boxen.js +3 -0
  85. package/dist/ux/boxen.js.map +1 -0
  86. package/dist/ux/colorizeJson.js +6 -6
  87. package/dist/ux/colorizeJson.js.map +1 -1
  88. package/dist/ux/prompts.js +3 -0
  89. package/dist/ux/prompts.js.map +1 -0
  90. package/dist/ux/spinner.js +1 -1
  91. package/dist/ux/spinner.js.map +1 -1
  92. package/package.json +53 -28
  93. package/dist/SanityCommand.d.ts +0 -56
  94. package/dist/config/__tests__/cliToken.test.js +0 -74
  95. package/dist/config/__tests__/cliToken.test.js.map +0 -1
  96. package/dist/config/__tests__/cliUserConfig.test.js +0 -131
  97. package/dist/config/__tests__/cliUserConfig.test.js.map +0 -1
  98. package/dist/config/__tests__/findProjectRoot.test.js +0 -159
  99. package/dist/config/__tests__/findProjectRoot.test.js.map +0 -1
  100. package/dist/config/__tests__/findProjectRootSync.test.js +0 -112
  101. package/dist/config/__tests__/findProjectRootSync.test.js.map +0 -1
  102. package/dist/config/__tests__/getCliConfigSync.test.js +0 -31
  103. package/dist/config/__tests__/getCliConfigSync.test.js.map +0 -1
  104. package/dist/config/cli/getCliConfig.d.ts +0 -16
  105. package/dist/config/cli/getCliConfig.worker.d.ts +0 -1
  106. package/dist/config/cli/getCliConfigSync.d.ts +0 -12
  107. package/dist/config/cli/schemas.d.ts +0 -255
  108. package/dist/config/cli/types/cliConfig.d.ts +0 -74
  109. package/dist/config/cli/types/userViteConfig.d.ts +0 -5
  110. package/dist/config/findProjectRoot.d.ts +0 -14
  111. package/dist/config/findProjectRootSync.d.ts +0 -27
  112. package/dist/config/studio/getStudioConfig.d.ts +0 -14
  113. package/dist/config/studio/readStudioConfig.d.ts +0 -190
  114. package/dist/config/studio/readStudioConfig.worker.d.ts +0 -1
  115. package/dist/config/util/configPathsSync.d.ts +0 -17
  116. package/dist/config/util/findAppConfigPath.d.ts +0 -8
  117. package/dist/config/util/findConfigsPaths.d.ts +0 -16
  118. package/dist/config/util/findStudioConfigPath.d.ts +0 -9
  119. package/dist/config/util/isSanityV2StudioRoot.d.ts +0 -8
  120. package/dist/config/util/recursivelyResolveProjectRoot.d.ts +0 -27
  121. package/dist/debug.d.ts +0 -15
  122. package/dist/loaders/studio/studioWorkerLoader.worker.d.ts +0 -1
  123. package/dist/loaders/studio/studioWorkerTask.d.ts +0 -40
  124. package/dist/loaders/tsx/tsxWorkerLoader.worker.d.ts +0 -1
  125. package/dist/loaders/tsx/tsxWorkerTask.d.ts +0 -28
  126. package/dist/services/apiClient.d.ts +0 -53
  127. package/dist/services/cliUserConfig.d.ts +0 -57
  128. package/dist/services/getCliToken.d.ts +0 -7
  129. package/dist/types.d.ts +0 -7
  130. package/dist/util/NotFoundError.d.ts +0 -20
  131. package/dist/util/__tests__/createExpiringConfig.test.js +0 -400
  132. package/dist/util/__tests__/createExpiringConfig.test.js.map +0 -1
  133. package/dist/util/createExpiringConfig.d.ts +0 -37
  134. package/dist/util/environment/getStudioEnvironmentVariables.d.ts +0 -12
  135. package/dist/util/environment/mockBrowserEnvironment.d.ts +0 -17
  136. package/dist/util/environment/setupBrowserStubs.d.ts +0 -10
  137. package/dist/util/environment/stubs.d.ts +0 -254
  138. package/dist/util/fileExists.d.ts +0 -9
  139. package/dist/util/generateHelpUrl.d.ts +0 -8
  140. package/dist/util/getEmptyAuth.d.ts +0 -5
  141. package/dist/util/getSanityEnvVar.d.ts +0 -19
  142. package/dist/util/getSanityUrl.d.ts +0 -5
  143. package/dist/util/getUserConfig.d.ts +0 -2
  144. package/dist/util/isCi.d.ts +0 -1
  145. package/dist/util/isHttpError.d.ts +0 -29
  146. package/dist/util/isHttpError.js +0 -18
  147. package/dist/util/isHttpError.js.map +0 -1
  148. package/dist/util/isInteractive.d.ts +0 -1
  149. package/dist/util/isRecord.d.ts +0 -8
  150. package/dist/util/isTrueish.d.ts +0 -1
  151. package/dist/util/readJsonFile.d.ts +0 -14
  152. package/dist/util/safeStructuredClone.d.ts +0 -8
  153. package/dist/util/tryGetDefaultExport.d.ts +0 -5
  154. package/dist/util/writeJsonFile.d.ts +0 -9
  155. package/dist/ux/colorizeJson.d.ts +0 -1
  156. package/dist/ux/formatObject.d.ts +0 -1
  157. package/dist/ux/logSymbols.d.ts +0 -1
  158. package/dist/ux/printKeyValue.d.ts +0 -1
  159. package/dist/ux/spinner.d.ts +0 -1
  160. package/dist/ux/timer.d.ts +0 -12
@@ -1,27 +0,0 @@
1
- /**
2
- * Result of finding a project configuration
3
- *
4
- * @internal
5
- */
6
- export interface ProjectRootResult {
7
- directory: string;
8
- /**
9
- * Path to the project configuration file, if found.
10
- */
11
- path: string;
12
- /**
13
- * Type of project root.
14
- */
15
- type: 'app' | 'studio';
16
- }
17
- /**
18
- * Generic recursive search function for project configuration files.
19
- *
20
- * @param basePath - The base path to start searching from
21
- * @param findConfigFn - Function that looks for config files in a given directory
22
- * @param projectType - The type of project ('app' | 'studio')
23
- * @param iterations - Current iteration count, passed internally to prevent infinite recursion
24
- * @returns A promise that resolves to an object if config is found, false otherwise
25
- * @internal
26
- */
27
- export declare function recursivelyResolveProjectRoot(basePath: string, findConfigFn: (path: string) => Promise<string | undefined>, projectType: 'app' | 'studio', iterations?: number): Promise<false | ProjectRootResult>;
package/dist/debug.d.ts DELETED
@@ -1,15 +0,0 @@
1
- import debugIt from 'debug';
2
- /**
3
- * `debug` instance for the CLI
4
- *
5
- * @internal
6
- */
7
- export declare const debug: debugIt.Debugger;
8
- /**
9
- * Get a `debug` instance which extends the CLI debug instance with the given namespace,
10
- * eg namespace would be `sanity:cli:<providedNamespace>`
11
- *
12
- * @param namespace - The namespace to extend the CLI debug instance with
13
- * @returns The extended `debug` instance
14
- */
15
- export declare const subdebug: (namespace: string) => debugIt.Debugger;
@@ -1 +0,0 @@
1
- export {};
@@ -1,40 +0,0 @@
1
- import { type WorkerOptions } from 'node:worker_threads';
2
- import { type RequireProps } from '../../types.js';
3
- /**
4
- * Options for the studio worker task
5
- *
6
- * @internal
7
- */
8
- interface StudioWorkerTaskOptions extends RequireProps<WorkerOptions, 'name'> {
9
- studioRootPath: string;
10
- }
11
- /**
12
- * Executes a worker file in a Sanity Studio browser context.
13
- *
14
- * This uses a combination of vite for "bundling" + jsdom for emulating a browser
15
- * environment under the hood, which means that the same thing that will work in vite
16
- * _should_ work in the worker - to a degree. If the user has defined any typescript
17
- * path aliases, these will have to be added as aliases to the vite config - the same
18
- * behavior as you would see with regular vite. Other things that are accounted for:
19
- *
20
- * - TypeScript support (+JSX, enums and other "compilation needed" features)
21
- * - CSS, font and other file imports will resolve to a file path
22
- * - CSS module imports will resolve to a javascript object of class names
23
- * - Environment variables are available both as `import.meta.env` and `process.env`,
24
- * and `.env` files are loaded in the same way that they would in a Sanity studio.
25
- * - Browser globals not available in a Node.js environment but _are_ provided by JSDOM
26
- * are defined directly to the Node environment as globals. While this polutes the
27
- * global namespace, it is done only in the worker thread.
28
- * - Certain browser globals that are _not_ available in JSDOM are also provided to the
29
- * global namespace - things like `requestIdleCallback`, `IntersectionObserver` etc.
30
- * These are provided with a minimal stub implementation to make them not crash.
31
- *
32
- * @param filePath - Path to the worker file (`.ts` works and is encouraged)
33
- * @param options - Options to pass to the worker
34
- * @returns A promise that resolves with the message from the worker
35
- * @throws If the file does not exist
36
- * @throws If the worker exits with a non-zero code
37
- * @internal
38
- */
39
- export declare function studioWorkerTask(filePath: URL, options: StudioWorkerTaskOptions): Promise<unknown>;
40
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1,28 +0,0 @@
1
- import { URL } from 'node:url';
2
- import { type WorkerOptions } from 'node:worker_threads';
3
- import { type RequireProps } from '../../types.js';
4
- /**
5
- * Options for the tsx worker task
6
- *
7
- * @internal
8
- */
9
- interface TsxWorkerTaskOptions extends RequireProps<WorkerOptions, 'name'> {
10
- rootPath: string;
11
- }
12
- /**
13
- * Executes a worker file with tsx registered. This means you can import other
14
- * typescript with fairly rich syntax, and still have that only apply to the worker
15
- * thread instead of the full parent process. The worker should emit a message when
16
- * complete using `parentPort`. Once it has received a single message will resolve the
17
- * returned promise with that message. If you are expecting multiple messages, you will
18
- * have to implement another method ;)
19
- *
20
- * @param filePath - Path to the worker file
21
- * @param options - Options to pass to the worker
22
- * @returns A promise that resolves with the message from the worker
23
- * @throws If the file does not exist
24
- * @throws If the worker exits with a non-zero code
25
- * @internal
26
- */
27
- export declare function tsxWorkerTask<T = unknown>(filePath: URL, options: TsxWorkerTaskOptions): Promise<T>;
28
- export {};
@@ -1,53 +0,0 @@
1
- import { type ClientConfig, type SanityClient } from '@sanity/client';
2
- /**
3
- * @internal
4
- */
5
- export interface GlobalCliClientOptions extends ClientConfig {
6
- /**
7
- * The API version to use for this client.
8
- */
9
- apiVersion: string;
10
- /**
11
- * Whether to require a user to be authenticated to use this client.
12
- * Default: `false`.
13
- * Throws an error if `true` and user is not authenticated.
14
- */
15
- requireUser?: boolean;
16
- }
17
- /**
18
- * Create a "global" (unscoped) Sanity API client.
19
- *
20
- * @param options - The options to use for the client.
21
- * @returns Promise that resolves to a configured Sanity API client.
22
- */
23
- export declare function getGlobalCliClient({ requireUser, ...config }: GlobalCliClientOptions): Promise<SanityClient>;
24
- /**
25
- * @internal
26
- */
27
- export interface ProjectCliClientOptions extends ClientConfig {
28
- /**
29
- * The API version to use for this client.
30
- */
31
- apiVersion: string;
32
- /**
33
- * The project ID to use for this client.
34
- */
35
- projectId: string;
36
- /**
37
- * The dataset to use for this client.
38
- */
39
- dataset?: string;
40
- /**
41
- * Whether to require a user to be authenticated to use this client.
42
- * Default: `false`.
43
- * Throws an error if `true` and user is not authenticated.
44
- */
45
- requireUser?: boolean;
46
- }
47
- /**
48
- * Create a "global" (unscoped) Sanity API client.
49
- *
50
- * @param options - The options to use for the client.
51
- * @returns Promise that resolves to a configured Sanity API client.
52
- */
53
- export declare function getProjectCliClient({ requireUser, ...config }: ProjectCliClientOptions): Promise<SanityClient>;
@@ -1,57 +0,0 @@
1
- import { z } from 'zod';
2
- declare const cliUserConfigSchema: {
3
- authToken: z.ZodOptional<z.ZodString>;
4
- telemetryConsent: z.ZodOptional<z.ZodObject<{
5
- updatedAt: z.ZodOptional<z.ZodNumber>;
6
- value: z.ZodObject<{
7
- status: z.ZodEnum<["undetermined", "unset", "granted", "denied"]>;
8
- type: z.ZodString;
9
- }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
10
- status: z.ZodEnum<["undetermined", "unset", "granted", "denied"]>;
11
- type: z.ZodString;
12
- }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
13
- status: z.ZodEnum<["undetermined", "unset", "granted", "denied"]>;
14
- type: z.ZodString;
15
- }, z.ZodTypeAny, "passthrough">>;
16
- }, "strip", z.ZodTypeAny, {
17
- value: {
18
- type: string;
19
- status: "undetermined" | "unset" | "granted" | "denied";
20
- } & {
21
- [k: string]: unknown;
22
- };
23
- updatedAt?: number | undefined;
24
- }, {
25
- value: {
26
- type: string;
27
- status: "undetermined" | "unset" | "granted" | "denied";
28
- } & {
29
- [k: string]: unknown;
30
- };
31
- updatedAt?: number | undefined;
32
- }>>;
33
- };
34
- /**
35
- * The CLI user configuration schema.
36
- *
37
- * @internal
38
- */
39
- type CliUserConfig = z.infer<z.ZodObject<typeof cliUserConfigSchema>>;
40
- /**
41
- * Set the config value for the given property.
42
- * Validates that the passed value adheres to the defined CLI config schema.
43
- *
44
- * @param prop - The property to set the value for
45
- * @param value - The value to set
46
- * @internal
47
- */
48
- export declare function setConfig<P extends keyof CliUserConfig>(prop: P, value: CliUserConfig[P]): Promise<void>;
49
- /**
50
- * Get the config value for the given property
51
- *
52
- * @param prop - The property to get the value for
53
- * @returns The value of the given property
54
- * @internal
55
- */
56
- export declare function getConfig<P extends keyof CliUserConfig>(prop: P): Promise<CliUserConfig[P]>;
57
- export {};
@@ -1,7 +0,0 @@
1
- /**
2
- * Get the CLI authentication token from the environment or the config file
3
- *
4
- * @returns A promise that resolves to a CLI token, or undefined if no token is found
5
- * @internal
6
- */
7
- export declare function getCliToken(): Promise<string | undefined>;
package/dist/types.d.ts DELETED
@@ -1,7 +0,0 @@
1
- import { type Command } from '@oclif/core';
2
- export interface Output {
3
- error: Command['error'];
4
- log: Command['log'];
5
- warn: Command['warn'];
6
- }
7
- export type RequireProps<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
@@ -1,20 +0,0 @@
1
- /**
2
- * Error thrown when a file or directory is not found
3
- *
4
- * `code` is always `ENOENT` to mirror Node.js behavior when a file is not found
5
- *
6
- * @internal
7
- */
8
- export declare class NotFoundError extends Error {
9
- code: string;
10
- path?: string;
11
- constructor(message: string, path?: string);
12
- }
13
- /**
14
- * Returns whether or not the given error is a `NotFoundError`
15
- *
16
- * @param err - The error to check
17
- * @returns `true` if the error is a `NotFoundError`, `false` otherwise
18
- * @internal
19
- */
20
- export declare function isNotFoundError(err: unknown): err is NotFoundError;
@@ -1,400 +0,0 @@
1
- import { beforeEach, describe, expect, test, vi } from 'vitest';
2
- import { createExpiringConfig } from '../createExpiringConfig.js';
3
- describe('createExpiringConfig', ()=>{
4
- let mockStore;
5
- let fetchValue;
6
- let onCacheHit;
7
- let onFetch;
8
- let onRevalidate;
9
- beforeEach(()=>{
10
- // Mock ConfigStore
11
- mockStore = {
12
- delete: vi.fn(),
13
- get: vi.fn(),
14
- set: vi.fn()
15
- };
16
- // Reset all mocks
17
- fetchValue = vi.fn();
18
- onCacheHit = vi.fn();
19
- onFetch = vi.fn();
20
- onRevalidate = vi.fn();
21
- });
22
- test('returns fetched value when cache is empty', async ()=>{
23
- const testValue = 'test-value';
24
- const config = createExpiringConfig({
25
- fetchValue: fetchValue.mockResolvedValue(testValue),
26
- key: 'test-key',
27
- onCacheHit,
28
- onFetch,
29
- onRevalidate,
30
- store: mockStore,
31
- ttl: 5000
32
- });
33
- // Mock empty cache
34
- vi.mocked(mockStore.get).mockReturnValue(undefined);
35
- const result = await config.get();
36
- expect(result).toBe(testValue);
37
- expect(fetchValue).toHaveBeenCalledOnce();
38
- expect(onFetch).toHaveBeenCalledOnce();
39
- expect(onCacheHit).not.toHaveBeenCalled();
40
- expect(onRevalidate).not.toHaveBeenCalled();
41
- expect(mockStore.set).toHaveBeenCalledWith('test-key', {
42
- updatedAt: expect.any(Number),
43
- value: testValue
44
- });
45
- });
46
- test('returns cached value when it has not expired', async ()=>{
47
- const cachedValue = 'cached-value';
48
- const ttl = 5000;
49
- const updatedAt = Date.now() - 1000 // 1 second ago (not expired)
50
- ;
51
- const config = createExpiringConfig({
52
- fetchValue,
53
- key: 'test-key',
54
- onCacheHit,
55
- onFetch,
56
- onRevalidate,
57
- store: mockStore,
58
- ttl
59
- });
60
- // Mock cached value that hasn't expired
61
- vi.mocked(mockStore.get).mockReturnValue({
62
- updatedAt,
63
- value: cachedValue
64
- });
65
- const result = await config.get();
66
- expect(result).toBe(cachedValue);
67
- expect(fetchValue).not.toHaveBeenCalled();
68
- expect(onCacheHit).toHaveBeenCalledOnce();
69
- expect(onFetch).not.toHaveBeenCalled();
70
- expect(onRevalidate).not.toHaveBeenCalled();
71
- expect(mockStore.set).not.toHaveBeenCalled();
72
- });
73
- test('fetches new value when cached value has expired', async ()=>{
74
- const newValue = 'new-value';
75
- const ttl = 1000;
76
- const updatedAt = Date.now() - 2000 // 2 seconds ago (expired)
77
- ;
78
- const config = createExpiringConfig({
79
- fetchValue: fetchValue.mockResolvedValue(newValue),
80
- key: 'test-key',
81
- onCacheHit,
82
- onFetch,
83
- onRevalidate,
84
- store: mockStore,
85
- ttl
86
- });
87
- // Mock expired cached value
88
- vi.mocked(mockStore.get).mockReturnValue({
89
- updatedAt,
90
- value: 'old-value'
91
- });
92
- const result = await config.get();
93
- expect(result).toBe(newValue);
94
- expect(fetchValue).toHaveBeenCalledOnce();
95
- expect(onRevalidate).toHaveBeenCalledOnce();
96
- expect(onFetch).toHaveBeenCalledOnce();
97
- expect(onCacheHit).not.toHaveBeenCalled();
98
- expect(mockStore.set).toHaveBeenCalledWith('test-key', {
99
- updatedAt: expect.any(Number),
100
- value: newValue
101
- });
102
- });
103
- test('deletes cached value from store', ()=>{
104
- const config = createExpiringConfig({
105
- fetchValue,
106
- key: 'test-key',
107
- store: mockStore,
108
- ttl: 5000
109
- });
110
- config.delete();
111
- expect(mockStore.delete).toHaveBeenCalledWith('test-key');
112
- });
113
- test('handles concurrent get() calls correctly', async ()=>{
114
- const testValue = 'test-value';
115
- let resolvePromise;
116
- const delayedFetch = new Promise((resolve)=>{
117
- resolvePromise = resolve;
118
- });
119
- const config = createExpiringConfig({
120
- fetchValue: fetchValue.mockReturnValue(delayedFetch),
121
- key: 'test-key',
122
- onFetch,
123
- store: mockStore,
124
- ttl: 5000
125
- });
126
- // Mock empty cache
127
- vi.mocked(mockStore.get).mockReturnValue(undefined);
128
- // Start multiple concurrent get() calls
129
- const promise1 = config.get();
130
- const promise2 = config.get();
131
- const promise3 = config.get();
132
- // Resolve the fetch
133
- resolvePromise(testValue);
134
- const [result1, result2, result3] = await Promise.all([
135
- promise1,
136
- promise2,
137
- promise3
138
- ]);
139
- expect(result1).toBe(testValue);
140
- expect(result2).toBe(testValue);
141
- expect(result3).toBe(testValue);
142
- expect(fetchValue).toHaveBeenCalledOnce(); // Only one fetch should happen
143
- expect(onFetch).toHaveBeenCalledOnce();
144
- });
145
- test('handles synchronous fetchValue function', async ()=>{
146
- const testValue = 'sync-value';
147
- const syncFetchValue = vi.fn().mockReturnValue(testValue);
148
- const config = createExpiringConfig({
149
- fetchValue: syncFetchValue,
150
- key: 'test-key',
151
- store: mockStore,
152
- ttl: 5000
153
- });
154
- // Mock empty cache
155
- vi.mocked(mockStore.get).mockReturnValue(undefined);
156
- const result = await config.get();
157
- expect(result).toBe(testValue);
158
- expect(syncFetchValue).toHaveBeenCalledOnce();
159
- });
160
- test('handles fetchValue throwing an error', async ()=>{
161
- const error = new Error('Fetch failed');
162
- const config = createExpiringConfig({
163
- fetchValue: fetchValue.mockRejectedValue(error),
164
- key: 'test-key',
165
- store: mockStore,
166
- ttl: 5000
167
- });
168
- // Mock empty cache
169
- vi.mocked(mockStore.get).mockReturnValue(undefined);
170
- await expect(config.get()).rejects.toThrow('Fetch failed');
171
- expect(fetchValue).toHaveBeenCalledOnce();
172
- expect(mockStore.set).not.toHaveBeenCalled();
173
- });
174
- test('handles different data types as cached values', async ()=>{
175
- const objectValue = {
176
- key: 'value',
177
- number: 42
178
- };
179
- const config = createExpiringConfig({
180
- fetchValue: fetchValue.mockResolvedValue(objectValue),
181
- key: 'test-key',
182
- store: mockStore,
183
- ttl: 5000
184
- });
185
- // Mock empty cache
186
- vi.mocked(mockStore.get).mockReturnValue(undefined);
187
- const result = await config.get();
188
- expect(result).toEqual(objectValue);
189
- expect(mockStore.set).toHaveBeenCalledWith('test-key', {
190
- updatedAt: expect.any(Number),
191
- value: objectValue
192
- });
193
- });
194
- test('works with TTL of 0 (immediate expiration)', async ()=>{
195
- const testValue = 'test-value';
196
- const config = createExpiringConfig({
197
- fetchValue: fetchValue.mockResolvedValue(testValue),
198
- key: 'test-key',
199
- onRevalidate,
200
- store: mockStore,
201
- ttl: 0
202
- });
203
- // Mock cached value that would be immediately expired
204
- // Use a timestamp from 1ms ago to ensure it's > ttl (0)
205
- vi.mocked(mockStore.get).mockReturnValue({
206
- updatedAt: Date.now() - 1,
207
- value: 'old-value'
208
- });
209
- const result = await config.get();
210
- expect(result).toBe(testValue);
211
- expect(fetchValue).toHaveBeenCalledOnce();
212
- expect(onRevalidate).toHaveBeenCalledOnce();
213
- });
214
- test('works without optional callback functions', async ()=>{
215
- const testValue = 'test-value';
216
- const config = createExpiringConfig({
217
- fetchValue: fetchValue.mockResolvedValue(testValue),
218
- key: 'test-key',
219
- store: mockStore,
220
- ttl: 5000
221
- });
222
- // Mock empty cache
223
- vi.mocked(mockStore.get).mockReturnValue(undefined);
224
- const result = await config.get();
225
- expect(result).toBe(testValue);
226
- expect(fetchValue).toHaveBeenCalledOnce();
227
- });
228
- test('handles cached value without updatedAt timestamp', async ()=>{
229
- const newValue = 'new-value';
230
- const config = createExpiringConfig({
231
- fetchValue: fetchValue.mockResolvedValue(newValue),
232
- key: 'test-key',
233
- onFetch,
234
- store: mockStore,
235
- ttl: 5000
236
- });
237
- // Mock cached value without updatedAt (invalid cache entry)
238
- vi.mocked(mockStore.get).mockReturnValue({
239
- value: 'old-value'
240
- });
241
- const result = await config.get();
242
- expect(result).toBe(newValue);
243
- expect(fetchValue).toHaveBeenCalledOnce();
244
- expect(onFetch).toHaveBeenCalledOnce();
245
- });
246
- test('handles cached value without value property', async ()=>{
247
- const newValue = 'new-value';
248
- const config = createExpiringConfig({
249
- fetchValue: fetchValue.mockResolvedValue(newValue),
250
- key: 'test-key',
251
- onFetch,
252
- store: mockStore,
253
- ttl: 5000
254
- });
255
- // Mock cached entry without value property
256
- vi.mocked(mockStore.get).mockReturnValue({
257
- updatedAt: Date.now()
258
- });
259
- const result = await config.get();
260
- expect(result).toBe(newValue);
261
- expect(fetchValue).toHaveBeenCalledOnce();
262
- expect(onFetch).toHaveBeenCalledOnce();
263
- });
264
- test('stores timestamp correctly when caching new values', async ()=>{
265
- const testValue = 'test-value';
266
- const config = createExpiringConfig({
267
- fetchValue: fetchValue.mockResolvedValue(testValue),
268
- key: 'test-key',
269
- store: mockStore,
270
- ttl: 5000
271
- });
272
- // Mock empty cache
273
- vi.mocked(mockStore.get).mockReturnValue(undefined);
274
- await config.get();
275
- expect(mockStore.set).toHaveBeenCalledWith('test-key', {
276
- updatedAt: expect.any(Number),
277
- value: testValue
278
- });
279
- });
280
- test('subsequent requests after cache is populated use cached value', async ()=>{
281
- const testValue = 'test-value';
282
- const config = createExpiringConfig({
283
- fetchValue: fetchValue.mockResolvedValue(testValue),
284
- key: 'test-key',
285
- onCacheHit,
286
- onFetch,
287
- store: mockStore,
288
- ttl: 5000
289
- });
290
- // Mock empty cache for first request
291
- vi.mocked(mockStore.get).mockReturnValueOnce(undefined);
292
- // First request should fetch
293
- const result1 = await config.get();
294
- // Mock cache populated for subsequent request
295
- vi.mocked(mockStore.get).mockReturnValueOnce({
296
- updatedAt: Date.now(),
297
- value: testValue
298
- });
299
- // Second request should hit cache
300
- const result2 = await config.get();
301
- expect(result1).toBe(testValue);
302
- expect(result2).toBe(testValue);
303
- expect(fetchValue).toHaveBeenCalledOnce();
304
- expect(onFetch).toHaveBeenCalledOnce();
305
- expect(onCacheHit).toHaveBeenCalledOnce();
306
- });
307
- test('throws when cached value fails validateValue', async ()=>{
308
- const invalidCached = 123;
309
- const ttl = 10_000;
310
- const validateValue = vi.fn((v)=>typeof v === 'string');
311
- const config = createExpiringConfig({
312
- fetchValue,
313
- key: 'test-key',
314
- onCacheHit,
315
- onFetch,
316
- onRevalidate,
317
- store: mockStore,
318
- ttl,
319
- // @ts-expect-error vitest mocks don't jive with assertions
320
- validateValue
321
- });
322
- // Cached entry that is not expired but invalid per validateValue
323
- vi.mocked(mockStore.get).mockReturnValue({
324
- updatedAt: Date.now(),
325
- value: invalidCached
326
- });
327
- await expect(config.get()).rejects.toThrow('Stored value is invalid');
328
- expect(validateValue).toHaveBeenCalledOnce();
329
- expect(onCacheHit).not.toHaveBeenCalled();
330
- expect(onFetch).not.toHaveBeenCalled();
331
- expect(onRevalidate).not.toHaveBeenCalled();
332
- expect(mockStore.set).not.toHaveBeenCalled();
333
- });
334
- test('throws when fetched value fails validateValue (cache miss)', async ()=>{
335
- const validateValue = vi.fn((v)=>typeof v === 'string');
336
- const config = createExpiringConfig({
337
- fetchValue: fetchValue.mockResolvedValue(42),
338
- key: 'test-key',
339
- onFetch,
340
- store: mockStore,
341
- ttl: 5000,
342
- // @ts-expect-error vitest mocks don't jive with assertions
343
- validateValue
344
- });
345
- // Empty cache
346
- vi.mocked(mockStore.get).mockReturnValue(undefined);
347
- await expect(config.get()).rejects.toThrow('Fetched value is invalid');
348
- expect(onFetch).toHaveBeenCalledOnce();
349
- expect(validateValue).toHaveBeenCalledOnce();
350
- expect(mockStore.set).not.toHaveBeenCalled();
351
- });
352
- test('returns cached value when validateValue accepts it', async ()=>{
353
- const cachedValue = 'ok';
354
- const validateValue = vi.fn((v)=>typeof v === 'string');
355
- const config = createExpiringConfig({
356
- fetchValue,
357
- key: 'test-key',
358
- onCacheHit,
359
- store: mockStore,
360
- ttl: 5000,
361
- // @ts-expect-error vitest mocks don't jive with assertions
362
- validateValue
363
- });
364
- vi.mocked(mockStore.get).mockReturnValue({
365
- updatedAt: Date.now(),
366
- value: cachedValue
367
- });
368
- const result = await config.get();
369
- expect(result).toBe(cachedValue);
370
- expect(validateValue).toHaveBeenCalledOnce();
371
- expect(onCacheHit).toHaveBeenCalledOnce();
372
- expect(fetchValue).not.toHaveBeenCalled();
373
- });
374
- test('revalidation path validates fetched value and throws if invalid', async ()=>{
375
- const validateValue = vi.fn((v)=>typeof v === 'string');
376
- const config = createExpiringConfig({
377
- fetchValue: fetchValue.mockResolvedValue(99),
378
- key: 'test-key',
379
- onFetch,
380
- onRevalidate,
381
- store: mockStore,
382
- ttl: 1,
383
- // @ts-expect-error vitest mocks don't jive with assertions
384
- validateValue
385
- });
386
- // Cached value that has expired but is otherwise valid in shape and passes validate
387
- vi.mocked(mockStore.get).mockReturnValue({
388
- updatedAt: Date.now() - 10,
389
- value: 'stale'
390
- });
391
- await expect(config.get()).rejects.toThrow('Fetched value is invalid');
392
- expect(onRevalidate).toHaveBeenCalledOnce();
393
- expect(onFetch).toHaveBeenCalledOnce();
394
- // validateValue called for stored value and fetched value
395
- expect(validateValue).toHaveBeenCalledTimes(2);
396
- expect(mockStore.set).not.toHaveBeenCalled();
397
- });
398
- });
399
-
400
- //# sourceMappingURL=createExpiringConfig.test.js.map