hotcodepush 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +53 -0
- package/dist/commands/app/create.js +49 -0
- package/dist/commands/app/delete.js +34 -0
- package/dist/commands/app/get.js +33 -0
- package/dist/commands/app/list.js +38 -0
- package/dist/commands/app/transfer.js +58 -0
- package/dist/commands/app/update.js +34 -0
- package/dist/commands/audience/get.js +69 -0
- package/dist/commands/audit-log/list.js +82 -0
- package/dist/commands/binary/create.js +189 -0
- package/dist/commands/binary/get.js +56 -0
- package/dist/commands/binary/list.js +58 -0
- package/dist/commands/bundle/delete.js +35 -0
- package/dist/commands/bundle/get.js +63 -0
- package/dist/commands/bundle/list.js +66 -0
- package/dist/commands/bundle/upload.js +176 -0
- package/dist/commands/channel/create.js +59 -0
- package/dist/commands/channel/delete.js +68 -0
- package/dist/commands/channel/get.js +45 -0
- package/dist/commands/channel/list.js +39 -0
- package/dist/commands/channel/pause.js +32 -0
- package/dist/commands/channel/resume.js +32 -0
- package/dist/commands/channel/update.js +40 -0
- package/dist/commands/checkout/create.js +25 -0
- package/dist/commands/completion.js +36 -0
- package/dist/commands/customer-portal-session/create.js +25 -0
- package/dist/commands/device/delete.js +29 -0
- package/dist/commands/device/get.js +60 -0
- package/dist/commands/device/list.js +97 -0
- package/dist/commands/device/probe.js +253 -0
- package/dist/commands/doctor.js +363 -0
- package/dist/commands/fingerprint/diff.js +133 -0
- package/dist/commands/fingerprint.js +41 -0
- package/dist/commands/init.js +492 -0
- package/dist/commands/invitation/accept.js +53 -0
- package/dist/commands/invitation/create.js +52 -0
- package/dist/commands/invitation/delete.js +60 -0
- package/dist/commands/invitation/list.js +40 -0
- package/dist/commands/limit/list.js +34 -0
- package/dist/commands/login.js +185 -0
- package/dist/commands/logout.js +79 -0
- package/dist/commands/member/delete.js +35 -0
- package/dist/commands/member/get.js +37 -0
- package/dist/commands/member/list.js +41 -0
- package/dist/commands/member/update.js +53 -0
- package/dist/commands/open.js +37 -0
- package/dist/commands/organization/create.js +30 -0
- package/dist/commands/organization/delete.js +39 -0
- package/dist/commands/organization/get.js +38 -0
- package/dist/commands/organization/list.js +41 -0
- package/dist/commands/organization/update.js +64 -0
- package/dist/commands/release/create.js +237 -0
- package/dist/commands/release/get.js +81 -0
- package/dist/commands/release/list.js +69 -0
- package/dist/commands/release/pause.js +36 -0
- package/dist/commands/release/resume.js +36 -0
- package/dist/commands/release/revoke.js +253 -0
- package/dist/commands/release/rollback.js +73 -0
- package/dist/commands/release/rollout.js +63 -0
- package/dist/commands/release/update.js +82 -0
- package/dist/commands/resource-file/write.js +76 -0
- package/dist/commands/signing-key/add.js +45 -0
- package/dist/commands/signing-key/create.js +84 -0
- package/dist/commands/signing-key/delete.js +67 -0
- package/dist/commands/signing-key/list.js +38 -0
- package/dist/commands/sso-provider/delete.js +32 -0
- package/dist/commands/sso-provider/get.js +63 -0
- package/dist/commands/sso-provider/set.js +161 -0
- package/dist/commands/sso-provider/verify.js +31 -0
- package/dist/commands/statistics/get.js +189 -0
- package/dist/commands/usage/get.js +31 -0
- package/dist/commands/usage/list.js +41 -0
- package/dist/commands/webhook/create.js +72 -0
- package/dist/commands/webhook/delete.js +32 -0
- package/dist/commands/webhook/get.js +34 -0
- package/dist/commands/webhook/list.js +40 -0
- package/dist/commands/webhook/test.js +32 -0
- package/dist/commands/webhook/update.js +56 -0
- package/dist/commands/webhook-delivery/list.js +63 -0
- package/dist/commands/webhook-delivery/redeliver.js +48 -0
- package/dist/commands/whoami.js +24 -0
- package/dist/completion/hotcodepush.bash +536 -0
- package/dist/completion/hotcodepush.fish +979 -0
- package/dist/completion/hotcodepush.zsh +1372 -0
- package/dist/config/consts.js +32 -0
- package/dist/index.js +5 -0
- package/dist/registry.js +83 -0
- package/dist/utils/api-client.js +17 -0
- package/dist/utils/audience.js +9 -0
- package/dist/utils/auth-client.js +35 -0
- package/dist/utils/boolean-flag.js +3 -0
- package/dist/utils/browser.js +6 -0
- package/dist/utils/build-step.js +104 -0
- package/dist/utils/bundle-files.js +80 -0
- package/dist/utils/bundle-resolution.js +46 -0
- package/dist/utils/channel-fields.js +130 -0
- package/dist/utils/cli.js +75 -0
- package/dist/utils/command-registry.js +79 -0
- package/dist/utils/completion/bash.js +48 -0
- package/dist/utils/completion/command-tree.js +72 -0
- package/dist/utils/completion/fish.js +44 -0
- package/dist/utils/completion/posix-case-arms.js +9 -0
- package/dist/utils/completion/posix-quote.js +4 -0
- package/dist/utils/completion/script-files.js +26 -0
- package/dist/utils/completion/zsh.js +72 -0
- package/dist/utils/compressed-files.js +31 -0
- package/dist/utils/credential.js +15 -0
- package/dist/utils/device-resolution.js +17 -0
- package/dist/utils/duration.js +16 -0
- package/dist/utils/environment.js +22 -0
- package/dist/utils/error-mapping.js +79 -0
- package/dist/utils/errors.js +266 -0
- package/dist/utils/fingerprint.js +76 -0
- package/dist/utils/flag-name.js +4 -0
- package/dist/utils/fleet.js +9 -0
- package/dist/utils/framework.js +74 -0
- package/dist/utils/frameworks/binary-create-step.js +70 -0
- package/dist/utils/frameworks/capacitor.js +150 -0
- package/dist/utils/frameworks/cordova.js +110 -0
- package/dist/utils/frameworks/expo.js +152 -0
- package/dist/utils/frameworks/index.js +13 -0
- package/dist/utils/frameworks/native-glue.js +10 -0
- package/dist/utils/frameworks/native-projects.js +27 -0
- package/dist/utils/frameworks/react-native-build.js +182 -0
- package/dist/utils/frameworks/react-native.js +127 -0
- package/dist/utils/frameworks/sdk-package.js +63 -0
- package/dist/utils/git-provenance.js +71 -0
- package/dist/utils/global-options.js +34 -0
- package/dist/utils/hosts.js +53 -0
- package/dist/utils/init-steps.js +51 -0
- package/dist/utils/json-file.js +28 -0
- package/dist/utils/member-resolution.js +49 -0
- package/dist/utils/native-project-edit.js +41 -0
- package/dist/utils/nullable-field.js +10 -0
- package/dist/utils/outcome.js +24 -0
- package/dist/utils/output.js +37 -0
- package/dist/utils/pack.js +20 -0
- package/dist/utils/package-json.js +5 -0
- package/dist/utils/package-manager.js +50 -0
- package/dist/utils/pagination.js +29 -0
- package/dist/utils/progress.js +23 -0
- package/dist/utils/progression.js +119 -0
- package/dist/utils/project-config.js +61 -0
- package/dist/utils/prompts.js +53 -0
- package/dist/utils/react-native-project.js +119 -0
- package/dist/utils/release-conditions.js +80 -0
- package/dist/utils/release-output.js +13 -0
- package/dist/utils/release-resolution.js +83 -0
- package/dist/utils/resource-file.js +81 -0
- package/dist/utils/resource-resolution.js +107 -0
- package/dist/utils/signing-private-key.js +105 -0
- package/dist/utils/spending-cap.js +16 -0
- package/dist/utils/telemetry.js +171 -0
- package/dist/utils/token-store.js +63 -0
- package/dist/utils/update-notice.js +67 -0
- package/dist/utils/upload.js +148 -0
- package/dist/utils/usage-month.js +14 -0
- package/dist/utils/user-config.js +46 -0
- package/dist/utils/webhook-resolution.js +47 -0
- package/dist/utils/xcode-project.js +181 -0
- package/package.json +68 -7
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { ISSUES_URL, PROJECT_CONFIG_FILE_NAME } from '../config/consts.js';
|
|
2
|
+
export const ExitCode = {
|
|
3
|
+
ConfirmationRequired: 4,
|
|
4
|
+
Error: 1,
|
|
5
|
+
MissingOrInvalidParameter: 2,
|
|
6
|
+
NotLoggedIn: 3,
|
|
7
|
+
Success: 0,
|
|
8
|
+
};
|
|
9
|
+
export class CliError extends Error {
|
|
10
|
+
code;
|
|
11
|
+
exitCode;
|
|
12
|
+
fix;
|
|
13
|
+
constructor({ cause, code, exitCode, fix, message }) {
|
|
14
|
+
super(message, { cause });
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.exitCode = exitCode;
|
|
17
|
+
this.fix = fix;
|
|
18
|
+
this.name = new.target.name;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
// Organizations can share a name, and so can apps of different organizations.
|
|
22
|
+
export class AmbiguousNameError extends CliError {
|
|
23
|
+
constructor(noun, name, source = `--${noun}`) {
|
|
24
|
+
super({
|
|
25
|
+
code: 'invalid_parameter',
|
|
26
|
+
exitCode: ExitCode.MissingOrInvalidParameter,
|
|
27
|
+
fix: `Pass the id instead, as "hotcodepush ${noun} list" prints it.`,
|
|
28
|
+
message: `Several ${noun}s carry the name "${name}" from ${source}.`,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// The message from the API carries its own fix.
|
|
33
|
+
// A 401 means the token is no longer valid, so the exit code is 3.
|
|
34
|
+
export class ApiError extends CliError {
|
|
35
|
+
constructor(code, message, status) {
|
|
36
|
+
super({
|
|
37
|
+
code,
|
|
38
|
+
exitCode: status === 401 ? ExitCode.NotLoggedIn : ExitCode.Error,
|
|
39
|
+
fix: null,
|
|
40
|
+
message,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Checked before any upload, so no upload is wasted on a refusal from the API.
|
|
45
|
+
export class BundleTooLargeError extends CliError {
|
|
46
|
+
constructor(what, bundleBytesLimit) {
|
|
47
|
+
super({
|
|
48
|
+
code: 'bundle_too_large',
|
|
49
|
+
exitCode: ExitCode.Error,
|
|
50
|
+
fix: 'A file or a bundle is at most 512 MB. Move large media out of the web build.',
|
|
51
|
+
message: `${what} is above the limit of ${bundleBytesLimit} bytes.`,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export class CommandFailedError extends CliError {
|
|
56
|
+
constructor(commandLine, exitCode) {
|
|
57
|
+
super({
|
|
58
|
+
code: 'command_failed',
|
|
59
|
+
exitCode: ExitCode.Error,
|
|
60
|
+
fix: 'Run it by hand and read its output.',
|
|
61
|
+
message: `The command "${commandLine}" exited with ${exitCode ?? 'a signal'}.`,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export class ConfirmationRequiredError extends CliError {
|
|
66
|
+
constructor(consequence, fix = 'Pass --yes to confirm.') {
|
|
67
|
+
super({
|
|
68
|
+
code: 'confirmation_required',
|
|
69
|
+
exitCode: ExitCode.ConfirmationRequired,
|
|
70
|
+
fix,
|
|
71
|
+
message: `A confirmation is required. This ${consequence}.`,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// A build without a fingerprint would match binaries it does not describe.
|
|
76
|
+
// The fix is the message of the protocol package as it stands.
|
|
77
|
+
export class FingerprintUnavailableError extends CliError {
|
|
78
|
+
constructor(cause) {
|
|
79
|
+
super({
|
|
80
|
+
cause,
|
|
81
|
+
code: 'fingerprint_unavailable',
|
|
82
|
+
exitCode: ExitCode.Error,
|
|
83
|
+
fix: cause.message,
|
|
84
|
+
message: 'The fingerprint cannot be computed.',
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Never names the file content, since `config.json` can hold the token.
|
|
89
|
+
export class InvalidJsonError extends CliError {
|
|
90
|
+
constructor(filePath, problem) {
|
|
91
|
+
super({
|
|
92
|
+
code: 'invalid_json',
|
|
93
|
+
exitCode: ExitCode.Error,
|
|
94
|
+
fix: 'Correct the JSON in that file and run the command again.',
|
|
95
|
+
message: `The file ${filePath} is not valid JSON. ${problem}.`,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
export class InvalidParameterError extends CliError {
|
|
100
|
+
constructor(message, fix = 'Run the command with --help to see its options.', cause) {
|
|
101
|
+
super({
|
|
102
|
+
cause,
|
|
103
|
+
code: 'invalid_parameter',
|
|
104
|
+
exitCode: ExitCode.MissingOrInvalidParameter,
|
|
105
|
+
fix,
|
|
106
|
+
message,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export class LoginDeniedError extends CliError {
|
|
111
|
+
constructor() {
|
|
112
|
+
super({
|
|
113
|
+
code: 'login_denied',
|
|
114
|
+
exitCode: ExitCode.Error,
|
|
115
|
+
fix: 'Run "hotcodepush login" again to approve it.',
|
|
116
|
+
message: 'The login was denied in the browser.',
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
export class LoginExpiredError extends CliError {
|
|
121
|
+
constructor() {
|
|
122
|
+
super({
|
|
123
|
+
code: 'login_expired',
|
|
124
|
+
exitCode: ExitCode.Error,
|
|
125
|
+
fix: 'Run "hotcodepush login" again and approve the code before it expires.',
|
|
126
|
+
message: 'The login code expired before it was approved.',
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
export class MissingParameterError extends CliError {
|
|
131
|
+
constructor(flag, fix = `Pass ${flag}, or run the command interactively to be asked for it.`) {
|
|
132
|
+
super({
|
|
133
|
+
code: 'missing_parameter',
|
|
134
|
+
exitCode: ExitCode.MissingOrInvalidParameter,
|
|
135
|
+
fix,
|
|
136
|
+
message: `No value was given for ${flag}.`,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
export class NativeProjectError extends CliError {
|
|
141
|
+
constructor(message, fix) {
|
|
142
|
+
super({
|
|
143
|
+
code: 'native_project',
|
|
144
|
+
exitCode: ExitCode.Error,
|
|
145
|
+
fix,
|
|
146
|
+
message,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// From a login that cannot wait for the approval, the fix carries the page and the code for an agent to relay.
|
|
151
|
+
export class NotLoggedInError extends CliError {
|
|
152
|
+
constructor(deviceAuthorizationPrompt) {
|
|
153
|
+
super({
|
|
154
|
+
code: 'not_logged_in',
|
|
155
|
+
exitCode: ExitCode.NotLoggedIn,
|
|
156
|
+
fix: deviceAuthorizationPrompt
|
|
157
|
+
? `Approve at ${deviceAuthorizationPrompt.verificationUrl} with code ${deviceAuthorizationPrompt.userCode}, then run "hotcodepush login" again.`
|
|
158
|
+
: 'Run "hotcodepush login" or set HOTCODEPUSH_TOKEN.',
|
|
159
|
+
message: 'You are not logged in.',
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// A shipped build must name its channel, so CI fails where a local build goes on offline.
|
|
164
|
+
export class PipelineNotLoggedInError extends CliError {
|
|
165
|
+
constructor() {
|
|
166
|
+
super({
|
|
167
|
+
code: 'not_logged_in',
|
|
168
|
+
exitCode: ExitCode.NotLoggedIn,
|
|
169
|
+
fix: 'Set HOTCODEPUSH_TOKEN in the pipeline, or HOTCODEPUSH_OFFLINE=1 for a build that is never shipped.',
|
|
170
|
+
message: 'You are not logged in, and a build in CI must name its channel.',
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// The command printed its outcome itself, so the entry point prints nothing more.
|
|
175
|
+
// The stopped step of `init` goes into its telemetry event.
|
|
176
|
+
export class ReportedFailureError extends CliError {
|
|
177
|
+
stoppedStep;
|
|
178
|
+
constructor(code, stoppedStep) {
|
|
179
|
+
super({
|
|
180
|
+
code,
|
|
181
|
+
exitCode: ExitCode.Error,
|
|
182
|
+
fix: null,
|
|
183
|
+
message: 'Reported above.',
|
|
184
|
+
});
|
|
185
|
+
this.stoppedStep = stoppedStep;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// The app accepts only signed bundles, so the upload stops before any byte moves.
|
|
189
|
+
export class SigningKeyUnavailableError extends CliError {
|
|
190
|
+
constructor() {
|
|
191
|
+
super({
|
|
192
|
+
code: 'signing_key_unavailable',
|
|
193
|
+
exitCode: ExitCode.Error,
|
|
194
|
+
fix: 'Pass --private-key-path with the file "hotcodepush signing-key create" wrote, or set HOTCODEPUSH_SIGNING_KEY to its content.',
|
|
195
|
+
message: `No private key is given to sign with, and ${PROJECT_CONFIG_FILE_NAME} lists public keys.`,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
export class UnexpectedError extends CliError {
|
|
200
|
+
constructor(cause) {
|
|
201
|
+
super({
|
|
202
|
+
cause,
|
|
203
|
+
code: 'unexpected',
|
|
204
|
+
exitCode: ExitCode.Error,
|
|
205
|
+
fix: `Run the command again with --verbose, and report it at ${ISSUES_URL} if it persists.`,
|
|
206
|
+
message: cause instanceof Error ? cause.message : String(cause),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// A proxy error page or a body without a catalog code.
|
|
211
|
+
// The typed client keeps its error as the cause; the Better Auth client gives an error without a code.
|
|
212
|
+
export class UnexpectedResponseError extends CliError {
|
|
213
|
+
constructor(status, cause) {
|
|
214
|
+
super({
|
|
215
|
+
cause,
|
|
216
|
+
code: 'unexpected_response',
|
|
217
|
+
exitCode: ExitCode.Error,
|
|
218
|
+
fix: `Check the API URL in config.json and the network, and report it at ${ISSUES_URL} if it persists.`,
|
|
219
|
+
message: `The API answered with status ${status} in an unexpected shape.`,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
export class XcodeProjectError extends CliError {
|
|
224
|
+
constructor(message, cause, projectFilePath, fix = `Add the build phase to the app target in ${projectFilePath} by hand.`) {
|
|
225
|
+
super({
|
|
226
|
+
cause,
|
|
227
|
+
code: 'xcode_project',
|
|
228
|
+
exitCode: ExitCode.Error,
|
|
229
|
+
fix,
|
|
230
|
+
message,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
export class UnknownFrameworkError extends CliError {
|
|
235
|
+
constructor() {
|
|
236
|
+
super({
|
|
237
|
+
code: 'unknown_framework',
|
|
238
|
+
exitCode: ExitCode.Error,
|
|
239
|
+
fix: 'Run it in a project with react-native, expo, @capacitor/core or cordova among its dependencies.',
|
|
240
|
+
message: 'No supported framework was found in package.json.',
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
export class UnknownCommandError extends CliError {
|
|
245
|
+
constructor(typedCommand, closestCommandName) {
|
|
246
|
+
super({
|
|
247
|
+
code: 'unknown_command',
|
|
248
|
+
exitCode: ExitCode.Error,
|
|
249
|
+
fix: closestCommandName
|
|
250
|
+
? `Did you mean "${closestCommandName}"?`
|
|
251
|
+
: 'Run "hotcodepush --help" to list the commands.',
|
|
252
|
+
message: `There is no command "${typedCommand}".`,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// An unknown id is the API's own `not_found` instead.
|
|
257
|
+
export class UnknownNameError extends CliError {
|
|
258
|
+
constructor(noun, name, source = `--${noun}`) {
|
|
259
|
+
super({
|
|
260
|
+
code: 'invalid_parameter',
|
|
261
|
+
exitCode: ExitCode.MissingOrInvalidParameter,
|
|
262
|
+
fix: `Run "hotcodepush ${noun} list" to see the names.`,
|
|
263
|
+
message: `The ${noun} "${name}" from ${source} does not exist.`,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { readdir, readFile, realpath } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join, relative, sep } from 'node:path';
|
|
4
|
+
import { computeFingerprint, FingerprintError, LOCKFILE_NAMES, readFingerprintContributors, } from '@hotcodepush/protocol/fingerprint';
|
|
5
|
+
import { FingerprintUnavailableError } from './errors.js';
|
|
6
|
+
// Errors for a path that holds nothing of the kind asked for.
|
|
7
|
+
const ABSENT_ERROR_CODES = new Set(['EISDIR', 'ENOENT', 'ENOTDIR']);
|
|
8
|
+
// The extra fingerprint paths are relative to the project root and may reach a sibling workspace through `..`.
|
|
9
|
+
export async function readFingerprint(projectDirectoryPath, extraFingerprintPaths) {
|
|
10
|
+
return computeFingerprint(await readProjectFingerprintContributors(projectDirectoryPath, extraFingerprintPaths));
|
|
11
|
+
}
|
|
12
|
+
export function hasLockfile(projectDirectoryPath) {
|
|
13
|
+
return findLockfileDirectoryPath(projectDirectoryPath) !== undefined;
|
|
14
|
+
}
|
|
15
|
+
// The recipe reads from the nearest lockfile directory, which is the monorepo root where the workspace installs.
|
|
16
|
+
// It resolves the extra paths against the project and refuses any outside that directory.
|
|
17
|
+
export async function readProjectFingerprintContributors(projectDirectoryPath, extraFingerprintPaths) {
|
|
18
|
+
const lockfileDirectoryPath = findLockfileDirectoryPath(projectDirectoryPath) ?? projectDirectoryPath;
|
|
19
|
+
const projectPath = relative(lockfileDirectoryPath, projectDirectoryPath)
|
|
20
|
+
.split(sep)
|
|
21
|
+
.join('/');
|
|
22
|
+
try {
|
|
23
|
+
return await readFingerprintContributors({
|
|
24
|
+
extraFingerprintPaths,
|
|
25
|
+
projectPath,
|
|
26
|
+
reader: createProjectReader(lockfileDirectoryPath),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (error instanceof FingerprintError) {
|
|
31
|
+
throw new FingerprintUnavailableError(error);
|
|
32
|
+
}
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function findLockfileDirectoryPath(directoryPath) {
|
|
37
|
+
if (LOCKFILE_NAMES.some(fileName => existsSync(join(directoryPath, fileName)))) {
|
|
38
|
+
return directoryPath;
|
|
39
|
+
}
|
|
40
|
+
const parentDirectoryPath = dirname(directoryPath);
|
|
41
|
+
return parentDirectoryPath === directoryPath
|
|
42
|
+
? undefined
|
|
43
|
+
: findLockfileDirectoryPath(parentDirectoryPath);
|
|
44
|
+
}
|
|
45
|
+
function createProjectReader(rootDirectoryPath) {
|
|
46
|
+
return {
|
|
47
|
+
readDirectory: path => readAbsentAsNull(async () => (await readdir(join(rootDirectoryPath, path), {
|
|
48
|
+
withFileTypes: true,
|
|
49
|
+
})).map(entry => ({
|
|
50
|
+
isDirectory: entry.isDirectory(),
|
|
51
|
+
name: entry.name,
|
|
52
|
+
}))),
|
|
53
|
+
readFile: path => readAbsentAsNull(() => readFile(join(rootDirectoryPath, path))),
|
|
54
|
+
readRealPath: path => readAbsentAsNull(async () => {
|
|
55
|
+
const [rootRealPath, targetRealPath] = await Promise.all([
|
|
56
|
+
realpath(rootDirectoryPath),
|
|
57
|
+
realpath(join(rootDirectoryPath, path)),
|
|
58
|
+
]);
|
|
59
|
+
// A link out of the root yields a path starting with `..`, which the recipe refuses.
|
|
60
|
+
return relative(rootRealPath, targetRealPath).split(sep).join('/');
|
|
61
|
+
}),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
async function readAbsentAsNull(read) {
|
|
65
|
+
try {
|
|
66
|
+
return await read();
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (error instanceof Error &&
|
|
70
|
+
'code' in error &&
|
|
71
|
+
ABSENT_ERROR_CODES.has(String(error.code))) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// The fleet counts only the devices seen in the last 30 days.
|
|
2
|
+
export const UNSEEN_IN_FLEET_WINDOW_TEXT = 'not in 30 days';
|
|
3
|
+
export function resolveBinaryBucket(fleet, binaryId) {
|
|
4
|
+
return fleet.binary.find(({ value }) => value === binaryId);
|
|
5
|
+
}
|
|
6
|
+
// A null release id stands for the embedded bundles.
|
|
7
|
+
export function resolveReleaseDeviceCount(fleet, releaseId) {
|
|
8
|
+
return (fleet.release.find(({ value }) => value === releaseId)?.deviceCount ?? 0);
|
|
9
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { isInteractive } from './environment.js';
|
|
4
|
+
import { MissingParameterError, UnknownFrameworkError } from './errors.js';
|
|
5
|
+
import { CAPACITOR_CONFIG_FILE_NAMES } from './frameworks/capacitor.js';
|
|
6
|
+
import { readPackageJson } from './package-json.js';
|
|
7
|
+
import { promptText } from './prompts.js';
|
|
8
|
+
const FRAMEWORK_PACKAGES = [
|
|
9
|
+
['@capacitor/core', 'capacitor'],
|
|
10
|
+
['cordova', 'cordova'],
|
|
11
|
+
['expo', 'expo'],
|
|
12
|
+
['react-native', 'react_native'],
|
|
13
|
+
];
|
|
14
|
+
// An Expo app also depends on `react-native`, so the framework's config file breaks a tie.
|
|
15
|
+
export function detectFramework(projectDirectoryPath) {
|
|
16
|
+
if (!existsSync(join(projectDirectoryPath, 'package.json'))) {
|
|
17
|
+
throw new UnknownFrameworkError();
|
|
18
|
+
}
|
|
19
|
+
const packageJson = readPackageJson(projectDirectoryPath);
|
|
20
|
+
const dependencyNames = new Set([
|
|
21
|
+
...Object.keys(packageJson.dependencies ?? {}),
|
|
22
|
+
...Object.keys(packageJson.devDependencies ?? {}),
|
|
23
|
+
]);
|
|
24
|
+
const detectedFrameworks = FRAMEWORK_PACKAGES.filter(([packageName]) => dependencyNames.has(packageName)).map(([, framework]) => framework);
|
|
25
|
+
const framework = detectedFrameworks.find(detectedFramework => hasFrameworkConfigFile(detectedFramework, projectDirectoryPath)) ?? detectedFrameworks[0];
|
|
26
|
+
if (framework === undefined) {
|
|
27
|
+
throw new UnknownFrameworkError();
|
|
28
|
+
}
|
|
29
|
+
return framework;
|
|
30
|
+
}
|
|
31
|
+
export async function resolveInputDirectoryPath(options, pathFlag, projectDirectoryPath, framework) {
|
|
32
|
+
if (options.path !== undefined) {
|
|
33
|
+
return resolve(options.path);
|
|
34
|
+
}
|
|
35
|
+
const buildDirectory = framework.readBuildDirectory(projectDirectoryPath);
|
|
36
|
+
if ('path' in buildDirectory) {
|
|
37
|
+
return join(projectDirectoryPath, buildDirectory.path);
|
|
38
|
+
}
|
|
39
|
+
if (!isInteractive(options)) {
|
|
40
|
+
throw new MissingParameterError(pathFlag, `Pass ${pathFlag}, since ${buildDirectory.missingReason}.`);
|
|
41
|
+
}
|
|
42
|
+
return resolve(await promptText(pathFlag, 'Where is the web build?', options));
|
|
43
|
+
}
|
|
44
|
+
export function assertMissingParameter(value, flag) {
|
|
45
|
+
if (value === undefined) {
|
|
46
|
+
throw new MissingParameterError(flag);
|
|
47
|
+
}
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
function hasFrameworkConfigFile(framework, projectDirectoryPath) {
|
|
51
|
+
switch (framework) {
|
|
52
|
+
case 'capacitor':
|
|
53
|
+
return CAPACITOR_CONFIG_FILE_NAMES.some(fileName => existsSync(join(projectDirectoryPath, fileName)));
|
|
54
|
+
case 'cordova':
|
|
55
|
+
return existsSync(join(projectDirectoryPath, 'config.xml'));
|
|
56
|
+
case 'expo':
|
|
57
|
+
return hasExpoAppJson(projectDirectoryPath);
|
|
58
|
+
case 'react_native':
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function hasExpoAppJson(projectDirectoryPath) {
|
|
63
|
+
const appJsonPath = join(projectDirectoryPath, 'app.json');
|
|
64
|
+
if (!existsSync(appJsonPath)) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const appJson = JSON.parse(readFileSync(appJsonPath, 'utf8'));
|
|
69
|
+
return typeof appJson === 'object' && appJson !== null && 'expo' in appJson;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { relative } from 'node:path';
|
|
2
|
+
import { INIT_MANUAL_STEP } from '../../config/consts.js';
|
|
3
|
+
import { NativeProjectError, XcodeProjectError } from '../errors.js';
|
|
4
|
+
import { resolveGradleEdit } from '../native-project-edit.js';
|
|
5
|
+
import { addBinaryCreatePhase, hasBinaryCreatePhase, resolveXcodeProjectFilePath, } from '../xcode-project.js';
|
|
6
|
+
export function checkBinaryCreateStep(projectDirectoryPath, nativeProjectPaths, packageName) {
|
|
7
|
+
const xcodeProjectFilePath = resolveXcodeProjectFilePath(nativeProjectPaths.ios);
|
|
8
|
+
const gradleEdit = resolveGradleEdit(nativeProjectPaths.android, packageName);
|
|
9
|
+
return checkEdits('hook', 'The Xcode phase and the Gradle task run the build step.', gradleEdit === undefined ? [] : [gradleEdit], projectDirectoryPath, xcodeProjectFilePath !== undefined &&
|
|
10
|
+
!hasBinaryCreatePhase(xcodeProjectFilePath));
|
|
11
|
+
}
|
|
12
|
+
export function checkEdits(check, wiredMessage, edits, projectDirectoryPath, isPhaseMissing = false) {
|
|
13
|
+
const missingFilePaths = edits
|
|
14
|
+
.filter(edit => !edit.isApplied())
|
|
15
|
+
.map(({ filePath }) => relative(projectDirectoryPath, filePath));
|
|
16
|
+
if (edits.length === 0 && !isPhaseMissing) {
|
|
17
|
+
return {
|
|
18
|
+
check,
|
|
19
|
+
message: 'There is no native project to check.',
|
|
20
|
+
status: 'skipped',
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
if (missingFilePaths.length > 0 || isPhaseMissing) {
|
|
24
|
+
return {
|
|
25
|
+
check,
|
|
26
|
+
code: 'wiring_missing',
|
|
27
|
+
manualStep: INIT_MANUAL_STEP,
|
|
28
|
+
message: `The wiring is missing from ${[...(isPhaseMissing ? ['the Xcode project'] : []), ...missingFilePaths].join(' and ')}.`,
|
|
29
|
+
status: 'failed',
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return { check, message: wiredMessage, status: 'ok' };
|
|
33
|
+
}
|
|
34
|
+
// A failed edit does not hold the others back. Once all were tried, the first failure stops the step.
|
|
35
|
+
export async function applyNativeProjectEdits(phaseEdit, edits, options) {
|
|
36
|
+
const errors = [];
|
|
37
|
+
const wired = [];
|
|
38
|
+
if (phaseEdit !== undefined) {
|
|
39
|
+
try {
|
|
40
|
+
if ((await addBinaryCreatePhase(phaseEdit.projectFilePath, phaseEdit.phase, options)) === 'added') {
|
|
41
|
+
wired.push('the Create HotCodePush binary phase in Xcode');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
errors.push(assertEditError(error));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
for (const edit of edits) {
|
|
49
|
+
try {
|
|
50
|
+
edit.apply();
|
|
51
|
+
wired.push(edit.description);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
errors.push(assertEditError(error));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const [firstError] = errors;
|
|
58
|
+
if (firstError !== undefined) {
|
|
59
|
+
throw firstError;
|
|
60
|
+
}
|
|
61
|
+
return wired;
|
|
62
|
+
}
|
|
63
|
+
// Only an edit that found no place in its file becomes the manual step. Any other error is rethrown.
|
|
64
|
+
function assertEditError(error) {
|
|
65
|
+
if (error instanceof NativeProjectError ||
|
|
66
|
+
error instanceof XcodeProjectError) {
|
|
67
|
+
return error;
|
|
68
|
+
}
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join, relative, sep } from 'node:path';
|
|
3
|
+
import { CAPACITOR_PACKAGE_NAME, CAPACITOR_PACKAGE_SPEC, } from '../../config/consts.js';
|
|
4
|
+
import { resolveGradleEdit } from '../native-project-edit.js';
|
|
5
|
+
import { buildPbxprojShellScript, hasBinaryCreatePhase, hasReadableResourceReference, removeResourceReference, resolveXcodeProjectFilePath, } from '../xcode-project.js';
|
|
6
|
+
import { applyNativeProjectEdits, checkBinaryCreateStep, } from './binary-create-step.js';
|
|
7
|
+
import { NATIVE_GLUE_PATHS } from './native-glue.js';
|
|
8
|
+
import { resolveNativeProjects } from './native-projects.js';
|
|
9
|
+
import { checkSdkPackage, installSdkPackage, isSdkPackageDeclared, } from './sdk-package.js';
|
|
10
|
+
const BINARY_CREATE_SCRIPT_PATH = `node_modules/${CAPACITOR_PACKAGE_NAME}/scripts/binary-create-xcode.sh`;
|
|
11
|
+
export const CAPACITOR_CONFIG_FILE_NAMES = [
|
|
12
|
+
'capacitor.config.json',
|
|
13
|
+
'capacitor.config.ts',
|
|
14
|
+
];
|
|
15
|
+
export const capacitorFramework = {
|
|
16
|
+
nativeGluePaths: NATIVE_GLUE_PATHS,
|
|
17
|
+
packageName: CAPACITOR_PACKAGE_NAME,
|
|
18
|
+
versionedPackageNames: ['@capacitor/core', CAPACITOR_PACKAGE_NAME],
|
|
19
|
+
checkWiring: project => [
|
|
20
|
+
checkSdkPackage(project, CAPACITOR_PACKAGE_NAME),
|
|
21
|
+
checkBinaryCreateStep(project.directoryPath, resolveNativeProjectPaths(project.directoryPath), CAPACITOR_PACKAGE_NAME),
|
|
22
|
+
],
|
|
23
|
+
readBuildDirectory: readWebDir,
|
|
24
|
+
resolveWiring,
|
|
25
|
+
};
|
|
26
|
+
function readCapacitorConfig(projectDirectoryPath) {
|
|
27
|
+
for (const fileName of CAPACITOR_CONFIG_FILE_NAMES) {
|
|
28
|
+
const filePath = join(projectDirectoryPath, fileName);
|
|
29
|
+
if (existsSync(filePath)) {
|
|
30
|
+
return { fileName, text: readFileSync(filePath, 'utf8') };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
function readConfigValue(capacitorConfig, pattern) {
|
|
36
|
+
return capacitorConfig === undefined
|
|
37
|
+
? undefined
|
|
38
|
+
: pattern.exec(capacitorConfig.text)?.[1];
|
|
39
|
+
}
|
|
40
|
+
function readWebDir(projectDirectoryPath) {
|
|
41
|
+
const capacitorConfig = readCapacitorConfig(projectDirectoryPath);
|
|
42
|
+
if (capacitorConfig === undefined) {
|
|
43
|
+
return {
|
|
44
|
+
missingReason: `there is no ${CAPACITOR_CONFIG_FILE_NAMES.join(' or ')} to read webDir from`,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const webDir = readConfigValue(capacitorConfig, /webDir['"]?\s*:\s*['"]([^'"]+)['"]/);
|
|
48
|
+
return webDir === undefined
|
|
49
|
+
? {
|
|
50
|
+
missingReason: `${capacitorConfig.fileName} sets no webDir the CLI can read as a quoted string`,
|
|
51
|
+
}
|
|
52
|
+
: { path: webDir };
|
|
53
|
+
}
|
|
54
|
+
function resolveNativeProjectPaths(projectDirectoryPath) {
|
|
55
|
+
const capacitorConfig = readCapacitorConfig(projectDirectoryPath);
|
|
56
|
+
return {
|
|
57
|
+
android: join(projectDirectoryPath, readConfigValue(capacitorConfig, /android:\s*\{[^}]*?path:\s*['"]([^'"]+)['"]/) ?? 'android'),
|
|
58
|
+
ios: join(projectDirectoryPath, readConfigValue(capacitorConfig, /ios:\s*\{[^}]*?path:\s*['"]([^'"]+)['"]/) ?? 'ios'),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
// The phase runs last, once the web build is copied into the app. The script finds Node itself.
|
|
62
|
+
function resolveBinaryCreatePhase(projectDirectoryPath, xcodeProjectFilePath) {
|
|
63
|
+
const projectRootPath = relative(dirname(dirname(xcodeProjectFilePath)), projectDirectoryPath)
|
|
64
|
+
.split(sep)
|
|
65
|
+
.join('/');
|
|
66
|
+
return {
|
|
67
|
+
anchorPhaseName: undefined,
|
|
68
|
+
fix: `Add a Run Script phase after the app target's last phase that runs ${BINARY_CREATE_SCRIPT_PATH}.`,
|
|
69
|
+
shellScript: buildPbxprojShellScript([
|
|
70
|
+
'set -e',
|
|
71
|
+
'',
|
|
72
|
+
'# hotcodepush: writes hotcodepush.json into the app and, in a store build, creates the binary',
|
|
73
|
+
`PROJECT_ROOT="$PROJECT_DIR${projectRootPath === '' ? '' : `/${projectRootPath}`}"`,
|
|
74
|
+
'export PROJECT_ROOT',
|
|
75
|
+
`/bin/sh "$PROJECT_ROOT/${BINARY_CREATE_SCRIPT_PATH}"`,
|
|
76
|
+
'',
|
|
77
|
+
]),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
async function resolveWiring({ directoryPath, packageJson }, options) {
|
|
81
|
+
const nativeProjects = await resolveNativeProjects(directoryPath, resolveNativeProjectPaths(directoryPath), options, 'Run "npx cap add ios" and "npx cap add android", or pass --ios-path and --android-path.');
|
|
82
|
+
const xcodeProjectFilePath = resolveXcodeProjectFilePath(nativeProjects.ios);
|
|
83
|
+
const isPackageInstalled = isSdkPackageDeclared(packageJson, CAPACITOR_PACKAGE_NAME);
|
|
84
|
+
return {
|
|
85
|
+
isPackageInstalled,
|
|
86
|
+
nativeFilePaths: [
|
|
87
|
+
...new Set([
|
|
88
|
+
...(xcodeProjectFilePath === undefined ||
|
|
89
|
+
hasBinaryCreatePhase(xcodeProjectFilePath)
|
|
90
|
+
? []
|
|
91
|
+
: [xcodeProjectFilePath]),
|
|
92
|
+
...resolvePendingEdits(nativeProjects, xcodeProjectFilePath)
|
|
93
|
+
.filter(edit => !edit.isRefused)
|
|
94
|
+
.map(({ filePath }) => filePath),
|
|
95
|
+
]),
|
|
96
|
+
].map(filePath => relative(directoryPath, filePath)),
|
|
97
|
+
packageFilePaths: isPackageInstalled ? [] : ['package.json'],
|
|
98
|
+
installPackage: () => installSdkPackage(directoryPath, CAPACITOR_PACKAGE_NAME, CAPACITOR_PACKAGE_SPEC),
|
|
99
|
+
wireBinaryCreateStep: editBlocker => wireBinaryCreateStep(directoryPath, nativeProjects, xcodeProjectFilePath, editBlocker, options),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function resolvePendingEdits(nativeProjectPaths, xcodeProjectFilePath) {
|
|
103
|
+
return [
|
|
104
|
+
xcodeProjectFilePath === undefined
|
|
105
|
+
? undefined
|
|
106
|
+
: resolveResourceReferenceEdit(xcodeProjectFilePath),
|
|
107
|
+
resolveGradleEdit(nativeProjectPaths.android, CAPACITOR_PACKAGE_NAME),
|
|
108
|
+
]
|
|
109
|
+
.filter(edit => edit !== undefined)
|
|
110
|
+
.filter(edit => !edit.isApplied());
|
|
111
|
+
}
|
|
112
|
+
function resolveResourceReferenceEdit(xcodeProjectFilePath) {
|
|
113
|
+
return {
|
|
114
|
+
description: 'the Xcode project without its hotcodepush.json reference',
|
|
115
|
+
filePath: xcodeProjectFilePath,
|
|
116
|
+
isApplied: () => !hasReadableResourceReference(xcodeProjectFilePath),
|
|
117
|
+
apply: () => {
|
|
118
|
+
removeResourceReference(xcodeProjectFilePath);
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
async function wireBinaryCreateStep(projectDirectoryPath, nativeProjects, xcodeProjectFilePath, editBlocker, options) {
|
|
123
|
+
if (nativeProjects.missingError !== undefined) {
|
|
124
|
+
throw nativeProjects.missingError;
|
|
125
|
+
}
|
|
126
|
+
const isPhaseWired = xcodeProjectFilePath === undefined ||
|
|
127
|
+
hasBinaryCreatePhase(xcodeProjectFilePath);
|
|
128
|
+
const pendingEdits = resolvePendingEdits(nativeProjects, xcodeProjectFilePath);
|
|
129
|
+
if (isPhaseWired && pendingEdits.length === 0) {
|
|
130
|
+
return {
|
|
131
|
+
message: 'The Xcode phase and the Gradle task are already wired.',
|
|
132
|
+
status: 'skipped',
|
|
133
|
+
value: undefined,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (editBlocker !== undefined) {
|
|
137
|
+
throw editBlocker;
|
|
138
|
+
}
|
|
139
|
+
const wired = await applyNativeProjectEdits(xcodeProjectFilePath === undefined
|
|
140
|
+
? undefined
|
|
141
|
+
: {
|
|
142
|
+
phase: resolveBinaryCreatePhase(projectDirectoryPath, xcodeProjectFilePath),
|
|
143
|
+
projectFilePath: xcodeProjectFilePath,
|
|
144
|
+
}, pendingEdits, options);
|
|
145
|
+
return {
|
|
146
|
+
message: `Wired ${wired.join(', ')}.`,
|
|
147
|
+
status: 'done',
|
|
148
|
+
value: undefined,
|
|
149
|
+
};
|
|
150
|
+
}
|