@cedarjs/api-server-watch 6.0.0-canary.2828

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/README.md ADDED
@@ -0,0 +1,20 @@
1
+ # @cedarjs/api-server-watch
2
+
3
+ The `cedar dev` file watcher for the api side: rebuilds and restarts the api
4
+ server when its source changes.
5
+
6
+ Split out of `@cedarjs/api-server` so that package can stay free of
7
+ `@cedarjs/internal` (the build/codegen toolchain) in production installs. This
8
+ package only ever runs under `cedar dev`, where that toolchain is already
9
+ present as part of the CLI, so it declares `@cedarjs/internal` as a regular
10
+ dependency.
11
+
12
+ ## Command
13
+
14
+ ```shell
15
+ cedar-api-server-watch
16
+ ```
17
+
18
+ Also installed as `cedarjs-api-server-watch`. Both names are what
19
+ `yarn cedar dev` shells out to; you shouldn't normally need to run this
20
+ directly.
package/dist/bin.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=bin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":""}
package/dist/bin.js ADDED
@@ -0,0 +1,379 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/watch.ts
4
+ import path3 from "node:path";
5
+ import ansis2 from "ansis";
6
+ import chokidar from "chokidar";
7
+ import { config } from "dotenv-defaults";
8
+ import {
9
+ buildApi,
10
+ cleanApiBuild,
11
+ rebuildApi
12
+ } from "@cedarjs/internal/dist/build/api";
13
+ import { loadAndValidateSdls } from "@cedarjs/internal/dist/validateSchema";
14
+ import { getPaths as getPaths3 } from "@cedarjs/project-config";
15
+
16
+ // src/utils.ts
17
+ function debounce(func, wait) {
18
+ let timeoutId = null;
19
+ const debounced = ((...args) => {
20
+ if (timeoutId !== null) {
21
+ clearTimeout(timeoutId);
22
+ }
23
+ timeoutId = setTimeout(() => {
24
+ timeoutId = null;
25
+ func(...args);
26
+ }, wait);
27
+ });
28
+ debounced.cancel = () => {
29
+ if (timeoutId !== null) {
30
+ clearTimeout(timeoutId);
31
+ timeoutId = null;
32
+ }
33
+ };
34
+ return debounced;
35
+ }
36
+
37
+ // src/buildManager.ts
38
+ var BuildManager = class {
39
+ shouldRebuild;
40
+ shouldClean;
41
+ debouncedBuild;
42
+ buildFn;
43
+ constructor(buildFn) {
44
+ this.shouldRebuild = true;
45
+ this.shouldClean = false;
46
+ this.buildFn = buildFn;
47
+ if (process.env.RWJS_DELAY_RESTART) {
48
+ console.warn(
49
+ "[DEPRECATED] RWJS_DELAY_RESTART is deprecated and will be removed in the next major release. Please rename it to CEDAR_DELAY_API_RESTART in your .env file."
50
+ );
51
+ }
52
+ const delay = process.env.CEDAR_DELAY_API_RESTART || process.env.RWJS_DELAY_RESTART;
53
+ this.debouncedBuild = debounce(
54
+ async (options) => {
55
+ try {
56
+ await this.buildFn({
57
+ ...options,
58
+ rebuild: this.shouldRebuild,
59
+ clean: this.shouldClean
60
+ });
61
+ } finally {
62
+ this.shouldRebuild = true;
63
+ this.shouldClean = false;
64
+ }
65
+ },
66
+ // We want to delay execution when multiple files are modified on the
67
+ // filesystem. This usually happens when running Cedar generator commands.
68
+ // Local writes are very fast, but writes in e2e environments are not, so
69
+ // allow the default to be adjusted with an env-var.
70
+ delay ? parseInt(delay, 10) : 500
71
+ );
72
+ }
73
+ // Wrapper method to handle options and set precedence flags.
74
+ // If we ever see a `rebuild: false` option while debouncing, we never want to rebuild.
75
+ // If we ever see a `clean: true` option, we always want to clean.
76
+ async run(options) {
77
+ if (options.rebuild === false) {
78
+ this.shouldRebuild = false;
79
+ }
80
+ if (options.clean) {
81
+ this.shouldClean = true;
82
+ }
83
+ await this.debouncedBuild(options);
84
+ }
85
+ cancelScheduledBuild() {
86
+ this.debouncedBuild.cancel();
87
+ }
88
+ };
89
+
90
+ // src/serverManager.ts
91
+ import { fork } from "child_process";
92
+ import fs from "node:fs";
93
+ import { createRequire } from "node:module";
94
+ import path from "path";
95
+ import ansis from "ansis";
96
+ import yargs from "yargs";
97
+ import { hideBin } from "yargs/helpers";
98
+ import { getConfig, getPaths, resolveFile } from "@cedarjs/project-config";
99
+ var require2 = createRequire(import.meta.url);
100
+ var argv = yargs(hideBin(process.argv)).option("debugPort", {
101
+ description: "Port on which to expose API server debugger",
102
+ type: "number",
103
+ alias: ["debug-port", "dp"]
104
+ }).option("port", {
105
+ description: "The port to listen at",
106
+ type: "number",
107
+ alias: "p"
108
+ }).parseSync();
109
+ var rwjsPaths = getPaths();
110
+ var ServerManager = class {
111
+ httpServerProcess = null;
112
+ async startApiServer() {
113
+ const forkOpts = {
114
+ execArgv: process.execArgv
115
+ };
116
+ if (getConfig().experimental.opentelemetry.enabled) {
117
+ const opentelemetrySDKScriptPath = path.join(
118
+ rwjsPaths.api.dist,
119
+ "opentelemetry.js"
120
+ );
121
+ const opentelemetrySDKScriptPathRelative = path.relative(
122
+ rwjsPaths.base,
123
+ opentelemetrySDKScriptPath
124
+ );
125
+ console.log(
126
+ `Setting up OpenTelemetry using the setup file: ${opentelemetrySDKScriptPathRelative}`
127
+ );
128
+ if (fs.existsSync(opentelemetrySDKScriptPath)) {
129
+ forkOpts.execArgv = forkOpts.execArgv.concat([
130
+ `--require=${opentelemetrySDKScriptPath}`
131
+ ]);
132
+ } else {
133
+ console.error(
134
+ `OpenTelemetry setup file does not exist at ${opentelemetrySDKScriptPathRelative}`
135
+ );
136
+ }
137
+ }
138
+ const debugPort = argv["debug-port"];
139
+ if (debugPort) {
140
+ forkOpts.execArgv = forkOpts.execArgv.concat([`--inspect=${debugPort}`]);
141
+ }
142
+ const port = argv.port ?? getConfig().api.port;
143
+ const serverFile = resolveFile(`${rwjsPaths.api.dist}/server`);
144
+ if (serverFile) {
145
+ this.httpServerProcess = fork(
146
+ serverFile,
147
+ ["--apiPort", port.toString()],
148
+ forkOpts
149
+ );
150
+ } else {
151
+ const apiServerPkgPath = require2.resolve("@cedarjs/api-server/package.json");
152
+ const apiServerDir = path.dirname(apiServerPkgPath);
153
+ const apiServerPkg = require2(apiServerPkgPath);
154
+ const binPath = path.join(
155
+ apiServerDir,
156
+ apiServerPkg.bin["cedarjs-server"]
157
+ );
158
+ const args = ["api", "--port", port.toString()];
159
+ this.httpServerProcess = fork(binPath, args, forkOpts);
160
+ }
161
+ }
162
+ async restartApiServer() {
163
+ await this.killApiServer();
164
+ await this.startApiServer();
165
+ }
166
+ async killApiServer() {
167
+ if (!this.httpServerProcess) {
168
+ return;
169
+ }
170
+ await new Promise((resolve) => {
171
+ console.log(ansis.yellow("Shutting down API server."));
172
+ const cleanup = () => {
173
+ this.httpServerProcess?.removeAllListeners("exit");
174
+ clearTimeout(forceKillTimeout);
175
+ };
176
+ this.httpServerProcess?.on("exit", () => {
177
+ console.log(ansis.yellow("API server exited."));
178
+ cleanup();
179
+ resolve();
180
+ });
181
+ const forceKillTimeout = setTimeout(() => {
182
+ console.log(
183
+ ansis.yellow(
184
+ "API server did not exit within 2 seconds, forcefully closing it."
185
+ )
186
+ );
187
+ cleanup();
188
+ this.httpServerProcess?.kill("SIGKILL");
189
+ resolve();
190
+ }, 2e3);
191
+ this.httpServerProcess?.kill();
192
+ });
193
+ }
194
+ };
195
+ var serverManager = new ServerManager();
196
+
197
+ // src/watchPaths.ts
198
+ import fs2 from "node:fs";
199
+ import path2 from "node:path";
200
+ import {
201
+ getDbDir,
202
+ getPaths as getPaths2,
203
+ importStatementPath
204
+ } from "@cedarjs/project-config";
205
+ async function workspacePackagesPaths() {
206
+ const cedarPaths2 = getPaths2();
207
+ const packagesDir = path2.join(cedarPaths2.packages);
208
+ const packages = [];
209
+ try {
210
+ const rootPackageJsonPath = path2.join(cedarPaths2.base, "package.json");
211
+ const rootPackageJson = JSON.parse(
212
+ fs2.readFileSync(rootPackageJsonPath, "utf8")
213
+ );
214
+ const hasPackageJsonWorkspaces = Array.isArray(rootPackageJson.workspaces) && rootPackageJson.workspaces.some((w) => w.startsWith("packages/"));
215
+ if (!hasPackageJsonWorkspaces || !fs2.existsSync(packagesDir)) {
216
+ return [];
217
+ }
218
+ const globPattern = path2.join(packagesDir, "*").replaceAll("\\", "/");
219
+ const packageDirs = await Array.fromAsync(fs2.promises.glob(globPattern));
220
+ const apiPackageJsonPath = path2.join(cedarPaths2.api.base, "package.json");
221
+ const apiPackageJson = JSON.parse(
222
+ fs2.readFileSync(apiPackageJsonPath, "utf8")
223
+ );
224
+ const deps = {
225
+ ...apiPackageJson.dependencies ?? {},
226
+ ...apiPackageJson.devDependencies ?? {},
227
+ ...apiPackageJson.peerDependencies ?? {}
228
+ };
229
+ const workspaceDepNames = /* @__PURE__ */ new Set();
230
+ for (const [name, version] of Object.entries(deps)) {
231
+ if (String(version).startsWith("workspace:")) {
232
+ workspaceDepNames.add(name);
233
+ }
234
+ }
235
+ for (const packageDir of packageDirs) {
236
+ const packageJsonPath = path2.join(packageDir, "package.json");
237
+ if (!fs2.existsSync(packageJsonPath)) {
238
+ continue;
239
+ }
240
+ const pkgJson = JSON.parse(fs2.readFileSync(packageJsonPath, "utf8"));
241
+ if (workspaceDepNames.has(pkgJson.name)) {
242
+ packages.push(path2.join(packageDir, "dist"));
243
+ }
244
+ }
245
+ } catch {
246
+ }
247
+ return packages;
248
+ }
249
+ async function apiIgnorePaths() {
250
+ const cedarPaths2 = getPaths2();
251
+ const dbDir = await getDbDir(cedarPaths2.api.prismaConfig);
252
+ if (dbDir === cedarPaths2.api.base) {
253
+ throw new Error(
254
+ "Database directory cannot be the same as the API directory"
255
+ );
256
+ }
257
+ const ignoredApiPaths = [
258
+ // TODO: Is this still true?
259
+ // use this, because using cedarPaths.api.dist seems to not ignore on first
260
+ // build
261
+ "api/dist",
262
+ cedarPaths2.api.types,
263
+ dbDir
264
+ ];
265
+ return ignoredApiPaths;
266
+ }
267
+ async function getIgnoreFunction() {
268
+ const cedarPaths2 = getPaths2();
269
+ const ignoredApiPaths = await apiIgnorePaths();
270
+ const ignoredExtensions = [
271
+ ".DS_Store",
272
+ ".db",
273
+ ".sqlite",
274
+ "-journal",
275
+ ".test.js",
276
+ ".test.ts",
277
+ ".scenarios.ts",
278
+ ".scenarios.js",
279
+ ".d.ts",
280
+ ".log"
281
+ ];
282
+ return (file) => {
283
+ if (file.includes("node_modules")) {
284
+ return true;
285
+ }
286
+ if (ignoredExtensions.some((ext) => file.endsWith(ext))) {
287
+ return true;
288
+ }
289
+ if (file.includes(importStatementPath(cedarPaths2.packages)) && file.includes("/src/")) {
290
+ return true;
291
+ }
292
+ if (ignoredApiPaths.some((ignoredPath) => file.includes(ignoredPath))) {
293
+ return true;
294
+ }
295
+ return false;
296
+ };
297
+ }
298
+ async function pathsToWatch() {
299
+ const cedarPaths2 = getPaths2();
300
+ const watchPaths = [cedarPaths2.api.src, ...await workspacePackagesPaths()];
301
+ return watchPaths.map((p) => importStatementPath(p));
302
+ }
303
+
304
+ // src/watch.ts
305
+ var cedarPaths = getPaths3();
306
+ if (!process.env.CEDAR_ENV_FILES_LOADED) {
307
+ config({
308
+ path: path3.join(cedarPaths.base, ".env"),
309
+ defaults: path3.join(cedarPaths.base, ".env.defaults"),
310
+ multiline: true
311
+ });
312
+ process.env.CEDAR_ENV_FILES_LOADED = "true";
313
+ }
314
+ async function buildAndServe(options) {
315
+ const buildTs = Date.now();
316
+ console.log(ansis2.dim.italic("Building..."));
317
+ if (options.clean) {
318
+ await cleanApiBuild();
319
+ }
320
+ if (options.rebuild) {
321
+ await rebuildApi();
322
+ } else {
323
+ await buildApi();
324
+ }
325
+ await serverManager.restartApiServer();
326
+ console.log(ansis2.dim.italic("Took " + (Date.now() - buildTs) + " ms"));
327
+ }
328
+ var buildManager = new BuildManager(buildAndServe);
329
+ async function validateSdls() {
330
+ try {
331
+ await loadAndValidateSdls();
332
+ return true;
333
+ } catch (e) {
334
+ serverManager.killApiServer();
335
+ console.error(
336
+ ansis2.redBright(`[GQL Server Error] - Schema validation failed`)
337
+ );
338
+ console.error(ansis2.red(e?.message));
339
+ console.error(ansis2.redBright("-".repeat(40)));
340
+ buildManager.cancelScheduledBuild();
341
+ return false;
342
+ }
343
+ }
344
+ async function startWatch() {
345
+ const patterns = await pathsToWatch();
346
+ const watcher = chokidar.watch(patterns, {
347
+ persistent: true,
348
+ ignoreInitial: true,
349
+ ignored: await getIgnoreFunction()
350
+ });
351
+ watcher.on("ready", async () => {
352
+ await buildManager.run({ clean: true, rebuild: false });
353
+ await validateSdls();
354
+ });
355
+ watcher.on("all", async (eventName, filePath) => {
356
+ if (eventName === "addDir" && filePath === cedarPaths.api.base) {
357
+ return;
358
+ }
359
+ if (eventName) {
360
+ if (filePath.includes(".sdl")) {
361
+ const isValid = await validateSdls();
362
+ if (!isValid) {
363
+ return;
364
+ }
365
+ }
366
+ }
367
+ const displayPath = path3.relative(cedarPaths.base, filePath);
368
+ console.log(ansis2.dim(`[${eventName}] ${displayPath}`));
369
+ buildManager.cancelScheduledBuild();
370
+ if (eventName === "add" || eventName === "unlink") {
371
+ await buildManager.run({ rebuild: false });
372
+ } else {
373
+ await buildManager.run({ rebuild: true });
374
+ }
375
+ });
376
+ }
377
+
378
+ // src/bin.ts
379
+ await startWatch();
@@ -0,0 +1,15 @@
1
+ export type BuildAndRestartOptions = {
2
+ rebuild?: boolean;
3
+ clean?: boolean;
4
+ };
5
+ declare class BuildManager {
6
+ private shouldRebuild;
7
+ private shouldClean;
8
+ private debouncedBuild;
9
+ private buildFn;
10
+ constructor(buildFn: (options: BuildAndRestartOptions) => Promise<void>);
11
+ run(options: BuildAndRestartOptions): Promise<void>;
12
+ cancelScheduledBuild(): void;
13
+ }
14
+ export { BuildManager };
15
+ //# sourceMappingURL=buildManager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"buildManager.d.ts","sourceRoot":"","sources":["../src/buildManager.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,sBAAsB,GAAG;IACnC,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB,CAAA;AAED,cAAM,YAAY;IAChB,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,cAAc,CAA6B;IACnD,OAAO,CAAC,OAAO,CAAoD;gBAEvD,OAAO,EAAE,CAAC,OAAO,EAAE,sBAAsB,KAAK,OAAO,CAAC,IAAI,CAAC;IAsCjE,GAAG,CAAC,OAAO,EAAE,sBAAsB;IAWzC,oBAAoB;CAGrB;AAED,OAAO,EAAE,YAAY,EAAE,CAAA"}
@@ -0,0 +1,55 @@
1
+ import { debounce } from "./utils.js";
2
+ class BuildManager {
3
+ shouldRebuild;
4
+ shouldClean;
5
+ debouncedBuild;
6
+ buildFn;
7
+ constructor(buildFn) {
8
+ this.shouldRebuild = true;
9
+ this.shouldClean = false;
10
+ this.buildFn = buildFn;
11
+ if (process.env.RWJS_DELAY_RESTART) {
12
+ console.warn(
13
+ "[DEPRECATED] RWJS_DELAY_RESTART is deprecated and will be removed in the next major release. Please rename it to CEDAR_DELAY_API_RESTART in your .env file."
14
+ );
15
+ }
16
+ const delay = process.env.CEDAR_DELAY_API_RESTART || process.env.RWJS_DELAY_RESTART;
17
+ this.debouncedBuild = debounce(
18
+ async (options) => {
19
+ try {
20
+ await this.buildFn({
21
+ ...options,
22
+ rebuild: this.shouldRebuild,
23
+ clean: this.shouldClean
24
+ });
25
+ } finally {
26
+ this.shouldRebuild = true;
27
+ this.shouldClean = false;
28
+ }
29
+ },
30
+ // We want to delay execution when multiple files are modified on the
31
+ // filesystem. This usually happens when running Cedar generator commands.
32
+ // Local writes are very fast, but writes in e2e environments are not, so
33
+ // allow the default to be adjusted with an env-var.
34
+ delay ? parseInt(delay, 10) : 500
35
+ );
36
+ }
37
+ // Wrapper method to handle options and set precedence flags.
38
+ // If we ever see a `rebuild: false` option while debouncing, we never want to rebuild.
39
+ // If we ever see a `clean: true` option, we always want to clean.
40
+ async run(options) {
41
+ if (options.rebuild === false) {
42
+ this.shouldRebuild = false;
43
+ }
44
+ if (options.clean) {
45
+ this.shouldClean = true;
46
+ }
47
+ await this.debouncedBuild(options);
48
+ }
49
+ cancelScheduledBuild() {
50
+ this.debouncedBuild.cancel();
51
+ }
52
+ }
53
+ export {
54
+ BuildManager
55
+ };
@@ -0,0 +1,8 @@
1
+ export declare class ServerManager {
2
+ private httpServerProcess;
3
+ private startApiServer;
4
+ restartApiServer(): Promise<void>;
5
+ killApiServer(): Promise<void>;
6
+ }
7
+ export declare const serverManager: ServerManager;
8
+ //# sourceMappingURL=serverManager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serverManager.d.ts","sourceRoot":"","sources":["../src/serverManager.ts"],"names":[],"mappings":"AA6BA,qBAAa,aAAa;IACxB,OAAO,CAAC,iBAAiB,CAA4B;YAEvC,cAAc;IAiEtB,gBAAgB;IAKhB,aAAa;CAmCpB;AAED,eAAO,MAAM,aAAa,eAAsB,CAAA"}
@@ -0,0 +1,109 @@
1
+ import { fork } from "child_process";
2
+ import fs from "node:fs";
3
+ import { createRequire } from "node:module";
4
+ import path from "path";
5
+ import ansis from "ansis";
6
+ import yargs from "yargs";
7
+ import { hideBin } from "yargs/helpers";
8
+ import { getConfig, getPaths, resolveFile } from "@cedarjs/project-config";
9
+ const require2 = createRequire(import.meta.url);
10
+ const argv = yargs(hideBin(process.argv)).option("debugPort", {
11
+ description: "Port on which to expose API server debugger",
12
+ type: "number",
13
+ alias: ["debug-port", "dp"]
14
+ }).option("port", {
15
+ description: "The port to listen at",
16
+ type: "number",
17
+ alias: "p"
18
+ }).parseSync();
19
+ const rwjsPaths = getPaths();
20
+ class ServerManager {
21
+ httpServerProcess = null;
22
+ async startApiServer() {
23
+ const forkOpts = {
24
+ execArgv: process.execArgv
25
+ };
26
+ if (getConfig().experimental.opentelemetry.enabled) {
27
+ const opentelemetrySDKScriptPath = path.join(
28
+ rwjsPaths.api.dist,
29
+ "opentelemetry.js"
30
+ );
31
+ const opentelemetrySDKScriptPathRelative = path.relative(
32
+ rwjsPaths.base,
33
+ opentelemetrySDKScriptPath
34
+ );
35
+ console.log(
36
+ `Setting up OpenTelemetry using the setup file: ${opentelemetrySDKScriptPathRelative}`
37
+ );
38
+ if (fs.existsSync(opentelemetrySDKScriptPath)) {
39
+ forkOpts.execArgv = forkOpts.execArgv.concat([
40
+ `--require=${opentelemetrySDKScriptPath}`
41
+ ]);
42
+ } else {
43
+ console.error(
44
+ `OpenTelemetry setup file does not exist at ${opentelemetrySDKScriptPathRelative}`
45
+ );
46
+ }
47
+ }
48
+ const debugPort = argv["debug-port"];
49
+ if (debugPort) {
50
+ forkOpts.execArgv = forkOpts.execArgv.concat([`--inspect=${debugPort}`]);
51
+ }
52
+ const port = argv.port ?? getConfig().api.port;
53
+ const serverFile = resolveFile(`${rwjsPaths.api.dist}/server`);
54
+ if (serverFile) {
55
+ this.httpServerProcess = fork(
56
+ serverFile,
57
+ ["--apiPort", port.toString()],
58
+ forkOpts
59
+ );
60
+ } else {
61
+ const apiServerPkgPath = require2.resolve("@cedarjs/api-server/package.json");
62
+ const apiServerDir = path.dirname(apiServerPkgPath);
63
+ const apiServerPkg = require2(apiServerPkgPath);
64
+ const binPath = path.join(
65
+ apiServerDir,
66
+ apiServerPkg.bin["cedarjs-server"]
67
+ );
68
+ const args = ["api", "--port", port.toString()];
69
+ this.httpServerProcess = fork(binPath, args, forkOpts);
70
+ }
71
+ }
72
+ async restartApiServer() {
73
+ await this.killApiServer();
74
+ await this.startApiServer();
75
+ }
76
+ async killApiServer() {
77
+ if (!this.httpServerProcess) {
78
+ return;
79
+ }
80
+ await new Promise((resolve) => {
81
+ console.log(ansis.yellow("Shutting down API server."));
82
+ const cleanup = () => {
83
+ this.httpServerProcess?.removeAllListeners("exit");
84
+ clearTimeout(forceKillTimeout);
85
+ };
86
+ this.httpServerProcess?.on("exit", () => {
87
+ console.log(ansis.yellow("API server exited."));
88
+ cleanup();
89
+ resolve();
90
+ });
91
+ const forceKillTimeout = setTimeout(() => {
92
+ console.log(
93
+ ansis.yellow(
94
+ "API server did not exit within 2 seconds, forcefully closing it."
95
+ )
96
+ );
97
+ cleanup();
98
+ this.httpServerProcess?.kill("SIGKILL");
99
+ resolve();
100
+ }, 2e3);
101
+ this.httpServerProcess?.kill();
102
+ });
103
+ }
104
+ }
105
+ const serverManager = new ServerManager();
106
+ export {
107
+ ServerManager,
108
+ serverManager
109
+ };
@@ -0,0 +1,4 @@
1
+ export declare function debounce<T extends (...args: any[]) => any>(func: T, wait: number): T & {
2
+ cancel: () => void;
3
+ };
4
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,wBAAgB,QAAQ,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,EACxD,IAAI,EAAE,CAAC,EACP,IAAI,EAAE,MAAM,GACX,CAAC,GAAG;IAAE,MAAM,EAAE,MAAM,IAAI,CAAA;CAAE,CAsB5B"}
package/dist/utils.js ADDED
@@ -0,0 +1,22 @@
1
+ function debounce(func, wait) {
2
+ let timeoutId = null;
3
+ const debounced = ((...args) => {
4
+ if (timeoutId !== null) {
5
+ clearTimeout(timeoutId);
6
+ }
7
+ timeoutId = setTimeout(() => {
8
+ timeoutId = null;
9
+ func(...args);
10
+ }, wait);
11
+ });
12
+ debounced.cancel = () => {
13
+ if (timeoutId !== null) {
14
+ clearTimeout(timeoutId);
15
+ timeoutId = null;
16
+ }
17
+ };
18
+ return debounced;
19
+ }
20
+ export {
21
+ debounce
22
+ };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Initialize the file watcher for the API server
3
+ * Watches for changes in the API source directory and rebuilds/restarts as
4
+ * needed
5
+ *
6
+ * Also watches package sources so that changes to workspace packages used by
7
+ * the API trigger a rebuild/restart (HMR for API-side workspace packages).
8
+ */
9
+ export declare function startWatch(): Promise<void>;
10
+ //# sourceMappingURL=watch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watch.d.ts","sourceRoot":"","sources":["../src/watch.ts"],"names":[],"mappings":"AAqEA;;;;;;;GAOG;AACH,wBAAsB,UAAU,kBAsD/B"}
package/dist/watch.js ADDED
@@ -0,0 +1,89 @@
1
+ import path from "node:path";
2
+ import ansis from "ansis";
3
+ import chokidar from "chokidar";
4
+ import { config } from "dotenv-defaults";
5
+ import {
6
+ buildApi,
7
+ cleanApiBuild,
8
+ rebuildApi
9
+ } from "@cedarjs/internal/dist/build/api";
10
+ import { loadAndValidateSdls } from "@cedarjs/internal/dist/validateSchema";
11
+ import { getPaths } from "@cedarjs/project-config";
12
+ import { BuildManager } from "./buildManager.js";
13
+ import { serverManager } from "./serverManager.js";
14
+ import { getIgnoreFunction, pathsToWatch } from "./watchPaths.js";
15
+ const cedarPaths = getPaths();
16
+ if (!process.env.CEDAR_ENV_FILES_LOADED) {
17
+ config({
18
+ path: path.join(cedarPaths.base, ".env"),
19
+ defaults: path.join(cedarPaths.base, ".env.defaults"),
20
+ multiline: true
21
+ });
22
+ process.env.CEDAR_ENV_FILES_LOADED = "true";
23
+ }
24
+ async function buildAndServe(options) {
25
+ const buildTs = Date.now();
26
+ console.log(ansis.dim.italic("Building..."));
27
+ if (options.clean) {
28
+ await cleanApiBuild();
29
+ }
30
+ if (options.rebuild) {
31
+ await rebuildApi();
32
+ } else {
33
+ await buildApi();
34
+ }
35
+ await serverManager.restartApiServer();
36
+ console.log(ansis.dim.italic("Took " + (Date.now() - buildTs) + " ms"));
37
+ }
38
+ const buildManager = new BuildManager(buildAndServe);
39
+ async function validateSdls() {
40
+ try {
41
+ await loadAndValidateSdls();
42
+ return true;
43
+ } catch (e) {
44
+ serverManager.killApiServer();
45
+ console.error(
46
+ ansis.redBright(`[GQL Server Error] - Schema validation failed`)
47
+ );
48
+ console.error(ansis.red(e?.message));
49
+ console.error(ansis.redBright("-".repeat(40)));
50
+ buildManager.cancelScheduledBuild();
51
+ return false;
52
+ }
53
+ }
54
+ async function startWatch() {
55
+ const patterns = await pathsToWatch();
56
+ const watcher = chokidar.watch(patterns, {
57
+ persistent: true,
58
+ ignoreInitial: true,
59
+ ignored: await getIgnoreFunction()
60
+ });
61
+ watcher.on("ready", async () => {
62
+ await buildManager.run({ clean: true, rebuild: false });
63
+ await validateSdls();
64
+ });
65
+ watcher.on("all", async (eventName, filePath) => {
66
+ if (eventName === "addDir" && filePath === cedarPaths.api.base) {
67
+ return;
68
+ }
69
+ if (eventName) {
70
+ if (filePath.includes(".sdl")) {
71
+ const isValid = await validateSdls();
72
+ if (!isValid) {
73
+ return;
74
+ }
75
+ }
76
+ }
77
+ const displayPath = path.relative(cedarPaths.base, filePath);
78
+ console.log(ansis.dim(`[${eventName}] ${displayPath}`));
79
+ buildManager.cancelScheduledBuild();
80
+ if (eventName === "add" || eventName === "unlink") {
81
+ await buildManager.run({ rebuild: false });
82
+ } else {
83
+ await buildManager.run({ rebuild: true });
84
+ }
85
+ });
86
+ }
87
+ export {
88
+ startWatch
89
+ };
@@ -0,0 +1,3 @@
1
+ export declare function getIgnoreFunction(): Promise<(file: string) => boolean>;
2
+ export declare function pathsToWatch(): Promise<string[]>;
3
+ //# sourceMappingURL=watchPaths.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watchPaths.d.ts","sourceRoot":"","sources":["../src/watchPaths.ts"],"names":[],"mappings":"AAmGA,wBAAsB,iBAAiB,mBAqBvB,MAAM,cAwBrB;AAED,wBAAsB,YAAY,sBAQjC"}
@@ -0,0 +1,109 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import {
4
+ getDbDir,
5
+ getPaths,
6
+ importStatementPath
7
+ } from "@cedarjs/project-config";
8
+ async function workspacePackagesPaths() {
9
+ const cedarPaths = getPaths();
10
+ const packagesDir = path.join(cedarPaths.packages);
11
+ const packages = [];
12
+ try {
13
+ const rootPackageJsonPath = path.join(cedarPaths.base, "package.json");
14
+ const rootPackageJson = JSON.parse(
15
+ fs.readFileSync(rootPackageJsonPath, "utf8")
16
+ );
17
+ const hasPackageJsonWorkspaces = Array.isArray(rootPackageJson.workspaces) && rootPackageJson.workspaces.some((w) => w.startsWith("packages/"));
18
+ if (!hasPackageJsonWorkspaces || !fs.existsSync(packagesDir)) {
19
+ return [];
20
+ }
21
+ const globPattern = path.join(packagesDir, "*").replaceAll("\\", "/");
22
+ const packageDirs = await Array.fromAsync(fs.promises.glob(globPattern));
23
+ const apiPackageJsonPath = path.join(cedarPaths.api.base, "package.json");
24
+ const apiPackageJson = JSON.parse(
25
+ fs.readFileSync(apiPackageJsonPath, "utf8")
26
+ );
27
+ const deps = {
28
+ ...apiPackageJson.dependencies ?? {},
29
+ ...apiPackageJson.devDependencies ?? {},
30
+ ...apiPackageJson.peerDependencies ?? {}
31
+ };
32
+ const workspaceDepNames = /* @__PURE__ */ new Set();
33
+ for (const [name, version] of Object.entries(deps)) {
34
+ if (String(version).startsWith("workspace:")) {
35
+ workspaceDepNames.add(name);
36
+ }
37
+ }
38
+ for (const packageDir of packageDirs) {
39
+ const packageJsonPath = path.join(packageDir, "package.json");
40
+ if (!fs.existsSync(packageJsonPath)) {
41
+ continue;
42
+ }
43
+ const pkgJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
44
+ if (workspaceDepNames.has(pkgJson.name)) {
45
+ packages.push(path.join(packageDir, "dist"));
46
+ }
47
+ }
48
+ } catch {
49
+ }
50
+ return packages;
51
+ }
52
+ async function apiIgnorePaths() {
53
+ const cedarPaths = getPaths();
54
+ const dbDir = await getDbDir(cedarPaths.api.prismaConfig);
55
+ if (dbDir === cedarPaths.api.base) {
56
+ throw new Error(
57
+ "Database directory cannot be the same as the API directory"
58
+ );
59
+ }
60
+ const ignoredApiPaths = [
61
+ // TODO: Is this still true?
62
+ // use this, because using cedarPaths.api.dist seems to not ignore on first
63
+ // build
64
+ "api/dist",
65
+ cedarPaths.api.types,
66
+ dbDir
67
+ ];
68
+ return ignoredApiPaths;
69
+ }
70
+ async function getIgnoreFunction() {
71
+ const cedarPaths = getPaths();
72
+ const ignoredApiPaths = await apiIgnorePaths();
73
+ const ignoredExtensions = [
74
+ ".DS_Store",
75
+ ".db",
76
+ ".sqlite",
77
+ "-journal",
78
+ ".test.js",
79
+ ".test.ts",
80
+ ".scenarios.ts",
81
+ ".scenarios.js",
82
+ ".d.ts",
83
+ ".log"
84
+ ];
85
+ return (file) => {
86
+ if (file.includes("node_modules")) {
87
+ return true;
88
+ }
89
+ if (ignoredExtensions.some((ext) => file.endsWith(ext))) {
90
+ return true;
91
+ }
92
+ if (file.includes(importStatementPath(cedarPaths.packages)) && file.includes("/src/")) {
93
+ return true;
94
+ }
95
+ if (ignoredApiPaths.some((ignoredPath) => file.includes(ignoredPath))) {
96
+ return true;
97
+ }
98
+ return false;
99
+ };
100
+ }
101
+ async function pathsToWatch() {
102
+ const cedarPaths = getPaths();
103
+ const watchPaths = [cedarPaths.api.src, ...await workspacePackagesPaths()];
104
+ return watchPaths.map((p) => importStatementPath(p));
105
+ }
106
+ export {
107
+ getIgnoreFunction,
108
+ pathsToWatch
109
+ };
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@cedarjs/api-server-watch",
3
+ "version": "6.0.0-canary.2828",
4
+ "description": "CedarJS's dev-time watcher that rebuilds and restarts the api server on change",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/cedarjs/cedar.git",
8
+ "directory": "packages/api-server-watch"
9
+ },
10
+ "license": "MIT",
11
+ "type": "module",
12
+ "main": "./dist/watch.js",
13
+ "types": "./dist/watch.d.ts",
14
+ "bin": {
15
+ "cedar-api-server-watch": "./dist/bin.js",
16
+ "cedarjs-api-server-watch": "./dist/bin.js"
17
+ },
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "scripts": {
22
+ "build": "node ./build.mts",
23
+ "build:pack": "yarn pack -o cedarjs-api-server-watch.tgz",
24
+ "build:types": "tsc --build --verbose ./tsconfig.build.json",
25
+ "build:watch": "nodemon --watch src --ext \"js,jsx,ts,tsx\" --ignore dist --exec \"yarn build && yarn fix:permissions\"",
26
+ "fix:permissions": "chmod +x dist/bin.js",
27
+ "prepublishOnly": "NODE_ENV=production yarn build",
28
+ "test": "vitest run",
29
+ "test:watch": "vitest watch"
30
+ },
31
+ "dependencies": {
32
+ "@cedarjs/api-server": "6.0.0-canary.2828",
33
+ "@cedarjs/internal": "6.0.0-canary.2828",
34
+ "@cedarjs/project-config": "6.0.0-canary.2828",
35
+ "ansis": "4.3.1",
36
+ "chokidar": "3.6.0",
37
+ "dotenv-defaults": "5.0.2",
38
+ "yargs": "17.7.3"
39
+ },
40
+ "devDependencies": {
41
+ "@cedarjs/framework-tools": "6.0.0-canary.2828",
42
+ "@types/dotenv-defaults": "^5.0.0",
43
+ "@types/yargs": "17.0.35",
44
+ "typescript": "5.9.3",
45
+ "vitest": "4.1.10"
46
+ },
47
+ "engines": {
48
+ "node": ">=24"
49
+ },
50
+ "publishConfig": {
51
+ "access": "public"
52
+ }
53
+ }