@joystick.js/cli-canary 0.0.0-canary.99 → 1.0.0-beta.74

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.
@@ -53,8 +53,10 @@ const handleBuildForNode = (nodePaths = [], options = {}) => {
53
53
  format: "esm",
54
54
  bundle: false,
55
55
  entryPoints: nodePaths?.map((file) => file.path),
56
+ entryNames: '[dir]/[name]',
56
57
  // TODO: Make sure we don't need outbase here so the paths map correctly.
57
58
  outdir: options?.outputPath || "./.joystick/build",
59
+ outbase: './',
58
60
  define: {
59
61
  "process.env.NODE_ENV": `'${options?.environment}'`,
60
62
  },
@@ -77,6 +79,8 @@ const handleBuildForBrowser = (browserPaths = [], options = {}) => {
77
79
  format: "esm",
78
80
  bundle: true,
79
81
  entryPoints: browserPaths?.map((file) => file.path),
82
+ entryNames: '[dir]/[name]',
83
+ outbase: './',
80
84
  // TODO: Make sure we don't need outbase here so the paths map correctly.
81
85
  outdir: options?.outputPath || "./.joystick/build",
82
86
  define: {
@@ -225,7 +229,6 @@ const buildFiles = async (options, { resolve, reject }) => {
225
229
  return { success: true };
226
230
  }).catch((exception) => {
227
231
  const file = handleParseFilePathFromException(exception);
228
- console.log(file, exception);
229
232
  const snippet = handleBuildException(exception, file);
230
233
 
231
234
  return {
@@ -5,5 +5,6 @@ export default [
5
5
  getPlatformSafePath("routes/"),
6
6
  getPlatformSafePath("fixtures/"),
7
7
  getPlatformSafePath("lib/node"),
8
+ getPlatformSafePath("tests/"),
8
9
  "index.server.js",
9
10
  ];
@@ -0,0 +1,78 @@
1
+ import fs from "fs";
2
+ import { WebSocketServer } from "ws";
3
+ import generateId from "../generateId.js";
4
+
5
+ // NOTE: This doesn't run on the app server (in node). This runs on your local computer
6
+ // as a totally separate process that's forked on joystick start.
7
+
8
+ export default (() => {
9
+ const websocketServer = new WebSocketServer({
10
+ port: parseInt(process.env.PORT, 10) + 1,
11
+ path: "/_joystick/hmr",
12
+ });
13
+
14
+ process.on("message", (message) => {
15
+ if (typeof process.HMR_CONNECTIONS === "object") {
16
+ const parsedMessage = JSON.parse(message);
17
+ const connections = Object.values(process.HMR_CONNECTIONS);
18
+ for (let i = 0; i < connections?.length; i += 1) {
19
+ const connection = connections[i];
20
+ if (connection?.connection?.send) {
21
+ connection.connection.send(
22
+ JSON.stringify({
23
+ type: "FILE_CHANGE",
24
+ settings: {
25
+ global: parsedMessage?.settings?.global,
26
+ public: parsedMessage?.settings?.public,
27
+ },
28
+ indexHTMLChanged: parsedMessage?.indexHTMLChanged,
29
+ })
30
+ );
31
+ }
32
+ }
33
+ }
34
+ });
35
+
36
+ websocketServer.on("connection", function connection(websocketConnection) {
37
+ const connectionId = generateId();
38
+
39
+ process.HMR_CONNECTIONS = {
40
+ ...(process.HMR_CONNECTIONS || {}),
41
+ [connectionId]: {
42
+ connection: websocketConnection,
43
+ watchlist: [],
44
+ },
45
+ };
46
+
47
+ if (Object.keys(process.HMR_CONNECTIONS || {})?.length > 0) {
48
+ process.send({ type: "HAS_HMR_CONNECTIONS" });
49
+ }
50
+
51
+ websocketConnection.on("message", (message) => {
52
+ const parsedMessage = JSON.parse(message);
53
+
54
+ if (parsedMessage?.type === "HMR_UPDATE_COMPLETE") {
55
+ process.send({ type: "HMR_UPDATE_COMPLETED", sessions: parsedMessage?.sessions });
56
+ }
57
+
58
+ if (parsedMessage?.type === "HMR_WATCHLIST") {
59
+ process.HMR_CONNECTIONS[connectionId]?.watchlist?.push(
60
+ ...(parsedMessage?.tags || [])
61
+ );
62
+ }
63
+ });
64
+
65
+ websocketConnection.on("close", () => {
66
+ if (process.HMR_CONNECTIONS[connectionId]) {
67
+ delete process.HMR_CONNECTIONS[connectionId];
68
+
69
+ if (Object.keys(process.HMR_CONNECTIONS || {})?.length === 0) {
70
+ process.send({ type: "HAS_NO_HMR_CONNECTIONS" });
71
+ }
72
+ }
73
+ });
74
+
75
+ });
76
+
77
+ return true;
78
+ })();
@@ -22,6 +22,8 @@ import getCodependenciesForFile from "./getCodependenciesForFile.js";
22
22
  import removeDeletedDependenciesFromMap from "../build/removeDeletedDependenciesFromMap.js";
23
23
  import chalk from "chalk";
24
24
 
25
+ const processIds = [];
26
+
25
27
  const getDatabaseProcessIds = () => {
26
28
  try {
27
29
  const databaseProcessIds = [];
@@ -75,6 +77,8 @@ const handleSignalEvents = (processIds = [], nodeMajorVersion = 0, __dirname = '
75
77
  }
76
78
  );
77
79
 
80
+ process.cleanupProcess = cleanupProcess;
81
+
78
82
  process.on("SIGINT", async () => {
79
83
  const databaseProcessIds = getDatabaseProcessIds();
80
84
  cleanupProcess.send(JSON.stringify(({ processIds: [...processIds, ...databaseProcessIds] })));
@@ -115,7 +119,7 @@ const handleHMRProcessMessages = (options = {}) => {
115
119
 
116
120
  if (message?.type === "HMR_UPDATE_COMPLETED") {
117
121
  // NOTE: Do a setTimeout to ensure that server is still available while the HMR update completes.
118
- // Necessary because some updates are instance, but others might mount a UI that needs a server
122
+ // Necessary because some updates are instant, but others might mount a UI that needs a server
119
123
  // available (e.g., runs API requests).
120
124
  setTimeout(() => {
121
125
  handleRestartApplicationProcess({
@@ -171,7 +175,7 @@ const handleServerProcessMessages = () => {
171
175
  }
172
176
  };
173
177
 
174
- const handleServerProcessSTDIO = () => {
178
+ const handleServerProcessSTDIO = (options = {}) => {
175
179
  try {
176
180
  if (process.serverProcess) {
177
181
  process.serverProcess.on("error", (error) => {
@@ -181,7 +185,7 @@ const handleServerProcessSTDIO = () => {
181
185
  process.serverProcess.stdout.on("data", (data) => {
182
186
  const message = data.toString();
183
187
 
184
- if (message && message.includes("App running at:")) {
188
+ if (message && message.includes("App running at:") && !options?.watch) {
185
189
  process.loader.stable(message);
186
190
  } else {
187
191
  if (message && !message.includes("BUILD_ERROR")) {
@@ -208,11 +212,10 @@ const handleAddOrChangeFile = async (context = {}, path = '', options = {}) => {
208
212
  try {
209
213
  if (context.isAddingOrChangingFile) {
210
214
  const codependencies = await getCodependenciesForFile(path);
211
- const fileResults = await buildFiles(
212
- [path, ...(codependencies?.existing || [])],
213
- null,
214
- process.env.NODE_ENV
215
- );
215
+ const fileResults = await buildFiles({
216
+ files: [path, ...(codependencies?.existing || [])],
217
+ environment: process.env.NODE_ENV,
218
+ });
216
219
 
217
220
  const fileResultsHaveErrors = fileResults
218
221
  .filter((result) => !!result)
@@ -305,21 +308,15 @@ const handleCopyDirectory = (context = {}, path = '', options = {}) => {
305
308
  const handleRestartApplicationProcess = async (options = {}) => {
306
309
  try {
307
310
  if (process.serverProcess && process.serverProcess.pid) {
308
- process.loader.text("Restarting app...");
309
311
  process.serverProcess.kill();
310
312
 
311
- await startApp({
312
- nodeMajorVersion: options?.nodeMajorVersion,
313
- port: options?.port,
314
- sessionsBeforeHMRUpdate: options?.sessionsBeforeHMRUpdate,
315
- });
313
+ await handleStartAppServer(options);
316
314
 
317
315
  return Promise.resolve();
318
316
  }
319
317
 
320
318
  // NOTE: Original process was never initialized due to an error.
321
- process.loader.text("Starting app...");
322
- startApplicationProcess();
319
+ await handleStartAppServer(options);
323
320
 
324
321
  if (!process.hmrProcess) {
325
322
  startHMR();
@@ -329,19 +326,41 @@ const handleRestartApplicationProcess = async (options = {}) => {
329
326
  }
330
327
  };
331
328
 
332
- const handleNotifyHMRClients = (indexHTMLChanged = false) => {
329
+ const handleStartAppServer = async (options = {}) => {
330
+ try {
331
+ const serverProcess = await startApp({
332
+ watch: options?.watch,
333
+ nodeMajorVersion: options?.nodeMajorVersion,
334
+ port: options?.port,
335
+ sessionsBeforeHMRUpdate: options?.sessionsBeforeHMRUpdate,
336
+ });
337
+
338
+ if (serverProcess) {
339
+ processIds.push(serverProcess.pid);
340
+ process.serverProcess = serverProcess;
341
+ handleServerProcessSTDIO(options);
342
+ handleServerProcessMessages(options);
343
+ }
344
+ } catch (exception) {
345
+ console.warn(exception);
346
+ throw new Error(`[dev.handleStartAppServer] ${exception.message}`);
347
+ }
348
+ };
349
+
350
+ const handleNotifyHMRClients = async (indexHTMLChanged = false) => {
333
351
  try {
334
352
  if (process.hmrProcess) {
335
- const settings = loadSettings(process.env.NODE_ENV);
353
+ const settings = await loadSettings({ environment: process.env.NODE_ENV });
336
354
  process.hmrProcess.send(
337
355
  JSON.stringify({
338
356
  type: "RESTART_SERVER",
339
- settings,
357
+ settings: settings.parsed,
340
358
  indexHTMLChanged,
341
359
  })
342
360
  );
343
361
  }
344
362
  } catch (exception) {
363
+ console.warn(exception);
345
364
  throw new Error(`[dev.handleNotifyHMRClients] ${exception.message}`);
346
365
  }
347
366
  };
@@ -359,7 +378,7 @@ const getWatchChangeContext = (event = '', path = '') => {
359
378
  const isAddDirectory = event === 'addDir' && isExistingDirectoryInSource && !isExistingDirectoryInBuild;
360
379
  const isFileToCopy = !!filesToCopy.find((fileToCopy) => fileToCopy.path === path);
361
380
  const isExistingFileInSource = isFile && fs.existsSync(path);
362
- const isAddingOrChangingFile = event === 'add' && isExistingFileInSource;
381
+ const isAddingOrChangingFile = ["add", "change"].includes(event) && isExistingFileInSource;
363
382
 
364
383
  return {
365
384
  isHTMLUpdate,
@@ -391,10 +410,16 @@ const startFileWatcher = (options = {}) => {
391
410
  // NOTE: Do this here in case a required file/folder goes missing inbetween builds.
392
411
  checkForRequiredFiles();
393
412
 
394
- process.loader.text("Rebuilding app...");
413
+ if (!options?.watch) {
414
+ process.loader.text("Rebuilding app...");
415
+ }
395
416
 
396
417
  const watchChangeContext = getWatchChangeContext(event, path);
397
-
418
+ //
419
+ // console.log({
420
+ // watchChangeContext,
421
+ // });
422
+ //
398
423
  handleCopyDirectory(watchChangeContext, path, options);
399
424
  handleCopyFile(watchChangeContext, path, options);
400
425
  handleAddDirectory(watchChangeContext, path, options);
@@ -578,21 +603,15 @@ const dev = async (options, { resolve, reject }) => {
578
603
  });
579
604
 
580
605
  await runInitialBuild(settings?.parsed?.config?.build);
581
- await startFileWatcher(options);
582
-
583
- const processIds = [];
584
-
585
- const serverProcess = await startApp({
606
+ await startFileWatcher({
607
+ ...options,
586
608
  nodeMajorVersion,
587
- port: options?.port,
588
609
  });
589
610
 
590
- if (serverProcess) {
591
- processIds.push(serverProcess.pid);
592
- process.serverProcess = serverProcess;
593
- handleServerProcessSTDIO();
594
- handleServerProcessMessages();
595
- }
611
+ await handleStartAppServer({
612
+ ...options,
613
+ nodeMajorVersion,
614
+ });
596
615
 
597
616
  if (options?.environment !== 'test') {
598
617
  const hmrProcess = await startHMR({
@@ -604,7 +623,10 @@ const dev = async (options, { resolve, reject }) => {
604
623
  process.hmrProcess = hmrProcess;
605
624
 
606
625
  handleHMRProcessSTDIO();
607
- handleHMRProcessMessages(options);
626
+ handleHMRProcessMessages({
627
+ ...options,
628
+ nodeMajorVersion,
629
+ });
608
630
  }
609
631
 
610
632
 
@@ -615,10 +637,23 @@ const dev = async (options, { resolve, reject }) => {
615
637
  );
616
638
 
617
639
  if (options?.environment === 'test') {
618
- process.loader.text('Running tests...');
640
+ process.loader.stop();
641
+ const databaseProcessIds = getDatabaseProcessIds();
642
+
619
643
  await runTests({
644
+ watch: options?.watch,
620
645
  __dirname,
646
+ processIds: [
647
+ ...processIds,
648
+ ...databaseProcessIds,
649
+ ],
650
+ cleanupProcess: process.cleanupProcess,
621
651
  });
652
+
653
+ if (!options?.watch) {
654
+ // NOTE: Kick out of the process after tests run if we're not in watch mode.
655
+ process.exit(0);
656
+ }
622
657
  }
623
658
 
624
659
  /*
@@ -1,46 +1,95 @@
1
1
  import child_process from "child_process";
2
+ import CLILog from "../CLILog.js";
2
3
 
3
- const handleAvaSTDIO = (ava = {}) => {
4
+ const handleAvaSTDERR = (stderr = '', options = {}) => {
5
+ try {
6
+ // NOTE: Squash output about using a configuration file (we always do in the framework).
7
+ if (stderr?.includes('Using configuration')) {
8
+ return null;
9
+ }
10
+
11
+ if (stderr?.includes('No tests found')) {
12
+ return CLILog('No tests found. Add tests in the /tests folder at the root of your Joystick app.', {
13
+ level: 'danger',
14
+ docs: 'https://cheatcode.co/docs/joystick/test/setup',
15
+ });
16
+ }
17
+
18
+ console.log(stderr);
19
+ } catch (exception) {
20
+ throw new Error(`[runTests.handleAvaSTDERR] ${exception.message}`);
21
+ }
22
+ };
23
+
24
+ const handleAvaSTDOUT = (stdout = '', options = {}) => {
25
+ try {
26
+ // NOTE: Squash output about using a configuration file (we always do in the framework).
27
+ if (stdout?.includes('Using configuration')) {
28
+ return null;
29
+ }
30
+
31
+ if (stdout?.includes('No tests found in')) {
32
+ const [message] = stdout?.split(',');
33
+ return console.log(`${message}\n`);
34
+ }
35
+
36
+ console.log(stdout);
37
+ } catch (exception) {
38
+ throw new Error(`[runTests.handleAvaSTDOUT] ${exception.message}`);
39
+ }
40
+ };
41
+
42
+ const handleAvaSTDIO = (ava = {}, options = {}) => {
4
43
  try {
5
44
  ava.stdout.on('data', function (data) {
6
45
  const string = data.toString();
7
-
8
- if (!string?.includes('ava')) {
9
- console.log(string);
10
- }
46
+ handleAvaSTDOUT(string, options);
11
47
  });
12
48
 
13
49
  ava.stderr.on('data', function (data) {
14
50
  const string = data.toString();
15
-
16
- if (!string?.includes('ava')) {
17
- console.log(string);
18
- }
51
+ handleAvaSTDERR(string, options);
19
52
  });
20
53
  } catch (exception) {
21
54
  throw new Error(`[runTests.handleAvaSTDIO] ${exception.message}`);
22
55
  }
23
56
  };
24
57
 
25
- const runAva = (__dirname = '') => {
58
+ const runAva = (options = {}) => {
26
59
  try {
27
- console.log(`node_modules/.bin/ava --config ${__dirname}/tests.config.js`);
60
+ // NOTE: A little bananas to reason through this. In order for Ava to run w/o errors,
61
+ // the Ava binary being run here has to be identical to the one used in @joystick.js/test.
62
+ // That would equal the copy of Ava that's installed in a Joystick app's node_modules
63
+ // directory, not the node_modules directory of the CLI here. We can guarantee that will
64
+ // exist for the CLI here because a developer has to install @joystick.js/test which will
65
+ // add Ava as a dependency to their app in order to write tests.
66
+ const avaPath = `${process.cwd()}/node_modules/.bin/ava`;
67
+
28
68
  return new Promise((resolve, reject) => {
29
- const ava = child_process.exec(`node_modules/.bin/ava --config ${__dirname}/tests.config.js`, {
69
+ // NOTE: Despite using the app's node_modules path to reference Ava, we still want to reference
70
+ // the internal path here for the default test config in /lib/dev/tests.config.js.
71
+ const ava = child_process.exec(`${avaPath} --config ${options?.__dirname}/tests.config.js ${options?.watch ? '--watch' : ''}`, {
30
72
  stdio: 'inherit',
31
73
  env: {
32
74
  ...(process.env),
75
+ databases: process.databases,
33
76
  FORCE_COLOR: "1"
34
77
  }
35
78
  }, (error) => {
36
- if (error) {
37
- return reject();
79
+ if (!error) {
80
+ // NOTE: Do this here because the standard SIGINT and SIGTERM hooks the dev process
81
+ // listens for don't catch a clean exit (and the process.exit() hook fires after exit).
82
+ options.cleanupProcess.send(JSON.stringify(({ processIds: options?.processIds })));
83
+ resolve();
84
+ } else {
85
+ // NOTE: Do not report any Ava errors here because they're picked up by the handleAvaSTDIO();
86
+ // hook below. Just do a clean exit here so Node doesn't hang.
87
+ options.cleanupProcess.send(JSON.stringify(({ processIds: options?.processIds })));
88
+ resolve();
38
89
  }
39
-
40
- return resolve();
41
90
  });
42
91
 
43
- handleAvaSTDIO(ava);
92
+ handleAvaSTDIO(ava, options);
44
93
  });
45
94
  } catch (exception) {
46
95
  throw new Error(`[runTests.runAva] ${exception.message}`);
@@ -60,7 +109,7 @@ const runTests = async (options, { resolve, reject }) => {
60
109
  try {
61
110
  validateOptions(options);
62
111
 
63
- await runAva(options?.__dirname);
112
+ await runAva(options);
64
113
 
65
114
  resolve();
66
115
  } catch (exception) {
@@ -1,9 +1,11 @@
1
1
  import child_process from "child_process";
2
2
  import path from "path";
3
3
 
4
- const handleStartServerProcess = (execArgv = {}, sessionsBeforeHMRUpdate = {}) => {
4
+ const handleStartServerProcess = (execArgv = {}, options = {}) => {
5
5
  try {
6
- process.loader.text('Starting app...');
6
+ if (!options?.watch) {
7
+ process.loader.text('Starting app...');
8
+ }
7
9
 
8
10
  return child_process.fork(
9
11
  path.resolve(".joystick/build/index.server.js"),
@@ -20,7 +22,7 @@ const handleStartServerProcess = (execArgv = {}, sessionsBeforeHMRUpdate = {}) =
20
22
  ROOT_URL: process.env.ROOT_URL,
21
23
  PORT: process.env.PORT,
22
24
  JOYSTICK_SETTINGS: process.env.JOYSTICK_SETTINGS,
23
- HMR_SESSIONS: JSON.stringify(sessionsBeforeHMRUpdate),
25
+ HMR_SESSIONS: options?.sessionsBeforeHMRUpdate || '{}',
24
26
  },
25
27
  }
26
28
  );
@@ -65,7 +67,7 @@ const startApp = (options, { resolve, reject }) => {
65
67
  validateOptions(options);
66
68
 
67
69
  const execArgv = getExecArgs(options?.nodeMajorVersion);
68
- const serverProcess = handleStartServerProcess(execArgv, options?.sessionsBeforeHMRUpdate);
70
+ const serverProcess = handleStartServerProcess(execArgv, options);
69
71
 
70
72
  return resolve(serverProcess);
71
73
  } catch (exception) {
@@ -0,0 +1,9 @@
1
+ export default {
2
+ files: [`tests/**/*.test.js`],
3
+ // NOTE: Intentionally limit to 1 test at a time to avoid concurrency creating
4
+ // race conditions in tests that have users or other "global" data that needs
5
+ // to be cleaned up. The extra time needed is worth it to guarantee that tests
6
+ // don't step on each other's toes and our code is properly verified.
7
+ concurrency: 1,
8
+ cache: false,
9
+ };