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

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/canary.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joystick.js/cli-canary",
3
- "version": "0.0.0-canary.203",
3
+ "version": "0.0.0-canary.239",
4
4
  "type": "module",
5
5
  "description": "CLI for the Joystick JavaScript framework.",
6
6
  "main": "development.js",
@@ -12,6 +12,10 @@ import getCodeFrame from "./getCodeFrame.js";
12
12
  import onWarn from "./onWarn.js";
13
13
  const handleBuildException = (exception = {}, file = "") => {
14
14
  try {
15
+ console.warn({
16
+ exception,
17
+ file
18
+ });
15
19
  const error = exception?.errors && exception?.errors[0];
16
20
  const snippet = fs.existsSync(file) ? getCodeFrame(file, {
17
21
  line: error?.location?.line,
@@ -28,7 +32,7 @@ const handleBuildException = (exception = {}, file = "") => {
28
32
  });
29
33
  return snippet;
30
34
  } catch (exception2) {
31
- throw new Error(`[actionName.handleBuildException] ${exception2.message}`);
35
+ throw new Error(`[buildFiles.handleBuildException] ${exception2.message}`);
32
36
  }
33
37
  };
34
38
  const handleParseFilePathFromException = (exception = {}) => {
@@ -36,18 +40,18 @@ const handleParseFilePathFromException = (exception = {}) => {
36
40
  const rawErrorMessage = exception?.message?.split(":");
37
41
  return rawErrorMessage[1] && rawErrorMessage[1]?.replace("\n", "") || "";
38
42
  } catch (exception2) {
39
- throw new Error(`[actionName.handleParseFilePathFromException] ${exception2.message}`);
43
+ throw new Error(`[buildFiles.handleParseFilePathFromException] ${exception2.message}`);
40
44
  }
41
45
  };
42
46
  const handleBuildForNode = (nodePaths2 = [], options = {}) => {
43
47
  try {
44
48
  return esbuild.build({
49
+ allowOverwrite: true,
45
50
  platform: "node",
46
51
  format: "esm",
47
52
  bundle: false,
48
53
  entryPoints: nodePaths2?.map((file) => file.path),
49
54
  entryNames: "[dir]/[name]",
50
- // TODO: Make sure we don't need outbase here so the paths map correctly.
51
55
  outdir: options?.outputPath || "./.joystick/build",
52
56
  outbase: "./",
53
57
  define: {
@@ -66,6 +70,7 @@ const handleBuildForNode = (nodePaths2 = [], options = {}) => {
66
70
  const handleBuildForBrowser = (browserPaths2 = [], options = {}) => {
67
71
  try {
68
72
  return esbuild.build({
73
+ allowOverwrite: true,
69
74
  target: "es2020",
70
75
  platform: "browser",
71
76
  format: "esm",
@@ -5,11 +5,13 @@ import {
5
5
  JOYSTICK_UI_REGEX,
6
6
  EXPORT_DEFAULT_REGEX,
7
7
  JOYSTICK_COMPONENT_REGEX,
8
- JOYSTICK_COMMENT_REGEX
8
+ JOYSTICK_COMMENT_REGEX,
9
+ EXAMPLE_CODE_REGEX
9
10
  } from "../regexes.js";
10
11
  import generateId from "../generateId.js";
11
12
  import getPlatformSafePath from "../getPlatformSafePath.js";
12
13
  import setComponentId from "./setComponentId.js";
14
+ import replaceExamples from "./replaceExamples.js";
13
15
  var buildPlugins_default = {
14
16
  bootstrapComponent: {
15
17
  name: "bootstrapComponent",
@@ -17,7 +19,7 @@ var buildPlugins_default = {
17
19
  const ssrId = generateId();
18
20
  build.onLoad({ filter: /\.js$/ }, (args = {}) => {
19
21
  try {
20
- const shouldSetSSRId = [getPlatformSafePath("ui/")].some(
22
+ const shouldSetComponentId = [getPlatformSafePath("ui/")].some(
21
23
  (bootstrapTarget) => {
22
24
  return args.path.includes(bootstrapTarget);
23
25
  }
@@ -37,7 +39,7 @@ var buildPlugins_default = {
37
39
  return args.path.includes(bootstrapTarget);
38
40
  }
39
41
  );
40
- if (shouldSetSSRId || isLayoutComponent || isPageComponent || isEmailComponent) {
42
+ if (shouldSetComponentId || isLayoutComponent || isPageComponent || isEmailComponent) {
41
43
  const code = fs.readFileSync(
42
44
  getPlatformSafePath(args.path),
43
45
  "utf-8"
@@ -56,7 +58,9 @@ var buildPlugins_default = {
56
58
  console.log(" ");
57
59
  return;
58
60
  }
59
- let contents = setComponentId(code)?.replace(
61
+ const examples = code.match(EXAMPLE_CODE_REGEX) || [];
62
+ let contents = replaceExamples(code);
63
+ contents = contents?.replace(
60
64
  /\.component\(\/\*\*\//g,
61
65
  ".component("
62
66
  );
@@ -103,6 +107,10 @@ var buildPlugins_default = {
103
107
  `
104
108
  );
105
109
  }
110
+ for (let i = 0; i < examples?.length; i += 1) {
111
+ const exampleToRestore = examples[i];
112
+ contents = contents.replace(`%example:${i}%`, exampleToRestore);
113
+ }
106
114
  return {
107
115
  contents,
108
116
  loader: "js"
@@ -125,11 +133,17 @@ var buildPlugins_default = {
125
133
  if (shouldSetComponentId) {
126
134
  const file = fs.readFileSync(`${build?.initialOptions?.outdir}/${entryPoint}`, "utf-8");
127
135
  const joystickUIMatches = file?.match(JOYSTICK_COMPONENT_REGEX) || [];
136
+ const examples = file.match(EXAMPLE_CODE_REGEX) || [];
137
+ let contents = replaceExamples(file);
128
138
  if (joystickUIMatches?.length > 0) {
129
- let contents = setComponentId(file)?.replace(
139
+ contents = setComponentId(contents)?.replace(
130
140
  /\.component\(\/\*\*\//g,
131
141
  ".component("
132
142
  );
143
+ for (let i2 = 0; i2 < examples?.length; i2 += 1) {
144
+ const exampleToRestore = examples[i2];
145
+ contents = contents.replace(`%example:${i2}%`, exampleToRestore);
146
+ }
133
147
  fs.writeFileSync(`${build?.initialOptions?.outdir}/${entryPoint}`, contents);
134
148
  }
135
149
  }
@@ -0,0 +1,13 @@
1
+ import { EXAMPLE_CODE_REGEX } from "../regexes.js";
2
+ var replaceExamples_default = (code = "") => {
3
+ let exampleIndex = 0;
4
+ return code.replace(
5
+ EXAMPLE_CODE_REGEX,
6
+ () => {
7
+ return `%example:${exampleIndex++}%`;
8
+ }
9
+ );
10
+ };
11
+ export {
12
+ replaceExamples_default as default
13
+ };
@@ -0,0 +1,27 @@
1
+ import net from "net";
2
+ const resetClient = (client = {}) => {
3
+ client.end();
4
+ client.destroy();
5
+ client.unref();
6
+ };
7
+ var checkIfPortOccupied_default = (port = 2600) => {
8
+ return new Promise((resolve) => {
9
+ const client = new net.Socket();
10
+ client.once("connect", () => {
11
+ resetClient(client);
12
+ resolve(true);
13
+ });
14
+ client.once("error", () => {
15
+ resetClient(client);
16
+ resolve(false);
17
+ });
18
+ client.connect({
19
+ port,
20
+ host: "127.0.0.1"
21
+ }, function() {
22
+ });
23
+ });
24
+ };
25
+ export {
26
+ checkIfPortOccupied_default as default
27
+ };
@@ -30,7 +30,7 @@ var getFilesToBuild_default = (excludedPaths = [], context = null) => {
30
30
  return path.includes(excludedPath);
31
31
  });
32
32
  }).filter((path) => {
33
- return !fs.lstatSync(path).isDirectory();
33
+ return fs.existsSync(path) && !fs.lstatSync(path).isDirectory();
34
34
  });
35
35
  };
36
36
  export {
@@ -19,6 +19,8 @@ import { SETTINGS_FILE_NAME_REGEX } from "../regexes.js";
19
19
  import getCodependenciesForFile from "./getCodependenciesForFile.js";
20
20
  import removeDeletedDependenciesFromMap from "../build/removeDeletedDependenciesFromMap.js";
21
21
  import chalk from "chalk";
22
+ import checkIfPortOccupied from "../checkIfPortOccupied.js";
23
+ import { killPortProcess } from "kill-port-process";
22
24
  const processIds = [];
23
25
  const getDatabaseProcessIds = () => {
24
26
  try {
@@ -173,6 +175,29 @@ const handleServerProcessSTDIO = (options = {}) => {
173
175
  throw new Error(`[dev.handleServerProcessSTDIO] ${exception.message}`);
174
176
  }
175
177
  };
178
+ const handleDeletePath = (context = {}, path2 = "", options = {}) => {
179
+ try {
180
+ if (context?.isDeletingPath) {
181
+ if (context.isExistingPathInBuild) {
182
+ const pathToUnlink = `./.joystick/build/${path2}`;
183
+ const stats = fs.lstatSync(pathToUnlink);
184
+ if (stats.isDirectory()) {
185
+ fs.rmdirSync(pathToUnlink, { recursive: true });
186
+ }
187
+ if (stats.isFile()) {
188
+ fs.unlinkSync(pathToUnlink);
189
+ }
190
+ }
191
+ if (context.isUIUpdate) {
192
+ handleNotifyHMRClients(context.isHTMLUpdate);
193
+ } else {
194
+ handleRestartApplicationProcess(options);
195
+ }
196
+ }
197
+ } catch (exception) {
198
+ throw new Error(`[dev.handleDeletePath] ${exception.message}`);
199
+ }
200
+ };
176
201
  const handleAddOrChangeFile = async (context = {}, path2 = "", options = {}) => {
177
202
  try {
178
203
  if (context.isAddingOrChangingFile) {
@@ -205,6 +230,7 @@ const handleAddOrChangeFile = async (context = {}, path2 = "", options = {}) =>
205
230
  }
206
231
  }
207
232
  } catch (exception) {
233
+ console.warn(exception);
208
234
  throw new Error(`[dev.handleAddOrChangeFile] ${exception.message}`);
209
235
  }
210
236
  };
@@ -238,7 +264,7 @@ const handleCopyFile = (context = {}, path2 = "", options = {}) => {
238
264
  };
239
265
  const handleCopyDirectory = (context = {}, path2 = "", options = {}) => {
240
266
  try {
241
- if (context.isFileToCopy && context.isDirectory && !context.isExistingDirectoryInBuild) {
267
+ if (context.isFileToCopy && context.isDirectory && !context.isExistingPathInBuild) {
242
268
  fs.mkdirSync(`./.joystick/build/${path2}`);
243
269
  if (context.isUIUpdate) {
244
270
  handleNotifyHMRClients(context.isHTMLUpdate);
@@ -287,7 +313,11 @@ const handleStartAppServer = async (options = {}) => {
287
313
  const handleNotifyHMRClients = async (indexHTMLChanged = false) => {
288
314
  try {
289
315
  if (process.hmrProcess) {
290
- const settings = await loadSettings({ environment: process.env.NODE_ENV });
316
+ const databaseProcessIds = getDatabaseProcessIds();
317
+ const settings = await loadSettings({
318
+ environment: process.env.NODE_ENV,
319
+ processIds: [...processIds, ...databaseProcessIds]
320
+ });
291
321
  process.hmrProcess.send(
292
322
  JSON.stringify({
293
323
  type: "RESTART_SERVER",
@@ -307,27 +337,31 @@ const getWatchChangeContext = (event = "", path2 = "") => {
307
337
  const isUIPath = path2?.includes("ui/") || path2 === "index.css" || isHTMLUpdate;
308
338
  const isUIUpdate = process.hmrProcess && process.hmrProcess.hasConnections && isUIPath || false;
309
339
  const isSettingsUpdate = path2?.match(SETTINGS_FILE_NAME_REGEX)?.length > 0;
310
- const isDirectory = fs.statSync(path2).isDirectory();
311
- const isFile = fs.statSync(path2).isFile();
312
- const isExistingDirectoryInSource = isDirectory && fs.existsSync(path2);
313
- const isExistingDirectoryInBuild = !!fs.existsSync(`./.joystick/build/${path2}`);
314
- const isAddDirectory = event === "addDir" && isExistingDirectoryInSource && !isExistingDirectoryInBuild;
340
+ const pathExists = fs.existsSync(path2);
341
+ const isDirectory = pathExists && fs.statSync(path2).isDirectory();
342
+ const isFile = pathExists && fs.statSync(path2).isFile();
343
+ const isExistingPathInSource = isDirectory && pathExists;
344
+ const isExistingPathInBuild = !!fs.existsSync(`./.joystick/build/${path2}`);
345
+ const isAddDirectory = event === "addDir" && isExistingPathInSource && !isExistingPathInBuild;
315
346
  const isFileToCopy = !!filesToCopy.find((fileToCopy) => fileToCopy.path === path2);
316
- const isExistingFileInSource = isFile && fs.existsSync(path2);
347
+ const isExistingFileInSource = isFile && pathExists;
317
348
  const isAddingOrChangingFile = ["add", "change"].includes(event) && isExistingFileInSource;
349
+ const isDeletingPath = ["unlink", "unlinkDir"].includes(event);
318
350
  return {
351
+ pathExists,
319
352
  isHTMLUpdate,
320
353
  isUIPath,
321
354
  isUIUpdate,
322
355
  isSettingsUpdate,
323
356
  isDirectory,
324
357
  isFile,
325
- isExistingDirectoryInSource,
326
- isExistingDirectoryInBuild,
358
+ isExistingPathInSource,
359
+ isExistingPathInBuild,
327
360
  isAddDirectory,
328
361
  isFileToCopy,
329
362
  isExistingFileInSource,
330
- isAddingOrChangingFile
363
+ isAddingOrChangingFile,
364
+ isDeletingPath
331
365
  };
332
366
  } catch (exception) {
333
367
  throw new Error(`[dev.getWatchChangeContext] ${exception.message}`);
@@ -349,10 +383,12 @@ const startFileWatcher = (options = {}) => {
349
383
  handleCopyFile(watchChangeContext, path2, options);
350
384
  handleAddDirectory(watchChangeContext, path2, options);
351
385
  await handleAddOrChangeFile(watchChangeContext, path2, options);
386
+ await handleDeletePath(watchChangeContext, path2, options);
352
387
  if (watchChangeContext?.isSettingsUpdate) {
388
+ const databaseProcessIds = getDatabaseProcessIds();
353
389
  await loadSettings({
354
390
  environment: options.environment,
355
- process: options.process
391
+ processIds: [...processIds, ...databaseProcessIds]
356
392
  });
357
393
  }
358
394
  });
@@ -470,6 +506,16 @@ const warnInvalidJoystickEnvironment = () => {
470
506
  throw new Error(`[dev.warnInvalidJoystickEnvironment] ${exception.message}`);
471
507
  }
472
508
  };
509
+ const handleCleanup = () => {
510
+ try {
511
+ if (process.cleanupProcess) {
512
+ const databaseProcessIds = getDatabaseProcessIds();
513
+ process.cleanupProcess.send(JSON.stringify({ processIds: [...processIds, ...databaseProcessIds] }));
514
+ }
515
+ } catch (exception) {
516
+ throw new Error(`[actionName.handleCleanup] ${exception.message}`);
517
+ }
518
+ };
473
519
  const validateOptions = (options) => {
474
520
  try {
475
521
  if (!options)
@@ -483,6 +529,18 @@ const validateOptions = (options) => {
483
529
  const dev = async (options, { resolve, reject }) => {
484
530
  try {
485
531
  validateOptions(options);
532
+ const port = parseInt(options?.port || 2600, 10);
533
+ const appPortOccupied = await checkIfPortOccupied(port);
534
+ const hmrPortOccupied = await checkIfPortOccupied(port + 1);
535
+ if (appPortOccupied) {
536
+ CLILog(`Port ${options?.port} is already occupied. To start Joystick on this port, clear it and try again.`, {
537
+ level: "danger"
538
+ });
539
+ process.exit(0);
540
+ }
541
+ if (hmrPortOccupied) {
542
+ await killPortProcess(port + 1);
543
+ }
486
544
  initProcess(options);
487
545
  const nodeMajorVersion = parseInt(
488
546
  process?.version?.split(".")[0]?.replace("v", ""),
@@ -1,7 +1,7 @@
1
1
  import fs from "fs";
2
2
  import CLILog from "../CLILog.js";
3
3
  import isValidJSONString from "../isValidJSONString.js";
4
- const warnIfInvalidJSONInSettings = (settings = "") => {
4
+ const warnIfInvalidJSONInSettings = (settings = "", processIds = []) => {
5
5
  try {
6
6
  const isValidJSON = isValidJSONString(settings);
7
7
  const context = process.env.NODE_ENV === "test" ? "test" : "start";
@@ -14,6 +14,9 @@ const warnIfInvalidJSONInSettings = (settings = "") => {
14
14
  tools: [{ title: "JSON Linter", url: "https://jsonlint.com/" }]
15
15
  }
16
16
  );
17
+ if (process.cleanupProcess) {
18
+ process.cleanupProcess.send(JSON.stringify({ processIds }));
19
+ }
17
20
  process.exit(0);
18
21
  }
19
22
  } catch (exception) {
@@ -27,7 +30,7 @@ const getSettings = (settingsPath = "") => {
27
30
  throw new Error(`[loadSettings.getSettings] ${exception.message}`);
28
31
  }
29
32
  };
30
- const warnIfSettingsNotFound = (settingsPath = "") => {
33
+ const warnIfSettingsNotFound = (settingsPath = "", processIds = []) => {
31
34
  try {
32
35
  const hasSettingsFile = fs.existsSync(settingsPath);
33
36
  const context = process.env.NODE_ENV === "test" ? "test" : "start";
@@ -39,6 +42,9 @@ const warnIfSettingsNotFound = (settingsPath = "") => {
39
42
  docs: `https://cheatcode.co/docs/joystick/cli/${context}`
40
43
  }
41
44
  );
45
+ if (process.cleanupProcess) {
46
+ process.cleanupProcess.send(JSON.stringify({ processIds }));
47
+ }
42
48
  process.exit(0);
43
49
  }
44
50
  } catch (exception) {
@@ -59,9 +65,9 @@ const loadSettings = (options, { resolve, reject }) => {
59
65
  try {
60
66
  validateOptions(options);
61
67
  const settingsPath = `${process.cwd()}/settings.${options.environment}.json`;
62
- warnIfSettingsNotFound(settingsPath);
68
+ warnIfSettingsNotFound(settingsPath, options.processIds);
63
69
  const settings = getSettings(settingsPath);
64
- warnIfInvalidJSONInSettings(settings);
70
+ warnIfInvalidJSONInSettings(settings, options.processIds);
65
71
  process.env.JOYSTICK_SETTINGS = settings;
66
72
  resolve({
67
73
  parsed: JSON.parse(settings),
@@ -1,3 +1,4 @@
1
+ const EXAMPLE_CODE_REGEX = new RegExp(/\<example\>[^%]+\<\/example\>/g);
1
2
  const EXPORT_DEFAULT_REGEX = new RegExp(/export default [a-zA-Z0-9]+/g);
2
3
  const JOYSTICK_COMMENT_REGEX = new RegExp(/\<\!\-\-(.|\n|\r)*?-->/g);
3
4
  const JOYSTICK_COMPONENT_REGEX = new RegExp(/\.component\(\{/g);
@@ -5,6 +6,7 @@ const JOYSTICK_UI_REGEX = new RegExp(/@joystick.js\/ui/g);
5
6
  const OBJECT_REGEX = new RegExp(/{([^;]*)}/g);
6
7
  const SETTINGS_FILE_NAME_REGEX = new RegExp(/settings.[a-zA-Z0-9]+.json/g);
7
8
  export {
9
+ EXAMPLE_CODE_REGEX,
8
10
  EXPORT_DEFAULT_REGEX,
9
11
  JOYSTICK_COMMENT_REGEX,
10
12
  JOYSTICK_COMPONENT_REGEX,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joystick.js/cli-canary",
3
- "version": "1.0.0-beta.74",
3
+ "version": "1.0.0-beta.75",
4
4
  "type": "module",
5
5
  "description": "CLI for the Joystick JavaScript framework.",
6
6
  "main": "development.js",
@@ -13,6 +13,11 @@ import onWarn from "./onWarn.js";
13
13
 
14
14
  const handleBuildException = (exception = {}, file = '') => {
15
15
  try {
16
+ console.warn({
17
+ exception,
18
+ file,
19
+ });
20
+
16
21
  const error = exception?.errors && exception?.errors[0];
17
22
  const snippet = fs.existsSync(file)
18
23
  ? getCodeFrame(file, {
@@ -33,7 +38,7 @@ const handleBuildException = (exception = {}, file = '') => {
33
38
 
34
39
  return snippet;
35
40
  } catch (exception) {
36
- throw new Error(`[actionName.handleBuildException] ${exception.message}`);
41
+ throw new Error(`[buildFiles.handleBuildException] ${exception.message}`);
37
42
  }
38
43
  };
39
44
 
@@ -42,19 +47,19 @@ const handleParseFilePathFromException = (exception = {}) => {
42
47
  const rawErrorMessage = exception?.message?.split(':');
43
48
  return rawErrorMessage[1] && rawErrorMessage[1]?.replace('\n', '') || '';
44
49
  } catch (exception) {
45
- throw new Error(`[actionName.handleParseFilePathFromException] ${exception.message}`);
50
+ throw new Error(`[buildFiles.handleParseFilePathFromException] ${exception.message}`);
46
51
  }
47
52
  };
48
53
 
49
54
  const handleBuildForNode = (nodePaths = [], options = {}) => {
50
55
  try {
51
56
  return esbuild.build({
57
+ allowOverwrite: true,
52
58
  platform: "node",
53
59
  format: "esm",
54
60
  bundle: false,
55
61
  entryPoints: nodePaths?.map((file) => file.path),
56
62
  entryNames: '[dir]/[name]',
57
- // TODO: Make sure we don't need outbase here so the paths map correctly.
58
63
  outdir: options?.outputPath || "./.joystick/build",
59
64
  outbase: './',
60
65
  define: {
@@ -74,6 +79,7 @@ const handleBuildForNode = (nodePaths = [], options = {}) => {
74
79
  const handleBuildForBrowser = (browserPaths = [], options = {}) => {
75
80
  try {
76
81
  return esbuild.build({
82
+ allowOverwrite: true,
77
83
  target: "es2020",
78
84
  platform: "browser",
79
85
  format: "esm",
@@ -5,11 +5,12 @@ import {
5
5
  JOYSTICK_UI_REGEX,
6
6
  EXPORT_DEFAULT_REGEX,
7
7
  JOYSTICK_COMPONENT_REGEX,
8
- JOYSTICK_COMMENT_REGEX,
8
+ JOYSTICK_COMMENT_REGEX, EXAMPLE_CODE_REGEX,
9
9
  } from "../regexes.js";
10
10
  import generateId from "../generateId.js";
11
11
  import getPlatformSafePath from "../getPlatformSafePath.js";
12
12
  import setComponentId from "./setComponentId.js";
13
+ import replaceExamples from "./replaceExamples.js";
13
14
 
14
15
  export default {
15
16
  bootstrapComponent: {
@@ -21,7 +22,7 @@ export default {
21
22
 
22
23
  build.onLoad({ filter: /\.js$/ }, (args = {}) => {
23
24
  try {
24
- const shouldSetSSRId = [getPlatformSafePath("ui/")].some(
25
+ const shouldSetComponentId = [getPlatformSafePath("ui/")].some(
25
26
  (bootstrapTarget) => {
26
27
  return args.path.includes(bootstrapTarget);
27
28
  }
@@ -46,7 +47,7 @@ export default {
46
47
  );
47
48
 
48
49
  if (
49
- shouldSetSSRId ||
50
+ shouldSetComponentId ||
50
51
  isLayoutComponent ||
51
52
  isPageComponent ||
52
53
  isEmailComponent
@@ -74,20 +75,20 @@ export default {
74
75
  return;
75
76
  }
76
77
 
77
- let contents = setComponentId(code)?.replace(
78
+ // NOTE: Replace any <example> bracketed code with a placeholder. This ensures
79
+ // that we don't accidentally include any mounting code in example code included
80
+ // as content on the page.
81
+ const examples = code.match(EXAMPLE_CODE_REGEX) || [];
82
+ let contents = replaceExamples(code);
83
+
84
+ contents = contents?.replace(
78
85
  /\.component\(\/\*\*\//g,
79
86
  ".component("
80
- );
81
-
82
- // contents = contents.replace(
83
- // "ui.component({",
84
- // `ui.component({\n _ssrId: '${ssrId}',`
85
- // );
87
+ );
86
88
 
87
89
  // NOTE: Remove any commented code inside of render() so it doesn't get rendered during SSR
88
90
  // or in the browser.
89
91
  contents = contents.replace(JOYSTICK_COMMENT_REGEX, "");
90
-
91
92
  const exportDefaultMatchParts =
92
93
  (exportDefaultMatch && exportDefaultMatch.split(" ")) || [];
93
94
  const componentName = exportDefaultMatchParts.pop();
@@ -134,6 +135,11 @@ export default {
134
135
  );
135
136
  }
136
137
 
138
+ for (let i = 0; i < examples?.length; i += 1) {
139
+ const exampleToRestore = examples[i];
140
+ contents = contents.replace(`%example:${i}%`, exampleToRestore);
141
+ }
142
+
137
143
  return {
138
144
  contents,
139
145
  loader: "js",
@@ -160,21 +166,32 @@ export default {
160
166
  const joystickUIMatches =
161
167
  file?.match(JOYSTICK_COMPONENT_REGEX) || [];
162
168
 
169
+ // NOTE: Replace any <example> bracketed code with a placeholder. This ensures
170
+ // that we don't accidentally add a componentId to any example code included
171
+ // as content on the page.
172
+ const examples = file.match(EXAMPLE_CODE_REGEX) || [];
173
+ let contents = replaceExamples(file);
174
+
163
175
  if (joystickUIMatches?.length > 0) {
164
- let contents = setComponentId(file)?.replace(
176
+ contents = setComponentId(contents)?.replace(
165
177
  /\.component\(\/\*\*\//g,
166
178
  ".component("
167
179
  );
180
+
181
+ for (let i = 0; i < examples?.length; i += 1) {
182
+ const exampleToRestore = examples[i];
183
+ contents = contents.replace(`%example:${i}%`, exampleToRestore);
184
+ }
168
185
 
169
186
  fs.writeFileSync(`${build?.initialOptions?.outdir}/${entryPoint}`, contents);
170
187
  }
171
- }
188
+ }
172
189
  }
173
190
 
174
191
  resolve();
175
192
  });
176
193
  });
177
- },
194
+ },
178
195
  },
179
196
  generateFileDependencyMap: {
180
197
  name: "generateFileDependencyMap",
@@ -0,0 +1,11 @@
1
+ import {EXAMPLE_CODE_REGEX} from "../regexes.js";
2
+
3
+ export default (code = '') => {
4
+ let exampleIndex = 0;
5
+ return code.replace(
6
+ EXAMPLE_CODE_REGEX,
7
+ () => {
8
+ return `%example:${exampleIndex++}%`;
9
+ }
10
+ );
11
+ };
@@ -27,7 +27,7 @@ export default (file = "") => {
27
27
  source: file.substring(
28
28
  part.index,
29
29
  nextPart ? nextPart.index : file.length
30
- ),
30
+ ),
31
31
  };
32
32
  });
33
33
 
@@ -45,7 +45,7 @@ export default (file = "") => {
45
45
  () => {
46
46
  return `.component({\n _componentId: '${componentId}',`;
47
47
  }
48
- );
48
+ );
49
49
 
50
50
  file = file.replace(component.source, tainted);
51
51
  }
@@ -0,0 +1,29 @@
1
+ import net from 'net';
2
+
3
+ const resetClient = (client = {}) => {
4
+ client.end();
5
+ client.destroy();
6
+ client.unref();
7
+ };
8
+
9
+ export default (port = 2600) => {
10
+ return new Promise((resolve) => {
11
+ const client = new net.Socket();
12
+
13
+ // NOTE: This is inverted. If we can connect, the port is occupied. If we can't, the port is free.
14
+ client.once('connect', () => {
15
+ resetClient(client);
16
+ resolve(true);
17
+ });
18
+
19
+ client.once('error', () => {
20
+ resetClient(client);
21
+ resolve(false);
22
+ });
23
+
24
+ client.connect({
25
+ port,
26
+ host: '127.0.0.1',
27
+ }, function() {});
28
+ });
29
+ };
@@ -42,6 +42,6 @@ export default (excludedPaths = [], context = null) => {
42
42
  });
43
43
  })
44
44
  .filter((path) => {
45
- return !fs.lstatSync(path).isDirectory();
45
+ return fs.existsSync(path) && !fs.lstatSync(path).isDirectory();
46
46
  });
47
47
  };
@@ -21,6 +21,8 @@ import { SETTINGS_FILE_NAME_REGEX } from "../regexes.js";
21
21
  import getCodependenciesForFile from "./getCodependenciesForFile.js";
22
22
  import removeDeletedDependenciesFromMap from "../build/removeDeletedDependenciesFromMap.js";
23
23
  import chalk from "chalk";
24
+ import checkIfPortOccupied from "../checkIfPortOccupied.js";
25
+ import {killPortProcess} from "kill-port-process";
24
26
 
25
27
  const processIds = [];
26
28
 
@@ -208,6 +210,33 @@ const handleServerProcessSTDIO = (options = {}) => {
208
210
  }
209
211
  };
210
212
 
213
+ const handleDeletePath = (context = {}, path = '', options = {}) => {
214
+ try {
215
+ if (context?.isDeletingPath) {
216
+ if (context.isExistingPathInBuild) {
217
+ const pathToUnlink = `./.joystick/build/${path}`;
218
+ const stats = fs.lstatSync(pathToUnlink);
219
+
220
+ if (stats.isDirectory()) {
221
+ fs.rmdirSync(pathToUnlink, { recursive: true });
222
+ }
223
+
224
+ if (stats.isFile()) {
225
+ fs.unlinkSync(pathToUnlink);
226
+ }
227
+ }
228
+
229
+ if (context.isUIUpdate) {
230
+ handleNotifyHMRClients(context.isHTMLUpdate);
231
+ } else {
232
+ handleRestartApplicationProcess(options);
233
+ }
234
+ }
235
+ } catch (exception) {
236
+ throw new Error(`[dev.handleDeletePath] ${exception.message}`);
237
+ }
238
+ };
239
+
211
240
  const handleAddOrChangeFile = async (context = {}, path = '', options = {}) => {
212
241
  try {
213
242
  if (context.isAddingOrChangingFile) {
@@ -253,6 +282,7 @@ const handleAddOrChangeFile = async (context = {}, path = '', options = {}) => {
253
282
  }
254
283
  }
255
284
  } catch (exception) {
285
+ console.warn(exception);
256
286
  throw new Error(`[dev.handleAddOrChangeFile] ${exception.message}`);
257
287
  }
258
288
  };
@@ -291,7 +321,7 @@ const handleCopyFile = (context = {}, path = '', options = {}) => {
291
321
 
292
322
  const handleCopyDirectory = (context = {}, path = '', options = {}) => {
293
323
  try {
294
- if (context.isFileToCopy && context.isDirectory && !context.isExistingDirectoryInBuild) {
324
+ if (context.isFileToCopy && context.isDirectory && !context.isExistingPathInBuild) {
295
325
  fs.mkdirSync(`./.joystick/build/${path}`);
296
326
 
297
327
  if (context.isUIUpdate) {
@@ -350,7 +380,12 @@ const handleStartAppServer = async (options = {}) => {
350
380
  const handleNotifyHMRClients = async (indexHTMLChanged = false) => {
351
381
  try {
352
382
  if (process.hmrProcess) {
353
- const settings = await loadSettings({ environment: process.env.NODE_ENV });
383
+ const databaseProcessIds = getDatabaseProcessIds();
384
+ const settings = await loadSettings({
385
+ environment: process.env.NODE_ENV,
386
+ processIds: [...processIds, ...databaseProcessIds],
387
+ });
388
+
354
389
  process.hmrProcess.send(
355
390
  JSON.stringify({
356
391
  type: "RESTART_SERVER",
@@ -371,28 +406,32 @@ const getWatchChangeContext = (event = '', path = '') => {
371
406
  const isUIPath = path?.includes("ui/") || path === 'index.css' || isHTMLUpdate;
372
407
  const isUIUpdate = (process.hmrProcess && process.hmrProcess.hasConnections && isUIPath) || false;
373
408
  const isSettingsUpdate = path?.match(SETTINGS_FILE_NAME_REGEX)?.length > 0;
374
- const isDirectory = fs.statSync(path).isDirectory();
375
- const isFile = fs.statSync(path).isFile();
376
- const isExistingDirectoryInSource = isDirectory && fs.existsSync(path);
377
- const isExistingDirectoryInBuild = !!fs.existsSync(`./.joystick/build/${path}`);
378
- const isAddDirectory = event === 'addDir' && isExistingDirectoryInSource && !isExistingDirectoryInBuild;
409
+ const pathExists = fs.existsSync(path);
410
+ const isDirectory = pathExists && fs.statSync(path).isDirectory();
411
+ const isFile = pathExists && fs.statSync(path).isFile();
412
+ const isExistingPathInSource = isDirectory && pathExists;
413
+ const isExistingPathInBuild = !!fs.existsSync(`./.joystick/build/${path}`);
414
+ const isAddDirectory = event === 'addDir' && isExistingPathInSource && !isExistingPathInBuild;
379
415
  const isFileToCopy = !!filesToCopy.find((fileToCopy) => fileToCopy.path === path);
380
- const isExistingFileInSource = isFile && fs.existsSync(path);
416
+ const isExistingFileInSource = isFile && pathExists;
381
417
  const isAddingOrChangingFile = ["add", "change"].includes(event) && isExistingFileInSource;
418
+ const isDeletingPath = ["unlink", "unlinkDir"].includes(event);
382
419
 
383
420
  return {
421
+ pathExists,
384
422
  isHTMLUpdate,
385
423
  isUIPath,
386
424
  isUIUpdate,
387
425
  isSettingsUpdate,
388
426
  isDirectory,
389
427
  isFile,
390
- isExistingDirectoryInSource,
391
- isExistingDirectoryInBuild,
428
+ isExistingPathInSource,
429
+ isExistingPathInBuild,
392
430
  isAddDirectory,
393
431
  isFileToCopy,
394
432
  isExistingFileInSource,
395
433
  isAddingOrChangingFile,
434
+ isDeletingPath,
396
435
  };
397
436
  } catch (exception) {
398
437
  throw new Error(`[dev.getWatchChangeContext] ${exception.message}`);
@@ -415,20 +454,19 @@ const startFileWatcher = (options = {}) => {
415
454
  }
416
455
 
417
456
  const watchChangeContext = getWatchChangeContext(event, path);
418
- //
419
- // console.log({
420
- // watchChangeContext,
421
- // });
422
- //
457
+
423
458
  handleCopyDirectory(watchChangeContext, path, options);
424
459
  handleCopyFile(watchChangeContext, path, options);
425
460
  handleAddDirectory(watchChangeContext, path, options);
461
+
426
462
  await handleAddOrChangeFile(watchChangeContext, path, options);
463
+ await handleDeletePath(watchChangeContext, path, options);
427
464
 
428
465
  if (watchChangeContext?.isSettingsUpdate) {
466
+ const databaseProcessIds = getDatabaseProcessIds();
429
467
  await loadSettings({
430
468
  environment: options.environment,
431
- process: options.process,
469
+ processIds: [...processIds, ...databaseProcessIds],
432
470
  });
433
471
  }
434
472
  });
@@ -567,6 +605,17 @@ const warnInvalidJoystickEnvironment = () => {
567
605
  }
568
606
  };
569
607
 
608
+ const handleCleanup = () => {
609
+ try {
610
+ if (process.cleanupProcess) {
611
+ const databaseProcessIds = getDatabaseProcessIds();
612
+ process.cleanupProcess.send(JSON.stringify(({ processIds: [...processIds, ...databaseProcessIds] })));
613
+ }
614
+ } catch (exception) {
615
+ throw new Error(`[actionName.handleCleanup] ${exception.message}`);
616
+ }
617
+ };
618
+
570
619
  const validateOptions = (options) => {
571
620
  try {
572
621
  if (!options) throw new Error('options object is required.');
@@ -579,6 +628,23 @@ const validateOptions = (options) => {
579
628
  const dev = async (options, { resolve, reject }) => {
580
629
  try {
581
630
  validateOptions(options);
631
+
632
+ const port = parseInt(options?.port || 2600, 10);
633
+ const appPortOccupied = await checkIfPortOccupied(port);
634
+ const hmrPortOccupied = await checkIfPortOccupied(port + 1);
635
+
636
+ if (appPortOccupied) {
637
+ CLILog(`Port ${options?.port} is already occupied. To start Joystick on this port, clear it and try again.`, {
638
+ level: 'danger',
639
+ });
640
+
641
+ process.exit(0);
642
+ }
643
+
644
+ if (hmrPortOccupied) {
645
+ await killPortProcess(port + 1);
646
+ }
647
+
582
648
  initProcess(options);
583
649
 
584
650
  const nodeMajorVersion = parseInt(
@@ -655,21 +721,6 @@ const dev = async (options, { resolve, reject }) => {
655
721
  process.exit(0);
656
722
  }
657
723
  }
658
-
659
- /*
660
- TODO:
661
-
662
- - [x] Make sure we're in a Joystick project.
663
- - [x] If environment === 'test', check that both a .joystick fo lder exists AND a /tests folder
664
- exists. If no /tests, log out docs on how to scaffold your tests.
665
- - [x] Load settings.<environment>.json.
666
- - [x] Start databases relative to options.environment (development|test) from settings.<environment>.json.
667
- - [x] Run the initial build
668
- - [x] Start the file watcher
669
- - [x] Start the app as normal from the build directory.
670
- - [ ] If environment === 'test', run the tests.
671
- - [ ] If environment === 'test', after tests, run process.exit(0).
672
- */
673
724
 
674
725
  resolve();
675
726
  } catch (exception) {
@@ -4,7 +4,7 @@ import fs from 'fs';
4
4
  import CLILog from "../CLILog.js";
5
5
  import isValidJSONString from "../isValidJSONString.js";
6
6
 
7
- const warnIfInvalidJSONInSettings = (settings = '') => {
7
+ const warnIfInvalidJSONInSettings = (settings = '', processIds = []) => {
8
8
  try {
9
9
  const isValidJSON = isValidJSONString(settings);
10
10
  const context = process.env.NODE_ENV === 'test' ? 'test' : 'start';
@@ -19,6 +19,10 @@ const warnIfInvalidJSONInSettings = (settings = '') => {
19
19
  }
20
20
  );
21
21
 
22
+ if (process.cleanupProcess) {
23
+ process.cleanupProcess.send(JSON.stringify(({ processIds })));
24
+ }
25
+
22
26
  process.exit(0);
23
27
  }
24
28
  } catch (exception) {
@@ -34,7 +38,7 @@ const getSettings = (settingsPath = '') => {
34
38
  }
35
39
  };
36
40
 
37
- const warnIfSettingsNotFound = (settingsPath = '') => {
41
+ const warnIfSettingsNotFound = (settingsPath = '', processIds = []) => {
38
42
  try {
39
43
  const hasSettingsFile = fs.existsSync(settingsPath);
40
44
  const context = process.env.NODE_ENV === 'test' ? 'test' : 'start';
@@ -48,6 +52,10 @@ const warnIfSettingsNotFound = (settingsPath = '') => {
48
52
  }
49
53
  );
50
54
 
55
+ if (process.cleanupProcess) {
56
+ process.cleanupProcess.send(JSON.stringify(({ processIds })));
57
+ }
58
+
51
59
  process.exit(0);
52
60
  }
53
61
  } catch (exception) {
@@ -69,9 +77,9 @@ const loadSettings = (options, { resolve, reject }) => {
69
77
  validateOptions(options);
70
78
 
71
79
  const settingsPath = `${process.cwd()}/settings.${options.environment}.json`;
72
- warnIfSettingsNotFound(settingsPath);
80
+ warnIfSettingsNotFound(settingsPath, options.processIds);
73
81
  const settings = getSettings(settingsPath);
74
- warnIfInvalidJSONInSettings(settings);
82
+ warnIfInvalidJSONInSettings(settings, options.processIds);
75
83
 
76
84
  process.env.JOYSTICK_SETTINGS = settings;
77
85
 
@@ -1,3 +1,4 @@
1
+ export const EXAMPLE_CODE_REGEX = new RegExp(/\<example\>[^%]+\<\/example\>/g);
1
2
  export const EXPORT_DEFAULT_REGEX = new RegExp(/export default [a-zA-Z0-9]+/g);
2
3
  export const JOYSTICK_COMMENT_REGEX = new RegExp(/\<\!\-\-(.|\n|\r)*?-->/g);
3
4
  export const JOYSTICK_COMPONENT_REGEX = new RegExp(/\.component\(\{/g);