@simplysm/sd-cli 14.0.64 → 14.0.65
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/dist/capacitor/capacitor-android.d.ts +2 -0
- package/dist/capacitor/capacitor-android.d.ts.map +1 -1
- package/dist/capacitor/capacitor-android.js +13 -0
- package/dist/capacitor/capacitor-android.js.map +1 -1
- package/dist/capacitor/capacitor-npm-config.d.ts.map +1 -1
- package/dist/capacitor/capacitor-npm-config.js +2 -6
- package/dist/capacitor/capacitor-npm-config.js.map +1 -1
- package/dist/electron/electron.d.ts.map +1 -1
- package/dist/electron/electron.js +1 -2
- package/dist/electron/electron.js.map +1 -1
- package/package.json +8 -8
- package/src/capacitor/capacitor-android.ts +14 -0
- package/src/capacitor/capacitor-npm-config.ts +2 -6
- package/src/electron/electron.ts +1 -2
- package/tests/angular/ngtsc-build-core.acc.spec.ts +36 -94
- package/tests/capacitor/capacitor-android.spec.ts +65 -28
- package/tests/capacitor/capacitor-build.spec.ts +40 -385
- package/tests/capacitor/capacitor-config-writer.acc.spec.ts +3 -17
- package/tests/capacitor/capacitor-config-writer.spec.ts +3 -17
- package/tests/capacitor/capacitor-init.spec.ts +40 -636
- package/tests/capacitor/capacitor-npm-config.acc.spec.ts +38 -168
- package/tests/capacitor/capacitor-npm-config.spec.ts +33 -71
- package/tests/commands/check.spec.ts +25 -36
- package/tests/commands/deployment-phase.acc.spec.ts +17 -26
- package/tests/commands/git-phase.acc.spec.ts +13 -112
- package/tests/commands/lint.spec.ts +7 -24
- package/tests/commands/post-publish-phase.acc.spec.ts +5 -10
- package/tests/commands/typecheck.spec.ts +43 -65
- package/tests/electron/electron.spec.ts +22 -46
- package/tests/engines/base-engine.spec.ts +4 -13
- package/tests/engines/engine-selection.spec.ts +14 -17
- package/tests/engines/engine-typecheck-selection.acc.spec.ts +13 -16
- package/tests/engines/esbuild-client-engine.acc.spec.ts +36 -40
- package/tests/engines/esbuild-client-engine.spec.ts +4 -23
- package/tests/engines/ngtsc-engine.spec.ts +3 -10
- package/tests/engines/server-esbuild-engine.spec.ts +3 -10
- package/tests/engines/tsc-engine.spec.ts +3 -10
- package/tests/esbuild/esbuild-tsc-plugin.acc.spec.ts +3 -8
- package/tests/esbuild/esbuild-tsc-plugin.spec.ts +3 -8
- package/tests/orchestrators/build-orchestrator.spec.ts +57 -102
- package/tests/orchestrators/dev-orchestrator.spec.ts +68 -109
- package/tests/orchestrators/typecheck-orchestrator.spec.ts +25 -57
- package/tests/orchestrators/watch-orchestrator.spec.ts +73 -99
- package/tests/sd-cli-entry.spec.ts +17 -20
- package/tests/utils/angular-source-file-cache.spec.ts +4 -8
- package/tests/utils/copy-src.spec.ts +9 -20
- package/tests/utils/esbuild-client-config.acc.spec.ts +9 -15
- package/tests/utils/esbuild-client-config.spec.ts +12 -24
- package/tests/utils/esbuild-config.spec.ts +51 -42
- package/tests/utils/lint-core.spec.ts +13 -19
- package/tests/utils/lint-utils.spec.ts +8 -15
- package/tests/utils/lint-with-program.spec.ts +3 -7
- package/tests/utils/ngtsc-build-core.spec.ts +2 -99
- package/tests/utils/orchestrator-utils.spec.ts +7 -20
- package/tests/utils/output-utils.spec.ts +5 -11
- package/tests/utils/sd-config.spec.ts +4 -12
- package/tests/utils/typecheck-env.spec.ts +49 -77
- package/tests/utils/typecheck-non-package.spec.ts +23 -16
- package/tests/workers/build-watch-paths.acc.spec.ts +4 -10
- package/tests/workers/build-watch-paths.spec.ts +4 -9
- package/tests/workers/client-worker.acc.spec.ts +64 -137
- package/tests/workers/client-worker.spec.ts +63 -89
- package/tests/workers/library-build-lint.spec.ts +19 -30
- package/tests/workers/library-build-worker.spec.ts +28 -55
- package/tests/workers/server-esbuild-context.acc.spec.ts +6 -15
- package/tests/workers/server-esbuild-context.spec.ts +7 -16
- package/tests/workers/server-runtime-worker.spec.ts +8 -10
- package/tests/workers/shared-worker-lifecycle.acc.spec.ts +3 -5
- package/tests/workers/shared-worker-lifecycle.spec.ts +4 -5
- package/tests/capacitor/capacitor-icon.spec.ts +0 -285
- package/tests/capacitor/capacitor-run.spec.ts +0 -256
- package/tests/capacitor/capacitor-workspace.spec.ts +0 -203
- package/tests/commands/device.spec.ts +0 -237
- package/tests/commands/publish.spec.ts +0 -1183
- package/tests/utils/external-modules.spec.ts +0 -217
- package/tests/workers/server-build-lint.spec.ts +0 -201
- package/tests/workers/server-build-worker.spec.ts +0 -765
- package/tests/workers/server-watch-manager.acc.spec.ts +0 -162
- package/tests/workers/server-watch-manager.spec.ts +0 -199
|
@@ -1,1183 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import path from "path";
|
|
3
|
-
|
|
4
|
-
const mocks = vi.hoisted(() => ({
|
|
5
|
-
loadSdConfig: vi.fn(),
|
|
6
|
-
runBuild: vi.fn(),
|
|
7
|
-
parseWorkspaceGlobs: vi.fn(),
|
|
8
|
-
execa: vi.fn(),
|
|
9
|
-
fsx: {
|
|
10
|
-
readJson: vi.fn(),
|
|
11
|
-
write: vi.fn(),
|
|
12
|
-
read: vi.fn(),
|
|
13
|
-
exists: vi.fn(),
|
|
14
|
-
glob: vi.fn(),
|
|
15
|
-
copy: vi.fn(),
|
|
16
|
-
},
|
|
17
|
-
storageConnect: vi.fn(),
|
|
18
|
-
SshClientInstance: {
|
|
19
|
-
on: vi.fn(),
|
|
20
|
-
connect: vi.fn(),
|
|
21
|
-
end: vi.fn(),
|
|
22
|
-
exec: vi.fn(),
|
|
23
|
-
},
|
|
24
|
-
SshClient: vi.fn(),
|
|
25
|
-
sshUtils: {
|
|
26
|
-
generateKeyPairSync: vi.fn(),
|
|
27
|
-
parseKey: vi.fn(),
|
|
28
|
-
},
|
|
29
|
-
passwordPrompt: vi.fn(),
|
|
30
|
-
fsExistsSync: vi.fn(),
|
|
31
|
-
fsReadFileSync: vi.fn(),
|
|
32
|
-
fsWriteFileSync: vi.fn(),
|
|
33
|
-
fsMkdirSync: vi.fn(),
|
|
34
|
-
homedir: vi.fn(),
|
|
35
|
-
}));
|
|
36
|
-
|
|
37
|
-
vi.mock("../../src/utils/sd-config", () => ({
|
|
38
|
-
loadSdConfig: mocks.loadSdConfig,
|
|
39
|
-
}));
|
|
40
|
-
|
|
41
|
-
vi.mock("../../src/commands/build", () => ({
|
|
42
|
-
runBuild: mocks.runBuild,
|
|
43
|
-
}));
|
|
44
|
-
|
|
45
|
-
vi.mock("../../src/deps/replace-deps/replace-deps", () => ({
|
|
46
|
-
parseWorkspaceGlobs: mocks.parseWorkspaceGlobs,
|
|
47
|
-
}));
|
|
48
|
-
|
|
49
|
-
vi.mock("@simplysm/core-node", () => ({
|
|
50
|
-
fsx: mocks.fsx,
|
|
51
|
-
cpx: {
|
|
52
|
-
spawn: mocks.execa,
|
|
53
|
-
spawnSync: vi.fn().mockReturnValue({ stdout: "", stderr: "", exitCode: 0 }),
|
|
54
|
-
},
|
|
55
|
-
}));
|
|
56
|
-
|
|
57
|
-
vi.mock("@simplysm/storage", () => ({
|
|
58
|
-
StorageFactory: { connect: mocks.storageConnect },
|
|
59
|
-
}));
|
|
60
|
-
|
|
61
|
-
vi.mock("ssh2", () => {
|
|
62
|
-
const ClientCtor = mocks.SshClient;
|
|
63
|
-
return {
|
|
64
|
-
default: {
|
|
65
|
-
Client: ClientCtor,
|
|
66
|
-
utils: mocks.sshUtils,
|
|
67
|
-
},
|
|
68
|
-
};
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
vi.mock("@inquirer/prompts", () => ({
|
|
72
|
-
password: mocks.passwordPrompt,
|
|
73
|
-
}));
|
|
74
|
-
|
|
75
|
-
vi.mock("fs", async (importOriginal: () => Promise<Record<string, unknown>>) => {
|
|
76
|
-
const actual = await importOriginal();
|
|
77
|
-
return {
|
|
78
|
-
...actual,
|
|
79
|
-
default: {
|
|
80
|
-
...(actual["default"] as Record<string, unknown>),
|
|
81
|
-
existsSync: mocks.fsExistsSync,
|
|
82
|
-
readFileSync: mocks.fsReadFileSync,
|
|
83
|
-
writeFileSync: mocks.fsWriteFileSync,
|
|
84
|
-
mkdirSync: mocks.fsMkdirSync,
|
|
85
|
-
},
|
|
86
|
-
};
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
vi.mock("os", async (importOriginal: () => Promise<Record<string, unknown>>) => {
|
|
90
|
-
const actual = await importOriginal();
|
|
91
|
-
return {
|
|
92
|
-
...actual,
|
|
93
|
-
default: {
|
|
94
|
-
...(actual["default"] as Record<string, unknown>),
|
|
95
|
-
homedir: mocks.homedir,
|
|
96
|
-
},
|
|
97
|
-
};
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
const { runPublish } = await import("../../src/commands/publish/publish-command");
|
|
101
|
-
|
|
102
|
-
const CWD = process.cwd();
|
|
103
|
-
|
|
104
|
-
function pkgPath(name: string): string {
|
|
105
|
-
return path.resolve(CWD, `packages/${name}`);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function createPkgJson(
|
|
109
|
-
name: string,
|
|
110
|
-
version: string,
|
|
111
|
-
deps: Record<string, string> = {},
|
|
112
|
-
): { name: string; version: string; dependencies: Record<string, string> } {
|
|
113
|
-
return { name: `@simplysm/${name}`, version, dependencies: deps };
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Set up a default happy-path mock environment for npm publish
|
|
118
|
-
*/
|
|
119
|
-
function setupHappyPath(opts: {
|
|
120
|
-
version?: string;
|
|
121
|
-
packages?: Record<string, { publish?: { type: string; [k: string]: unknown }; target?: string }>;
|
|
122
|
-
packageDeps?: Record<string, Record<string, string>>;
|
|
123
|
-
templateFiles?: string[];
|
|
124
|
-
hasGit?: boolean;
|
|
125
|
-
} = {}) {
|
|
126
|
-
const version = opts.version ?? "14.0.0";
|
|
127
|
-
const packages = opts.packages ?? {
|
|
128
|
-
"pkg-a": { target: "node", publish: { type: "npm" } },
|
|
129
|
-
"pkg-b": { target: "node", publish: { type: "npm" } },
|
|
130
|
-
};
|
|
131
|
-
const packageDeps = opts.packageDeps ?? {};
|
|
132
|
-
const templateFiles = opts.templateFiles ?? [];
|
|
133
|
-
const hasGit = opts.hasGit ?? true;
|
|
134
|
-
const pkgNames = Object.keys(packages);
|
|
135
|
-
|
|
136
|
-
// loadSdConfig
|
|
137
|
-
mocks.loadSdConfig.mockResolvedValue({ packages });
|
|
138
|
-
|
|
139
|
-
// parseWorkspaceGlobs
|
|
140
|
-
mocks.parseWorkspaceGlobs.mockReturnValue(["packages/*"]);
|
|
141
|
-
|
|
142
|
-
// fsx.readJson — route by path
|
|
143
|
-
mocks.fsx.readJson.mockImplementation((p: string) => {
|
|
144
|
-
const basename = path.basename(path.dirname(p));
|
|
145
|
-
// Root package.json
|
|
146
|
-
if (p === path.resolve(CWD, "package.json")) {
|
|
147
|
-
return createPkgJson("simplysm", version);
|
|
148
|
-
}
|
|
149
|
-
// Package package.json
|
|
150
|
-
if (pkgNames.includes(basename)) {
|
|
151
|
-
return createPkgJson(basename, version, packageDeps[basename] ?? {});
|
|
152
|
-
}
|
|
153
|
-
throw new Error(`Unexpected readJson path: ${p}`);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
// fsx.write
|
|
157
|
-
mocks.fsx.write.mockResolvedValue(undefined);
|
|
158
|
-
|
|
159
|
-
// fsx.read
|
|
160
|
-
mocks.fsx.read.mockImplementation((p: string) => {
|
|
161
|
-
if (p.includes("pnpm-workspace.yaml")) {
|
|
162
|
-
return "packages:\n - packages/*";
|
|
163
|
-
}
|
|
164
|
-
if (p.endsWith(".hbs")) {
|
|
165
|
-
return `"@simplysm/core-common": "~${version}"`;
|
|
166
|
-
}
|
|
167
|
-
return "";
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
// fsx.exists
|
|
171
|
-
mocks.fsx.exists.mockImplementation((p: string) => {
|
|
172
|
-
if (p.endsWith("pnpm-workspace.yaml")) return true;
|
|
173
|
-
if (p.endsWith(".git")) return hasGit;
|
|
174
|
-
return false;
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
// fsx.glob
|
|
178
|
-
mocks.fsx.glob.mockImplementation((pattern: string) => {
|
|
179
|
-
if (pattern.includes("templates")) {
|
|
180
|
-
return templateFiles.map((f) => path.resolve(CWD, f));
|
|
181
|
-
}
|
|
182
|
-
// packages/*
|
|
183
|
-
return pkgNames.map((n) => pkgPath(n));
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
// fsx.copy
|
|
187
|
-
mocks.fsx.copy.mockResolvedValue(undefined);
|
|
188
|
-
|
|
189
|
-
// runBuild
|
|
190
|
-
mocks.runBuild.mockResolvedValue(undefined);
|
|
191
|
-
|
|
192
|
-
// execa — default: all succeed
|
|
193
|
-
mocks.execa.mockImplementation(
|
|
194
|
-
(cmd: string, _args?: string[], _opts?: unknown) => {
|
|
195
|
-
if (cmd === "npm") return { stdout: "testuser", stderr: "", exitCode: 0 };
|
|
196
|
-
return { stdout: "", stderr: "", exitCode: 0 };
|
|
197
|
-
},
|
|
198
|
-
);
|
|
199
|
-
|
|
200
|
-
// SSH-related: not needed for npm-only, but set defaults
|
|
201
|
-
mocks.homedir.mockReturnValue("/mock/home");
|
|
202
|
-
mocks.fsExistsSync.mockImplementation((p: string) => {
|
|
203
|
-
// package.json existence check for workspace package filtering
|
|
204
|
-
if (p.endsWith("package.json")) {
|
|
205
|
-
return pkgNames.some((n) => p.includes(n));
|
|
206
|
-
}
|
|
207
|
-
return true;
|
|
208
|
-
});
|
|
209
|
-
mocks.fsReadFileSync.mockReturnValue(new TextEncoder().encode("fake-key"));
|
|
210
|
-
mocks.sshUtils.parseKey.mockReturnValue({});
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Get all execa calls matching a command prefix
|
|
215
|
-
*/
|
|
216
|
-
function getExecaCalls(
|
|
217
|
-
cmd: string,
|
|
218
|
-
firstArg?: string,
|
|
219
|
-
): Array<{ cmd: string; args: string[] }> {
|
|
220
|
-
return mocks.execa.mock.calls
|
|
221
|
-
.filter(
|
|
222
|
-
(c: unknown[]) =>
|
|
223
|
-
c[0] === cmd && (firstArg == null || (c[1] as string[] | undefined)?.[0] === firstArg),
|
|
224
|
-
)
|
|
225
|
-
.map((c: unknown[]) => ({ cmd: c[0] as string, args: (c[1] as string[] | undefined) ?? [] }));
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
describe("runPublish", () => {
|
|
229
|
-
let savedExitCode: typeof process.exitCode;
|
|
230
|
-
|
|
231
|
-
beforeEach(() => {
|
|
232
|
-
vi.clearAllMocks();
|
|
233
|
-
savedExitCode = process.exitCode;
|
|
234
|
-
process.exitCode = undefined;
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
afterEach(() => {
|
|
238
|
-
process.exitCode = savedExitCode;
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
describe("package selection", () => {
|
|
242
|
-
it("publishes only targeted packages when targets specified", async () => {
|
|
243
|
-
setupHappyPath();
|
|
244
|
-
|
|
245
|
-
await runPublish({
|
|
246
|
-
targets: ["pkg-a"],
|
|
247
|
-
noBuild: false,
|
|
248
|
-
dryRun: false,
|
|
249
|
-
options: [],
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
const publishCalls = getExecaCalls("pnpm", "publish");
|
|
253
|
-
expect(publishCalls).toHaveLength(1);
|
|
254
|
-
// The publish call should be for pkg-a's directory
|
|
255
|
-
const call = mocks.execa.mock.calls.find(
|
|
256
|
-
(c: unknown[]) => c[0] === "pnpm" && (c[1] as string[] | undefined)?.[0] === "publish",
|
|
257
|
-
);
|
|
258
|
-
expect(call?.[2]).toHaveProperty("cwd", pkgPath("pkg-a"));
|
|
259
|
-
expect(call?.[2]).toHaveProperty("shell", true);
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
it("publishes all packages with publish config when targets empty", async () => {
|
|
263
|
-
setupHappyPath();
|
|
264
|
-
|
|
265
|
-
await runPublish({
|
|
266
|
-
targets: [],
|
|
267
|
-
noBuild: false,
|
|
268
|
-
dryRun: false,
|
|
269
|
-
options: [],
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
const publishCalls = getExecaCalls("pnpm", "publish");
|
|
273
|
-
expect(publishCalls).toHaveLength(2);
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
it("ignores packages without publish config", async () => {
|
|
277
|
-
setupHappyPath({
|
|
278
|
-
packages: {
|
|
279
|
-
"pkg-a": { target: "node", publish: { type: "npm" } },
|
|
280
|
-
"pkg-b": { target: "node" }, // no publish
|
|
281
|
-
},
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
await runPublish({
|
|
285
|
-
targets: [],
|
|
286
|
-
noBuild: false,
|
|
287
|
-
dryRun: false,
|
|
288
|
-
options: [],
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
const publishCalls = getExecaCalls("pnpm", "publish");
|
|
292
|
-
expect(publishCalls).toHaveLength(1);
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
it("outputs no-op message when no packages to deploy", async () => {
|
|
296
|
-
setupHappyPath({
|
|
297
|
-
packages: {
|
|
298
|
-
"pkg-a": { target: "node" }, // no publish
|
|
299
|
-
},
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
const { consola } = await import("consola");
|
|
303
|
-
const infoSpy = vi.fn();
|
|
304
|
-
const origWithTag = consola.withTag.bind(consola);
|
|
305
|
-
const withTagSpy = vi.spyOn(consola, "withTag").mockImplementation((tag: string) => {
|
|
306
|
-
const logger = origWithTag(tag);
|
|
307
|
-
logger.info = infoSpy as any;
|
|
308
|
-
return logger;
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
// Re-import to pick up the spy (module already loaded, but logger is created at call time)
|
|
312
|
-
await runPublish({
|
|
313
|
-
targets: [],
|
|
314
|
-
noBuild: false,
|
|
315
|
-
dryRun: false,
|
|
316
|
-
options: [],
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
const infoArgs = infoSpy.mock.calls.map((c: unknown[]) => String(c[0]));
|
|
320
|
-
expect(infoArgs.some((a: string) => a.includes("배포할 패키지가 없습니다"))).toBe(true);
|
|
321
|
-
withTagSpy.mockRestore();
|
|
322
|
-
});
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
describe("pre-validation", () => {
|
|
326
|
-
it("passes when npm whoami returns valid username", async () => {
|
|
327
|
-
setupHappyPath();
|
|
328
|
-
|
|
329
|
-
await runPublish({
|
|
330
|
-
targets: [],
|
|
331
|
-
noBuild: false,
|
|
332
|
-
dryRun: false,
|
|
333
|
-
options: [],
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
expect(process.exitCode).toBeUndefined();
|
|
337
|
-
expect(getExecaCalls("npm", "whoami")).toHaveLength(1);
|
|
338
|
-
expect(mocks.execa).toHaveBeenCalledWith(
|
|
339
|
-
"npm",
|
|
340
|
-
["whoami"],
|
|
341
|
-
expect.objectContaining({ shell: true }),
|
|
342
|
-
);
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
it("aborts when npm whoami fails", async () => {
|
|
346
|
-
setupHappyPath();
|
|
347
|
-
mocks.execa.mockImplementation((cmd: string) => {
|
|
348
|
-
if (cmd === "npm") throw new Error("npm not authenticated");
|
|
349
|
-
return { stdout: "", stderr: "", exitCode: 0 };
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
await runPublish({
|
|
353
|
-
targets: [],
|
|
354
|
-
noBuild: false,
|
|
355
|
-
dryRun: false,
|
|
356
|
-
options: [],
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
expect(process.exitCode).toBe(1);
|
|
360
|
-
// Should not reach build phase
|
|
361
|
-
expect(mocks.runBuild).not.toHaveBeenCalled();
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
it("skips npm auth check when no npm publish packages", async () => {
|
|
365
|
-
setupHappyPath({
|
|
366
|
-
packages: {
|
|
367
|
-
"pkg-a": {
|
|
368
|
-
target: "node",
|
|
369
|
-
publish: {
|
|
370
|
-
type: "local-directory",
|
|
371
|
-
path: "/deploy/%VER%",
|
|
372
|
-
},
|
|
373
|
-
},
|
|
374
|
-
},
|
|
375
|
-
hasGit: false,
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
await runPublish({
|
|
379
|
-
targets: [],
|
|
380
|
-
noBuild: false,
|
|
381
|
-
dryRun: false,
|
|
382
|
-
options: [],
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
expect(getExecaCalls("npm", "whoami")).toHaveLength(0);
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
it("auto-commits with codex when uncommitted changes detected", async () => {
|
|
389
|
-
setupHappyPath();
|
|
390
|
-
mocks.execa.mockImplementation(
|
|
391
|
-
(cmd: string, args?: string[], _opts?: unknown) => {
|
|
392
|
-
if (cmd === "npm") return { stdout: "testuser", stderr: "", exitCode: 0 };
|
|
393
|
-
if (cmd === "git" && args?.[0] === "diff") {
|
|
394
|
-
return { stdout: "file.txt", stderr: "", exitCode: 0 };
|
|
395
|
-
}
|
|
396
|
-
return { stdout: "", stderr: "", exitCode: 0 };
|
|
397
|
-
},
|
|
398
|
-
);
|
|
399
|
-
|
|
400
|
-
await runPublish({
|
|
401
|
-
targets: [],
|
|
402
|
-
noBuild: false,
|
|
403
|
-
dryRun: false,
|
|
404
|
-
options: [],
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
// codex CLI should have been called for auto-commit
|
|
408
|
-
const codexCalls = mocks.execa.mock.calls.filter(
|
|
409
|
-
(c: unknown[]) => c[0] === "codex",
|
|
410
|
-
);
|
|
411
|
-
expect(codexCalls).toHaveLength(1);
|
|
412
|
-
expect((codexCalls[0][1] as string[])).toContain("exec");
|
|
413
|
-
expect((codexCalls[0][1] as string[])).toContain("gpt-5.3-codex-spark");
|
|
414
|
-
expect((codexCalls[0][1] as string[])).toContain('model_reasoning_effort="low"');
|
|
415
|
-
expect((codexCalls[0][1] as string[]).some((arg) => arg.includes("$sd-commit"))).toBe(true);
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
it("aborts when auto-commit codex command fails", async () => {
|
|
419
|
-
setupHappyPath();
|
|
420
|
-
mocks.execa.mockImplementation((cmd: string, args?: string[]) => {
|
|
421
|
-
if (cmd === "npm") return { stdout: "testuser", stderr: "", exitCode: 0 };
|
|
422
|
-
if (cmd === "git" && args?.[0] === "diff") {
|
|
423
|
-
return { stdout: "file.txt", stderr: "", exitCode: 0 };
|
|
424
|
-
}
|
|
425
|
-
if (cmd === "codex") {
|
|
426
|
-
throw new Error("codex commit failed");
|
|
427
|
-
}
|
|
428
|
-
return { stdout: "", stderr: "", exitCode: 0 };
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
await runPublish({
|
|
432
|
-
targets: [],
|
|
433
|
-
noBuild: false,
|
|
434
|
-
dryRun: false,
|
|
435
|
-
options: [],
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
expect(process.exitCode).toBe(1);
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
it("skips auto-commit when no uncommitted changes", async () => {
|
|
442
|
-
setupHappyPath();
|
|
443
|
-
|
|
444
|
-
await runPublish({
|
|
445
|
-
targets: [],
|
|
446
|
-
noBuild: false,
|
|
447
|
-
dryRun: false,
|
|
448
|
-
options: [],
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
// No codex CLI calls
|
|
452
|
-
const codexCalls = mocks.execa.mock.calls.filter(
|
|
453
|
-
(c: unknown[]) => c[0] === "codex",
|
|
454
|
-
);
|
|
455
|
-
expect(codexCalls).toHaveLength(0);
|
|
456
|
-
});
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
describe("version upgrade and git", () => {
|
|
460
|
-
it("increments patch for stable version", async () => {
|
|
461
|
-
setupHappyPath({ version: "14.0.0" });
|
|
462
|
-
|
|
463
|
-
await runPublish({
|
|
464
|
-
targets: [],
|
|
465
|
-
noBuild: false,
|
|
466
|
-
dryRun: false,
|
|
467
|
-
options: [],
|
|
468
|
-
});
|
|
469
|
-
|
|
470
|
-
// Check that fsx.write was called with new version
|
|
471
|
-
const writeCalls = mocks.fsx.write.mock.calls;
|
|
472
|
-
const rootPkgWrite = writeCalls.find((c: unknown[]) =>
|
|
473
|
-
(c[0] as string).endsWith("package.json") &&
|
|
474
|
-
!(c[0] as string).includes("packages/"),
|
|
475
|
-
);
|
|
476
|
-
expect(rootPkgWrite).toBeDefined();
|
|
477
|
-
expect(rootPkgWrite![1]).toContain('"14.0.1"');
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
it("increments prerelease for prerelease version", async () => {
|
|
481
|
-
setupHappyPath({ version: "14.0.0-beta.1" });
|
|
482
|
-
|
|
483
|
-
await runPublish({
|
|
484
|
-
targets: [],
|
|
485
|
-
noBuild: false,
|
|
486
|
-
dryRun: false,
|
|
487
|
-
options: [],
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
const writeCalls = mocks.fsx.write.mock.calls;
|
|
491
|
-
const rootPkgWrite = writeCalls.find((c: unknown[]) =>
|
|
492
|
-
(c[0] as string).endsWith("package.json") &&
|
|
493
|
-
!(c[0] as string).includes("packages/"),
|
|
494
|
-
);
|
|
495
|
-
expect(rootPkgWrite).toBeDefined();
|
|
496
|
-
expect(rootPkgWrite![1]).toContain('"14.0.0-beta.2"');
|
|
497
|
-
});
|
|
498
|
-
|
|
499
|
-
it("syncs version across all workspace packages", async () => {
|
|
500
|
-
setupHappyPath({ version: "14.0.0" });
|
|
501
|
-
|
|
502
|
-
await runPublish({
|
|
503
|
-
targets: [],
|
|
504
|
-
noBuild: false,
|
|
505
|
-
dryRun: false,
|
|
506
|
-
options: [],
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
// Each package's package.json should be written with new version
|
|
510
|
-
const writeCalls = mocks.fsx.write.mock.calls.filter((c: unknown[]) => {
|
|
511
|
-
const p = (c[0] as string).replace(/\\/g, "/");
|
|
512
|
-
return p.includes("packages/") && p.endsWith("package.json");
|
|
513
|
-
});
|
|
514
|
-
expect(writeCalls.length).toBeGreaterThanOrEqual(2);
|
|
515
|
-
for (const call of writeCalls) {
|
|
516
|
-
expect(call[1]).toContain('"14.0.1"');
|
|
517
|
-
}
|
|
518
|
-
});
|
|
519
|
-
|
|
520
|
-
it("syncs @simplysm version in template files", async () => {
|
|
521
|
-
setupHappyPath({
|
|
522
|
-
version: "14.0.0",
|
|
523
|
-
templateFiles: ["packages/sd-cli/templates/test.hbs"],
|
|
524
|
-
});
|
|
525
|
-
|
|
526
|
-
await runPublish({
|
|
527
|
-
targets: [],
|
|
528
|
-
noBuild: false,
|
|
529
|
-
dryRun: false,
|
|
530
|
-
options: [],
|
|
531
|
-
});
|
|
532
|
-
|
|
533
|
-
// Template file should be written with updated version
|
|
534
|
-
const templateWrite = mocks.fsx.write.mock.calls.find((c: unknown[]) =>
|
|
535
|
-
(c[0] as string).endsWith(".hbs"),
|
|
536
|
-
);
|
|
537
|
-
expect(templateWrite).toBeDefined();
|
|
538
|
-
expect(templateWrite![1]).toContain("~14.0.1");
|
|
539
|
-
});
|
|
540
|
-
|
|
541
|
-
it("commits version files and creates annotated tag", async () => {
|
|
542
|
-
setupHappyPath({ version: "14.0.0" });
|
|
543
|
-
|
|
544
|
-
await runPublish({
|
|
545
|
-
targets: [],
|
|
546
|
-
noBuild: false,
|
|
547
|
-
dryRun: false,
|
|
548
|
-
options: [],
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
const commitCalls = getExecaCalls("git", "commit");
|
|
552
|
-
expect(commitCalls).toHaveLength(1);
|
|
553
|
-
expect(commitCalls[0].args).toContain("v14.0.1");
|
|
554
|
-
|
|
555
|
-
const tagCalls = getExecaCalls("git", "tag");
|
|
556
|
-
expect(tagCalls).toHaveLength(1);
|
|
557
|
-
expect(tagCalls[0].args).toContain("v14.0.1");
|
|
558
|
-
expect(tagCalls[0].args).toContain("-a");
|
|
559
|
-
|
|
560
|
-
expect(getExecaCalls("git", "push")).toHaveLength(2); // push + push --tags
|
|
561
|
-
});
|
|
562
|
-
|
|
563
|
-
it("skips git operations when .git directory absent", async () => {
|
|
564
|
-
setupHappyPath({ hasGit: false });
|
|
565
|
-
|
|
566
|
-
await runPublish({
|
|
567
|
-
targets: [],
|
|
568
|
-
noBuild: false,
|
|
569
|
-
dryRun: false,
|
|
570
|
-
options: [],
|
|
571
|
-
});
|
|
572
|
-
|
|
573
|
-
expect(getExecaCalls("git", "commit")).toHaveLength(0);
|
|
574
|
-
expect(getExecaCalls("git", "tag")).toHaveLength(0);
|
|
575
|
-
// Should still publish
|
|
576
|
-
expect(getExecaCalls("pnpm", "publish").length).toBeGreaterThan(0);
|
|
577
|
-
});
|
|
578
|
-
|
|
579
|
-
it("aborts with recovery message when git operations fail", async () => {
|
|
580
|
-
setupHappyPath();
|
|
581
|
-
mocks.execa.mockImplementation((cmd: string, args?: string[]) => {
|
|
582
|
-
if (cmd === "npm") return { stdout: "testuser", stderr: "", exitCode: 0 };
|
|
583
|
-
if (cmd === "git" && args?.[0] === "commit") {
|
|
584
|
-
throw new Error("git commit failed");
|
|
585
|
-
}
|
|
586
|
-
return { stdout: "", stderr: "", exitCode: 0 };
|
|
587
|
-
});
|
|
588
|
-
|
|
589
|
-
await runPublish({
|
|
590
|
-
targets: [],
|
|
591
|
-
noBuild: false,
|
|
592
|
-
dryRun: false,
|
|
593
|
-
options: [],
|
|
594
|
-
});
|
|
595
|
-
|
|
596
|
-
expect(process.exitCode).toBe(1);
|
|
597
|
-
expect(getExecaCalls("pnpm", "publish")).toHaveLength(0);
|
|
598
|
-
});
|
|
599
|
-
});
|
|
600
|
-
|
|
601
|
-
describe("deployment", () => {
|
|
602
|
-
it("publishes npm stable version without --tag", async () => {
|
|
603
|
-
setupHappyPath({ version: "14.0.0" });
|
|
604
|
-
|
|
605
|
-
await runPublish({
|
|
606
|
-
targets: [],
|
|
607
|
-
noBuild: false,
|
|
608
|
-
dryRun: false,
|
|
609
|
-
options: [],
|
|
610
|
-
});
|
|
611
|
-
|
|
612
|
-
const publishCalls = getExecaCalls("pnpm", "publish");
|
|
613
|
-
expect(publishCalls.length).toBeGreaterThan(0);
|
|
614
|
-
for (const call of publishCalls) {
|
|
615
|
-
expect(call.args).toContain("--access");
|
|
616
|
-
expect(call.args).toContain("public");
|
|
617
|
-
expect(call.args).toContain("--no-git-checks");
|
|
618
|
-
expect(call.args).not.toContain("--tag");
|
|
619
|
-
}
|
|
620
|
-
});
|
|
621
|
-
|
|
622
|
-
it("publishes npm prerelease version with --tag", async () => {
|
|
623
|
-
setupHappyPath({ version: "14.0.0-beta.1" });
|
|
624
|
-
|
|
625
|
-
await runPublish({
|
|
626
|
-
targets: [],
|
|
627
|
-
noBuild: false,
|
|
628
|
-
dryRun: false,
|
|
629
|
-
options: [],
|
|
630
|
-
});
|
|
631
|
-
|
|
632
|
-
const publishCalls = getExecaCalls("pnpm", "publish");
|
|
633
|
-
expect(publishCalls.length).toBeGreaterThan(0);
|
|
634
|
-
for (const call of publishCalls) {
|
|
635
|
-
expect(call.args).toContain("--tag");
|
|
636
|
-
expect(call.args).toContain("beta");
|
|
637
|
-
}
|
|
638
|
-
});
|
|
639
|
-
|
|
640
|
-
it("copies dist to local directory with env var substitution", async () => {
|
|
641
|
-
setupHappyPath({
|
|
642
|
-
version: "14.0.0",
|
|
643
|
-
packages: {
|
|
644
|
-
"pkg-a": {
|
|
645
|
-
target: "node",
|
|
646
|
-
publish: { type: "local-directory", path: "/deploy/%VER%" },
|
|
647
|
-
},
|
|
648
|
-
},
|
|
649
|
-
hasGit: false,
|
|
650
|
-
});
|
|
651
|
-
|
|
652
|
-
await runPublish({
|
|
653
|
-
targets: [],
|
|
654
|
-
noBuild: false,
|
|
655
|
-
dryRun: false,
|
|
656
|
-
options: [],
|
|
657
|
-
});
|
|
658
|
-
|
|
659
|
-
expect(mocks.fsx.copy).toHaveBeenCalledWith(
|
|
660
|
-
path.resolve(pkgPath("pkg-a"), "dist"),
|
|
661
|
-
"/deploy/14.0.1",
|
|
662
|
-
);
|
|
663
|
-
});
|
|
664
|
-
|
|
665
|
-
it("uploads dist to SFTP server", async () => {
|
|
666
|
-
setupHappyPath({
|
|
667
|
-
version: "14.0.0",
|
|
668
|
-
packages: {
|
|
669
|
-
"pkg-a": {
|
|
670
|
-
target: "node",
|
|
671
|
-
publish: {
|
|
672
|
-
type: "sftp",
|
|
673
|
-
host: "example.com",
|
|
674
|
-
port: 22,
|
|
675
|
-
user: "deploy",
|
|
676
|
-
password: "secret",
|
|
677
|
-
path: "/app",
|
|
678
|
-
},
|
|
679
|
-
},
|
|
680
|
-
},
|
|
681
|
-
hasGit: false,
|
|
682
|
-
});
|
|
683
|
-
|
|
684
|
-
mocks.storageConnect.mockImplementation(
|
|
685
|
-
async (
|
|
686
|
-
_type: string,
|
|
687
|
-
_opts: unknown,
|
|
688
|
-
cb: (storage: { uploadDir: (...args: unknown[]) => Promise<void> }) => Promise<void>,
|
|
689
|
-
) => {
|
|
690
|
-
await cb({ uploadDir: vi.fn() });
|
|
691
|
-
},
|
|
692
|
-
);
|
|
693
|
-
|
|
694
|
-
await runPublish({
|
|
695
|
-
targets: [],
|
|
696
|
-
noBuild: false,
|
|
697
|
-
dryRun: false,
|
|
698
|
-
options: [],
|
|
699
|
-
});
|
|
700
|
-
|
|
701
|
-
expect(mocks.storageConnect).toHaveBeenCalledWith(
|
|
702
|
-
"sftp",
|
|
703
|
-
expect.objectContaining({ host: "example.com" }),
|
|
704
|
-
expect.any(Function),
|
|
705
|
-
);
|
|
706
|
-
});
|
|
707
|
-
|
|
708
|
-
it("uploads to root path when remote path is not specified", async () => {
|
|
709
|
-
setupHappyPath({
|
|
710
|
-
version: "14.0.0",
|
|
711
|
-
packages: {
|
|
712
|
-
"pkg-a": {
|
|
713
|
-
target: "node",
|
|
714
|
-
publish: {
|
|
715
|
-
type: "sftp",
|
|
716
|
-
host: "example.com",
|
|
717
|
-
user: "deploy",
|
|
718
|
-
password: "secret",
|
|
719
|
-
},
|
|
720
|
-
},
|
|
721
|
-
},
|
|
722
|
-
hasGit: false,
|
|
723
|
-
});
|
|
724
|
-
|
|
725
|
-
let uploadedPath: string | undefined;
|
|
726
|
-
mocks.storageConnect.mockImplementation(
|
|
727
|
-
async (
|
|
728
|
-
_type: string,
|
|
729
|
-
_opts: unknown,
|
|
730
|
-
cb: (storage: { uploadDir: (src: string, dest: string) => Promise<void> }) => Promise<void>,
|
|
731
|
-
) => {
|
|
732
|
-
await cb({
|
|
733
|
-
uploadDir: vi.fn((_src: string, dest: string) => {
|
|
734
|
-
uploadedPath = dest;
|
|
735
|
-
return Promise.resolve();
|
|
736
|
-
}),
|
|
737
|
-
});
|
|
738
|
-
},
|
|
739
|
-
);
|
|
740
|
-
|
|
741
|
-
await runPublish({
|
|
742
|
-
targets: [],
|
|
743
|
-
noBuild: false,
|
|
744
|
-
dryRun: false,
|
|
745
|
-
options: [],
|
|
746
|
-
});
|
|
747
|
-
|
|
748
|
-
expect(uploadedPath).toBe("/");
|
|
749
|
-
});
|
|
750
|
-
|
|
751
|
-
it("deploys packages in dependency order (Level 0 before Level 1)", async () => {
|
|
752
|
-
setupHappyPath({
|
|
753
|
-
packages: {
|
|
754
|
-
"pkg-a": { target: "node", publish: { type: "npm" } },
|
|
755
|
-
"pkg-b": { target: "node", publish: { type: "npm" } },
|
|
756
|
-
},
|
|
757
|
-
packageDeps: {
|
|
758
|
-
"pkg-a": {},
|
|
759
|
-
"pkg-b": { "@simplysm/pkg-a": "~14.0.0" },
|
|
760
|
-
},
|
|
761
|
-
});
|
|
762
|
-
|
|
763
|
-
const publishOrder: string[] = [];
|
|
764
|
-
mocks.execa.mockImplementation(
|
|
765
|
-
(cmd: string, args?: string[], opts?: { cwd?: string }) => {
|
|
766
|
-
if (cmd === "pnpm" && args?.[0] === "publish") {
|
|
767
|
-
publishOrder.push(path.basename(opts?.cwd ?? ""));
|
|
768
|
-
}
|
|
769
|
-
if (cmd === "npm") return { stdout: "testuser", stderr: "", exitCode: 0 };
|
|
770
|
-
return { stdout: "", stderr: "", exitCode: 0 };
|
|
771
|
-
},
|
|
772
|
-
);
|
|
773
|
-
|
|
774
|
-
await runPublish({
|
|
775
|
-
targets: [],
|
|
776
|
-
noBuild: false,
|
|
777
|
-
dryRun: false,
|
|
778
|
-
options: [],
|
|
779
|
-
});
|
|
780
|
-
|
|
781
|
-
const aIdx = publishOrder.indexOf("pkg-a");
|
|
782
|
-
const bIdx = publishOrder.indexOf("pkg-b");
|
|
783
|
-
expect(aIdx).toBeLessThan(bIdx);
|
|
784
|
-
});
|
|
785
|
-
|
|
786
|
-
it("only tracks @simplysm scoped dependencies for level computation", async () => {
|
|
787
|
-
setupHappyPath({
|
|
788
|
-
packages: {
|
|
789
|
-
"pkg-a": { target: "node", publish: { type: "npm" } },
|
|
790
|
-
},
|
|
791
|
-
packageDeps: {
|
|
792
|
-
// External dep should not affect level
|
|
793
|
-
"pkg-a": { "lodash": "^4.0.0" },
|
|
794
|
-
},
|
|
795
|
-
});
|
|
796
|
-
|
|
797
|
-
await runPublish({
|
|
798
|
-
targets: [],
|
|
799
|
-
noBuild: false,
|
|
800
|
-
dryRun: false,
|
|
801
|
-
options: [],
|
|
802
|
-
});
|
|
803
|
-
|
|
804
|
-
// Should succeed without issues (lodash doesn't create level dependency)
|
|
805
|
-
expect(process.exitCode).toBeUndefined();
|
|
806
|
-
expect(getExecaCalls("pnpm", "publish")).toHaveLength(1);
|
|
807
|
-
});
|
|
808
|
-
|
|
809
|
-
it("retries failed publish up to 3 times then reports error", async () => {
|
|
810
|
-
setupHappyPath({
|
|
811
|
-
packages: {
|
|
812
|
-
"pkg-a": { target: "node", publish: { type: "npm" } },
|
|
813
|
-
},
|
|
814
|
-
});
|
|
815
|
-
|
|
816
|
-
let publishAttempts = 0;
|
|
817
|
-
mocks.execa.mockImplementation(
|
|
818
|
-
(cmd: string, args?: string[]) => {
|
|
819
|
-
if (cmd === "pnpm" && args?.[0] === "publish") {
|
|
820
|
-
publishAttempts++;
|
|
821
|
-
throw new Error("publish failed");
|
|
822
|
-
}
|
|
823
|
-
if (cmd === "npm") return { stdout: "testuser", stderr: "", exitCode: 0 };
|
|
824
|
-
return { stdout: "", stderr: "", exitCode: 0 };
|
|
825
|
-
},
|
|
826
|
-
);
|
|
827
|
-
|
|
828
|
-
await runPublish({
|
|
829
|
-
targets: [],
|
|
830
|
-
noBuild: false,
|
|
831
|
-
dryRun: false,
|
|
832
|
-
options: [],
|
|
833
|
-
});
|
|
834
|
-
|
|
835
|
-
expect(publishAttempts).toBe(3);
|
|
836
|
-
expect(process.exitCode).toBe(1);
|
|
837
|
-
});
|
|
838
|
-
|
|
839
|
-
it("succeeds on retry after initial failure", async () => {
|
|
840
|
-
setupHappyPath({
|
|
841
|
-
packages: {
|
|
842
|
-
"pkg-a": { target: "node", publish: { type: "npm" } },
|
|
843
|
-
},
|
|
844
|
-
});
|
|
845
|
-
|
|
846
|
-
let publishAttempts = 0;
|
|
847
|
-
mocks.execa.mockImplementation(
|
|
848
|
-
(cmd: string, args?: string[]) => {
|
|
849
|
-
if (cmd === "pnpm" && args?.[0] === "publish") {
|
|
850
|
-
publishAttempts++;
|
|
851
|
-
if (publishAttempts === 1) throw new Error("temporary failure");
|
|
852
|
-
return { stdout: "", stderr: "", exitCode: 0 };
|
|
853
|
-
}
|
|
854
|
-
if (cmd === "npm") return { stdout: "testuser", stderr: "", exitCode: 0 };
|
|
855
|
-
return { stdout: "", stderr: "", exitCode: 0 };
|
|
856
|
-
},
|
|
857
|
-
);
|
|
858
|
-
|
|
859
|
-
await runPublish({
|
|
860
|
-
targets: [],
|
|
861
|
-
noBuild: false,
|
|
862
|
-
dryRun: false,
|
|
863
|
-
options: [],
|
|
864
|
-
});
|
|
865
|
-
|
|
866
|
-
expect(publishAttempts).toBe(2);
|
|
867
|
-
expect(process.exitCode).toBeUndefined();
|
|
868
|
-
});
|
|
869
|
-
});
|
|
870
|
-
|
|
871
|
-
describe("noBuild, dry-run, postPublish", () => {
|
|
872
|
-
it("skips version upgrade, build, and git when noBuild is true", async () => {
|
|
873
|
-
setupHappyPath();
|
|
874
|
-
|
|
875
|
-
await runPublish({
|
|
876
|
-
targets: [],
|
|
877
|
-
noBuild: true,
|
|
878
|
-
dryRun: false,
|
|
879
|
-
options: [],
|
|
880
|
-
});
|
|
881
|
-
|
|
882
|
-
// No version upgrade (fsx.write for package.json should not be called)
|
|
883
|
-
const versionWrites = mocks.fsx.write.mock.calls.filter((c: unknown[]) =>
|
|
884
|
-
(c[0] as string).endsWith("package.json"),
|
|
885
|
-
);
|
|
886
|
-
expect(versionWrites).toHaveLength(0);
|
|
887
|
-
|
|
888
|
-
// No build
|
|
889
|
-
expect(mocks.runBuild).not.toHaveBeenCalled();
|
|
890
|
-
|
|
891
|
-
// No git commit/tag
|
|
892
|
-
expect(getExecaCalls("git", "commit")).toHaveLength(0);
|
|
893
|
-
expect(getExecaCalls("git", "tag")).toHaveLength(0);
|
|
894
|
-
|
|
895
|
-
// But still publishes
|
|
896
|
-
expect(getExecaCalls("pnpm", "publish").length).toBeGreaterThan(0);
|
|
897
|
-
});
|
|
898
|
-
|
|
899
|
-
it("skips git uncommitted check when noBuild is true", async () => {
|
|
900
|
-
setupHappyPath();
|
|
901
|
-
mocks.execa.mockImplementation((cmd: string, args?: string[]) => {
|
|
902
|
-
if (cmd === "npm") return { stdout: "testuser", stderr: "", exitCode: 0 };
|
|
903
|
-
if (cmd === "git" && args?.[0] === "diff") {
|
|
904
|
-
return { stdout: "changed-file.txt", stderr: "", exitCode: 0 };
|
|
905
|
-
}
|
|
906
|
-
return { stdout: "", stderr: "", exitCode: 0 };
|
|
907
|
-
});
|
|
908
|
-
|
|
909
|
-
await runPublish({
|
|
910
|
-
targets: [],
|
|
911
|
-
noBuild: true,
|
|
912
|
-
dryRun: false,
|
|
913
|
-
options: [],
|
|
914
|
-
});
|
|
915
|
-
|
|
916
|
-
// Should not auto-commit since noBuild skips the check
|
|
917
|
-
const autoCommitCalls = mocks.execa.mock.calls.filter(
|
|
918
|
-
(c: unknown[]) => c[0] === "git" && (c[1] as string[])[0] === "add",
|
|
919
|
-
);
|
|
920
|
-
expect(autoCommitCalls).toHaveLength(0);
|
|
921
|
-
// Should succeed
|
|
922
|
-
expect(process.exitCode).toBeUndefined();
|
|
923
|
-
});
|
|
924
|
-
|
|
925
|
-
it("builds only publish-configured packages", async () => {
|
|
926
|
-
setupHappyPath({
|
|
927
|
-
packages: {
|
|
928
|
-
"pkg-a": { target: "node", publish: { type: "npm" } },
|
|
929
|
-
"pkg-b": { target: "node", publish: { type: "npm" } },
|
|
930
|
-
"pkg-c": { target: "node" }, // no publish
|
|
931
|
-
},
|
|
932
|
-
});
|
|
933
|
-
|
|
934
|
-
await runPublish({
|
|
935
|
-
targets: [],
|
|
936
|
-
noBuild: false,
|
|
937
|
-
dryRun: false,
|
|
938
|
-
options: [],
|
|
939
|
-
});
|
|
940
|
-
|
|
941
|
-
// publish 설정이 없는 pkg-c는 배포되지 않아야 한다
|
|
942
|
-
const publishCalls = getExecaCalls("pnpm", "publish");
|
|
943
|
-
expect(publishCalls).toHaveLength(2);
|
|
944
|
-
const publishCwds = mocks.execa.mock.calls
|
|
945
|
-
.filter((c: unknown[]) => {
|
|
946
|
-
const args = c[1] as string[] | undefined;
|
|
947
|
-
return c[0] === "pnpm" && args != null && args[0] === "publish";
|
|
948
|
-
})
|
|
949
|
-
.map((c: unknown[]) => {
|
|
950
|
-
const opts = c[2] as { cwd?: string } | undefined;
|
|
951
|
-
return opts != null ? (opts.cwd ?? "") : "";
|
|
952
|
-
});
|
|
953
|
-
for (const cwd of publishCwds) {
|
|
954
|
-
expect(cwd).not.toContain("pkg-c");
|
|
955
|
-
}
|
|
956
|
-
});
|
|
957
|
-
|
|
958
|
-
it("aborts with recovery message when build fails", async () => {
|
|
959
|
-
setupHappyPath();
|
|
960
|
-
mocks.runBuild.mockRejectedValue(new Error("build failed"));
|
|
961
|
-
|
|
962
|
-
await runPublish({
|
|
963
|
-
targets: [],
|
|
964
|
-
noBuild: false,
|
|
965
|
-
dryRun: false,
|
|
966
|
-
options: [],
|
|
967
|
-
});
|
|
968
|
-
|
|
969
|
-
expect(process.exitCode).toBe(1);
|
|
970
|
-
expect(getExecaCalls("pnpm", "publish")).toHaveLength(0);
|
|
971
|
-
});
|
|
972
|
-
|
|
973
|
-
it("does not modify files in dry-run version upgrade", async () => {
|
|
974
|
-
setupHappyPath({ version: "14.0.0" });
|
|
975
|
-
|
|
976
|
-
await runPublish({
|
|
977
|
-
targets: [],
|
|
978
|
-
noBuild: false,
|
|
979
|
-
dryRun: true,
|
|
980
|
-
options: [],
|
|
981
|
-
});
|
|
982
|
-
|
|
983
|
-
// fsx.write should not be called for version changes
|
|
984
|
-
const pkgJsonWrites = mocks.fsx.write.mock.calls.filter((c: unknown[]) =>
|
|
985
|
-
(c[0] as string).endsWith("package.json"),
|
|
986
|
-
);
|
|
987
|
-
expect(pkgJsonWrites).toHaveLength(0);
|
|
988
|
-
});
|
|
989
|
-
|
|
990
|
-
it("adds --dry-run to pnpm publish in dry-run mode", async () => {
|
|
991
|
-
setupHappyPath();
|
|
992
|
-
|
|
993
|
-
await runPublish({
|
|
994
|
-
targets: [],
|
|
995
|
-
noBuild: false,
|
|
996
|
-
dryRun: true,
|
|
997
|
-
options: [],
|
|
998
|
-
});
|
|
999
|
-
|
|
1000
|
-
const publishCalls = getExecaCalls("pnpm", "publish");
|
|
1001
|
-
expect(publishCalls.length).toBeGreaterThan(0);
|
|
1002
|
-
for (const call of publishCalls) {
|
|
1003
|
-
expect(call.args).toContain("--dry-run");
|
|
1004
|
-
}
|
|
1005
|
-
});
|
|
1006
|
-
|
|
1007
|
-
it("executes postPublish scripts with env var substitution", async () => {
|
|
1008
|
-
setupHappyPath({ version: "14.0.0" });
|
|
1009
|
-
mocks.loadSdConfig.mockResolvedValue({
|
|
1010
|
-
packages: {
|
|
1011
|
-
"pkg-a": { target: "node", publish: { type: "npm" } },
|
|
1012
|
-
},
|
|
1013
|
-
postPublish: [
|
|
1014
|
-
{ type: "script", cmd: "echo", args: ["v%VER%"] },
|
|
1015
|
-
],
|
|
1016
|
-
});
|
|
1017
|
-
|
|
1018
|
-
await runPublish({
|
|
1019
|
-
targets: [],
|
|
1020
|
-
noBuild: false,
|
|
1021
|
-
dryRun: false,
|
|
1022
|
-
options: [],
|
|
1023
|
-
});
|
|
1024
|
-
|
|
1025
|
-
const echoCalls = mocks.execa.mock.calls.filter(
|
|
1026
|
-
(c: unknown[]) => c[0] === "echo",
|
|
1027
|
-
);
|
|
1028
|
-
expect(echoCalls).toHaveLength(1);
|
|
1029
|
-
expect(echoCalls[0][1]).toEqual(["v14.0.1"]);
|
|
1030
|
-
});
|
|
1031
|
-
|
|
1032
|
-
it("warns but does not fail when postPublish script fails", async () => {
|
|
1033
|
-
setupHappyPath({ version: "14.0.0" });
|
|
1034
|
-
mocks.loadSdConfig.mockResolvedValue({
|
|
1035
|
-
packages: {
|
|
1036
|
-
"pkg-a": { target: "node", publish: { type: "npm" } },
|
|
1037
|
-
},
|
|
1038
|
-
postPublish: [
|
|
1039
|
-
{ type: "script", cmd: "failing-cmd", args: [] },
|
|
1040
|
-
],
|
|
1041
|
-
});
|
|
1042
|
-
mocks.execa.mockImplementation((cmd: string) => {
|
|
1043
|
-
if (cmd === "failing-cmd") throw new Error("script failed");
|
|
1044
|
-
if (cmd === "npm") return { stdout: "testuser", stderr: "", exitCode: 0 };
|
|
1045
|
-
return { stdout: "", stderr: "", exitCode: 0 };
|
|
1046
|
-
});
|
|
1047
|
-
|
|
1048
|
-
await runPublish({
|
|
1049
|
-
targets: [],
|
|
1050
|
-
noBuild: false,
|
|
1051
|
-
dryRun: false,
|
|
1052
|
-
options: [],
|
|
1053
|
-
});
|
|
1054
|
-
|
|
1055
|
-
// Should NOT set exit code to 1
|
|
1056
|
-
expect(process.exitCode).toBeUndefined();
|
|
1057
|
-
});
|
|
1058
|
-
|
|
1059
|
-
it("passes options to loadSdConfig", async () => {
|
|
1060
|
-
setupHappyPath();
|
|
1061
|
-
|
|
1062
|
-
await runPublish({
|
|
1063
|
-
targets: [],
|
|
1064
|
-
noBuild: true,
|
|
1065
|
-
dryRun: false,
|
|
1066
|
-
options: ["production"],
|
|
1067
|
-
});
|
|
1068
|
-
|
|
1069
|
-
expect(mocks.loadSdConfig).toHaveBeenCalledWith(
|
|
1070
|
-
expect.objectContaining({ opt: ["production"] }),
|
|
1071
|
-
);
|
|
1072
|
-
});
|
|
1073
|
-
|
|
1074
|
-
it("throws error for unresolved environment variables", async () => {
|
|
1075
|
-
setupHappyPath({
|
|
1076
|
-
version: "14.0.0",
|
|
1077
|
-
packages: {
|
|
1078
|
-
"pkg-a": {
|
|
1079
|
-
target: "node",
|
|
1080
|
-
publish: {
|
|
1081
|
-
type: "local-directory",
|
|
1082
|
-
path: "/deploy/%UNDEFINED_VAR%",
|
|
1083
|
-
},
|
|
1084
|
-
},
|
|
1085
|
-
},
|
|
1086
|
-
hasGit: false,
|
|
1087
|
-
});
|
|
1088
|
-
|
|
1089
|
-
await runPublish({
|
|
1090
|
-
targets: [],
|
|
1091
|
-
noBuild: false,
|
|
1092
|
-
dryRun: false,
|
|
1093
|
-
options: [],
|
|
1094
|
-
});
|
|
1095
|
-
|
|
1096
|
-
// Should fail due to unresolved env var
|
|
1097
|
-
expect(process.exitCode).toBe(1);
|
|
1098
|
-
});
|
|
1099
|
-
});
|
|
1100
|
-
|
|
1101
|
-
describe("publish security + stability", () => {
|
|
1102
|
-
it("dry-run does not execute git push (no network)", async () => {
|
|
1103
|
-
setupHappyPath({ version: "14.0.0" });
|
|
1104
|
-
|
|
1105
|
-
await runPublish({
|
|
1106
|
-
targets: [],
|
|
1107
|
-
noBuild: false,
|
|
1108
|
-
dryRun: true,
|
|
1109
|
-
options: [],
|
|
1110
|
-
});
|
|
1111
|
-
|
|
1112
|
-
// git push should NOT be called in dry-run mode
|
|
1113
|
-
const pushCalls = mocks.execa.mock.calls.filter(
|
|
1114
|
-
(c: unknown[]) =>
|
|
1115
|
-
c[0] === "git" && (c[1] as string[])[0] === "push",
|
|
1116
|
-
);
|
|
1117
|
-
expect(pushCalls).toHaveLength(0);
|
|
1118
|
-
});
|
|
1119
|
-
|
|
1120
|
-
it("includes workspace packages whose directory name contains a dot", async () => {
|
|
1121
|
-
setupHappyPath({
|
|
1122
|
-
packages: {
|
|
1123
|
-
"pkg.v2": { target: "node", publish: { type: "npm" } },
|
|
1124
|
-
},
|
|
1125
|
-
});
|
|
1126
|
-
|
|
1127
|
-
// fsx.glob returns a directory with a dot in its name
|
|
1128
|
-
mocks.fsx.glob.mockImplementation((pattern: string) => {
|
|
1129
|
-
if (pattern.includes("templates")) return [];
|
|
1130
|
-
return [pkgPath("pkg.v2")];
|
|
1131
|
-
});
|
|
1132
|
-
|
|
1133
|
-
// fs.existsSync should report package.json exists in pkg.v2
|
|
1134
|
-
mocks.fsExistsSync.mockImplementation((p: string) => {
|
|
1135
|
-
if (typeof p === "string" && p.endsWith("package.json")) {
|
|
1136
|
-
return p.includes("pkg.v2");
|
|
1137
|
-
}
|
|
1138
|
-
return true;
|
|
1139
|
-
});
|
|
1140
|
-
|
|
1141
|
-
// fsx.readJson for pkg.v2
|
|
1142
|
-
mocks.fsx.readJson.mockImplementation((p: string) => {
|
|
1143
|
-
if (p === path.resolve(CWD, "package.json")) {
|
|
1144
|
-
return createPkgJson("simplysm", "14.0.0");
|
|
1145
|
-
}
|
|
1146
|
-
return createPkgJson("pkg.v2", "14.0.0");
|
|
1147
|
-
});
|
|
1148
|
-
|
|
1149
|
-
await runPublish({
|
|
1150
|
-
targets: [],
|
|
1151
|
-
noBuild: false,
|
|
1152
|
-
dryRun: false,
|
|
1153
|
-
options: [],
|
|
1154
|
-
});
|
|
1155
|
-
|
|
1156
|
-
// pkg.v2 should have been published (not filtered out by the dot)
|
|
1157
|
-
const publishCalls = getExecaCalls("pnpm", "publish");
|
|
1158
|
-
expect(publishCalls).toHaveLength(1);
|
|
1159
|
-
});
|
|
1160
|
-
});
|
|
1161
|
-
|
|
1162
|
-
describe("target validation", () => {
|
|
1163
|
-
it("throws error for unknown target", async () => {
|
|
1164
|
-
mocks.loadSdConfig.mockResolvedValue({
|
|
1165
|
-
packages: {
|
|
1166
|
-
"pkg-a": { target: "node", publish: { type: "npm" } },
|
|
1167
|
-
},
|
|
1168
|
-
});
|
|
1169
|
-
|
|
1170
|
-
await expect(
|
|
1171
|
-
runPublish({ targets: ["nonexistent"], noBuild: false, dryRun: false, options: [] }),
|
|
1172
|
-
).rejects.toThrow("Unknown target: nonexistent");
|
|
1173
|
-
});
|
|
1174
|
-
|
|
1175
|
-
it("does not throw for valid target", async () => {
|
|
1176
|
-
setupHappyPath();
|
|
1177
|
-
|
|
1178
|
-
// Should not throw for valid target
|
|
1179
|
-
await runPublish({ targets: ["pkg-a"], noBuild: false, dryRun: false, options: [] });
|
|
1180
|
-
// If it didn't throw, validation passed
|
|
1181
|
-
});
|
|
1182
|
-
});
|
|
1183
|
-
});
|