@cedarjs/cli 7.1.0-rc.34 → 7.1.1-next.468

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/dist/commands/build/buildHandler.js +2 -1
  2. package/dist/commands/build.js +1 -1
  3. package/dist/commands/check.js +2 -1
  4. package/dist/commands/deploy/baremetal/baremetalHandler.js +364 -73
  5. package/dist/commands/deploy/baremetal/monitors.js +66 -0
  6. package/dist/commands/deploy/baremetal.js +7 -1
  7. package/dist/commands/deploy/flightcontrolHandler.js +1 -1
  8. package/dist/commands/deploy/helpers/deployHandler.js +1 -1
  9. package/dist/commands/deploy/netlify.js +1 -1
  10. package/dist/commands/deploy/renderHandler.js +1 -1
  11. package/dist/commands/deploy/serverlessHandler.js +2 -1
  12. package/dist/commands/deploy/vercel.js +1 -1
  13. package/dist/commands/destroy/handlerHelpers.js +1 -1
  14. package/dist/commands/destroy/page/pageHandler.js +2 -1
  15. package/dist/commands/destroy/scaffold/scaffoldHandler.js +65 -26
  16. package/dist/commands/destroy/sdl/sdlHandler.js +2 -1
  17. package/dist/commands/dev/devHandler.js +3 -4
  18. package/dist/commands/dev/packageWatchCommands.js +8 -3
  19. package/dist/commands/dev.js +1 -1
  20. package/dist/commands/execHandler.js +2 -1
  21. package/dist/commands/experimental/live-queries/liveQueries.js +1 -1
  22. package/dist/commands/experimental/live-queries/liveQueriesHandler.js +3 -3
  23. package/dist/commands/experimental/setupInngest.js +1 -1
  24. package/dist/commands/experimental/setupInngestHandler.js +1 -1
  25. package/dist/commands/experimental/setupOpentelemetry.js +1 -1
  26. package/dist/commands/experimental/setupOpentelemetryHandler.js +2 -1
  27. package/dist/commands/experimental/setupReactCompiler.js +1 -1
  28. package/dist/commands/experimental/setupReactCompilerHandler.js +4 -4
  29. package/dist/commands/experimental/setupRsc.js +1 -1
  30. package/dist/commands/experimental/setupRscHandler.js +2 -1
  31. package/dist/commands/experimental/setupStreamingSsr.js +1 -1
  32. package/dist/commands/experimental/setupStreamingSsrHandler.js +3 -2
  33. package/dist/commands/generate/cell/cell.js +20 -0
  34. package/dist/commands/generate/cell/cellHandler.js +68 -20
  35. package/dist/commands/generate/cell/templates/cell.tsx.template +21 -2
  36. package/dist/commands/generate/cell/templates/cellFragment.tsx.template +43 -0
  37. package/dist/commands/generate/cell/templates/cellList.tsx.template +21 -2
  38. package/dist/commands/generate/cell/templates/storiesFragment.tsx.template +23 -0
  39. package/dist/commands/generate/cell/templates/test.js.template +1 -1
  40. package/dist/commands/generate/cell/templates/testFragment.js.template +30 -0
  41. package/dist/commands/generate/dataMigration/dataMigration.js +2 -1
  42. package/dist/commands/generate/dbAuth/dbAuth.js +6 -2
  43. package/dist/commands/generate/dbAuth/dbAuthHandler.js +80 -17
  44. package/dist/commands/generate/dbAuth/templates/changePassword.tsx.template +131 -0
  45. package/dist/commands/generate/dbAuth/templates/login.oauth.tsx.template +164 -0
  46. package/dist/commands/generate/dbAuth/templates/login.webAuthn.tsx.template +20 -12
  47. package/dist/commands/generate/dbAuth/templates/signup.oauth.tsx.template +147 -0
  48. package/dist/commands/generate/directive/directiveHandler.js +2 -1
  49. package/dist/commands/generate/function/functionHandler.js +2 -1
  50. package/dist/commands/generate/helpers.js +26 -0
  51. package/dist/commands/generate/job/jobHandler.js +2 -1
  52. package/dist/commands/generate/model/modelHandler.js +2 -1
  53. package/dist/commands/generate/ogImage/ogImageHandler.js +2 -1
  54. package/dist/commands/generate/ogImage/templates/ogImage.og.tsx.template +1 -1
  55. package/dist/commands/generate/package/filesTask.js +9 -12
  56. package/dist/commands/generate/package/packageHandler.js +3 -4
  57. package/dist/commands/generate/package/templates/tsconfig.json.template +1 -0
  58. package/dist/commands/generate/page/pageHandler.js +2 -1
  59. package/dist/commands/generate/realtime/realtime.js +1 -1
  60. package/dist/commands/generate/realtime/realtimeHandler.js +2 -9
  61. package/dist/commands/generate/realtime/templates/liveQueries/blank/blank.service.ts.template +12 -4
  62. package/dist/commands/generate/realtime/templates/subscriptions/blank/blank.service.ts.template +2 -2
  63. package/dist/commands/generate/realtime/templates/subscriptions/blank/blank.ts.template +4 -4
  64. package/dist/commands/generate/scaffold/scaffold.js +1 -1
  65. package/dist/commands/generate/scaffold/scaffoldHandler.js +93 -49
  66. package/dist/commands/generate/scaffold/templates/assets/scaffold.tailwind.css.template +12 -7
  67. package/dist/commands/generate/scaffold/templates/components/EditNameCell.tsx.template +5 -7
  68. package/dist/commands/generate/scaffold/templates/components/NameForm.tsx.template +10 -9
  69. package/dist/commands/generate/scaffold/templates/layouts/NamesScaffoldLayout.tsx.template +18 -0
  70. package/dist/commands/generate/scaffold/templates/lib/formatters.test.tsx.template +11 -0
  71. package/dist/commands/generate/scaffold/templates/lib/formatters.tsx.template +16 -4
  72. package/dist/commands/generate/script/scriptHandler.js +2 -1
  73. package/dist/commands/generate/script/templates/tsconfig.json.template +1 -0
  74. package/dist/commands/generate/sdl/sdlHandler.js +10 -4
  75. package/dist/commands/generate/secret/secret.js +1 -1
  76. package/dist/commands/generate/service/serviceHandler.js +79 -49
  77. package/dist/commands/generate/service/templates/service.ts.template +40 -13
  78. package/dist/commands/generate/service/templates/test.ts.template +8 -6
  79. package/dist/commands/generate/yargsCommandHelpers.js +1 -1
  80. package/dist/commands/generate/yargsHandlerHelpers.js +2 -1
  81. package/dist/commands/generate.js +1 -1
  82. package/dist/commands/info.js +1 -1
  83. package/dist/commands/jobsHandler.js +5 -3
  84. package/dist/commands/lint.js +1 -1
  85. package/dist/commands/prerenderHandler.js +11 -4
  86. package/dist/commands/prismaHandler.js +7 -2
  87. package/dist/commands/record/init.js +1 -1
  88. package/dist/commands/serve.js +2 -1
  89. package/dist/commands/setup/auth/auth.js +2 -4
  90. package/dist/commands/setup/cache/cache.js +1 -1
  91. package/dist/commands/setup/cache/cacheHandler.js +2 -1
  92. package/dist/commands/setup/cache/templates/memcached.ts.template +14 -17
  93. package/dist/commands/setup/cache/templates/redis.ts.template +12 -20
  94. package/dist/commands/setup/database/postgres.js +1 -1
  95. package/dist/commands/setup/database/postgresHandler.js +3 -1
  96. package/dist/commands/setup/deploy/providers/baremetal.js +7 -0
  97. package/dist/commands/setup/deploy/providers/baremetalHandler.js +130 -21
  98. package/dist/commands/setup/deploy/providers/coherence.js +1 -1
  99. package/dist/commands/setup/deploy/providers/coherenceHandler.js +7 -6
  100. package/dist/commands/setup/deploy/providers/flightcontrolHandler.js +2 -1
  101. package/dist/commands/setup/deploy/providers/netlifyHandler.js +2 -1
  102. package/dist/commands/setup/deploy/providers/renderHandler.js +2 -1
  103. package/dist/commands/setup/deploy/providers/serverlessHandler.js +2 -1
  104. package/dist/commands/setup/deploy/providers/universalDeployHandler.js +4 -6
  105. package/dist/commands/setup/deploy/providers/vercelHandler.js +2 -1
  106. package/dist/commands/setup/deploy/templates/baremetal.js +119 -9
  107. package/dist/commands/setup/deploy/templates/netlifyUD.js +3 -0
  108. package/dist/commands/setup/docker/docker.js +1 -1
  109. package/dist/commands/setup/docker/dockerHandler.js +2 -1
  110. package/dist/commands/setup/generator/generator.js +1 -1
  111. package/dist/commands/setup/generator/generatorHandler.js +1 -1
  112. package/dist/commands/setup/graphql/features/fragments/fragments.js +1 -1
  113. package/dist/commands/setup/graphql/features/fragments/fragmentsHandler.js +3 -5
  114. package/dist/commands/setup/graphql/features/trustedDocuments/trustedDocuments.js +1 -1
  115. package/dist/commands/setup/graphql/features/trustedDocuments/trustedDocumentsHandler.js +2 -1
  116. package/dist/commands/setup/i18n/i18n.js +1 -1
  117. package/dist/commands/setup/i18n/i18nHandler.js +1 -1
  118. package/dist/commands/setup/jobs/jobs.js +1 -1
  119. package/dist/commands/setup/jobs/jobsHandler.js +4 -1
  120. package/dist/commands/setup/mailer/mailer.js +1 -1
  121. package/dist/commands/setup/mailer/mailerHandler.js +2 -1
  122. package/dist/commands/setup/middleware/ogImage/ogImage.js +1 -1
  123. package/dist/commands/setup/middleware/ogImage/ogImageHandler.js +2 -1
  124. package/dist/commands/setup/monitoring/sentry/sentry.js +1 -1
  125. package/dist/commands/setup/monitoring/sentry/sentryHandler.js +13 -9
  126. package/dist/commands/setup/monitoring/sentry/templates/sentryApi.ts.template +1 -4
  127. package/dist/commands/setup/monitoring/sentry/templates/sentryWeb.ts.template +7 -4
  128. package/dist/commands/setup/neon/neon.js +1 -1
  129. package/dist/commands/setup/neon/neonHandler.js +5 -9
  130. package/dist/commands/setup/package/package.js +1 -1
  131. package/dist/commands/setup/package/packageHandler.js +4 -4
  132. package/dist/commands/setup/realtime/addRealtimeToGraphql.js +1 -1
  133. package/dist/commands/setup/realtime/realtime.js +1 -1
  134. package/dist/commands/setup/realtime/realtimeHandler.js +4 -16
  135. package/dist/commands/setup/realtime/templates/defer/fastAndSlowFields/fastAndSlowFields.ts.template +4 -1
  136. package/dist/commands/setup/realtime/templates/liveQueries/auctions/auctions.ts.template +12 -4
  137. package/dist/commands/setup/realtime/templates/subscriptions/countdown/countdown.ts.template +1 -1
  138. package/dist/commands/setup/realtime/templates/subscriptions/newMessage/newMessage.ts.template +2 -2
  139. package/dist/commands/setup/server-file/serverFile.js +1 -1
  140. package/dist/commands/setup/server-file/serverFileHandler.js +2 -1
  141. package/dist/commands/setup/tenancy/authCodemod.js +66 -0
  142. package/dist/commands/setup/tenancy/dbCodemod.js +64 -0
  143. package/dist/commands/setup/tenancy/graphqlCodemod.js +88 -0
  144. package/dist/commands/setup/tenancy/preflight.js +49 -0
  145. package/dist/commands/setup/tenancy/schemaPrisma.js +81 -0
  146. package/dist/commands/setup/tenancy/signupCodemod.js +154 -0
  147. package/dist/commands/setup/tenancy/templates/InvitePage.tsx.template +72 -0
  148. package/dist/commands/setup/tenancy/templates/OrgScope.tsx.template +40 -0
  149. package/dist/commands/setup/tenancy/templates/ensureDefaultOrganizations.dataMigration.ts.template +39 -0
  150. package/dist/commands/setup/tenancy/templates/organizations.scenarios.ts.template +57 -0
  151. package/dist/commands/setup/tenancy/templates/organizations.sdl.ts.template +89 -0
  152. package/dist/commands/setup/tenancy/templates/organizations.test.ts.template +381 -0
  153. package/dist/commands/setup/tenancy/templates/organizations.ts.template +394 -0
  154. package/dist/commands/setup/tenancy/templates/requireMembership.test.ts.template +35 -0
  155. package/dist/commands/setup/tenancy/templates/requireMembership.ts.template +25 -0
  156. package/dist/commands/setup/tenancy/tenancy.js +30 -0
  157. package/dist/commands/setup/tenancy/tenancyHandler.js +425 -0
  158. package/dist/commands/setup/ui/libraries/chakra-uiHandler.js +2 -1
  159. package/dist/commands/setup/ui/libraries/mantineHandler.js +2 -1
  160. package/dist/commands/setup/ui/libraries/tailwindcssHandler.js +63 -82
  161. package/dist/commands/setup/uploads/gitignore.js +17 -0
  162. package/dist/commands/setup/uploads/schemaPrisma.js +41 -0
  163. package/dist/commands/setup/uploads/serverFile.js +217 -0
  164. package/dist/commands/setup/uploads/templates/requireUploadToken.ts.template +15 -0
  165. package/dist/commands/setup/uploads/templates/srcLibUploads.ts.template +23 -24
  166. package/dist/commands/setup/uploads/templates/uploads.sdl.ts.template +79 -0
  167. package/dist/commands/setup/uploads/templates/uploads.ts.template +98 -0
  168. package/dist/commands/setup/uploads/templates/withDataUri.ts.template +14 -0
  169. package/dist/commands/setup/uploads/templates/withSignedUrl.ts.template +16 -0
  170. package/dist/commands/setup/uploads/uploads.js +49 -7
  171. package/dist/commands/setup/uploads/uploadsHandler.js +277 -69
  172. package/dist/commands/setup.js +2 -1
  173. package/dist/commands/studio.js +1 -1
  174. package/dist/commands/test/testHandler.js +25 -71
  175. package/dist/commands/test/viteVersionCheck.js +75 -0
  176. package/dist/commands/test.js +10 -18
  177. package/dist/commands/type-checkHandler.js +1 -1
  178. package/dist/commands/upgrade/preUpgradeScripts.js +3 -3
  179. package/dist/commands/upgrade/upgrade.js +1 -1
  180. package/dist/commands/upgrade/upgradeHandler.js +2 -1
  181. package/dist/index.js +5 -8
  182. package/dist/lib/exit.js +1 -1
  183. package/dist/lib/index.js +16 -31
  184. package/dist/lib/loadEnvFiles.js +1 -1
  185. package/dist/lib/plugin.js +1 -1
  186. package/dist/lib/test.js +1 -1
  187. package/dist/lib/updateCheck.js +4 -4
  188. package/dist/middleware/checkNodeVersion.js +4 -4
  189. package/package.json +27 -27
  190. package/dist/commands/setup/ui/templates/postcss.config.cjs.template +0 -9
  191. package/dist/commands/setup/uploads/dbCodemod.js +0 -28
  192. package/dist/commands/setup/uploads/templates/signedUrl.ts.template +0 -21
  193. package/dist/commands/test/testHandlerEsm.js +0 -109
  194. package/dist/commands/testEsm.js +0 -41
@@ -4,12 +4,13 @@ import path from "node:path";
4
4
  import execa from "execa";
5
5
  import { Listr } from "listr2";
6
6
  import { terminalLink } from "termi-link";
7
- import { recordTelemetryAttributes, colors as c } from "@cedarjs/cli-helpers";
7
+ import { colors as c } from "@cedarjs/cli-helpers/colors";
8
8
  import {
9
9
  formatCedarCommand,
10
10
  formatRunWorkspaceScriptCommand
11
11
  } from "@cedarjs/cli-helpers/packageManager/display";
12
12
  import { runBin } from "@cedarjs/cli-helpers/packageManager/exec";
13
+ import { recordTelemetryAttributes } from "@cedarjs/cli-helpers/telemetry";
13
14
  import {
14
15
  buildApi,
15
16
  buildApiWithVite,
@@ -1,5 +1,5 @@
1
1
  import { terminalLink } from "termi-link";
2
- import { colors as c } from "@cedarjs/cli-helpers";
2
+ import { colors as c } from "@cedarjs/cli-helpers/colors";
3
3
  import { exitWithError } from "../lib/exit.js";
4
4
  import { workspaces } from "../lib/project.js";
5
5
  import { checkNodeVersion } from "../middleware/checkNodeVersion.js";
@@ -1,4 +1,5 @@
1
- import { recordTelemetryAttributes, colors as c } from "@cedarjs/cli-helpers";
1
+ import { colors as c } from "@cedarjs/cli-helpers/colors";
2
+ import { recordTelemetryAttributes } from "@cedarjs/cli-helpers/telemetry";
2
3
  const command = "check";
3
4
  const aliases = ["diagnostics"];
4
5
  const description = "Get structural diagnostics for a Redwood project (experimental)";
@@ -6,26 +6,37 @@ import { Listr } from "listr2";
6
6
  import * as toml from "smol-toml";
7
7
  import { env as envInterpolation } from "string-env-interpolation";
8
8
  import { titleCase } from "title-case";
9
- import { colors as c } from "@cedarjs/cli-helpers";
9
+ import { colors as c } from "@cedarjs/cli-helpers/colors";
10
10
  import { formatCedarCommand } from "@cedarjs/cli-helpers/packageManager/display";
11
11
  import { getPackageManager } from "@cedarjs/project-config/packageManager";
12
- import { getPaths } from "../../../lib/index.js";
12
+ import { getConfig, getPaths } from "../../../lib/index.js";
13
+ import { MONITOR_ADAPTERS, MONITORS, isMonitor } from "./monitors.js";
13
14
  const CONFIG_FILENAME = "deploy.toml";
15
+ const DOCS_URL = "https://cedarjs.com/docs/deploy/baremetal";
14
16
  const SYMLINK_FLAGS = "-nsf";
15
17
  const CURRENT_RELEASE_SYMLINK_NAME = "current";
16
18
  const LIFECYCLE_HOOKS = ["before", "after"];
19
+ const DEFAULT_HEALTH_CHECK_PATH = "/graphql/health";
20
+ const HEALTH_CHECK_INTERVAL_MS = 2e3;
21
+ const HEALTH_CHECK_REQUEST_TIMEOUT_SECONDS = 5;
22
+ const WEB_DIST_DIRS = ["web/dist", "web/dist/browser"];
23
+ const BUILD_MANIFEST_FILENAME = "client-build-manifest.json";
24
+ const LINK_BATCH_SIZE = 100;
17
25
  const RELEASE_DIR_PATTERN = "^[0-9]{14}$";
18
26
  const releaseDirRegExp = new RegExp(RELEASE_DIR_PATTERN);
27
+ const DEFAULT_MONITOR = "pm2";
19
28
  const DEFAULT_SERVER_CONFIG = {
20
29
  port: 22,
21
30
  branch: "main",
22
31
  packageManagerCommand: getPackageManager(),
23
- monitorCommand: "pm2",
32
+ monitor: DEFAULT_MONITOR,
24
33
  sides: ["api", "web"],
25
34
  keepReleases: 5,
26
- freeSpaceRequired: 2048
35
+ freeSpaceRequired: 2048,
36
+ healthCheckTimeout: 30
27
37
  };
28
38
  const pathJoin = path.posix.join;
39
+ const shellQuote = (value) => `'${value.replaceAll("'", `'\\''`)}'`;
29
40
  const throwMissingConfig = (name) => {
30
41
  throw new Error(
31
42
  `"${name}" config option not set. See https://cedarjs.com/docs/deployment/baremetal#deploytoml`
@@ -42,6 +53,17 @@ const verifyConfig = (config, yargs) => {
42
53
  }
43
54
  return true;
44
55
  };
56
+ const isHttpUrl = (value) => {
57
+ if (typeof value !== "string") {
58
+ return false;
59
+ }
60
+ try {
61
+ const { protocol } = new URL(value);
62
+ return protocol === "http:" || protocol === "https:";
63
+ } catch {
64
+ return false;
65
+ }
66
+ };
45
67
  const verifyServerConfig = (config) => {
46
68
  if (!config.host) {
47
69
  throwMissingConfig("host");
@@ -55,6 +77,17 @@ const verifyServerConfig = (config) => {
55
77
  if (!/^\d+$/.test(String(config.freeSpaceRequired))) {
56
78
  throw new Error('"freeSpaceRequired" must be an integer >= 0');
57
79
  }
80
+ if (config.healthCheckUrl !== void 0 && config.healthCheckUrl !== false && !isHttpUrl(config.healthCheckUrl)) {
81
+ throw new Error('"healthCheckUrl" must be an http(s) URL or `false`');
82
+ }
83
+ if (!/^\d+$/.test(String(config.healthCheckTimeout))) {
84
+ throw new Error('"healthCheckTimeout" must be an integer >= 0');
85
+ }
86
+ if (!isMonitor(config.monitor)) {
87
+ throw new Error(
88
+ `"monitor" must be one of ${MONITORS.map((monitor) => `"${monitor}"`).join(", ")}`
89
+ );
90
+ }
58
91
  return true;
59
92
  };
60
93
  const symlinkCurrentCommand = async (dir, ssh, deployPath) => {
@@ -64,17 +97,110 @@ const symlinkCurrentCommand = async (dir, ssh, deployPath) => {
64
97
  CURRENT_RELEASE_SYMLINK_NAME
65
98
  ]);
66
99
  };
67
- const restartProcessCommand = async (processName, ssh, serverConfig, deployPath) => {
68
- return await ssh.exec(deployPath, serverConfig.monitorCommand, [
69
- "restart",
70
- processName
71
- ]);
100
+ const monitorAdapter = (serverConfig) => MONITOR_ADAPTERS[serverConfig.monitor];
101
+ const runMonitorCommand = async (command, ssh, serverConfig) => {
102
+ return await ssh.exec(
103
+ serverConfig.path,
104
+ serverConfig.monitorCommand,
105
+ command.args
106
+ );
107
+ };
108
+ const monitorTask = (command, ssh, serverConfig, skip) => {
109
+ return {
110
+ title: command.title,
111
+ task: async () => {
112
+ await runMonitorCommand(command, ssh, serverConfig);
113
+ },
114
+ ...skip ? { skip } : {}
115
+ };
116
+ };
117
+ const healthCheckUrl = (serverConfig) => {
118
+ if (serverConfig.healthCheckUrl === false) {
119
+ return void 0;
120
+ }
121
+ if (serverConfig.healthCheckUrl) {
122
+ return serverConfig.healthCheckUrl;
123
+ }
124
+ if (!serverConfig.sides.includes("api")) {
125
+ return void 0;
126
+ }
127
+ return `http://localhost:${getConfig().api.port}${DEFAULT_HEALTH_CHECK_PATH}`;
128
+ };
129
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
130
+ const waitForHealthCheck = async ({
131
+ url,
132
+ timeoutSeconds,
133
+ ssh,
134
+ serverConfig,
135
+ nextSteps,
136
+ onFailedAttempt
137
+ }) => {
138
+ const deadline = Date.now() + timeoutSeconds * 1e3;
139
+ let lastFailure = "";
140
+ let keepTrying = true;
141
+ while (keepTrying) {
142
+ try {
143
+ await ssh.exec(serverConfig.path, "curl", [
144
+ "--fail",
145
+ "--location",
146
+ "--silent",
147
+ "--show-error",
148
+ "--output",
149
+ "/dev/null",
150
+ "--max-time",
151
+ String(HEALTH_CHECK_REQUEST_TIMEOUT_SECONDS),
152
+ shellQuote(url)
153
+ ]);
154
+ return;
155
+ } catch (e) {
156
+ lastFailure = e instanceof Error ? e.message : String(e);
157
+ onFailedAttempt?.(lastFailure);
158
+ }
159
+ keepTrying = Date.now() + HEALTH_CHECK_INTERVAL_MS <= deadline;
160
+ if (keepTrying) {
161
+ await sleep(HEALTH_CHECK_INTERVAL_MS);
162
+ }
163
+ }
164
+ throw new Error(
165
+ [
166
+ `Health check failed: ${url} did not respond successfully within ${timeoutSeconds} seconds of restarting the ${serverConfig.processNames?.join(", ")} process(es).`,
167
+ `Last attempt: ${lastFailure}`,
168
+ "",
169
+ ...nextSteps,
170
+ `If your app serves its health check somewhere else, set \`healthCheckUrl\` in deploy.toml. Set it to \`false\` to skip the check. See ${DOCS_URL}#health-check`
171
+ ].join("\n")
172
+ );
173
+ };
174
+ const healthCheckTask = (url, ssh, serverConfig, nextSteps) => {
175
+ return {
176
+ title: url ? `Checking ${url}...` : "Checking health...",
177
+ task: async (_ctx, task) => {
178
+ if (!url) {
179
+ return;
180
+ }
181
+ await waitForHealthCheck({
182
+ url,
183
+ timeoutSeconds: parseInt(String(serverConfig.healthCheckTimeout), 10),
184
+ ssh,
185
+ serverConfig,
186
+ nextSteps,
187
+ onFailedAttempt: (reason) => {
188
+ task.output = reason;
189
+ }
190
+ });
191
+ },
192
+ skip: () => !url
193
+ };
72
194
  };
73
195
  const serverConfigWithDefaults = (serverConfig, yargs) => {
196
+ const monitor = serverConfig.monitor ?? DEFAULT_SERVER_CONFIG.monitor;
197
+ const monitorCommand = serverConfig.monitorCommand ?? (isMonitor(monitor) ? MONITOR_ADAPTERS[monitor].defaultCommand : "");
74
198
  return {
75
199
  ...DEFAULT_SERVER_CONFIG,
76
200
  ...serverConfig,
77
- branch: yargs.branch || serverConfig.branch || DEFAULT_SERVER_CONFIG.branch
201
+ branch: yargs.branch || serverConfig.branch || DEFAULT_SERVER_CONFIG.branch,
202
+ monitor,
203
+ monitorCommand
78
204
  };
79
205
  };
80
206
  const maintenanceTasks = (status, ssh, serverConfig) => {
@@ -96,26 +222,24 @@ const maintenanceTasks = (status, ssh, serverConfig) => {
96
222
  }
97
223
  });
98
224
  if (serverConfig.processNames) {
99
- tasks.push({
100
- title: `Stopping ${serverConfig.processNames.join(", ")} processes...`,
101
- task: async () => {
102
- await ssh.exec(serverConfig.path, serverConfig.monitorCommand, [
103
- "stop",
104
- serverConfig.processNames.join(" ")
105
- ]);
106
- }
107
- });
225
+ tasks.push(
226
+ monitorTask(
227
+ monitorAdapter(serverConfig).stop(serverConfig.processNames),
228
+ ssh,
229
+ serverConfig
230
+ )
231
+ );
108
232
  }
109
233
  } else if (status === "down") {
110
- tasks.push({
111
- title: `Starting ${serverConfig.processNames?.join(", ")} processes...`,
112
- task: async () => {
113
- await ssh.exec(serverConfig.path, serverConfig.monitorCommand, [
114
- "start",
115
- serverConfig.processNames.join(" ")
116
- ]);
117
- }
118
- });
234
+ if (serverConfig.processNames) {
235
+ tasks.push(
236
+ monitorTask(
237
+ monitorAdapter(serverConfig).start(serverConfig.processNames),
238
+ ssh,
239
+ serverConfig
240
+ )
241
+ );
242
+ }
119
243
  if (serverConfig.processNames) {
120
244
  tasks.push({
121
245
  title: `Disabling maintenance page...`,
@@ -167,19 +291,15 @@ const rollbackTasks = (count, ssh, serverConfig) => {
167
291
  }
168
292
  ];
169
293
  if (serverConfig.processNames) {
294
+ const adapter = monitorAdapter(serverConfig);
170
295
  for (const processName of serverConfig.processNames) {
171
- tasks.push({
172
- title: `Restarting ${processName} process...`,
173
- task: async () => {
174
- await restartProcessCommand(
175
- processName,
176
- ssh,
177
- serverConfig,
178
- serverConfig.path
179
- );
180
- }
181
- });
296
+ tasks.push(monitorTask(adapter.restart(processName), ssh, serverConfig));
182
297
  }
298
+ tasks.push(
299
+ healthCheckTask(healthCheckUrl(serverConfig), ssh, serverConfig, [
300
+ "The release you rolled back to is live as `current`. Check the process logs on the server, or run the rollback again with `--rollback` to move one more release back."
301
+ ])
302
+ );
183
303
  }
184
304
  return tasks;
185
305
  };
@@ -210,6 +330,134 @@ const commandWithLifecycleEvents = ({
210
330
  tasks.push(lifecycleTask("after", name, skip, config));
211
331
  return tasks.flat().filter((t) => Boolean(t));
212
332
  };
333
+ const manifestAssetFiles = (manifestJson) => {
334
+ const manifest = JSON.parse(manifestJson);
335
+ const files = /* @__PURE__ */ new Set();
336
+ if (!manifest || typeof manifest !== "object") {
337
+ return [];
338
+ }
339
+ for (const entry of Object.values(manifest)) {
340
+ if (!entry || typeof entry !== "object") {
341
+ continue;
342
+ }
343
+ if ("file" in entry && typeof entry.file === "string") {
344
+ files.add(entry.file);
345
+ }
346
+ for (const key of ["css", "assets"]) {
347
+ if (!(key in entry) || !Array.isArray(entry[key])) {
348
+ continue;
349
+ }
350
+ for (const item of entry[key]) {
351
+ if (typeof item === "string") {
352
+ files.add(item);
353
+ }
354
+ }
355
+ }
356
+ }
357
+ return [...files];
358
+ };
359
+ const readBuildManifest = async (ssh, releasePath) => {
360
+ for (const distDir of WEB_DIST_DIRS) {
361
+ let stdout;
362
+ try {
363
+ ;
364
+ ({ stdout } = await ssh.exec(pathJoin(releasePath, distDir), "cat", [
365
+ BUILD_MANIFEST_FILENAME
366
+ ]));
367
+ } catch {
368
+ continue;
369
+ }
370
+ const jsonStart = stdout.indexOf("{");
371
+ const json = jsonStart === -1 ? stdout : stdout.slice(jsonStart);
372
+ try {
373
+ return { distDir, files: manifestAssetFiles(json) };
374
+ } catch (e) {
375
+ const message = e instanceof Error ? e.message : String(e);
376
+ throw new Error(
377
+ `Could not parse ${pathJoin(releasePath, distDir, BUILD_MANIFEST_FILENAME)}: ${message}`
378
+ );
379
+ }
380
+ }
381
+ return void 0;
382
+ };
383
+ const keepPreviousAssets = async (yargs, ssh, serverConfig) => {
384
+ const newReleasePath = pathJoin(serverConfig.path, yargs.releaseDir);
385
+ let newRelease;
386
+ try {
387
+ newRelease = await readBuildManifest(ssh, newReleasePath);
388
+ } catch (e) {
389
+ const reason = e instanceof Error ? e.message : String(e);
390
+ return { status: "skipped", reason };
391
+ }
392
+ if (!newRelease) {
393
+ return {
394
+ status: "skipped",
395
+ reason: `No ${BUILD_MANIFEST_FILENAME} in ` + WEB_DIST_DIRS.map((dir) => pathJoin(yargs.releaseDir, dir)).join(
396
+ " or "
397
+ )
398
+ };
399
+ }
400
+ const { stdout } = await ssh.exec(serverConfig.path, "ls");
401
+ const previousReleases = stdout.split("\n").map((line) => line.trim()).filter((dir) => releaseDirRegExp.test(dir) && dir !== yargs.releaseDir).sort().reverse().slice(0, Math.max(serverConfig.keepReleases - 1, 0));
402
+ const presentFiles = new Set(newRelease.files);
403
+ const warnings = [];
404
+ let linkedCount = 0;
405
+ for (const release of previousReleases) {
406
+ const releasePath = pathJoin(serverConfig.path, release);
407
+ let previousRelease;
408
+ try {
409
+ previousRelease = await readBuildManifest(ssh, releasePath);
410
+ } catch (e) {
411
+ const message = e instanceof Error ? e.message : String(e);
412
+ warnings.push(`Skipping the web assets of release ${release}: ${message}`);
413
+ continue;
414
+ }
415
+ if (!previousRelease) {
416
+ warnings.push(
417
+ `Release ${release} has no web build manifest, skipping its assets`
418
+ );
419
+ continue;
420
+ }
421
+ const missingFiles = previousRelease.files.filter(
422
+ (file) => !presentFiles.has(file)
423
+ );
424
+ if (missingFiles.length === 0) {
425
+ continue;
426
+ }
427
+ const filesByDir = /* @__PURE__ */ new Map();
428
+ for (const file of missingFiles) {
429
+ const dir = path.posix.dirname(file);
430
+ filesByDir.set(dir, [...filesByDir.get(dir) ?? [], file]);
431
+ }
432
+ try {
433
+ for (const [dir, files] of filesByDir) {
434
+ const targetDir = pathJoin(newReleasePath, newRelease.distDir, dir);
435
+ await ssh.exec(newReleasePath, "mkdir", ["-p", shellQuote(targetDir)]);
436
+ for (let i = 0; i < files.length; i += LINK_BATCH_SIZE) {
437
+ const sources = files.slice(i, i + LINK_BATCH_SIZE).map(
438
+ (file) => shellQuote(pathJoin(releasePath, previousRelease.distDir, file))
439
+ );
440
+ await ssh.exec(newReleasePath, "ln", [
441
+ "-f",
442
+ ...sources,
443
+ shellQuote(targetDir)
444
+ ]);
445
+ }
446
+ }
447
+ } catch (e) {
448
+ const message = e instanceof Error ? e.message : String(e);
449
+ warnings.push(
450
+ `Could not keep the web assets of release ${release}: ${message}`
451
+ );
452
+ continue;
453
+ }
454
+ for (const file of missingFiles) {
455
+ presentFiles.add(file);
456
+ }
457
+ linkedCount += missingFiles.length;
458
+ }
459
+ return { status: "done", linkedCount, warnings };
460
+ };
213
461
  const deployTasks = (yargs, ssh, serverConfig, serverLifecycle) => {
214
462
  const cmdPath = pathJoin(serverConfig.path, yargs.releaseDir);
215
463
  const config = {
@@ -358,6 +606,46 @@ const deployTasks = (yargs, ssh, serverConfig, serverLifecycle) => {
358
606
  })
359
607
  );
360
608
  }
609
+ tasks.push(
610
+ ...commandWithLifecycleEvents({
611
+ name: "keepAssets",
612
+ config,
613
+ skip: !yargs.keepAssets || !yargs.build || !serverConfig.sides.includes("web"),
614
+ command: {
615
+ title: `Keeping web assets of previous releases...`,
616
+ task: async (_ctx, task) => {
617
+ const result = await keepPreviousAssets(yargs, ssh, serverConfig);
618
+ if (result.status === "skipped") {
619
+ return task.skip(
620
+ c.warning(
621
+ `Warning: ${result.reason}, not keeping the web assets of previous releases`
622
+ )
623
+ );
624
+ }
625
+ task.output = [
626
+ `Linked ${result.linkedCount} file(s) from previous releases`,
627
+ ...result.warnings.map((warning) => c.warning(warning))
628
+ ].join("\n");
629
+ }
630
+ }
631
+ })
632
+ );
633
+ if (serverConfig.processNames && yargs.firstRun) {
634
+ const configFile = monitorAdapter(serverConfig).firstRunConfigFile;
635
+ if (configFile) {
636
+ tasks.push({
637
+ title: `Checking for ${configFile}...`,
638
+ task: () => {
639
+ if (!fs.existsSync(path.join(getPaths().base, configFile))) {
640
+ throw new Error(
641
+ `${configFile} is missing. The ${serverConfig.monitor} monitor reads it when starting processes for the first time. Run \`${formatCedarCommand(["setup", "deploy", "baremetal"])}\` to generate it, or deploy without \`--first-run\` if the processes are already running.`
642
+ );
643
+ }
644
+ },
645
+ skip: () => !yargs.restart
646
+ });
647
+ }
648
+ }
361
649
  tasks.push(
362
650
  ...commandWithLifecycleEvents({
363
651
  name: "symlinkCurrent",
@@ -373,56 +661,56 @@ const deployTasks = (yargs, ssh, serverConfig, serverLifecycle) => {
373
661
  })
374
662
  );
375
663
  if (serverConfig.processNames) {
664
+ const adapter = monitorAdapter(serverConfig);
665
+ const skipRestart = () => !yargs.restart;
666
+ if (yargs.firstRun) {
667
+ for (const command of adapter.firstRunSetup) {
668
+ tasks.push(monitorTask(command, ssh, serverConfig, skipRestart));
669
+ }
670
+ }
376
671
  for (const processName of serverConfig.processNames) {
377
672
  if (yargs.firstRun) {
673
+ const [firstCommand, ...followUpCommands] = adapter.firstRun(processName);
674
+ if (!firstCommand) {
675
+ continue;
676
+ }
378
677
  tasks.push(
379
678
  ...commandWithLifecycleEvents({
380
679
  name: "restart",
381
680
  config,
382
681
  skip: !yargs.restart,
383
- command: {
384
- title: `Starting ${processName} process for the first time...`,
385
- task: async () => {
386
- await ssh.exec(serverConfig.path, serverConfig.monitorCommand, [
387
- "start",
388
- pathJoin(CURRENT_RELEASE_SYMLINK_NAME, "ecosystem.config.js"),
389
- "--only",
390
- processName
391
- ]);
392
- }
393
- }
682
+ command: monitorTask(firstCommand, ssh, serverConfig)
394
683
  })
395
684
  );
396
- tasks.push({
397
- title: `Saving ${processName} state for future startup...`,
398
- task: async () => {
399
- await ssh.exec(serverConfig.path, serverConfig.monitorCommand, [
400
- "save"
401
- ]);
402
- },
403
- skip: () => !yargs.restart
404
- });
685
+ for (const command of followUpCommands) {
686
+ tasks.push(monitorTask(command, ssh, serverConfig, skipRestart));
687
+ }
405
688
  } else {
406
689
  tasks.push(
407
690
  ...commandWithLifecycleEvents({
408
691
  name: "restart",
409
692
  config,
410
693
  skip: !yargs.restart,
411
- command: {
412
- title: `Restarting ${processName} process...`,
413
- task: async () => {
414
- await restartProcessCommand(
415
- processName,
416
- ssh,
417
- serverConfig,
418
- serverConfig.path
419
- );
420
- }
421
- }
694
+ command: monitorTask(
695
+ adapter.restart(processName),
696
+ ssh,
697
+ serverConfig
698
+ )
422
699
  })
423
700
  );
424
701
  }
425
702
  }
703
+ const url = healthCheckUrl(serverConfig);
704
+ tasks.push(
705
+ ...commandWithLifecycleEvents({
706
+ name: "healthCheck",
707
+ config: { ...config, cmdPath: serverConfig.path },
708
+ skip: !yargs.restart || !url,
709
+ command: healthCheckTask(url, ssh, serverConfig, [
710
+ `The new release is live as \`current\`. Check the process logs on the server, or roll back to the previous release with \`${formatCedarCommand(["deploy", "baremetal", yargs.environment, "--rollback"])}\`.`
711
+ ])
712
+ })
713
+ );
426
714
  }
427
715
  tasks.push(
428
716
  ...commandWithLifecycleEvents({
@@ -559,9 +847,8 @@ const warnIfUnpushedCommits = async () => {
559
847
  };
560
848
  const handler = async (yargs) => {
561
849
  const { SshExecutor } = await import("./SshExecutor.js");
562
- const tomlPath = path.join(getPaths().base, "deploy.toml");
563
- const ecosystemPath = path.join(getPaths().base, "ecosystem.config.js");
564
- if (!fs.existsSync(tomlPath) || !fs.existsSync(ecosystemPath)) {
850
+ const tomlPath = path.join(getPaths().base, CONFIG_FILENAME);
851
+ if (!fs.existsSync(tomlPath)) {
565
852
  console.error(
566
853
  c.error("\nError: Baremetal deploy has not been properly setup.\n") + `Please run \`${formatCedarCommand(["setup", "deploy", "baremetal"])}\` before deploying`
567
854
  );
@@ -599,13 +886,17 @@ export {
599
886
  commands,
600
887
  deployTasks,
601
888
  handler,
889
+ healthCheckUrl,
890
+ keepPreviousAssets,
602
891
  lifecycleTask,
603
892
  maintenanceTasks,
893
+ manifestAssetFiles,
604
894
  parseConfig,
605
895
  rollbackTasks,
606
896
  serverConfigWithDefaults,
607
897
  throwMissingConfig,
608
898
  verifyConfig,
609
899
  verifyServerConfig,
900
+ waitForHealthCheck,
610
901
  warnIfUnpushedCommits
611
902
  };
@@ -0,0 +1,66 @@
1
+ const MONITORS = ["pm2", "systemd-user", "systemd-system"];
2
+ const pm2 = {
3
+ defaultCommand: "pm2",
4
+ firstRunConfigFile: "ecosystem.config.js",
5
+ firstRunSetup: [],
6
+ firstRun: (processName) => [
7
+ {
8
+ title: `Starting ${processName} process for the first time...`,
9
+ args: ["start", "current/ecosystem.config.js", "--only", processName]
10
+ },
11
+ {
12
+ title: `Saving ${processName} state for future startup...`,
13
+ args: ["save"]
14
+ }
15
+ ],
16
+ restart: (processName) => ({
17
+ title: `Restarting ${processName} process...`,
18
+ args: ["restart", processName]
19
+ }),
20
+ stop: (processNames) => ({
21
+ title: `Stopping ${processNames.join(", ")} processes...`,
22
+ args: ["stop", ...processNames]
23
+ }),
24
+ start: (processNames) => ({
25
+ title: `Starting ${processNames.join(", ")} processes...`,
26
+ args: ["start", ...processNames]
27
+ })
28
+ };
29
+ const systemd = (defaultCommand) => ({
30
+ defaultCommand,
31
+ firstRunSetup: [
32
+ {
33
+ title: "Reloading systemd unit files...",
34
+ args: ["daemon-reload"]
35
+ }
36
+ ],
37
+ firstRun: (processName) => [
38
+ {
39
+ title: `Enabling and starting ${processName} for the first time...`,
40
+ args: ["enable", "--now", processName]
41
+ }
42
+ ],
43
+ restart: (processName) => ({
44
+ title: `Restarting ${processName}...`,
45
+ args: ["restart", processName]
46
+ }),
47
+ stop: (processNames) => ({
48
+ title: `Stopping ${processNames.join(", ")}...`,
49
+ args: ["stop", ...processNames]
50
+ }),
51
+ start: (processNames) => ({
52
+ title: `Starting ${processNames.join(", ")}...`,
53
+ args: ["start", ...processNames]
54
+ })
55
+ });
56
+ const MONITOR_ADAPTERS = {
57
+ pm2,
58
+ "systemd-user": systemd("systemctl --user"),
59
+ "systemd-system": systemd("sudo systemctl")
60
+ };
61
+ const isMonitor = (value) => typeof value === "string" && MONITORS.includes(value);
62
+ export {
63
+ MONITORS,
64
+ MONITOR_ADAPTERS,
65
+ isMonitor
66
+ };
@@ -1,5 +1,5 @@
1
1
  import { terminalLink } from "termi-link";
2
- import { recordTelemetryAttributes } from "@cedarjs/cli-helpers";
2
+ import { recordTelemetryAttributes } from "@cedarjs/cli-helpers/telemetry";
3
3
  import { getPackageManager } from "@cedarjs/project-config/packageManager";
4
4
  const command = "baremetal [environment]";
5
5
  const description = "Deploy to baremetal server(s)";
@@ -48,6 +48,11 @@ const builder = (yargs) => {
48
48
  default: true,
49
49
  type: "boolean"
50
50
  });
51
+ yargs.option("keep-assets", {
52
+ describe: "Hardlink previous releases' hashed web assets into the new release so open browser tabs keep working after the deploy",
53
+ default: true,
54
+ type: "boolean"
55
+ });
51
56
  yargs.option("releaseDir", {
52
57
  describe: "Directory to create for the latest release, defaults to timestamp",
53
58
  default: (/* @__PURE__ */ new Date()).toISOString().replace(/[:\-TZ]/g, "").replace(/\.\d+$/, ""),
@@ -95,6 +100,7 @@ async function handler(yargs) {
95
100
  build: yargs.build,
96
101
  restart: yargs.restart,
97
102
  cleanup: yargs.cleanup,
103
+ keepAssets: yargs.keepAssets,
98
104
  maintenance: yargs.maintenance,
99
105
  rollback: yargs.rollback,
100
106
  verbose: yargs.verbose,