@fedify/cli 2.0.0-pr.469.1873 → 2.0.0-pr.479.1900

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 (59) hide show
  1. package/deno.json +1 -1
  2. package/dist/deno.js +1 -1
  3. package/dist/imagerenderer.js +1 -1
  4. package/dist/init/action/configs.js +22 -13
  5. package/dist/init/action/const.js +10 -0
  6. package/dist/init/action/deps.js +26 -28
  7. package/dist/init/action/install.js +11 -13
  8. package/dist/init/action/mod.js +1 -1
  9. package/dist/init/action/notice.js +8 -19
  10. package/dist/init/action/patch.js +34 -28
  11. package/dist/init/action/precommand.js +7 -2
  12. package/dist/init/action/recommend.js +1 -1
  13. package/dist/init/action/set.js +1 -1
  14. package/dist/init/action/templates.js +2 -1
  15. package/dist/init/ask/kv.js +24 -13
  16. package/dist/init/ask/mq.js +26 -13
  17. package/dist/init/command.js +20 -3
  18. package/dist/init/lib.js +16 -10
  19. package/dist/init/mod.js +2 -1
  20. package/dist/init/templates/nitro/.env.test.tpl +1 -0
  21. package/dist/init/templates/nitro/nitro.config.ts.tpl +12 -3
  22. package/dist/init/test/action.js +15 -0
  23. package/dist/init/test/create.js +100 -0
  24. package/dist/init/test/fill.js +26 -0
  25. package/dist/init/test/lookup.js +189 -0
  26. package/dist/init/test/run.js +26 -0
  27. package/dist/init/test/utils.js +17 -0
  28. package/dist/init/webframeworks.js +35 -33
  29. package/dist/mod.js +4 -2
  30. package/dist/utils.js +75 -8
  31. package/package.json +5 -5
  32. package/src/init/action/configs.ts +56 -13
  33. package/src/init/action/const.ts +9 -0
  34. package/src/init/action/deps.ts +98 -30
  35. package/src/init/action/install.ts +17 -52
  36. package/src/init/action/notice.ts +12 -13
  37. package/src/init/action/patch.ts +49 -28
  38. package/src/init/action/precommand.ts +9 -2
  39. package/src/init/action/set.ts +2 -15
  40. package/src/init/action/templates.ts +3 -2
  41. package/src/init/action/utils.ts +1 -1
  42. package/src/init/ask/kv.ts +64 -21
  43. package/src/init/ask/mq.ts +69 -20
  44. package/src/init/command.ts +39 -1
  45. package/src/init/lib.ts +24 -12
  46. package/src/init/mod.ts +2 -1
  47. package/src/init/templates/nitro/.env.test.tpl +1 -0
  48. package/src/init/templates/nitro/nitro.config.ts.tpl +12 -3
  49. package/src/init/test/action.ts +25 -0
  50. package/src/init/test/create.ts +137 -0
  51. package/src/init/test/fill.ts +61 -0
  52. package/src/init/test/lookup.ts +253 -0
  53. package/src/init/test/run.ts +42 -0
  54. package/src/init/test/types.ts +34 -0
  55. package/src/init/test/utils.ts +21 -0
  56. package/src/init/types.ts +3 -3
  57. package/src/init/webframeworks.ts +39 -23
  58. package/src/mod.ts +10 -1
  59. package/src/utils.ts +128 -10
@@ -1,7 +1,7 @@
1
- import { apply, entries, forEach, pipe, pipeLazy, tap } from "@fxts/core";
1
+ import { always, apply, entries, map, pipe, pipeLazy, tap } from "@fxts/core";
2
2
  import { toMerged } from "es-toolkit";
3
3
  import { readFile } from "node:fs/promises";
4
- import { formatJson, merge, set } from "../../utils.ts";
4
+ import { formatJson, merge, replaceAll, set } from "../../utils.ts";
5
5
  import { createFile, throwUnlessNotExists } from "../lib.ts";
6
6
  import type { InitCommandData } from "../types.ts";
7
7
  import {
@@ -23,7 +23,8 @@ import { joinDir, stringifyEnvs } from "./utils.ts";
23
23
  * Handles both dry-run mode (recommending files) and actual file creation.
24
24
  * Orchestrates the entire file generation and writing process.
25
25
  *
26
- * @param data - The initialization command data containing project configuration
26
+ * @param data - The initialization command data containing project
27
+ * configuration
27
28
  * @returns A processed data object with files and JSONs ready for creation
28
29
  */
29
30
  export const patchFiles = (data: InitCommandData) =>
@@ -44,8 +45,9 @@ export const recommendPatchFiles = (data: InitCommandData) =>
44
45
 
45
46
  /**
46
47
  * Generates text-based files (TypeScript, environment files) for the project.
47
- * Creates federation configuration, logging setup, environment variables, and framework-specific files
48
- * by processing templates and combining them with project-specific data.
48
+ * Creates federation configuration, logging setup, environment variables, and
49
+ * framework-specific files by processing templates and combining them with
50
+ * project-specific data.
49
51
  *
50
52
  * @param data - The initialization command data
51
53
  * @returns A record of file paths to their string content
@@ -53,7 +55,7 @@ export const recommendPatchFiles = (data: InitCommandData) =>
53
55
  const getFiles = <
54
56
  T extends InitCommandData,
55
57
  >(data: T) => ({
56
- [data.initializer.federationFile.toString()]: loadFederation({
58
+ [data.initializer.federationFile]: loadFederation({
57
59
  imports: getImports(data),
58
60
  ...data,
59
61
  }),
@@ -64,8 +66,9 @@ const getFiles = <
64
66
 
65
67
  /**
66
68
  * Generates JSON configuration files based on the package manager type.
67
- * Creates different sets of configuration files for Deno vs Node.js/Bun environments,
68
- * including compiler configs, package manifests, and development tool configurations.
69
+ * Creates different sets of configuration files for Deno vs other environments,
70
+ * including compiler configs, package manifests, and development
71
+ * tool configurations.
69
72
  *
70
73
  * @param data - The initialization command data
71
74
  * @returns A record of file paths to their JSON object content
@@ -80,7 +83,9 @@ const getJsons = <
80
83
  [devToolConfigs["vscExtDeno"].path]: devToolConfigs["vscExtDeno"].data,
81
84
  }
82
85
  : {
83
- "tsconfig.json": loadTsConfig(data).data,
86
+ ...(data.initializer.compilerOptions
87
+ ? { "tsconfig.json": loadTsConfig(data).data }
88
+ : {}),
84
89
  "package.json": loadPackageJson(data).data,
85
90
  [devToolConfigs["biome"].path]: devToolConfigs["biome"].data,
86
91
  [devToolConfigs["vscSet"].path]: devToolConfigs["vscSet"].data,
@@ -88,9 +93,10 @@ const getJsons = <
88
93
  };
89
94
 
90
95
  /**
91
- * Handles dry-run mode by recommending files to be created without actually creating them.
92
- * Displays what files would be created and shows their content for user review.
93
- * This allows users to preview the initialization process before committing to it.
96
+ * Handles dry-run mode by recommending files to be created without actually
97
+ * creating them.
98
+ * Displays what files would be created and shows their content for user review,
99
+ * so users can preview the initialization process before committing to it.
94
100
  *
95
101
  * @param data - The initialization command data with files and JSONs prepared
96
102
  * @returns The processed data with recommendations displayed
@@ -106,7 +112,7 @@ const recommendFiles = (data: InitCommandWithFiles) =>
106
112
  );
107
113
 
108
114
  /**
109
- * Actually creates files on the filesystem during normal (non-dry-run) execution.
115
+ * Actually creates files on the filesystem during normal execution.
110
116
  * Merges text files and JSON files together and writes them to disk.
111
117
  * This performs the actual file system operations to initialize the project.
112
118
  *
@@ -126,8 +132,8 @@ interface InitCommandWithFiles extends InitCommandData {
126
132
  }
127
133
 
128
134
  /**
129
- * Higher-order function that processes all files with a given processing function.
130
- * Takes a processing function (either display or create) and applies it to all files
135
+ * Processes all files with a given processing function.
136
+ * Takes a processor (either display or create) and applies it to all files
131
137
  * in the target directory, handling path resolution and content patching.
132
138
  *
133
139
  * @param process - Function to process each file (either display or create)
@@ -140,19 +146,20 @@ const processAllFiles = (
140
146
  pipe(
141
147
  files,
142
148
  entries,
143
- forEach(
149
+ map(
144
150
  pipeLazy(
145
151
  joinDir(dir),
146
152
  apply(patchContent),
147
153
  apply(process),
148
154
  ),
149
155
  ),
156
+ Array.fromAsync,
150
157
  );
151
158
 
152
159
  /**
153
- * Patches file content by either merging JSON objects or appending text content.
154
- * Handles existing files by reading their current content and intelligently combining
155
- * it with new content based on the content type (JSON vs text).
160
+ * Patches file content by either merging JSON or appending text content.
161
+ * Handles existing files by reading their current content and intelligently
162
+ * combining it with new content based on the content type (JSON vs text).
156
163
  *
157
164
  * @param path - The file path to patch
158
165
  * @param content - The new content (either string or object)
@@ -173,13 +180,32 @@ async function patchContent(
173
180
  * Merges new JSON data with existing JSON content and formats the result.
174
181
  * Parses existing JSON content (if any) and deep merges it with new data,
175
182
  * then formats the result for consistent output.
183
+ * Supports JSONC (JSON with Comments) by removing comments before parsing.
176
184
  *
177
185
  * @param prev - The previous JSON content as string
178
186
  * @param data - The new data object to merge
179
187
  * @returns Formatted JSON string with merged content
180
188
  */
181
189
  const mergeJson = (prev: string, data: object): string =>
182
- pipe(prev ? JSON.parse(prev) : {}, merge(data), formatJson);
190
+ pipe(
191
+ prev ? JSON.parse(removeJsonComments(prev)) : {},
192
+ merge(data),
193
+ formatJson,
194
+ );
195
+
196
+ /**
197
+ * Removes single-line (//) and multi-line (/* *\/) comments from JSON string.
198
+ * This allows parsing JSONC (JSON with Comments) files.
199
+ *
200
+ * @param jsonString - The JSON string potentially containing comments
201
+ * @returns JSON string with comments removed
202
+ */
203
+ const removeJsonComments = (jsonString: string): string =>
204
+ pipe(
205
+ jsonString,
206
+ replaceAll(/\/\/.*$/gm, ""),
207
+ replaceAll(/\/\*[\s\S]*?\*\//g, ""),
208
+ );
183
209
 
184
210
  /**
185
211
  * Appends new text content to existing text content line by line.
@@ -202,11 +228,6 @@ const appendText = (prev: string, data: string) =>
202
228
  * @returns The file content as string, or empty string if file doesn't exist
203
229
  * @throws Error if file access fails for reasons other than file not existing
204
230
  */
205
- async function readFileIfExists(path: string): Promise<string> {
206
- try {
207
- return await readFile(path, "utf8");
208
- } catch (e) {
209
- throwUnlessNotExists(e);
210
- return "";
211
- }
212
- }
231
+ const readFileIfExists = (path: string): Promise<string> =>
232
+ readFile(path, "utf8")
233
+ .catch(pipeLazy(tap(throwUnlessNotExists), always("")));
@@ -1,4 +1,4 @@
1
- import { exit, runSubCommand } from "../../utils.ts";
1
+ import { CommandError, exit, runSubCommand } from "../../utils.ts";
2
2
  import type { InitCommandData } from "../types.ts";
3
3
 
4
4
  /**
@@ -15,7 +15,14 @@ const runPrecommand = ({
15
15
  cwd: dir,
16
16
  stdio: "inherit",
17
17
  }).catch((e) => {
18
- console.error("Failed to run the precommand:", e);
18
+ if (e instanceof CommandError) {
19
+ console.error("Failed to run the precommand.");
20
+ console.error("Command:", e.commandLine);
21
+ if (e.stderr) console.error("Error:", e.stderr);
22
+ if (e.stdout) console.error("Output:", e.stdout);
23
+ } else {
24
+ console.error("Failed to run the precommand:", e);
25
+ }
19
26
  exit(1);
20
27
  });
21
28
 
@@ -11,7 +11,6 @@ import type {
11
11
  KvStoreDescription,
12
12
  MessageQueue,
13
13
  MessageQueueDescription,
14
- PackageManager,
15
14
  } from "../types.ts";
16
15
  import webFrameworks from "../webframeworks.ts";
17
16
 
@@ -45,20 +44,8 @@ const setProjectName = set(
45
44
  );
46
45
 
47
46
  const setInitializer = set("initializer", <
48
- T extends {
49
- webFramework: keyof typeof webFrameworks;
50
- projectName: string;
51
- packageManager: PackageManager;
52
- },
53
- >({
54
- webFramework,
55
- projectName,
56
- packageManager,
57
- }: T) =>
58
- webFrameworks[webFramework].init(
59
- projectName,
60
- packageManager,
61
- ));
47
+ T extends InitCommandOptions & { projectName: string },
48
+ >(data: T) => webFrameworks[data.webFramework].init(data));
62
49
 
63
50
  const setKv = set("kv", <
64
51
  T extends { kvStore: KvStore },
@@ -81,6 +81,7 @@ export const getAlias = (imports: Record<string, string>) =>
81
81
  join(", "),
82
82
  );
83
83
 
84
+ const ENV_REG_EXP = /process\.env\.(\w+)/g;
84
85
  /**
85
86
  * Converts Node.js environment variable access to Deno-compatible syntax when needed.
86
87
  * Transforms `process.env.VAR_NAME` to `Deno.env.get("VAR_NAME")` for Deno projects.
@@ -90,6 +91,6 @@ export const getAlias = (imports: Record<string, string>) =>
90
91
  * @returns The converted object string with appropriate environment variable access syntax
91
92
  */
92
93
  export const convertEnv = (obj: string, pm: PackageManager) =>
93
- pm === "deno" && /process\.env\.(\w+)/.test(obj)
94
- ? obj.replaceAll(/process\.env\.(\w+)/, (_, g1) => `Deno.env.get("${g1}")`)
94
+ pm === "deno" && ENV_REG_EXP.test(obj)
95
+ ? obj.replaceAll(ENV_REG_EXP, (_, g1) => `Deno.env.get("${g1}")`)
95
96
  : obj;
@@ -6,7 +6,7 @@ export const isDry = ({ dryRun }: InitCommandData) => dryRun;
6
6
  export const hasCommand = (data: InitCommandData) => !!data.initializer.command;
7
7
 
8
8
  export const isDeno = (
9
- { packageManager }: InitCommandData,
9
+ { packageManager }: Pick<InitCommandData, "packageManager">,
10
10
  ) => packageManager === "deno";
11
11
 
12
12
  export const joinDir =
@@ -1,6 +1,8 @@
1
+ import { pipe, tap, throwError, unless, when } from "@fxts/core/index.js";
1
2
  import { select } from "@inquirer/prompts";
3
+ import { printErrorMessage } from "../../utils.ts";
2
4
  import { KV_STORE } from "../const.ts";
3
- import { kvStores } from "../lib.ts";
5
+ import { isTest, kvStores } from "../lib.ts";
4
6
  import type { KvStore, PackageManager } from "../types.ts";
5
7
 
6
8
  /**
@@ -11,29 +13,70 @@ import type { KvStore, PackageManager } from "../types.ts";
11
13
  * @returns A promise resolving to options with a guaranteed kvStore
12
14
  */
13
15
  const fillKvStore: //
14
- <T extends { kvStore?: KvStore; packageManager: PackageManager }>(
15
- options: T,
16
- ) => Promise<T & { kvStore: KvStore }> = async (options) => //
17
- ({
18
- ...options,
19
- kvStore: options.kvStore ?? await askKvStore(options.packageManager),
20
- });
16
+ <
17
+ T extends {
18
+ kvStore?: KvStore;
19
+ packageManager: PackageManager;
20
+ testMode: boolean;
21
+ },
22
+ >(options: T) => Promise<KvDefined<T>> //
23
+ = (options) =>
24
+ pipe(
25
+ options,
26
+ when(isKvStoreEmpty, askKvStore) as <
27
+ T extends { kvStore?: KvStore; packageManager: PackageManager },
28
+ >(options: T) => KvDefined<T>,
29
+ unless(
30
+ isKvSupportsPm,
31
+ (opt: KvDefined<typeof options>) =>
32
+ pipe(
33
+ opt,
34
+ when(isTest, throwError(unmatchedWhileTesting)),
35
+ tap(noticeUnmatched),
36
+ fillKvStore,
37
+ ),
38
+ ),
39
+ ) as Promise<KvDefined<typeof options>>;
21
40
 
22
41
  export default fillKvStore;
23
42
 
24
- const askKvStore = (pm: PackageManager) =>
25
- select<KvStore>({
43
+ type KvDefined<T> = Omit<T, "kvStore"> & { kvStore: KvStore };
44
+
45
+ const isKvStoreEmpty = <T extends { kvStore?: KvStore }>(
46
+ options: T,
47
+ ): options is T & { kvStore: undefined } => !options.kvStore;
48
+
49
+ const askKvStore = async <
50
+ T extends { packageManager: PackageManager },
51
+ >(data: T): Promise<Omit<T, "kvStore"> & { kvStore: KvStore }> => ({
52
+ ...data,
53
+ kvStore: await select<KvStore>({
26
54
  message: "Choose the key-value store to use",
27
- choices: KV_STORE.map(choiceKvStore(pm)),
28
- });
29
-
30
- const choiceKvStore = (pm: PackageManager) => (value: KvStore) => ({
31
- name: isKvSupportsPm(value, pm)
32
- ? value
33
- : `${value} (not supported with ${pm})`,
34
- value,
35
- disabled: !isKvSupportsPm(value, pm),
55
+ choices: KV_STORE.map(choiceKvStore(data.packageManager)),
56
+ }),
57
+ });
58
+
59
+ const unmatchedWhileTesting = <
60
+ T extends { kvStore: KvStore; packageManager: PackageManager },
61
+ >({ kvStore: kv, packageManager: pm }: T) =>
62
+ new Error(
63
+ `Key-value store '${kv}' is not compatible with package manager '${pm}'`,
64
+ );
65
+
66
+ const noticeUnmatched = <
67
+ T extends { kvStore: KvStore; packageManager: PackageManager },
68
+ >({ kvStore: kv, packageManager: pm }: T) =>
69
+ printErrorMessage`Error: Key-value store '${kv}' is not compatible with package manager '${pm}'`;
70
+
71
+ const choiceKvStore = (pm: PackageManager) => (kv: KvStore) => ({
72
+ name: isKvSupportsPm({ kvStore: kv, packageManager: pm })
73
+ ? kv
74
+ : `${kv} (not supported with ${pm})`,
75
+ value: kv,
76
+ disabled: !isKvSupportsPm({ kvStore: kv, packageManager: pm }),
36
77
  });
37
78
 
38
- const isKvSupportsPm = (kv: KvStore, pm: PackageManager) =>
39
- kvStores[kv].packageManagers.includes(pm);
79
+ const isKvSupportsPm = <
80
+ T extends { kvStore: KvStore; packageManager: PackageManager },
81
+ >({ kvStore, packageManager }: T) =>
82
+ kvStores[kvStore].packageManagers.includes(packageManager);
@@ -1,6 +1,8 @@
1
+ import { pipe, tap, throwError, unless, when } from "@fxts/core/index.js";
1
2
  import { select } from "@inquirer/prompts";
3
+ import { printErrorMessage } from "../../utils.ts";
2
4
  import { MESSAGE_QUEUE } from "../const.ts";
3
- import { messageQueues } from "../lib.ts";
5
+ import { isTest, messageQueues } from "../lib.ts";
4
6
  import type { MessageQueue, PackageManager } from "../types.ts";
5
7
 
6
8
  /**
@@ -11,27 +13,74 @@ import type { MessageQueue, PackageManager } from "../types.ts";
11
13
  * @returns A promise resolving to options with a guaranteed messageQueue
12
14
  */
13
15
  const fillMessageQueue: //
14
- <T extends { messageQueue?: MessageQueue; packageManager: PackageManager }> //
15
- (options: T) => Promise<T & { messageQueue: MessageQueue }> = //
16
- async (options) => ({
17
- ...options,
18
- messageQueue: options.messageQueue ??
19
- await askMessageQueue(options.packageManager),
20
- });
16
+ <
17
+ T extends {
18
+ messageQueue?: MessageQueue;
19
+ packageManager: PackageManager;
20
+ testMode: boolean;
21
+ },
22
+ >(options: T) => Promise<MqDefined<T>> //
23
+ = (options) =>
24
+ pipe(
25
+ options,
26
+ when(isMessageQueueEmpty, askMessageQueue) as <
27
+ T extends { messageQueue?: MessageQueue; packageManager: PackageManager },
28
+ >(options: T) => MqDefined<T>,
29
+ unless(
30
+ isMqSupportsPm,
31
+ (opt: MqDefined<typeof options>) =>
32
+ pipe(
33
+ opt,
34
+ when(isTest, throwError(unmatchedWhileTesting)),
35
+ tap(noticeUnmatched),
36
+ fillMessageQueue,
37
+ ),
38
+ ),
39
+ ) as Promise<MqDefined<typeof options>>;
21
40
 
22
41
  export default fillMessageQueue;
23
42
 
24
- const askMessageQueue = (pm: PackageManager) =>
25
- select<MessageQueue>({
43
+ type MqDefined<T> = Omit<T, "messageQueue"> & { messageQueue: MessageQueue };
44
+
45
+ const isMessageQueueEmpty = <T extends { messageQueue?: MessageQueue }>(
46
+ options: T,
47
+ ): options is T & { messageQueue: undefined } => !options.messageQueue;
48
+
49
+ const askMessageQueue = async <
50
+ T extends { packageManager: PackageManager },
51
+ >(
52
+ data: T,
53
+ ): Promise<Omit<T, "messageQueue"> & { messageQueue: MessageQueue }> => ({
54
+ ...data,
55
+ messageQueue: await select<MessageQueue>({
26
56
  message: "Choose the message queue to use",
27
- choices: MESSAGE_QUEUE.map(choiceMessageQueue(pm)),
28
- });
29
- const choiceMessageQueue = (pm: PackageManager) => (value: MessageQueue) => ({
30
- name: isMqSupportsPm(value, pm)
31
- ? value
32
- : `${value} (not supported with ${pm})`,
33
- value,
34
- disabled: !isMqSupportsPm(value, pm),
57
+ choices: MESSAGE_QUEUE.map(choiceMessageQueue(data.packageManager)),
58
+ }),
35
59
  });
36
- const isMqSupportsPm = (mq: MessageQueue, pm: PackageManager) =>
37
- messageQueues[mq].packageManagers.includes(pm);
60
+
61
+ const unmatchedWhileTesting = <
62
+ T extends { messageQueue: MessageQueue; packageManager: PackageManager },
63
+ >({ messageQueue: mq, packageManager: pm }: T) =>
64
+ new Error(
65
+ `Message queue '${mq}' is not compatible with package manager '${pm}'`,
66
+ );
67
+
68
+ const noticeUnmatched = <
69
+ T extends { messageQueue: MessageQueue; packageManager: PackageManager },
70
+ >({ messageQueue: mq, packageManager: pm }: T) => {
71
+ printErrorMessage`Error: Message queue '${mq}' is not compatible with package manager '${pm}'`;
72
+ };
73
+
74
+ const choiceMessageQueue =
75
+ (packageManager: PackageManager) => (messageQueue: MessageQueue) => ({
76
+ name: isMqSupportsPm({ messageQueue, packageManager })
77
+ ? messageQueue
78
+ : `${messageQueue} (not supported with ${packageManager})`,
79
+ value: messageQueue,
80
+ disabled: !isMqSupportsPm({ messageQueue, packageManager }),
81
+ });
82
+
83
+ const isMqSupportsPm = <
84
+ T extends { messageQueue: MessageQueue; packageManager: PackageManager },
85
+ >({ messageQueue, packageManager }: T) =>
86
+ messageQueues[messageQueue].packageManagers.includes(packageManager);
@@ -5,19 +5,20 @@ import {
5
5
  constant,
6
6
  type InferValue,
7
7
  message,
8
+ multiple,
8
9
  object,
9
10
  option,
10
11
  optional,
11
12
  optionNames,
12
13
  } from "@optique/core";
13
14
  import { path } from "@optique/run";
15
+ import { debugOption } from "../globals.ts";
14
16
  import {
15
17
  KV_STORE,
16
18
  MESSAGE_QUEUE,
17
19
  PACKAGE_MANAGER,
18
20
  WEB_FRAMEWORK,
19
21
  } from "./const.ts";
20
- import { debugOption } from "../globals.ts";
21
22
 
22
23
  const webFramework = optional(option(
23
24
  "-w",
@@ -53,6 +54,12 @@ const messageQueue = optional(option(
53
54
  description: message`The message queue to use for background tasks.`,
54
55
  },
55
56
  ));
57
+ const testMode = option(
58
+ "--test-mode",
59
+ {
60
+ description: message`The test mode to use for testing purposes.`,
61
+ },
62
+ );
56
63
 
57
64
  export const initCommand = command(
58
65
  "init",
@@ -70,6 +77,7 @@ export const initCommand = command(
70
77
  description: message`Perform a trial run with no changes made.`,
71
78
  }),
72
79
  debugOption,
80
+ testMode,
73
81
  }),
74
82
  {
75
83
  brief: message`Initialize a new Fedify project directory.`,
@@ -86,3 +94,33 @@ Unless you specify all options (${optionNames(["-w", "--web-framework"])}, ${
86
94
  );
87
95
 
88
96
  export type InitCommand = InferValue<typeof initCommand>;
97
+
98
+ export const testInitCommand = command(
99
+ "test-init",
100
+ object("Initialization options", {
101
+ command: constant("test-init"),
102
+ webFramework: multiple(webFramework),
103
+ packageManager: multiple(packageManager),
104
+ kvStore: multiple(kvStore),
105
+ messageQueue: multiple(messageQueue),
106
+ hydRun: option("-h", "--hyd-run", {
107
+ description: message`Test with files creations and installations.`,
108
+ }),
109
+ dryRun: option("-d", "--dry-run", {
110
+ description: message`Log outputs without creating files.`,
111
+ }),
112
+ debugOption,
113
+ }),
114
+ {
115
+ brief: message`Test an initializing command .`,
116
+ description: message`Test an initializing command on temporary directories.
117
+
118
+ Unless you specify all options (${optionNames(["-w", "--web-framework"])}, ${
119
+ optionNames(["-p", "--package-manager"])
120
+ }, ${optionNames(["-k", "--kv-store"])}, and ${
121
+ optionNames(["-m", "--message-queue"])
122
+ }), it will test all combinations of the options.`,
123
+ },
124
+ );
125
+
126
+ export type TestInitCommand = InferValue<typeof testInitCommand>;
package/src/init/lib.ts CHANGED
@@ -110,7 +110,8 @@ export const readTemplate: (templatePath: string) => string = (
110
110
 
111
111
  export const getInstruction: (
112
112
  packageManager: PackageManager,
113
- ) => Message = (pm) =>
113
+ port: number,
114
+ ) => Message = (pm, port) =>
114
115
  message`
115
116
  To start the server, run the following command:
116
117
 
@@ -118,11 +119,11 @@ To start the server, run the following command:
118
119
 
119
120
  Then, try look up an actor from your server:
120
121
 
121
- ${commandLine("fedify lookup http://localhost:8000/users/john")}
122
+ ${commandLine(`fedify lookup http://localhost:${port}/users/john`)}
122
123
 
123
124
  `;
124
125
 
125
- const getDevCommand = (pm: PackageManager) =>
126
+ export const getDevCommand = (pm: PackageManager) =>
126
127
  pm === "deno" ? "deno task dev" : pm === "bun" ? "bun dev" : `${pm} run dev`;
127
128
 
128
129
  async function isCommandAvailable(
@@ -172,20 +173,23 @@ export const isDirectoryEmpty = async (
172
173
  }
173
174
  };
174
175
 
176
+ /**
177
+ * Converts a package manager to its corresponding runtime.
178
+ * @param pm - The package manager (deno, bun, npm, yarn, pnpm)
179
+ * @returns The runtime name (deno, bun, or node)
180
+ */
181
+ export const packageManagerToRuntime = (
182
+ pm: PackageManager,
183
+ ): "deno" | "bun" | "node" =>
184
+ pm === "deno" ? "deno" : pm === "bun" ? "bun" : "node";
185
+
175
186
  export const getNextInitCommand = (
176
187
  pm: PackageManager,
177
- ): string[] => [
178
- ...createNextAppCommand(pm),
179
- ".",
180
- "--ts",
181
- "--app",
182
- "--biome",
183
- "--skip-install",
184
- ];
188
+ ): string[] => [...createNextAppCommand(pm), ".", "--yes"];
185
189
 
186
190
  const createNextAppCommand = (pm: PackageManager): string[] =>
187
191
  pm === "deno"
188
- ? ["deno", "run", "-A", "npm:create-next-app@latest"]
192
+ ? ["deno", "-Ar", "npm:create-next-app@latest"]
189
193
  : pm === "bun"
190
194
  ? ["bun", "create", "next-app"]
191
195
  : pm === "npm"
@@ -199,6 +203,10 @@ export const getNitroInitCommand = (
199
203
  pm === "deno" ? "npm:giget@latest" : "giget@latest",
200
204
  "nitro",
201
205
  ".",
206
+ "&&",
207
+ "rm",
208
+ "nitro.config.ts", // Remove default nitro config file
209
+ // This file will be created from template
202
210
  ];
203
211
 
204
212
  const createNitroAppCommand = (pm: PackageManager): string[] =>
@@ -209,3 +217,7 @@ const createNitroAppCommand = (pm: PackageManager): string[] =>
209
217
  : pm === "npm"
210
218
  ? ["npx"]
211
219
  : [pm, "dlx"];
220
+
221
+ export const isTest: <
222
+ T extends { testMode: boolean },
223
+ >({ testMode }: T) => boolean = ({ testMode }) => testMode;
package/src/init/mod.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export { default as runInit } from "./action/mod.ts";
2
- export { initCommand } from "./command.ts";
2
+ export { initCommand, testInitCommand } from "./command.ts";
3
+ export { default as runTestInit } from "./test/action.ts";
@@ -0,0 +1 @@
1
+ HOST=127.0.0.1
@@ -1,5 +1,14 @@
1
- // https://nitro.unjs.io/config
1
+ import { defineNitroConfig } from "nitropack/config"
2
+
3
+ // https://nitro.build/config
2
4
  export default defineNitroConfig({
5
+ errorHandler: "~/error",
6
+ esbuild: {
7
+ options: {
8
+ target: "esnext",
9
+ },
10
+ },
11
+ compatibilityDate: "latest",
3
12
  srcDir: "server",
4
- errorHandler: "~/error"
5
- });
13
+ imports: false
14
+ });
@@ -0,0 +1,25 @@
1
+ import { pipe, tap, when } from "@fxts/core";
2
+ import { set } from "../../utils.ts";
3
+ import type { TestInitCommand } from "../command.ts";
4
+ import { fillEmptyOptions } from "./fill.ts";
5
+ import { isDryRun, isHydRun, runTests } from "./run.ts";
6
+ import {
7
+ emptyTestDir,
8
+ genRunId,
9
+ genTestDirPrefix,
10
+ logTestDir,
11
+ } from "./utils.ts";
12
+
13
+ const runTestInit = (options: TestInitCommand) =>
14
+ pipe(
15
+ options,
16
+ set("runId", genRunId),
17
+ set("testDirPrefix", genTestDirPrefix),
18
+ tap(emptyTestDir),
19
+ fillEmptyOptions,
20
+ tap(when(isHydRun, runTests(false))),
21
+ tap(when(isDryRun, runTests(true))),
22
+ tap(logTestDir),
23
+ );
24
+
25
+ export default runTestInit;