@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.
- package/.github/copilot-instructions.md +326 -0
- package/.github/dependabot.yml +9 -0
- package/.github/pull_request_template.md +12 -0
- package/.github/workflows/ci.yml +58 -0
- package/.github/workflows/release.yml +49 -0
- package/.husky/commit-msg +1 -0
- package/.prettierrc +9 -0
- package/LICENSE +29 -0
- package/README.md +154 -0
- package/commitlint.config.js +4 -0
- package/eslint.config.js +36 -0
- package/package.json +63 -0
- package/src/api/library/fetchLibraryFile.ts +18 -0
- package/src/api/retry.ts +28 -0
- package/src/api/source/fetchSourceFile.ts +33 -0
- package/src/api/source/listSourceFiles.ts +13 -0
- package/src/api/source/putSourceFile.ts +14 -0
- package/src/api/source/schema.ts +10 -0
- package/src/api/utils.ts +52 -0
- package/src/bootstrap.sh +26 -0
- package/src/commander.ts +119 -0
- package/src/config/authConfig.ts +42 -0
- package/src/config/config.ts +109 -0
- package/src/config/envConfig.ts +23 -0
- package/src/config/fileConfig.ts +39 -0
- package/src/config/schema.ts +70 -0
- package/src/config/searchTemplatesConfig.ts +33 -0
- package/src/console/logger.ts +93 -0
- package/src/console/userPrompt.ts +16 -0
- package/src/errors/InvalidLoginResponseError.ts +14 -0
- package/src/errors/MissingConfigurationError.ts +14 -0
- package/src/errors/NostoError.ts +13 -0
- package/src/errors/NotNostoTemplateError.ts +15 -0
- package/src/errors/withErrorHandler.ts +50 -0
- package/src/exports.ts +8 -0
- package/src/filesystem/asserts/assertGitRepo.ts +19 -0
- package/src/filesystem/asserts/assertNostoTemplate.ts +34 -0
- package/src/filesystem/calculateTreeHash.ts +28 -0
- package/src/filesystem/esbuild.ts +37 -0
- package/src/filesystem/esbuildPlugins.ts +72 -0
- package/src/filesystem/filesystem.ts +40 -0
- package/src/filesystem/isIgnored.ts +65 -0
- package/src/filesystem/legacyUtils.ts +10 -0
- package/src/filesystem/loadLibrary.ts +31 -0
- package/src/filesystem/processInBatches.ts +38 -0
- package/src/filesystem/utils/getLoaderScript.ts +28 -0
- package/src/index.ts +3 -0
- package/src/modules/login.ts +87 -0
- package/src/modules/logout.ts +13 -0
- package/src/modules/search-templates/build.ts +61 -0
- package/src/modules/search-templates/dev.ts +50 -0
- package/src/modules/search-templates/pull.ts +89 -0
- package/src/modules/search-templates/push.ts +121 -0
- package/src/modules/setup.ts +96 -0
- package/src/modules/status.ts +71 -0
- package/src/utils/withSafeEnvironment.ts +22 -0
- package/test/api/fetchSourceFile.test.ts +30 -0
- package/test/api/putSourceFile.test.ts +34 -0
- package/test/api/retry.test.ts +102 -0
- package/test/api/utils.test.ts +27 -0
- package/test/commander.test.ts +271 -0
- package/test/config/envConfig.test.ts +62 -0
- package/test/config/fileConfig.test.ts +63 -0
- package/test/config/schema.test.ts +96 -0
- package/test/config/searchTemplatesConfig.test.ts +43 -0
- package/test/console/logger.test.ts +96 -0
- package/test/errors/withErrorHandler.test.ts +64 -0
- package/test/filesystem/filesystem.test.ts +53 -0
- package/test/filesystem/plugins.test.ts +35 -0
- package/test/index.test.ts +15 -0
- package/test/modules/search-templates/build.legacy.test.ts +74 -0
- package/test/modules/search-templates/build.modern.test.ts +33 -0
- package/test/modules/search-templates/dev.legacy.test.ts +75 -0
- package/test/modules/search-templates/dev.modern.test.ts +44 -0
- package/test/modules/search-templates/pull.test.ts +96 -0
- package/test/modules/search-templates/push.test.ts +109 -0
- package/test/modules/setup.test.ts +49 -0
- package/test/modules/status.test.ts +22 -0
- package/test/setup.ts +28 -0
- package/test/utils/generateEndpointMock.ts +60 -0
- package/test/utils/mockCommander.ts +22 -0
- package/test/utils/mockConfig.ts +37 -0
- package/test/utils/mockConsole.ts +65 -0
- package/test/utils/mockFileSystem.ts +52 -0
- package/test/utils/mockServer.ts +76 -0
- package/test/utils/mockStarterManifest.ts +42 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +33 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { describe, it } from "vitest"
|
|
2
|
+
|
|
3
|
+
import { pullSearchTemplate } from "#modules/search-templates/pull.ts"
|
|
4
|
+
import { setupMockConsole } from "#test/utils/mockConsole.ts"
|
|
5
|
+
import { setupMockFileSystem } from "#test/utils/mockFileSystem.ts"
|
|
6
|
+
import { mockFetchSourceFile, mockListSourceFiles, setupMockServer } from "#test/utils/mockServer.ts"
|
|
7
|
+
|
|
8
|
+
const fs = setupMockFileSystem()
|
|
9
|
+
const server = setupMockServer()
|
|
10
|
+
const terminal = setupMockConsole()
|
|
11
|
+
|
|
12
|
+
describe("Pull Search Template", () => {
|
|
13
|
+
it("should fetch all files when no paths specified", async () => {
|
|
14
|
+
mockListSourceFiles(server, {
|
|
15
|
+
response: [
|
|
16
|
+
{ path: "index.js", size: 10 },
|
|
17
|
+
{ path: "wizard.js", size: 25 }
|
|
18
|
+
]
|
|
19
|
+
})
|
|
20
|
+
mockFetchSourceFile(server, {
|
|
21
|
+
path: "index.js",
|
|
22
|
+
response: "index.js content"
|
|
23
|
+
})
|
|
24
|
+
mockFetchSourceFile(server, {
|
|
25
|
+
path: "wizard.js",
|
|
26
|
+
response: "wizard.js content"
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
await pullSearchTemplate({ paths: [], force: true })
|
|
30
|
+
fs.expectFile("index.js").toContain('"index.js content"')
|
|
31
|
+
fs.expectFile("wizard.js").toContain('"wizard.js content"')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it("should filter by specified paths", async () => {
|
|
35
|
+
mockListSourceFiles(server, {
|
|
36
|
+
response: [
|
|
37
|
+
{ path: "index.js", size: 10 },
|
|
38
|
+
{ path: "wizard.js", size: 25 }
|
|
39
|
+
]
|
|
40
|
+
})
|
|
41
|
+
mockFetchSourceFile(server, {
|
|
42
|
+
path: "index.js",
|
|
43
|
+
response: "index.js content"
|
|
44
|
+
})
|
|
45
|
+
mockFetchSourceFile(server, {
|
|
46
|
+
path: "wizard.js",
|
|
47
|
+
response: "wizard.js content"
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
await pullSearchTemplate({ paths: ["index.js"], force: true })
|
|
51
|
+
|
|
52
|
+
fs.expectFile("index.js").toContain('"index.js content"')
|
|
53
|
+
fs.expectFile("wizard.js").not.toExist()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it("should prompt for confirmation when files will be overridden", async () => {
|
|
57
|
+
fs.writeFile("index.js", "old content")
|
|
58
|
+
mockListSourceFiles(server, {
|
|
59
|
+
response: [{ path: "index.js", size: 10 }]
|
|
60
|
+
})
|
|
61
|
+
mockFetchSourceFile(server, {
|
|
62
|
+
path: "index.js",
|
|
63
|
+
response: "index.js content"
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
terminal.setUserResponse("N")
|
|
67
|
+
await pullSearchTemplate({ paths: [], force: false })
|
|
68
|
+
terminal.expect.user.toHaveBeenPromptedWith("Are you sure you want to override your local data? (y/N):")
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it("should cancel operation when user declines override", async () => {
|
|
72
|
+
fs.writeFile("index.js", "old content")
|
|
73
|
+
mockListSourceFiles(server, {
|
|
74
|
+
response: [{ path: "index.js", size: 10 }]
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
terminal.setUserResponse("N")
|
|
78
|
+
await pullSearchTemplate({ paths: [], force: false })
|
|
79
|
+
fs.expectFile("/index.js").toContain("old content")
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it("should proceed with download when user confirms override", async () => {
|
|
83
|
+
fs.writeFile("index.js", "old content")
|
|
84
|
+
mockListSourceFiles(server, {
|
|
85
|
+
response: [{ path: "index.js", size: 10 }]
|
|
86
|
+
})
|
|
87
|
+
mockFetchSourceFile(server, {
|
|
88
|
+
path: "index.js",
|
|
89
|
+
response: "index.js content"
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
terminal.setUserResponse("Y")
|
|
93
|
+
await pullSearchTemplate({ paths: [], force: false })
|
|
94
|
+
fs.expectFile("index.js").toContain('"index.js content"')
|
|
95
|
+
})
|
|
96
|
+
})
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
|
|
3
|
+
import { pushSearchTemplate } from "#modules/search-templates/push.ts"
|
|
4
|
+
import { setupMockConfig } from "#test/utils/mockConfig.ts"
|
|
5
|
+
import { setupMockConsole } from "#test/utils/mockConsole.ts"
|
|
6
|
+
import { setupMockFileSystem } from "#test/utils/mockFileSystem.ts"
|
|
7
|
+
import { mockPutSourceFile, setupMockServer } from "#test/utils/mockServer.ts"
|
|
8
|
+
|
|
9
|
+
const fs = setupMockFileSystem()
|
|
10
|
+
const server = setupMockServer()
|
|
11
|
+
const terminal = setupMockConsole()
|
|
12
|
+
|
|
13
|
+
describe("Push Search Template", () => {
|
|
14
|
+
it("should throw error if target folder does not exist", async () => {
|
|
15
|
+
setupMockConfig({ projectPath: "/nonexistent/path" })
|
|
16
|
+
|
|
17
|
+
await expect(pushSearchTemplate({ paths: [], force: true })).rejects.toThrow(
|
|
18
|
+
"ENOENT: no such file or directory, scandir '/nonexistent/path'"
|
|
19
|
+
)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it("should throw error if target path is not a directory", async () => {
|
|
23
|
+
fs.writeFile("file.txt", "file content")
|
|
24
|
+
setupMockConfig({
|
|
25
|
+
projectPath: "./file.txt"
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
await expect(pushSearchTemplate({ paths: [], force: true })).rejects.toThrow(
|
|
29
|
+
"ENOTDIR: not a directory, scandir '/file.txt'"
|
|
30
|
+
)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it("should exit early if no files to push", async () => {
|
|
34
|
+
fs.writeFile(".gitignore", "*.js")
|
|
35
|
+
fs.writeFile("index.js", "content with @nosto/preact")
|
|
36
|
+
|
|
37
|
+
await pushSearchTemplate({ paths: [], force: true })
|
|
38
|
+
|
|
39
|
+
expect(terminal.getSpy("warn")).toHaveBeenCalledWith("No files to push. Exiting.")
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it("should process files and display summary", async () => {
|
|
43
|
+
fs.writeFile(".gitignore", "*.log")
|
|
44
|
+
fs.writeFile("index.js", "content with @nosto/preact")
|
|
45
|
+
fs.writeFile("file1.js", "file1 content")
|
|
46
|
+
fs.writeFile("file2.js", "file2 content")
|
|
47
|
+
fs.writeFile("ignored.log", "log content")
|
|
48
|
+
|
|
49
|
+
mockPutSourceFile(server, { path: "index.js" })
|
|
50
|
+
mockPutSourceFile(server, { path: "file1.js" })
|
|
51
|
+
mockPutSourceFile(server, { path: "file2.js" })
|
|
52
|
+
|
|
53
|
+
await pushSearchTemplate({ paths: [], force: true })
|
|
54
|
+
|
|
55
|
+
expect(terminal.getSpy("info")).toHaveBeenCalledWith("Pushing template from: /")
|
|
56
|
+
expect(terminal.getSpy("info")).toHaveBeenCalledWith("Found 4 files to push (3 source, 1 built, 1 ignored).")
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it("should not ignore build directory", async () => {
|
|
60
|
+
fs.writeFile(".gitignore", "build/")
|
|
61
|
+
fs.writeFile("index.js", "content with @nosto/preact")
|
|
62
|
+
fs.writeFile("build/index.js", "build content")
|
|
63
|
+
|
|
64
|
+
mockPutSourceFile(server, { path: "index.js" })
|
|
65
|
+
mockPutSourceFile(server, { path: "build/index.js" })
|
|
66
|
+
|
|
67
|
+
await pushSearchTemplate({ paths: [], force: true })
|
|
68
|
+
|
|
69
|
+
expect(terminal.getSpy("info")).toHaveBeenCalledWith("Pushing template from: /")
|
|
70
|
+
expect(terminal.getSpy("info")).toHaveBeenCalledWith("Found 3 files to push (1 source, 2 built, 0 ignored).")
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it("should cancel operation when user declines", async () => {
|
|
74
|
+
fs.writeFile("index.js", "content with @nosto/preact")
|
|
75
|
+
fs.writeFile("file1.js", "file1 content")
|
|
76
|
+
|
|
77
|
+
terminal.setUserResponse("N")
|
|
78
|
+
|
|
79
|
+
await pushSearchTemplate({ paths: [], force: false })
|
|
80
|
+
expect(terminal.getSpy("info")).toHaveBeenCalledWith("Push operation cancelled by user.")
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it("should filter files by specified paths", async () => {
|
|
84
|
+
fs.writeFile("index.js", "content with @nosto/preact")
|
|
85
|
+
fs.writeFile("file1.js", "file1 content")
|
|
86
|
+
fs.writeFile("file2.js", "file2 content")
|
|
87
|
+
fs.writeFile("file3.js", "file3 content")
|
|
88
|
+
|
|
89
|
+
mockPutSourceFile(server, { path: "index.js" })
|
|
90
|
+
mockPutSourceFile(server, { path: "file1.js" })
|
|
91
|
+
mockPutSourceFile(server, { path: "file3.js" })
|
|
92
|
+
|
|
93
|
+
await pushSearchTemplate({ paths: ["index.js", "file1.js", "file3.js"], force: true })
|
|
94
|
+
expect(terminal.getSpy("warn")).not.toHaveBeenCalledWith("No files to push. Exiting.")
|
|
95
|
+
expect(terminal.getSpy("info")).toHaveBeenCalledWith("Pushing template from: /")
|
|
96
|
+
expect(terminal.getSpy("info")).toHaveBeenCalledWith("Found 4 files to push (3 source, 1 built, 0 ignored).")
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
// TODO: Times out because of retry delay. Should be configurable in tests.
|
|
100
|
+
it.skip("should handle upload failures gracefully", async () => {
|
|
101
|
+
fs.writeFile("index.js", "content with @nosto/preact")
|
|
102
|
+
fs.writeFile("file1.js", "file1 content")
|
|
103
|
+
|
|
104
|
+
mockPutSourceFile(server, { path: "index.js", error: { status: 500, message: "Upload failed" } })
|
|
105
|
+
mockPutSourceFile(server, { path: "file1.js", error: { status: 500, message: "Upload failed" } })
|
|
106
|
+
|
|
107
|
+
await pushSearchTemplate({ paths: [], force: true })
|
|
108
|
+
})
|
|
109
|
+
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
|
|
3
|
+
import { printSetupHelp } from "#modules/setup.ts"
|
|
4
|
+
import { setupMockConsole } from "#test/utils/mockConsole.ts"
|
|
5
|
+
import { setupMockFileSystem } from "#test/utils/mockFileSystem.ts"
|
|
6
|
+
|
|
7
|
+
const fs = setupMockFileSystem()
|
|
8
|
+
const terminal = setupMockConsole()
|
|
9
|
+
|
|
10
|
+
describe("Setup Module", () => {
|
|
11
|
+
it("should print configuration help information", async () => {
|
|
12
|
+
fs.writeFile(".nosto.json", '{"apiKey": "test"}')
|
|
13
|
+
|
|
14
|
+
expect(async () => await printSetupHelp(".", {})).not.toThrow()
|
|
15
|
+
expect(terminal.getSpy("warn")).not.toHaveBeenCalledWith("Configuration file not found in project directory.")
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it("should show warning when config file not found", async () => {
|
|
19
|
+
terminal.setUserResponse("N")
|
|
20
|
+
|
|
21
|
+
expect(async () => await printSetupHelp(".", {})).not.toThrow()
|
|
22
|
+
expect(terminal.getSpy("warn")).toHaveBeenCalledWith("Configuration file not found in project directory.")
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it("should create config file when user confirms", async () => {
|
|
26
|
+
terminal.setUserResponse("Y")
|
|
27
|
+
|
|
28
|
+
await printSetupHelp(".", {})
|
|
29
|
+
|
|
30
|
+
terminal.expect.user.toHaveBeenPromptedWith("Would you like to create a configuration file? (Y/n):")
|
|
31
|
+
fs.expectFile(".nosto.json").toExist()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it("should not create config file when user declines", async () => {
|
|
35
|
+
terminal.setUserResponse("N")
|
|
36
|
+
|
|
37
|
+
await printSetupHelp(".", {})
|
|
38
|
+
|
|
39
|
+
fs.expectFile(".nosto.json").not.toExist()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it("should not prompt when config file already exists", async () => {
|
|
43
|
+
fs.writeFile(".nosto.json", '{"apiKey": "test"}')
|
|
44
|
+
|
|
45
|
+
await printSetupHelp(".", {})
|
|
46
|
+
|
|
47
|
+
terminal.expect.user.not.toHaveBeenPrompted()
|
|
48
|
+
})
|
|
49
|
+
})
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
|
|
3
|
+
import { printStatus } from "#modules/status.ts"
|
|
4
|
+
import { setupMockConfig } from "#test/utils/mockConfig.ts"
|
|
5
|
+
import { setupMockConsole } from "#test/utils/mockConsole.ts"
|
|
6
|
+
|
|
7
|
+
const terminal = setupMockConsole()
|
|
8
|
+
|
|
9
|
+
describe("Status Module", () => {
|
|
10
|
+
it("should print configuration status by default", async () => {
|
|
11
|
+
await expect(printStatus(".")).resolves.not.toThrow()
|
|
12
|
+
expect(terminal.getSpy("info")).toHaveBeenCalledWith("Configuration is not valid:")
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it("should indicate valid configuration", async () => {
|
|
16
|
+
setupMockConfig({ merchant: "test-merchant" })
|
|
17
|
+
|
|
18
|
+
await expect(printStatus(".")).resolves.not.toThrow()
|
|
19
|
+
expect(terminal.getSpy("info")).toHaveBeenCalledWith("Configuration seems to be valid:")
|
|
20
|
+
expect(terminal.getSpy("error")).toHaveBeenCalledWith("Some required configuration is missing\n")
|
|
21
|
+
})
|
|
22
|
+
})
|
package/test/setup.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Volume } from "memfs"
|
|
2
|
+
import { beforeEach } from "vitest"
|
|
3
|
+
import { vi } from "vitest"
|
|
4
|
+
|
|
5
|
+
import { mockedConsoleIn, mockedConsoleOut } from "./utils/mockConsole.ts"
|
|
6
|
+
|
|
7
|
+
export const testVolume = Volume.fromJSON({}, "/")
|
|
8
|
+
|
|
9
|
+
vi.mock("fs", () => {
|
|
10
|
+
return {
|
|
11
|
+
default: testVolume
|
|
12
|
+
}
|
|
13
|
+
})
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
testVolume.reset()
|
|
16
|
+
process.chdir("/")
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
vi.mock("readline/promises", () => {
|
|
20
|
+
return {
|
|
21
|
+
createInterface: () => mockedConsoleIn.interface
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
vi.mock("#/console/logger.ts", () => mockedConsoleOut)
|
|
25
|
+
|
|
26
|
+
vi.mock("node:test", () => {
|
|
27
|
+
throw new Error("You seem to have accidentally imported node:test instead of vitest.")
|
|
28
|
+
})
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type DefaultBodyType, http, HttpResponse, StrictRequest } from "msw"
|
|
2
|
+
import type { SetupServer } from "msw/node"
|
|
3
|
+
|
|
4
|
+
type HttpMethod = keyof typeof http
|
|
5
|
+
|
|
6
|
+
export type MockParams<ResponseT extends DefaultBodyType | void> =
|
|
7
|
+
| (ResponseT extends void ? object : { response: ResponseT })
|
|
8
|
+
| {
|
|
9
|
+
error: { status: number; message: string }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const generateEndpointMock = (
|
|
13
|
+
server: SetupServer,
|
|
14
|
+
{ method, path, ...params }: { method: HttpMethod; path: string } & MockParams<DefaultBodyType>
|
|
15
|
+
) => {
|
|
16
|
+
let invocations: unknown[] = []
|
|
17
|
+
|
|
18
|
+
const handler = http[method](path, async ({ request }) => {
|
|
19
|
+
const status = (() => {
|
|
20
|
+
if ("error" in params) {
|
|
21
|
+
return params.error.status
|
|
22
|
+
} else if ("response" in params) {
|
|
23
|
+
return 200
|
|
24
|
+
}
|
|
25
|
+
return 204
|
|
26
|
+
})()
|
|
27
|
+
|
|
28
|
+
const returnedResponse = (() => {
|
|
29
|
+
if ("error" in params) {
|
|
30
|
+
return params.error
|
|
31
|
+
} else if ("response" in params) {
|
|
32
|
+
return params.response
|
|
33
|
+
}
|
|
34
|
+
return undefined
|
|
35
|
+
})()
|
|
36
|
+
|
|
37
|
+
const requestsWithBody = ["POST", "PUT", "PATCH"]
|
|
38
|
+
invocations.push(requestsWithBody.includes(request.method) ? await toBody(request) : {})
|
|
39
|
+
|
|
40
|
+
return HttpResponse.json(returnedResponse, { status })
|
|
41
|
+
})
|
|
42
|
+
server.use(handler)
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
invocations,
|
|
46
|
+
clearInvocations: () => {
|
|
47
|
+
invocations = []
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function toBody(request: StrictRequest<DefaultBodyType>) {
|
|
53
|
+
const text = await request.text()
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(text)
|
|
56
|
+
} catch {
|
|
57
|
+
// Throwing here would trigger actual API request in test
|
|
58
|
+
}
|
|
59
|
+
return text
|
|
60
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { expect } from "vitest"
|
|
2
|
+
|
|
3
|
+
import { runCLI } from "#commander.ts"
|
|
4
|
+
|
|
5
|
+
export function setupMockCommander() {
|
|
6
|
+
return {
|
|
7
|
+
async run(command: string) {
|
|
8
|
+
return runCLI(["/cwd", ...command.split(" ")])
|
|
9
|
+
},
|
|
10
|
+
|
|
11
|
+
expect(command: string) {
|
|
12
|
+
return {
|
|
13
|
+
toThrow: async () => {
|
|
14
|
+
await expect(this.run(command)).rejects.toThrow()
|
|
15
|
+
},
|
|
16
|
+
toResolve: async () => {
|
|
17
|
+
await expect(this.run(command)).resolves.not.toThrow()
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { beforeEach, vi } from "vitest"
|
|
2
|
+
|
|
3
|
+
import * as config from "#config/config.ts"
|
|
4
|
+
import { type Config, SearchTemplatesConfigSchema } from "#config/schema.ts"
|
|
5
|
+
|
|
6
|
+
export function setupMockConfig(overrides: Partial<Config> = {}) {
|
|
7
|
+
const mockConfig: Config = {
|
|
8
|
+
apiKey: "test-api-key",
|
|
9
|
+
merchant: "test-merchant",
|
|
10
|
+
templatesEnv: "main",
|
|
11
|
+
apiUrl: "https://api.nosto.com",
|
|
12
|
+
logLevel: "info",
|
|
13
|
+
maxRequests: 15,
|
|
14
|
+
projectPath: "/",
|
|
15
|
+
libraryUrl: "https://library.nosto.com",
|
|
16
|
+
dryRun: false,
|
|
17
|
+
verbose: false,
|
|
18
|
+
auth: {
|
|
19
|
+
user: "",
|
|
20
|
+
token: "",
|
|
21
|
+
expiresAt: new Date(0)
|
|
22
|
+
},
|
|
23
|
+
searchTemplates: {
|
|
24
|
+
mode: "unknown",
|
|
25
|
+
data: SearchTemplatesConfigSchema.parse({})
|
|
26
|
+
},
|
|
27
|
+
...overrides
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Connect immediately and on beforeEach so that this can be used at top level or in beforeEach
|
|
31
|
+
vi.spyOn(config, "getCachedConfig").mockReturnValue(mockConfig)
|
|
32
|
+
vi.spyOn(config, "getCachedSearchTemplatesConfig").mockReturnValue(mockConfig.searchTemplates.data)
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
vi.spyOn(config, "getCachedConfig").mockReturnValue(mockConfig)
|
|
35
|
+
vi.spyOn(config, "getCachedSearchTemplatesConfig").mockReturnValue(mockConfig.searchTemplates.data)
|
|
36
|
+
})
|
|
37
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { afterEach, expect, vi } from "vitest"
|
|
2
|
+
|
|
3
|
+
export const mockedConsoleIn = {
|
|
4
|
+
userResponse: "q",
|
|
5
|
+
recordedPrompts: [] as string[],
|
|
6
|
+
interface: {
|
|
7
|
+
question: (prompt: string) => {
|
|
8
|
+
mockedConsoleIn.recordedPrompts.push(prompt)
|
|
9
|
+
return mockedConsoleIn.userResponse
|
|
10
|
+
},
|
|
11
|
+
close: vi.fn()
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const mockedConsoleOut = {
|
|
16
|
+
Logger: {
|
|
17
|
+
context: {
|
|
18
|
+
logLevel: "info",
|
|
19
|
+
merchantId: "",
|
|
20
|
+
isDryRun: false
|
|
21
|
+
},
|
|
22
|
+
raw: vi.fn(),
|
|
23
|
+
debug: vi.fn(),
|
|
24
|
+
info: vi.fn(),
|
|
25
|
+
warn: vi.fn(),
|
|
26
|
+
error: vi.fn()
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function setupMockConsole() {
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
mockedConsoleIn.recordedPrompts = []
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
setUserResponse: (response: string) => {
|
|
37
|
+
mockedConsoleIn.userResponse = response
|
|
38
|
+
},
|
|
39
|
+
clearPrompts: () => {
|
|
40
|
+
mockedConsoleIn.recordedPrompts = []
|
|
41
|
+
},
|
|
42
|
+
getSpy: (method: Exclude<keyof typeof mockedConsoleOut.Logger, "context">) => {
|
|
43
|
+
return mockedConsoleOut.Logger[method]
|
|
44
|
+
},
|
|
45
|
+
expect: {
|
|
46
|
+
user: {
|
|
47
|
+
toHaveBeenPromptedWith: (prompt: string) => {
|
|
48
|
+
expect(
|
|
49
|
+
mockedConsoleIn.recordedPrompts.map(p => p.trim()),
|
|
50
|
+
"No matching prompt found.\nExpected:\n" +
|
|
51
|
+
prompt +
|
|
52
|
+
"\nGot: \n" +
|
|
53
|
+
mockedConsoleIn.recordedPrompts.map(p => p.trim()).join("\n") +
|
|
54
|
+
"\n\n"
|
|
55
|
+
).toContain(prompt.trim())
|
|
56
|
+
},
|
|
57
|
+
not: {
|
|
58
|
+
toHaveBeenPrompted: () => {
|
|
59
|
+
expect(mockedConsoleIn.recordedPrompts.length, "Prompt was recorded when it was not expected").toBe(0)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import fs from "fs"
|
|
2
|
+
import path from "path"
|
|
3
|
+
import { expect } from "vitest"
|
|
4
|
+
|
|
5
|
+
import { testVolume } from "#test/setup.ts"
|
|
6
|
+
|
|
7
|
+
export function setupMockFileSystem() {
|
|
8
|
+
const fs = testVolume
|
|
9
|
+
return {
|
|
10
|
+
writeFile: (targetFile: string, content: string) => {
|
|
11
|
+
const dir = path.join("/", targetFile.substring(0, targetFile.lastIndexOf("/")))
|
|
12
|
+
const filePath = path.join("/", targetFile)
|
|
13
|
+
// Ensure parent directory exists
|
|
14
|
+
if (dir && !fs.existsSync(dir)) {
|
|
15
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
16
|
+
}
|
|
17
|
+
if (fs.existsSync(filePath)) {
|
|
18
|
+
fs.unlinkSync(filePath)
|
|
19
|
+
}
|
|
20
|
+
fs.writeFileSync(filePath, content)
|
|
21
|
+
},
|
|
22
|
+
writeFolder: (targetFolder: string) => {
|
|
23
|
+
const dir = path.join("/", targetFolder)
|
|
24
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
25
|
+
},
|
|
26
|
+
expectFile: (targetFile: string) => {
|
|
27
|
+
const filePath = path.join("/", targetFile)
|
|
28
|
+
return makeFileMatcher(filePath)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function makeFileMatcher(path: string) {
|
|
34
|
+
return {
|
|
35
|
+
toContain: (expectedContent: string) => {
|
|
36
|
+
const content = fs.readFileSync(path, "utf8")
|
|
37
|
+
expect(content, `File ${path} has content ${content}`).toEqual(expectedContent)
|
|
38
|
+
},
|
|
39
|
+
toExist: () => {
|
|
40
|
+
return expect(fs.existsSync(path), `File ${path} does not exist when it was expected to`).toBe(true)
|
|
41
|
+
},
|
|
42
|
+
not: {
|
|
43
|
+
toContain: (expectedContent: string) => {
|
|
44
|
+
const content = fs.readFileSync(path, "utf8")
|
|
45
|
+
expect(content, `File ${path} has content ${content}`).not.toEqual(expectedContent)
|
|
46
|
+
},
|
|
47
|
+
toExist: () => {
|
|
48
|
+
return expect(fs.existsSync(path), `File ${path} exists when it was not expected to`).toBe(false)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { SetupServer, setupServer } from "msw/node"
|
|
2
|
+
import { afterAll, afterEach, beforeAll, beforeEach } from "vitest"
|
|
3
|
+
|
|
4
|
+
import { fetchLibraryFile } from "#api/library/fetchLibraryFile.ts"
|
|
5
|
+
import { fetchSourceFile } from "#api/source/fetchSourceFile.ts"
|
|
6
|
+
import { listSourceFiles } from "#api/source/listSourceFiles.ts"
|
|
7
|
+
import { putSourceFile } from "#api/source/putSourceFile.ts"
|
|
8
|
+
import { getSourceUrl } from "#api/utils.ts"
|
|
9
|
+
import { generateEndpointMock, MockParams } from "#test/utils/generateEndpointMock.ts"
|
|
10
|
+
|
|
11
|
+
export const setupMockServer = () => {
|
|
12
|
+
const server = setupServer()
|
|
13
|
+
beforeAll(() => {
|
|
14
|
+
server.listen({ onUnhandledRequest: "error" })
|
|
15
|
+
})
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
mockFetchSourceFile(server, {
|
|
18
|
+
path: "build/hash",
|
|
19
|
+
error: { status: 404, message: "Not Found" }
|
|
20
|
+
})
|
|
21
|
+
mockPutSourceFile(server, {
|
|
22
|
+
path: "build/hash"
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
server.resetHandlers()
|
|
27
|
+
})
|
|
28
|
+
afterAll(() => server.close())
|
|
29
|
+
|
|
30
|
+
return server
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function mockListSourceFiles(
|
|
34
|
+
server: SetupServer,
|
|
35
|
+
params: MockParams<Awaited<ReturnType<typeof listSourceFiles>>>
|
|
36
|
+
) {
|
|
37
|
+
return generateEndpointMock(server, {
|
|
38
|
+
...params,
|
|
39
|
+
method: "get",
|
|
40
|
+
path: getSourceUrl("source/{env}")
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function mockFetchSourceFile(
|
|
45
|
+
server: SetupServer,
|
|
46
|
+
params: { path: string } & MockParams<Awaited<ReturnType<typeof fetchSourceFile>>>
|
|
47
|
+
) {
|
|
48
|
+
return generateEndpointMock(server, {
|
|
49
|
+
...params,
|
|
50
|
+
method: "get",
|
|
51
|
+
path: getSourceUrl(`source/{env}/${params.path}`)
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function mockPutSourceFile(
|
|
56
|
+
server: SetupServer,
|
|
57
|
+
params: { path: string } & MockParams<Awaited<ReturnType<typeof putSourceFile>>>
|
|
58
|
+
) {
|
|
59
|
+
const { path, ...mockParams } = params
|
|
60
|
+
return generateEndpointMock(server, {
|
|
61
|
+
method: "put",
|
|
62
|
+
path: getSourceUrl(`source/{env}/${path}`),
|
|
63
|
+
...mockParams
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function mockFetchLibraryFile(
|
|
68
|
+
server: SetupServer,
|
|
69
|
+
params: { path: string } & MockParams<Awaited<ReturnType<typeof fetchLibraryFile>>>
|
|
70
|
+
) {
|
|
71
|
+
return generateEndpointMock(server, {
|
|
72
|
+
...params,
|
|
73
|
+
method: "get",
|
|
74
|
+
path: `https://library.nosto.com/${params.path}`
|
|
75
|
+
})
|
|
76
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import path from "path"
|
|
2
|
+
import { vi } from "vitest"
|
|
3
|
+
|
|
4
|
+
import { SearchTemplatesConfig } from "#config/schema.ts"
|
|
5
|
+
import { makeConfig } from "#exports.ts"
|
|
6
|
+
|
|
7
|
+
import { setupMockFileSystem } from "./mockFileSystem.ts"
|
|
8
|
+
|
|
9
|
+
type Props = {
|
|
10
|
+
projectPath?: string
|
|
11
|
+
mockScript?: Partial<SearchTemplatesConfig>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function setupMockStarterManifest({ projectPath, mockScript }: Props = {}) {
|
|
15
|
+
const fs = setupMockFileSystem()
|
|
16
|
+
const manifest = makeConfig({
|
|
17
|
+
onBuild: async () => {
|
|
18
|
+
console.log("Building...")
|
|
19
|
+
},
|
|
20
|
+
onBuildWatch: async () => {
|
|
21
|
+
console.log("Watching...")
|
|
22
|
+
},
|
|
23
|
+
...mockScript
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const filePath = path.join("/", projectPath ?? "", "nosto.config.ts")
|
|
27
|
+
|
|
28
|
+
vi.doMock(filePath, () => ({
|
|
29
|
+
default: manifest
|
|
30
|
+
}))
|
|
31
|
+
|
|
32
|
+
let scriptBuilder = `export default {`
|
|
33
|
+
Object.entries(manifest).forEach(([key, value]) => {
|
|
34
|
+
scriptBuilder += `
|
|
35
|
+
${key}: ${value.toString()},
|
|
36
|
+
`
|
|
37
|
+
})
|
|
38
|
+
scriptBuilder += `}`
|
|
39
|
+
|
|
40
|
+
fs.writeFile(filePath, scriptBuilder)
|
|
41
|
+
return manifest
|
|
42
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2024",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"allowImportingTsExtensions": true,
|
|
11
|
+
"rootDir": ".",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"paths": {
|
|
14
|
+
"#*": ["./src/*"],
|
|
15
|
+
"#test/*": ["./test/*"]
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*", "test/**/*", "vitest.config.ts"],
|
|
19
|
+
"exclude": ["node_modules"]
|
|
20
|
+
}
|