@nosto/nosto-cli 1.0.1

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 (88) hide show
  1. package/.github/copilot-instructions.md +326 -0
  2. package/.github/dependabot.yml +9 -0
  3. package/.github/pull_request_template.md +12 -0
  4. package/.github/workflows/ci.yml +58 -0
  5. package/.github/workflows/release.yml +49 -0
  6. package/.husky/commit-msg +1 -0
  7. package/.prettierrc +9 -0
  8. package/LICENSE +29 -0
  9. package/README.md +154 -0
  10. package/commitlint.config.js +4 -0
  11. package/eslint.config.js +36 -0
  12. package/package.json +63 -0
  13. package/src/api/library/fetchLibraryFile.ts +18 -0
  14. package/src/api/retry.ts +28 -0
  15. package/src/api/source/fetchSourceFile.ts +33 -0
  16. package/src/api/source/listSourceFiles.ts +13 -0
  17. package/src/api/source/putSourceFile.ts +14 -0
  18. package/src/api/source/schema.ts +10 -0
  19. package/src/api/utils.ts +52 -0
  20. package/src/bootstrap.sh +26 -0
  21. package/src/commander.ts +119 -0
  22. package/src/config/authConfig.ts +42 -0
  23. package/src/config/config.ts +109 -0
  24. package/src/config/envConfig.ts +23 -0
  25. package/src/config/fileConfig.ts +39 -0
  26. package/src/config/schema.ts +70 -0
  27. package/src/config/searchTemplatesConfig.ts +33 -0
  28. package/src/console/logger.ts +93 -0
  29. package/src/console/userPrompt.ts +16 -0
  30. package/src/errors/InvalidLoginResponseError.ts +14 -0
  31. package/src/errors/MissingConfigurationError.ts +14 -0
  32. package/src/errors/NostoError.ts +13 -0
  33. package/src/errors/NotNostoTemplateError.ts +15 -0
  34. package/src/errors/withErrorHandler.ts +50 -0
  35. package/src/exports.ts +8 -0
  36. package/src/filesystem/asserts/assertGitRepo.ts +19 -0
  37. package/src/filesystem/asserts/assertNostoTemplate.ts +34 -0
  38. package/src/filesystem/calculateTreeHash.ts +28 -0
  39. package/src/filesystem/esbuild.ts +37 -0
  40. package/src/filesystem/esbuildPlugins.ts +72 -0
  41. package/src/filesystem/filesystem.ts +40 -0
  42. package/src/filesystem/isIgnored.ts +65 -0
  43. package/src/filesystem/legacyUtils.ts +10 -0
  44. package/src/filesystem/loadLibrary.ts +31 -0
  45. package/src/filesystem/processInBatches.ts +38 -0
  46. package/src/filesystem/utils/getLoaderScript.ts +28 -0
  47. package/src/index.ts +3 -0
  48. package/src/modules/login.ts +87 -0
  49. package/src/modules/logout.ts +13 -0
  50. package/src/modules/search-templates/build.ts +61 -0
  51. package/src/modules/search-templates/dev.ts +50 -0
  52. package/src/modules/search-templates/pull.ts +89 -0
  53. package/src/modules/search-templates/push.ts +121 -0
  54. package/src/modules/setup.ts +96 -0
  55. package/src/modules/status.ts +71 -0
  56. package/src/utils/withSafeEnvironment.ts +22 -0
  57. package/test/api/fetchSourceFile.test.ts +30 -0
  58. package/test/api/putSourceFile.test.ts +34 -0
  59. package/test/api/retry.test.ts +102 -0
  60. package/test/api/utils.test.ts +27 -0
  61. package/test/commander.test.ts +271 -0
  62. package/test/config/envConfig.test.ts +62 -0
  63. package/test/config/fileConfig.test.ts +63 -0
  64. package/test/config/schema.test.ts +96 -0
  65. package/test/config/searchTemplatesConfig.test.ts +43 -0
  66. package/test/console/logger.test.ts +96 -0
  67. package/test/errors/withErrorHandler.test.ts +64 -0
  68. package/test/filesystem/filesystem.test.ts +53 -0
  69. package/test/filesystem/plugins.test.ts +35 -0
  70. package/test/index.test.ts +15 -0
  71. package/test/modules/search-templates/build.legacy.test.ts +74 -0
  72. package/test/modules/search-templates/build.modern.test.ts +33 -0
  73. package/test/modules/search-templates/dev.legacy.test.ts +75 -0
  74. package/test/modules/search-templates/dev.modern.test.ts +44 -0
  75. package/test/modules/search-templates/pull.test.ts +96 -0
  76. package/test/modules/search-templates/push.test.ts +109 -0
  77. package/test/modules/setup.test.ts +49 -0
  78. package/test/modules/status.test.ts +22 -0
  79. package/test/setup.ts +28 -0
  80. package/test/utils/generateEndpointMock.ts +60 -0
  81. package/test/utils/mockCommander.ts +22 -0
  82. package/test/utils/mockConfig.ts +37 -0
  83. package/test/utils/mockConsole.ts +65 -0
  84. package/test/utils/mockFileSystem.ts +52 -0
  85. package/test/utils/mockServer.ts +76 -0
  86. package/test/utils/mockStarterManifest.ts +42 -0
  87. package/tsconfig.json +20 -0
  88. package/vitest.config.ts +33 -0
@@ -0,0 +1,22 @@
1
+ import { loadConfig, type LoadConfigProps } from "#config/config.ts"
2
+ import { withErrorHandler } from "#errors/withErrorHandler.ts"
3
+ import { assertGitRepo } from "#filesystem/asserts/assertGitRepo.ts"
4
+ import { assertNostoTemplate } from "#filesystem/asserts/assertNostoTemplate.ts"
5
+
6
+ type Props = LoadConfigProps & {
7
+ skipSanityCheck?: boolean
8
+ }
9
+
10
+ export async function withSafeEnvironment(
11
+ { skipSanityCheck, ...props }: Props,
12
+ fn: () => void | Promise<void>
13
+ ): Promise<void> {
14
+ await withErrorHandler(async () => {
15
+ await loadConfig(props)
16
+ if (!skipSanityCheck) {
17
+ assertNostoTemplate()
18
+ }
19
+ assertGitRepo()
20
+ await fn()
21
+ })
22
+ }
@@ -0,0 +1,30 @@
1
+ import { describe, expect, it } from "vitest"
2
+
3
+ import { fetchSourceFileIfExists } from "#api/source/fetchSourceFile.js"
4
+ import { mockFetchSourceFile, setupMockServer } from "#test/utils/mockServer.ts"
5
+
6
+ const server = setupMockServer()
7
+
8
+ describe("fetchSourceFileIfExists", () => {
9
+ it("fetches the file if exists", async () => {
10
+ mockFetchSourceFile(server, { path: "test.txt", response: "file content" })
11
+
12
+ const file = await fetchSourceFileIfExists("test.txt")
13
+
14
+ expect(file).toBe('"file content"')
15
+ })
16
+
17
+ it("returns null if server returns 404", async () => {
18
+ mockFetchSourceFile(server, { path: "test.txt", error: { status: 404, message: "Not Found" } })
19
+
20
+ const file = await fetchSourceFileIfExists("test.txt")
21
+
22
+ expect(file).toBe(null)
23
+ })
24
+
25
+ it("throws an error for status code 500", async () => {
26
+ mockFetchSourceFile(server, { path: "test.txt", error: { status: 500, message: "Server Error" } })
27
+
28
+ await expect(fetchSourceFileIfExists("test.txt")).rejects.toThrow()
29
+ })
30
+ })
@@ -0,0 +1,34 @@
1
+ import { describe, expect, it } from "vitest"
2
+
3
+ import { putSourceFile } from "#api/source/putSourceFile.js"
4
+ import { setupMockConfig } from "#test/utils/mockConfig.ts"
5
+ import { mockFetchSourceFile, mockPutSourceFile, setupMockServer } from "#test/utils/mockServer.ts"
6
+
7
+ const server = setupMockServer()
8
+
9
+ describe("putSourceFile", () => {
10
+ it("sends the file content", async () => {
11
+ const endpoint = mockPutSourceFile(server, { path: "test.txt" })
12
+
13
+ await putSourceFile("test.txt", "file content")
14
+
15
+ const invocations = endpoint.invocations
16
+ expect(endpoint.invocations.length).toBeGreaterThan(0)
17
+ expect(invocations.some(invocation => invocation === "file content")).toBe(true)
18
+ })
19
+
20
+ it("throws an error for status code 404", async () => {
21
+ mockFetchSourceFile(server, { path: "test.txt", error: { status: 404, message: "Not Found" } })
22
+
23
+ await expect(putSourceFile("test.txt", "file content")).rejects.toThrow()
24
+ })
25
+
26
+ it("returns early for a dry run", async () => {
27
+ const endpoint = mockPutSourceFile(server, { path: "test.txt" })
28
+ setupMockConfig({ dryRun: true })
29
+
30
+ await putSourceFile("test.txt", "file content")
31
+
32
+ expect(endpoint.invocations.length).toBe(0)
33
+ })
34
+ })
@@ -0,0 +1,102 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
2
+
3
+ import { fetchWithRetry } from "#api/retry.ts"
4
+ import { setupMockConsole } from "#test/utils/mockConsole.ts"
5
+
6
+ const terminal = setupMockConsole()
7
+
8
+ describe("API Retry", () => {
9
+ beforeEach(() => {
10
+ vi.useFakeTimers()
11
+ })
12
+
13
+ afterEach(() => {
14
+ vi.useRealTimers()
15
+ })
16
+
17
+ describe("fetchWithRetry", () => {
18
+ it("should return result on first successful attempt", async () => {
19
+ const mockApiFunction = vi.fn().mockResolvedValue("success")
20
+
21
+ const result = await fetchWithRetry(mockApiFunction, "test-file.txt")
22
+
23
+ expect(result).toBe("success")
24
+ expect(mockApiFunction).toHaveBeenCalledTimes(1)
25
+ expect(mockApiFunction).toHaveBeenCalledWith("test-file.txt")
26
+ })
27
+
28
+ it("should retry on failure and eventually succeed", async () => {
29
+ const mockApiFunction = vi
30
+ .fn()
31
+ .mockRejectedValueOnce(new Error("First failure"))
32
+ .mockRejectedValueOnce(new Error("Second failure"))
33
+ .mockResolvedValue("success")
34
+
35
+ const retryPromise = fetchWithRetry(mockApiFunction, "test-file.txt")
36
+
37
+ // Fast forward through the retry delays
38
+ await vi.runAllTimersAsync()
39
+
40
+ const result = await retryPromise
41
+
42
+ expect(result).toBe("success")
43
+ expect(mockApiFunction).toHaveBeenCalledTimes(3)
44
+ })
45
+
46
+ it("should throw error after max retries exceeded", async () => {
47
+ const mockApiFunction = vi.fn().mockRejectedValue(new Error("Persistent failure"))
48
+
49
+ const retryPromise = fetchWithRetry(mockApiFunction, "test-file.txt")
50
+
51
+ // Fast forward through all retry delays
52
+ const assertion = expect(retryPromise).rejects.toThrow(
53
+ "Failed to fetch test-file.txt after 3 retries: Persistent failure"
54
+ )
55
+ await vi.runAllTimersAsync()
56
+
57
+ await assertion
58
+
59
+ expect(mockApiFunction).toHaveBeenCalledTimes(4) // Initial + 3 retries
60
+ })
61
+
62
+ it("should log warnings during retries", async () => {
63
+ const mockApiFunction = vi.fn().mockRejectedValueOnce(new Error("Temporary failure")).mockResolvedValue("success")
64
+
65
+ const retryPromise = fetchWithRetry(mockApiFunction, "test-file.txt")
66
+
67
+ // Fast forward through the retry delay
68
+ await vi.runAllTimersAsync()
69
+
70
+ await retryPromise
71
+
72
+ expect(terminal.getSpy("warn")).toHaveBeenCalledWith(
73
+ expect.stringContaining("Failed to fetch test-file.txt: Retrying in 1000ms (attempt 1/3)")
74
+ )
75
+ })
76
+
77
+ it("should log error on final failure", async () => {
78
+ const mockApiFunction = vi.fn().mockRejectedValue(new Error("Final error"))
79
+
80
+ const retryPromise = fetchWithRetry(mockApiFunction, "test-file.txt")
81
+
82
+ const assertion = expect(retryPromise).rejects.toThrow()
83
+ await vi.runAllTimersAsync()
84
+
85
+ await assertion
86
+
87
+ expect(terminal.getSpy("error")).toHaveBeenCalledWith(expect.stringContaining("test-file.txt: Final error"))
88
+ })
89
+
90
+ it("should handle non-Error objects", async () => {
91
+ const mockApiFunction = vi.fn().mockRejectedValue("String error")
92
+
93
+ const retryPromise = fetchWithRetry(mockApiFunction, "test-file.txt")
94
+
95
+ const assertion = expect(retryPromise).rejects.toThrow(
96
+ "Failed to fetch test-file.txt after 3 retries: String error"
97
+ )
98
+ await vi.runAllTimersAsync()
99
+ await assertion
100
+ })
101
+ })
102
+ })
@@ -0,0 +1,27 @@
1
+ import { describe, expect, it } from "vitest"
2
+
3
+ import { cleanUrl } from "#api/utils.ts"
4
+
5
+ describe("API Utils", () => {
6
+ describe("cleanUrl", () => {
7
+ it("should remove leading slash", () => {
8
+ expect(cleanUrl("/path/to/resource")).toBe("path/to/resource")
9
+ })
10
+
11
+ it("should remove trailing slash", () => {
12
+ expect(cleanUrl("path/to/resource/")).toBe("path/to/resource")
13
+ })
14
+
15
+ it("should remove both leading and trailing slashes", () => {
16
+ expect(cleanUrl("/path/to/resource/")).toBe("path/to/resource")
17
+ })
18
+
19
+ it("should not modify URL without slashes", () => {
20
+ expect(cleanUrl("path/to/resource")).toBe("path/to/resource")
21
+ })
22
+
23
+ it("should handle empty string", () => {
24
+ expect(cleanUrl("")).toBe("")
25
+ })
26
+ })
27
+ })
@@ -0,0 +1,271 @@
1
+ import { beforeEach, describe, expect, it, MockInstance, vi } from "vitest"
2
+
3
+ import { MissingConfigurationError } from "#errors/MissingConfigurationError.ts"
4
+ import * as build from "#modules/search-templates/build.ts"
5
+ import * as dev from "#modules/search-templates/dev.ts"
6
+ import * as pull from "#modules/search-templates/pull.ts"
7
+ import * as push from "#modules/search-templates/push.ts"
8
+ import * as setup from "#modules/setup.ts"
9
+ import * as status from "#modules/status.ts"
10
+
11
+ import { setupMockCommander } from "./utils/mockCommander.ts"
12
+ import { setupMockFileSystem } from "./utils/mockFileSystem.ts"
13
+
14
+ const fs = setupMockFileSystem()
15
+ const commander = setupMockCommander()
16
+
17
+ let setupSpy: MockInstance
18
+ let statusSpy: MockInstance
19
+ let buildSpy: MockInstance
20
+ let pullSpy: MockInstance
21
+ let pushSpy: MockInstance
22
+ let devSpy: MockInstance
23
+
24
+ describe("commander", () => {
25
+ // Make sure the actual functions are never called
26
+ beforeEach(() => {
27
+ setupSpy = vi.spyOn(setup, "printSetupHelp").mockImplementation(() => Promise.resolve())
28
+ statusSpy = vi.spyOn(status, "printStatus").mockImplementation(() => Promise.resolve())
29
+ buildSpy = vi.spyOn(build, "buildSearchTemplate").mockImplementation(() => Promise.resolve())
30
+ pullSpy = vi.spyOn(pull, "pullSearchTemplate").mockImplementation(() => Promise.resolve())
31
+ pushSpy = vi.spyOn(push, "pushSearchTemplate").mockImplementation(() => Promise.resolve())
32
+ devSpy = vi.spyOn(dev, "searchTemplateDevMode").mockImplementation(() => Promise.resolve())
33
+ })
34
+
35
+ describe("nosto setup", () => {
36
+ it("should call the function", async () => {
37
+ await commander.run("nosto setup")
38
+ expect(setupSpy).toHaveBeenCalledWith(".", {})
39
+ })
40
+
41
+ it("should call the function with project path", async () => {
42
+ await commander.run("nosto setup /path/to/project")
43
+ expect(setupSpy).toHaveBeenCalledWith("/path/to/project", {})
44
+ })
45
+
46
+ it("should handle MissingConfigurationError", async () => {
47
+ vi.spyOn(setup, "printSetupHelp").mockImplementation(() => {
48
+ throw new MissingConfigurationError("Missing configuration")
49
+ })
50
+
51
+ await commander.expect("nosto setup").toResolve()
52
+ })
53
+
54
+ it("should rethrow other errors", async () => {
55
+ vi.spyOn(setup, "printSetupHelp").mockImplementation(() => {
56
+ throw new Error("Unknown error")
57
+ })
58
+
59
+ await commander.expect("nosto setup").toThrow()
60
+ })
61
+
62
+ it("does not call other modules", async () => {
63
+ await commander.run("nosto setup")
64
+ expect(statusSpy).not.toHaveBeenCalled()
65
+ expect(buildSpy).not.toHaveBeenCalled()
66
+ expect(pullSpy).not.toHaveBeenCalled()
67
+ expect(pushSpy).not.toHaveBeenCalled()
68
+ expect(devSpy).not.toHaveBeenCalled()
69
+ })
70
+ })
71
+
72
+ describe("nosto status", () => {
73
+ it("should call the function", async () => {
74
+ await commander.run("nosto status")
75
+ expect(statusSpy).toHaveBeenCalledWith(".")
76
+ })
77
+
78
+ it("should call the function with project path", async () => {
79
+ await commander.run("nosto status /path/to/project")
80
+ expect(statusSpy).toHaveBeenCalledWith("/path/to/project")
81
+ })
82
+
83
+ it("should handle MissingConfigurationError", async () => {
84
+ vi.spyOn(status, "printStatus").mockImplementation(() => {
85
+ throw new MissingConfigurationError("Missing configuration")
86
+ })
87
+
88
+ await commander.expect("nosto status").toResolve()
89
+ })
90
+
91
+ it("should rethrow other errors", async () => {
92
+ vi.spyOn(status, "printStatus").mockImplementation(() => {
93
+ throw new Error("Unknown error")
94
+ })
95
+
96
+ await commander.expect("nosto status").toThrow()
97
+ })
98
+
99
+ it("does not call other modules", async () => {
100
+ await commander.run("nosto status")
101
+ expect(setupSpy).not.toHaveBeenCalled()
102
+ expect(buildSpy).not.toHaveBeenCalled()
103
+ expect(pullSpy).not.toHaveBeenCalled()
104
+ expect(pushSpy).not.toHaveBeenCalled()
105
+ expect(devSpy).not.toHaveBeenCalled()
106
+ })
107
+ })
108
+
109
+ describe("nosto search-templates build", () => {
110
+ it("should fail sanity check", async () => {
111
+ await commander.run("nosto st build")
112
+ expect(buildSpy).not.toHaveBeenCalled()
113
+ })
114
+
115
+ describe("with valid environment", () => {
116
+ beforeEach(() => {
117
+ fs.writeFile(".nosto.json", JSON.stringify({ apiKey: "123", merchant: "456" }))
118
+ fs.writeFile("index.js", "@nosto/preact")
119
+ })
120
+
121
+ it("should call the function", async () => {
122
+ await commander.run("nosto st build")
123
+ expect(buildSpy).toHaveBeenCalledWith({ watch: false })
124
+ })
125
+
126
+ it("should rethrow errors", async () => {
127
+ vi.spyOn(build, "buildSearchTemplate").mockImplementation(() => {
128
+ throw new Error("Unknown error")
129
+ })
130
+
131
+ await commander.expect("nosto st build").toThrow()
132
+ })
133
+
134
+ it("does not call other modules", async () => {
135
+ await commander.run("nosto st build")
136
+ expect(setupSpy).not.toHaveBeenCalled()
137
+ expect(statusSpy).not.toHaveBeenCalled()
138
+ expect(pullSpy).not.toHaveBeenCalled()
139
+ expect(pushSpy).not.toHaveBeenCalled()
140
+ expect(devSpy).not.toHaveBeenCalled()
141
+ })
142
+ })
143
+ })
144
+
145
+ describe("nosto search-templates pull", () => {
146
+ it("should pull even without files present", async () => {
147
+ await commander.run("nosto st pull")
148
+ expect(pullSpy).toHaveBeenCalled()
149
+ })
150
+
151
+ describe("with valid environment", () => {
152
+ beforeEach(() => {
153
+ fs.writeFile(".nosto.json", JSON.stringify({ apiKey: "123", merchant: "456" }))
154
+ fs.writeFile("index.js", "@nosto/preact")
155
+ })
156
+
157
+ it("should call the function", async () => {
158
+ await commander.run("nosto st pull")
159
+ expect(pullSpy).toHaveBeenCalledWith({ force: false, paths: [] })
160
+ })
161
+
162
+ it("should call the function with short parameters", async () => {
163
+ await commander.run("nosto st pull -f -p build index.js")
164
+ expect(pullSpy).toHaveBeenCalledWith({ force: true, paths: ["build", "index.js"] })
165
+ })
166
+
167
+ it("should call the function with full parameters", async () => {
168
+ await commander.run("nosto st pull --force --paths build index.js")
169
+ expect(pullSpy).toHaveBeenCalledWith({ force: true, paths: ["build", "index.js"] })
170
+ })
171
+
172
+ it("should rethrow errors", async () => {
173
+ vi.spyOn(pull, "pullSearchTemplate").mockImplementation(() => {
174
+ throw new Error("Unknown error")
175
+ })
176
+
177
+ await commander.expect("nosto st pull").toThrow()
178
+ })
179
+
180
+ it("does not call other modules", async () => {
181
+ await commander.run("nosto st pull")
182
+ expect(setupSpy).not.toHaveBeenCalled()
183
+ expect(statusSpy).not.toHaveBeenCalled()
184
+ expect(buildSpy).not.toHaveBeenCalled()
185
+ })
186
+ })
187
+ })
188
+
189
+ describe("nosto search-templates push", () => {
190
+ it("should fail sanity check", async () => {
191
+ await commander.run("nosto st push")
192
+ expect(pushSpy).not.toHaveBeenCalled()
193
+ })
194
+
195
+ describe("with valid environment", () => {
196
+ beforeEach(() => {
197
+ fs.writeFile(".nosto.json", JSON.stringify({ apiKey: "123", merchant: "456" }))
198
+ fs.writeFile("index.js", "@nosto/preact")
199
+ })
200
+
201
+ it("should call the build and push functions", async () => {
202
+ await commander.run("nosto st push")
203
+ expect(buildSpy).toHaveBeenCalledWith({ watch: false })
204
+ expect(pushSpy).toHaveBeenCalledWith({ force: false, paths: [] })
205
+ })
206
+
207
+ it("should call the function with short parameters", async () => {
208
+ await commander.run("nosto st push -f -p build index.js")
209
+ expect(buildSpy).toHaveBeenCalledWith({ watch: false })
210
+ expect(pushSpy).toHaveBeenCalledWith({ force: true, paths: ["build", "index.js"] })
211
+ })
212
+
213
+ it("should call the function with full parameters", async () => {
214
+ await commander.run("nosto st push --force --paths build index.js")
215
+ expect(buildSpy).toHaveBeenCalledWith({ watch: false })
216
+ expect(pushSpy).toHaveBeenCalledWith({ force: true, paths: ["build", "index.js"] })
217
+ })
218
+
219
+ it("should rethrow errors", async () => {
220
+ vi.spyOn(push, "pushSearchTemplate").mockImplementation(() => {
221
+ throw new Error("Unknown error")
222
+ })
223
+
224
+ await commander.expect("nosto st push").toThrow()
225
+ })
226
+
227
+ it("does not call other modules", async () => {
228
+ await commander.run("nosto st push")
229
+ expect(setupSpy).not.toHaveBeenCalled()
230
+ expect(statusSpy).not.toHaveBeenCalled()
231
+ expect(pullSpy).not.toHaveBeenCalled()
232
+ })
233
+ })
234
+ })
235
+
236
+ describe("nosto search-templates dev", () => {
237
+ it("should fail sanity check", async () => {
238
+ await commander.run("nosto st dev")
239
+ expect(devSpy).not.toHaveBeenCalled()
240
+ })
241
+
242
+ describe("with valid environment", () => {
243
+ beforeEach(() => {
244
+ fs.writeFile(".nosto.json", JSON.stringify({ apiKey: "123", merchant: "456" }))
245
+ fs.writeFile("index.js", "@nosto/preact")
246
+ })
247
+
248
+ it("should call the function", async () => {
249
+ await commander.run("nosto st dev")
250
+ expect(devSpy).toHaveBeenCalled()
251
+ })
252
+
253
+ it("should rethrow errors", async () => {
254
+ vi.spyOn(dev, "searchTemplateDevMode").mockImplementation(() => {
255
+ throw new Error("Unknown error")
256
+ })
257
+
258
+ await commander.expect("nosto st dev").toThrow()
259
+ })
260
+
261
+ it("does not call other modules", async () => {
262
+ await commander.run("nosto st dev")
263
+ expect(setupSpy).not.toHaveBeenCalled()
264
+ expect(statusSpy).not.toHaveBeenCalled()
265
+ expect(buildSpy).not.toHaveBeenCalled()
266
+ expect(pullSpy).not.toHaveBeenCalled()
267
+ expect(pushSpy).not.toHaveBeenCalled()
268
+ })
269
+ })
270
+ })
271
+ })
@@ -0,0 +1,62 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from "vitest"
2
+
3
+ import { EnvVariables, getEnvConfig } from "#config/envConfig.ts"
4
+
5
+ describe("Env Config", () => {
6
+ const originalEnv = process.env
7
+
8
+ beforeEach(() => {
9
+ process.env = {}
10
+ })
11
+
12
+ afterEach(() => {
13
+ process.env = originalEnv
14
+ })
15
+
16
+ describe("getEnvConfig", () => {
17
+ it("should return empty config when no environment variables are set", () => {
18
+ const result = getEnvConfig()
19
+ expect(result).toEqual({})
20
+ })
21
+
22
+ it("should parse environment variables correctly", () => {
23
+ process.env[EnvVariables.apiKey] = "env-api-key"
24
+ process.env[EnvVariables.merchant] = "env-merchant"
25
+ process.env[EnvVariables.logLevel] = "debug"
26
+ process.env[EnvVariables.maxRequests] = "25"
27
+
28
+ const result = getEnvConfig()
29
+
30
+ expect(result).toEqual(
31
+ expect.objectContaining({
32
+ apiKey: "env-api-key",
33
+ merchant: "env-merchant",
34
+ logLevel: "debug",
35
+ maxRequests: 25 // Coerced to number by schema
36
+ })
37
+ )
38
+ })
39
+
40
+ it("should handle all possible environment variables", () => {
41
+ process.env[EnvVariables.apiKey] = "another-key"
42
+ process.env[EnvVariables.merchant] = "another-merchant"
43
+ process.env[EnvVariables.templatesEnv] = "staging"
44
+ process.env[EnvVariables.apiUrl] = "https://custom-api.com"
45
+ process.env[EnvVariables.libraryUrl] = "https://custom-library.com"
46
+ process.env[EnvVariables.logLevel] = "warn"
47
+ process.env[EnvVariables.maxRequests] = "50"
48
+
49
+ const result = getEnvConfig()
50
+
51
+ expect(result).toEqual({
52
+ apiKey: "another-key",
53
+ merchant: "another-merchant",
54
+ templatesEnv: "staging",
55
+ apiUrl: "https://custom-api.com",
56
+ libraryUrl: "https://custom-library.com",
57
+ logLevel: "warn",
58
+ maxRequests: 50 // Coerced to number by schema
59
+ })
60
+ })
61
+ })
62
+ })
@@ -0,0 +1,63 @@
1
+ import { describe, expect, it } from "vitest"
2
+
3
+ import { parseConfigFile } from "#config/fileConfig.ts"
4
+ import { setupMockFileSystem } from "#test/utils/mockFileSystem.ts"
5
+
6
+ const fs = setupMockFileSystem()
7
+
8
+ describe("File Config", () => {
9
+ describe("parseConfigFile", () => {
10
+ it("should return empty object when config file does not exist", () => {
11
+ const result = parseConfigFile({ projectPath: "." })
12
+ expect(result).toEqual({})
13
+ })
14
+
15
+ it("should parse valid JSON config file", () => {
16
+ const mockConfig = {
17
+ apiKey: "another-key",
18
+ merchant: "another-merchant",
19
+ logLevel: "debug",
20
+ apiUrl: "https://api.nosto.com"
21
+ }
22
+
23
+ fs.writeFile(".nosto.json", JSON.stringify(mockConfig))
24
+
25
+ const result = parseConfigFile({ projectPath: "." })
26
+
27
+ expect(result).toEqual({
28
+ apiKey: "another-key",
29
+ apiUrl: "https://api.nosto.com",
30
+ libraryUrl: "https://d11ffvpvtnmt0d.cloudfront.net/library",
31
+ logLevel: "debug",
32
+ maxRequests: 15,
33
+ merchant: "another-merchant",
34
+ templatesEnv: "main"
35
+ })
36
+ })
37
+
38
+ it("should throw error for invalid JSON", () => {
39
+ fs.writeFile(".nosto.json", "invalid json")
40
+
41
+ expect(() => parseConfigFile({ projectPath: "." })).toThrow("Invalid JSON in configuration file")
42
+ })
43
+
44
+ it("should throw error for invalid config schema", () => {
45
+ const invalidConfig = {
46
+ apiKey: "test-key",
47
+ logLevel: "invalid-level"
48
+ }
49
+
50
+ fs.writeFile(".nosto.json", JSON.stringify(invalidConfig))
51
+
52
+ expect(() => parseConfigFile({ projectPath: "." })).toThrow("Invalid configuration file")
53
+ })
54
+
55
+ it("should rethrow other errors", () => {
56
+ fs.writeFolder(".nosto.json")
57
+
58
+ expect(() => parseConfigFile({ projectPath: "." })).toThrow(
59
+ "EISDIR: illegal operation on a directory, open '/.nosto.json'"
60
+ )
61
+ })
62
+ })
63
+ })