@optique/discover 1.2.0-dev.2169 → 1.2.0-dev.2180
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 +27 -0
- package/dist/chunk-CUT6urMc.cjs +30 -0
- package/dist/cli.cjs +144 -0
- package/dist/cli.d.cts +10 -0
- package/dist/cli.d.ts +10 -0
- package/dist/cli.js +143 -0
- package/dist/command.cjs +1 -1
- package/dist/command.d.ts +1 -1
- package/dist/generator-C12pIuFe.js +270 -0
- package/dist/generator-e4RwnDKG.cjs +288 -0
- package/dist/generator.cjs +7 -0
- package/dist/generator.d.cts +136 -0
- package/dist/generator.d.ts +136 -0
- package/dist/generator.js +5 -0
- package/dist/index.cjs +7 -780
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -751
- package/dist/main-check-CwunSNpK.cjs +60 -0
- package/dist/main-check-mMnKfUZs.js +48 -0
- package/dist/main-check.cjs +4 -0
- package/dist/main-check.d.cts +72 -0
- package/dist/main-check.d.ts +72 -0
- package/dist/main-check.js +3 -0
- package/dist/src-BYGxdHGJ.cjs +784 -0
- package/dist/src-Ci75oZo-.js +754 -0
- package/package.json +18 -3
- /package/dist/{command-B0lV6NBO.cjs → command-CUn2_NIA.cjs} +0 -0
- /package/dist/{command-DyiVIMUh.d.ts → command-DrmNW0HO.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -110,6 +110,33 @@ the module list visible to bundlers. For smaller registries, you can also
|
|
|
110
110
|
import commands manually, declare `path` in each `defineCommand()` call, and
|
|
111
111
|
pass those commands to `runProgram({ commands })`.
|
|
112
112
|
|
|
113
|
+
If you do not want to maintain the static module map by hand, generate one:
|
|
114
|
+
|
|
115
|
+
~~~~ bash
|
|
116
|
+
optique-discover ./commands --output ./commands.generated.ts --extension .ts
|
|
117
|
+
~~~~
|
|
118
|
+
|
|
119
|
+
Then import the generated module from your CLI entry point:
|
|
120
|
+
|
|
121
|
+
~~~~ typescript
|
|
122
|
+
// cli.ts
|
|
123
|
+
import { runProgram } from "@optique/discover";
|
|
124
|
+
import { message } from "@optique/core/message";
|
|
125
|
+
import commands from "./commands.generated.ts";
|
|
126
|
+
|
|
127
|
+
await runProgram({
|
|
128
|
+
commands,
|
|
129
|
+
metadata: {
|
|
130
|
+
name: "admin",
|
|
131
|
+
version: "1.0.0",
|
|
132
|
+
brief: message`Administrative command-line tools.`,
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
~~~~
|
|
136
|
+
|
|
137
|
+
Use `--watch` during development to regenerate only when command files are
|
|
138
|
+
added, removed, or renamed.
|
|
139
|
+
|
|
113
140
|
By default, Deno and Bun discover `.ts`, `.mts`, `.js`, and `.mjs` files.
|
|
114
141
|
Node.js discovers `.js`, `.mjs`, and `.cjs` files, plus `.ts`, `.mts`, and
|
|
115
142
|
`.cts` when it reports native TypeScript support or runs with a recognized
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
//#region rolldown:runtime
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
10
|
+
key = keys[i];
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
12
|
+
get: ((k) => from[k]).bind(null, key),
|
|
13
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
19
|
+
value: mod,
|
|
20
|
+
enumerable: true
|
|
21
|
+
}) : target, mod));
|
|
22
|
+
|
|
23
|
+
//#endregion
|
|
24
|
+
|
|
25
|
+
Object.defineProperty(exports, '__toESM', {
|
|
26
|
+
enumerable: true,
|
|
27
|
+
get: function () {
|
|
28
|
+
return __toESM;
|
|
29
|
+
}
|
|
30
|
+
});
|
package/dist/cli.cjs
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const require_chunk = require('./chunk-CUT6urMc.cjs');
|
|
3
|
+
require('./command-CUn2_NIA.cjs');
|
|
4
|
+
require('./src-BYGxdHGJ.cjs');
|
|
5
|
+
const require_generator = require('./generator-e4RwnDKG.cjs');
|
|
6
|
+
const require_main_check = require('./main-check-CwunSNpK.cjs');
|
|
7
|
+
const __optique_core_constructs = require_chunk.__toESM(require("@optique/core/constructs"));
|
|
8
|
+
const __optique_core_message = require_chunk.__toESM(require("@optique/core/message"));
|
|
9
|
+
const __optique_core_modifiers = require_chunk.__toESM(require("@optique/core/modifiers"));
|
|
10
|
+
const __optique_core_primitives = require_chunk.__toESM(require("@optique/core/primitives"));
|
|
11
|
+
const __optique_core_program = require_chunk.__toESM(require("@optique/core/program"));
|
|
12
|
+
const __optique_core_valueparser = require_chunk.__toESM(require("@optique/core/valueparser"));
|
|
13
|
+
const __optique_run = require_chunk.__toESM(require("@optique/run"));
|
|
14
|
+
const node_process = require_chunk.__toESM(require("node:process"));
|
|
15
|
+
|
|
16
|
+
//#region deno.json
|
|
17
|
+
var name = "@optique/discover";
|
|
18
|
+
var version = "1.2.0-dev.2180+f59f99d2";
|
|
19
|
+
var license = "MIT";
|
|
20
|
+
var exports$1 = {
|
|
21
|
+
".": "./src/index.ts",
|
|
22
|
+
"./command": "./src/command.ts",
|
|
23
|
+
"./generator": "./src/generator.ts",
|
|
24
|
+
"./cli": "./src/cli.ts"
|
|
25
|
+
};
|
|
26
|
+
var imports = { "#src/": "./src/" };
|
|
27
|
+
var exclude = [
|
|
28
|
+
"dist/",
|
|
29
|
+
"node_modules",
|
|
30
|
+
"tsdown.config.ts"
|
|
31
|
+
];
|
|
32
|
+
var tasks = {
|
|
33
|
+
"build": "pnpm build",
|
|
34
|
+
"test": "deno test --allow-read --allow-write --allow-env",
|
|
35
|
+
"test:node": {
|
|
36
|
+
"dependencies": ["build"],
|
|
37
|
+
"command": "node --test"
|
|
38
|
+
},
|
|
39
|
+
"test:bun": {
|
|
40
|
+
"dependencies": ["build"],
|
|
41
|
+
"command": "bun test"
|
|
42
|
+
},
|
|
43
|
+
"test-all": { "dependencies": [
|
|
44
|
+
"test",
|
|
45
|
+
"test:node",
|
|
46
|
+
"test:bun"
|
|
47
|
+
] }
|
|
48
|
+
};
|
|
49
|
+
var deno_default = {
|
|
50
|
+
name,
|
|
51
|
+
version,
|
|
52
|
+
license,
|
|
53
|
+
exports: exports$1,
|
|
54
|
+
imports,
|
|
55
|
+
exclude,
|
|
56
|
+
tasks
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
//#endregion
|
|
60
|
+
//#region src/cli.ts
|
|
61
|
+
const EXIT_GENERATION_FAILED = 1;
|
|
62
|
+
const EXIT_INVALID_OPTIONS = 2;
|
|
63
|
+
const cliProgram = (0, __optique_core_program.defineProgram)({
|
|
64
|
+
parser: (0, __optique_core_constructs.object)({
|
|
65
|
+
dir: (0, __optique_core_primitives.argument)((0, __optique_core_valueparser.string)({ metavar: "DIR" }), { description: __optique_core_message.message`Directory containing command modules.` }),
|
|
66
|
+
outputFile: (0, __optique_core_primitives.option)("-o", "--output", (0, __optique_core_valueparser.string)({ metavar: "FILE" }), { description: __optique_core_message.message`Generated TypeScript module path.` }),
|
|
67
|
+
base: (0, __optique_core_modifiers.optional)((0, __optique_core_primitives.option)("--base", (0, __optique_core_valueparser.string)({ metavar: "PATH" }), { description: __optique_core_message.message`Module map base path passed to commandsFromModules().
|
|
68
|
+
Defaults to the command directory relative to the generated module.` })),
|
|
69
|
+
extensions: (0, __optique_core_modifiers.withDefault)((0, __optique_core_modifiers.multiple)((0, __optique_core_primitives.option)("--extension", (0, __optique_core_valueparser.string)({ metavar: "EXT" }))), []),
|
|
70
|
+
entryFileName: (0, __optique_core_modifiers.optional)((0, __optique_core_primitives.option)("--entry-file-name", (0, __optique_core_valueparser.string)({ metavar: "NAME" }), { description: __optique_core_message.message`File name that maps to the containing command path.
|
|
71
|
+
Defaults to commandsFromModules() behavior.` })),
|
|
72
|
+
noEntryFileName: (0, __optique_core_primitives.option)("--no-entry-file-name", { description: __optique_core_message.message`Treat entry files as ordinary command names.` }),
|
|
73
|
+
watch: (0, __optique_core_primitives.option)("--watch", { description: __optique_core_message.message`Regenerate when command files are added, removed,
|
|
74
|
+
or renamed.` })
|
|
75
|
+
}),
|
|
76
|
+
metadata: {
|
|
77
|
+
name: "optique-discover",
|
|
78
|
+
version: deno_default.version,
|
|
79
|
+
brief: __optique_core_message.message`Generate static command modules for @optique/discover`
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
/**
|
|
83
|
+
* Runs the optique-discover command-line interface.
|
|
84
|
+
*
|
|
85
|
+
* @returns A promise that resolves after generation or watch mode finishes.
|
|
86
|
+
* @since 1.2.0
|
|
87
|
+
*/
|
|
88
|
+
async function main() {
|
|
89
|
+
const args = (0, __optique_run.runSync)(cliProgram, {
|
|
90
|
+
help: "option",
|
|
91
|
+
version: {
|
|
92
|
+
value: deno_default.version,
|
|
93
|
+
option: true
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
if (args.noEntryFileName && args.entryFileName != null) {
|
|
97
|
+
(0, __optique_run.printError)(__optique_core_message.message`Use either --entry-file-name or --no-entry-file-name, not both.`, { exitCode: EXIT_INVALID_OPTIONS });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const options = {
|
|
101
|
+
dir: args.dir,
|
|
102
|
+
outputFile: args.outputFile,
|
|
103
|
+
...args.base != null ? { base: args.base } : {},
|
|
104
|
+
...args.extensions.length > 0 ? { extensions: args.extensions } : {},
|
|
105
|
+
...args.noEntryFileName ? { entryFileName: false } : args.entryFileName != null ? { entryFileName: args.entryFileName } : {}
|
|
106
|
+
};
|
|
107
|
+
try {
|
|
108
|
+
if (args.watch) await require_generator.watchCommandsModule({
|
|
109
|
+
...options,
|
|
110
|
+
onGenerate(result) {
|
|
111
|
+
writeGeneratedMessage(result.files.length);
|
|
112
|
+
},
|
|
113
|
+
onError(error) {
|
|
114
|
+
printGenerationError(error);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
else {
|
|
118
|
+
const result = await require_generator.writeCommandsModule(options);
|
|
119
|
+
writeGeneratedMessage(result.files.length);
|
|
120
|
+
}
|
|
121
|
+
} catch (error) {
|
|
122
|
+
printGenerationError(error, EXIT_GENERATION_FAILED);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function printGenerationError(error, exitCode) {
|
|
126
|
+
const messageText = error instanceof Error ? error.message : String(error);
|
|
127
|
+
if (exitCode == null) (0, __optique_run.printError)(__optique_core_message.message`Failed to generate command module: ${messageText}`);
|
|
128
|
+
else (0, __optique_run.printError)(__optique_core_message.message`Failed to generate command module: ${messageText}`, { exitCode });
|
|
129
|
+
}
|
|
130
|
+
function writeGeneratedMessage(count) {
|
|
131
|
+
const noun = count === 1 ? "command module" : "command modules";
|
|
132
|
+
console.log(`Generated ${count} ${noun}.`);
|
|
133
|
+
}
|
|
134
|
+
const isMain = require_main_check.isMainModuleUrl({
|
|
135
|
+
importMetaMain: "main" in import.meta ? void 0 : void 0,
|
|
136
|
+
moduleUrl: require("url").pathToFileURL(__filename).href,
|
|
137
|
+
argvEntry: node_process.default.argv[1]
|
|
138
|
+
});
|
|
139
|
+
if (isMain) main().catch((error) => {
|
|
140
|
+
printGenerationError(error, EXIT_GENERATION_FAILED);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
//#endregion
|
|
144
|
+
exports.main = main;
|
package/dist/cli.d.cts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region src/cli.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Runs the optique-discover command-line interface.
|
|
4
|
+
*
|
|
5
|
+
* @returns A promise that resolves after generation or watch mode finishes.
|
|
6
|
+
* @since 1.2.0
|
|
7
|
+
*/
|
|
8
|
+
declare function main(): Promise<void>;
|
|
9
|
+
//#endregion
|
|
10
|
+
export { main };
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region src/cli.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Runs the optique-discover command-line interface.
|
|
4
|
+
*
|
|
5
|
+
* @returns A promise that resolves after generation or watch mode finishes.
|
|
6
|
+
* @since 1.2.0
|
|
7
|
+
*/
|
|
8
|
+
declare function main(): Promise<void>;
|
|
9
|
+
//#endregion
|
|
10
|
+
export { main };
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "./command-DO5zgkvS.js";
|
|
3
|
+
import "./src-Ci75oZo-.js";
|
|
4
|
+
import { watchCommandsModule, writeCommandsModule } from "./generator-C12pIuFe.js";
|
|
5
|
+
import { isMainModuleUrl } from "./main-check-mMnKfUZs.js";
|
|
6
|
+
import { object } from "@optique/core/constructs";
|
|
7
|
+
import { message } from "@optique/core/message";
|
|
8
|
+
import { multiple, optional, withDefault } from "@optique/core/modifiers";
|
|
9
|
+
import { argument, option } from "@optique/core/primitives";
|
|
10
|
+
import { defineProgram } from "@optique/core/program";
|
|
11
|
+
import { string } from "@optique/core/valueparser";
|
|
12
|
+
import { printError, runSync } from "@optique/run";
|
|
13
|
+
import process from "node:process";
|
|
14
|
+
|
|
15
|
+
//#region deno.json
|
|
16
|
+
var name = "@optique/discover";
|
|
17
|
+
var version = "1.2.0-dev.2180+f59f99d2";
|
|
18
|
+
var license = "MIT";
|
|
19
|
+
var exports = {
|
|
20
|
+
".": "./src/index.ts",
|
|
21
|
+
"./command": "./src/command.ts",
|
|
22
|
+
"./generator": "./src/generator.ts",
|
|
23
|
+
"./cli": "./src/cli.ts"
|
|
24
|
+
};
|
|
25
|
+
var imports = { "#src/": "./src/" };
|
|
26
|
+
var exclude = [
|
|
27
|
+
"dist/",
|
|
28
|
+
"node_modules",
|
|
29
|
+
"tsdown.config.ts"
|
|
30
|
+
];
|
|
31
|
+
var tasks = {
|
|
32
|
+
"build": "pnpm build",
|
|
33
|
+
"test": "deno test --allow-read --allow-write --allow-env",
|
|
34
|
+
"test:node": {
|
|
35
|
+
"dependencies": ["build"],
|
|
36
|
+
"command": "node --test"
|
|
37
|
+
},
|
|
38
|
+
"test:bun": {
|
|
39
|
+
"dependencies": ["build"],
|
|
40
|
+
"command": "bun test"
|
|
41
|
+
},
|
|
42
|
+
"test-all": { "dependencies": [
|
|
43
|
+
"test",
|
|
44
|
+
"test:node",
|
|
45
|
+
"test:bun"
|
|
46
|
+
] }
|
|
47
|
+
};
|
|
48
|
+
var deno_default = {
|
|
49
|
+
name,
|
|
50
|
+
version,
|
|
51
|
+
license,
|
|
52
|
+
exports,
|
|
53
|
+
imports,
|
|
54
|
+
exclude,
|
|
55
|
+
tasks
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
//#endregion
|
|
59
|
+
//#region src/cli.ts
|
|
60
|
+
const EXIT_GENERATION_FAILED = 1;
|
|
61
|
+
const EXIT_INVALID_OPTIONS = 2;
|
|
62
|
+
const cliProgram = defineProgram({
|
|
63
|
+
parser: object({
|
|
64
|
+
dir: argument(string({ metavar: "DIR" }), { description: message`Directory containing command modules.` }),
|
|
65
|
+
outputFile: option("-o", "--output", string({ metavar: "FILE" }), { description: message`Generated TypeScript module path.` }),
|
|
66
|
+
base: optional(option("--base", string({ metavar: "PATH" }), { description: message`Module map base path passed to commandsFromModules().
|
|
67
|
+
Defaults to the command directory relative to the generated module.` })),
|
|
68
|
+
extensions: withDefault(multiple(option("--extension", string({ metavar: "EXT" }))), []),
|
|
69
|
+
entryFileName: optional(option("--entry-file-name", string({ metavar: "NAME" }), { description: message`File name that maps to the containing command path.
|
|
70
|
+
Defaults to commandsFromModules() behavior.` })),
|
|
71
|
+
noEntryFileName: option("--no-entry-file-name", { description: message`Treat entry files as ordinary command names.` }),
|
|
72
|
+
watch: option("--watch", { description: message`Regenerate when command files are added, removed,
|
|
73
|
+
or renamed.` })
|
|
74
|
+
}),
|
|
75
|
+
metadata: {
|
|
76
|
+
name: "optique-discover",
|
|
77
|
+
version: deno_default.version,
|
|
78
|
+
brief: message`Generate static command modules for @optique/discover`
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
/**
|
|
82
|
+
* Runs the optique-discover command-line interface.
|
|
83
|
+
*
|
|
84
|
+
* @returns A promise that resolves after generation or watch mode finishes.
|
|
85
|
+
* @since 1.2.0
|
|
86
|
+
*/
|
|
87
|
+
async function main() {
|
|
88
|
+
const args = runSync(cliProgram, {
|
|
89
|
+
help: "option",
|
|
90
|
+
version: {
|
|
91
|
+
value: deno_default.version,
|
|
92
|
+
option: true
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
if (args.noEntryFileName && args.entryFileName != null) {
|
|
96
|
+
printError(message`Use either --entry-file-name or --no-entry-file-name, not both.`, { exitCode: EXIT_INVALID_OPTIONS });
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const options = {
|
|
100
|
+
dir: args.dir,
|
|
101
|
+
outputFile: args.outputFile,
|
|
102
|
+
...args.base != null ? { base: args.base } : {},
|
|
103
|
+
...args.extensions.length > 0 ? { extensions: args.extensions } : {},
|
|
104
|
+
...args.noEntryFileName ? { entryFileName: false } : args.entryFileName != null ? { entryFileName: args.entryFileName } : {}
|
|
105
|
+
};
|
|
106
|
+
try {
|
|
107
|
+
if (args.watch) await watchCommandsModule({
|
|
108
|
+
...options,
|
|
109
|
+
onGenerate(result) {
|
|
110
|
+
writeGeneratedMessage(result.files.length);
|
|
111
|
+
},
|
|
112
|
+
onError(error) {
|
|
113
|
+
printGenerationError(error);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
else {
|
|
117
|
+
const result = await writeCommandsModule(options);
|
|
118
|
+
writeGeneratedMessage(result.files.length);
|
|
119
|
+
}
|
|
120
|
+
} catch (error) {
|
|
121
|
+
printGenerationError(error, EXIT_GENERATION_FAILED);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function printGenerationError(error, exitCode) {
|
|
125
|
+
const messageText = error instanceof Error ? error.message : String(error);
|
|
126
|
+
if (exitCode == null) printError(message`Failed to generate command module: ${messageText}`);
|
|
127
|
+
else printError(message`Failed to generate command module: ${messageText}`, { exitCode });
|
|
128
|
+
}
|
|
129
|
+
function writeGeneratedMessage(count) {
|
|
130
|
+
const noun = count === 1 ? "command module" : "command modules";
|
|
131
|
+
console.log(`Generated ${count} ${noun}.`);
|
|
132
|
+
}
|
|
133
|
+
const isMain = isMainModuleUrl({
|
|
134
|
+
importMetaMain: "main" in import.meta ? import.meta.main : void 0,
|
|
135
|
+
moduleUrl: import.meta.url,
|
|
136
|
+
argvEntry: process.argv[1]
|
|
137
|
+
});
|
|
138
|
+
if (isMain) main().catch((error) => {
|
|
139
|
+
printGenerationError(error, EXIT_GENERATION_FAILED);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
//#endregion
|
|
143
|
+
export { main };
|
package/dist/command.cjs
CHANGED
package/dist/command.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { AnyCommand, AnyStaticCommand, Command, CommandDefinition, CommandMetadata, CommandPath, StaticCommand, defineCommand, isCommand } from "./command-
|
|
1
|
+
import { AnyCommand, AnyStaticCommand, Command, CommandDefinition, CommandMetadata, CommandPath, StaticCommand, defineCommand, isCommand } from "./command-DrmNW0HO.js";
|
|
2
2
|
export { AnyCommand, AnyStaticCommand, Command, CommandDefinition, CommandMetadata, CommandPath, StaticCommand, defineCommand, isCommand };
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { getDefaultExtensions } from "./src-Ci75oZo-.js";
|
|
2
|
+
import { mkdir, readdir, realpath, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, posix, relative, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
//#region src/generator.ts
|
|
7
|
+
/**
|
|
8
|
+
* Generates a TypeScript module that exports command entries.
|
|
9
|
+
*
|
|
10
|
+
* @param options Generation options.
|
|
11
|
+
* @returns The generated source code and command module metadata.
|
|
12
|
+
* @throws {TypeError} If options are invalid or no command modules are found.
|
|
13
|
+
* @since 1.2.0
|
|
14
|
+
*/
|
|
15
|
+
async function generateCommandsModule(options) {
|
|
16
|
+
const normalized = normalizeGenerateOptions(options);
|
|
17
|
+
const files = await collectGeneratedCommandFiles(normalized);
|
|
18
|
+
return generateCommandsModuleFromFiles(normalized, files);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Writes a generated command module to disk.
|
|
22
|
+
*
|
|
23
|
+
* @param options Generation options.
|
|
24
|
+
* @returns The generated source code and command module metadata.
|
|
25
|
+
* @throws {TypeError} If options are invalid or no command modules are found.
|
|
26
|
+
* @throws {Error} If the generated module cannot be written.
|
|
27
|
+
* @since 1.2.0
|
|
28
|
+
*/
|
|
29
|
+
async function writeCommandsModule(options) {
|
|
30
|
+
const result = await generateCommandsModule(options);
|
|
31
|
+
await mkdir(dirname(pathFromFile(options.outputFile)), { recursive: true });
|
|
32
|
+
await writeFile(pathFromFile(options.outputFile), result.code, "utf-8");
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Watches the command file set and rewrites the generated module when it
|
|
37
|
+
* changes.
|
|
38
|
+
*
|
|
39
|
+
* Content-only edits do not trigger regeneration because they do not change
|
|
40
|
+
* the static module map.
|
|
41
|
+
*
|
|
42
|
+
* @param options Watch options.
|
|
43
|
+
* @returns A promise that resolves when the watch signal is aborted.
|
|
44
|
+
* @throws {TypeError} If options are invalid.
|
|
45
|
+
* @since 1.2.0
|
|
46
|
+
*/
|
|
47
|
+
async function watchCommandsModule(options) {
|
|
48
|
+
const normalized = normalizeGenerateOptions(options);
|
|
49
|
+
const intervalMs = options.intervalMs ?? 250;
|
|
50
|
+
if (!Number.isInteger(intervalMs) || intervalMs < 1) throw new TypeError(`Watch interval must be a positive integer: ${intervalMs}`);
|
|
51
|
+
let previousSignature;
|
|
52
|
+
while (options.signal?.aborted !== true) {
|
|
53
|
+
try {
|
|
54
|
+
const files = await collectGeneratedCommandFiles(normalized, { allowEmpty: true });
|
|
55
|
+
const signature = files.map((file) => file.filePath).join("\0");
|
|
56
|
+
if (signature !== previousSignature) {
|
|
57
|
+
const result = generateCommandsModuleFromFiles(normalized, files);
|
|
58
|
+
await mkdir(dirname(normalized.outputFile), { recursive: true });
|
|
59
|
+
await writeFile(normalized.outputFile, result.code, "utf-8");
|
|
60
|
+
options.onGenerate?.(result);
|
|
61
|
+
previousSignature = signature;
|
|
62
|
+
}
|
|
63
|
+
} catch (error) {
|
|
64
|
+
options.onError?.(error);
|
|
65
|
+
}
|
|
66
|
+
await delay(intervalMs, options.signal);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async function collectGeneratedCommandFiles(options, collectOptions = {}) {
|
|
70
|
+
const filePaths = await collectCommandFiles(options.dir, options.extensions, options.outputFile);
|
|
71
|
+
if (filePaths.length < 1) {
|
|
72
|
+
if (collectOptions.allowEmpty === true) return [];
|
|
73
|
+
throw new TypeError(`No command modules found in ${options.dir}.`);
|
|
74
|
+
}
|
|
75
|
+
const files = filePaths.map((filePath, index) => {
|
|
76
|
+
const importSpecifier = relativeImportSpecifier(options.outputDir, filePath);
|
|
77
|
+
const defaultModulePath = relativeModuleSpecifier(options.outputDir, filePath);
|
|
78
|
+
const modulePath = options.baseWasProvided ? joinModulePath(options.base, normalizeRelativePath(relative(options.dir, filePath))) : defaultModulePath;
|
|
79
|
+
return {
|
|
80
|
+
filePath,
|
|
81
|
+
importSpecifier,
|
|
82
|
+
modulePath,
|
|
83
|
+
identifier: `cmd${index}`
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
rejectDuplicateCommandPaths(files, options);
|
|
87
|
+
return files;
|
|
88
|
+
}
|
|
89
|
+
function generateCommandsModuleFromFiles(options, files) {
|
|
90
|
+
if (files.length < 1) return {
|
|
91
|
+
code: "import type { ModuleCommand } from \"@optique/discover\";\n\nexport default [] satisfies readonly ModuleCommand[];\n",
|
|
92
|
+
files
|
|
93
|
+
};
|
|
94
|
+
const imports = files.map(formatCommandModuleImport).join("\n");
|
|
95
|
+
const entries = files.map((file) => ` ${JSON.stringify(file.modulePath)}: ${file.identifier},`).join("\n");
|
|
96
|
+
const optionEntries = [
|
|
97
|
+
` base: ${JSON.stringify(options.base)},`,
|
|
98
|
+
` extensions: ${formatStringArray(options.extensions)},`,
|
|
99
|
+
...options.entryFileName === void 0 ? [] : [` entryFileName: ${JSON.stringify(options.entryFileName)},`]
|
|
100
|
+
].join("\n");
|
|
101
|
+
return {
|
|
102
|
+
code: `import { commandsFromModules } from "@optique/discover";\n${imports}\n\nexport default commandsFromModules(\n {\n${entries}\n },\n {\n${optionEntries}\n },\n);\n`,
|
|
103
|
+
files
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function formatCommandModuleImport(file) {
|
|
107
|
+
const importSpecifier = JSON.stringify(file.importSpecifier);
|
|
108
|
+
const importDeclaration = `import * as ${file.identifier} from ${importSpecifier};`;
|
|
109
|
+
if (requiresTypeScriptResolutionIgnore(file.importSpecifier)) return `// @ts-ignore: Percent-encoded import preserves URL-significant command paths.\n${importDeclaration}`;
|
|
110
|
+
return importDeclaration;
|
|
111
|
+
}
|
|
112
|
+
function requiresTypeScriptResolutionIgnore(specifier) {
|
|
113
|
+
return specifier.includes("%");
|
|
114
|
+
}
|
|
115
|
+
function normalizeGenerateOptions(options) {
|
|
116
|
+
const dir = resolve(pathFromFile(options.dir));
|
|
117
|
+
const outputFile = resolve(pathFromFile(options.outputFile));
|
|
118
|
+
const outputDir = dirname(outputFile);
|
|
119
|
+
const baseWasProvided = options.base !== void 0;
|
|
120
|
+
const base = normalizeBase(options.base ?? relativeModuleSpecifier(outputDir, dir));
|
|
121
|
+
return {
|
|
122
|
+
dir,
|
|
123
|
+
outputFile,
|
|
124
|
+
outputDir,
|
|
125
|
+
base,
|
|
126
|
+
baseWasProvided,
|
|
127
|
+
extensions: normalizeExtensions(options.extensions ?? getDefaultExtensions()),
|
|
128
|
+
entryFileName: normalizeEntryFileName(options.entryFileName)
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function pathFromFile(path) {
|
|
132
|
+
return typeof path === "string" ? path : fileURLToPath(path);
|
|
133
|
+
}
|
|
134
|
+
function normalizeBase(base) {
|
|
135
|
+
if (typeof base !== "string" || base.length < 1) throw new TypeError(`Module base path must be a non-empty string: ${base}`);
|
|
136
|
+
return normalizeModulePath(base);
|
|
137
|
+
}
|
|
138
|
+
function normalizeExtensions(extensions) {
|
|
139
|
+
if (extensions.length < 1) throw new TypeError("At least one command file extension is required.");
|
|
140
|
+
const normalized = [];
|
|
141
|
+
for (const extension of extensions) {
|
|
142
|
+
if (!extension.startsWith(".") || extension.length < 2) throw new TypeError(`Command file extension must start with a dot: ${extension}`);
|
|
143
|
+
if (!normalized.includes(extension)) normalized.push(extension);
|
|
144
|
+
}
|
|
145
|
+
return normalized.toSorted((a, b) => b.length - a.length || a.localeCompare(b));
|
|
146
|
+
}
|
|
147
|
+
function normalizeEntryFileName(entryFileName) {
|
|
148
|
+
if (entryFileName === void 0) return void 0;
|
|
149
|
+
if (entryFileName === false) return false;
|
|
150
|
+
if (typeof entryFileName !== "string" || entryFileName.length < 1 || entryFileName.includes("/") || entryFileName.includes("\\")) throw new TypeError(`Command entry file name must be a non-empty file name: ${entryFileName}`);
|
|
151
|
+
return entryFileName;
|
|
152
|
+
}
|
|
153
|
+
async function collectCommandFiles(dir, extensions, excludedFile, activeDirs = /* @__PURE__ */ new Set()) {
|
|
154
|
+
const canonicalDir = await realpath(dir);
|
|
155
|
+
if (activeDirs.has(canonicalDir)) return [];
|
|
156
|
+
const nextActiveDirs = new Set(activeDirs);
|
|
157
|
+
nextActiveDirs.add(canonicalDir);
|
|
158
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
159
|
+
const files = [];
|
|
160
|
+
for (const entry of entries.toSorted((a, b) => a.name.localeCompare(b.name))) {
|
|
161
|
+
const path = resolve(dir, entry.name);
|
|
162
|
+
const entryType = await getCommandFileEntryType(path, entry);
|
|
163
|
+
if (entryType === "directory") files.push(...await collectCommandFiles(path, extensions, excludedFile, nextActiveDirs));
|
|
164
|
+
else if (entryType === "file" && path !== excludedFile && !isDeclarationFile(entry.name) && extensions.some((ext) => entry.name.endsWith(ext))) files.push(path);
|
|
165
|
+
}
|
|
166
|
+
return files;
|
|
167
|
+
}
|
|
168
|
+
async function getCommandFileEntryType(path, entry) {
|
|
169
|
+
if (entry.isDirectory()) return "directory";
|
|
170
|
+
if (entry.isFile()) return "file";
|
|
171
|
+
if (!entry.isSymbolicLink()) return void 0;
|
|
172
|
+
try {
|
|
173
|
+
const target = await stat(path);
|
|
174
|
+
if (target.isDirectory()) return "directory";
|
|
175
|
+
if (target.isFile()) return "file";
|
|
176
|
+
return void 0;
|
|
177
|
+
} catch {
|
|
178
|
+
return void 0;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function isDeclarationFile(fileName) {
|
|
182
|
+
return /\.d\.[cm]?ts$/.test(fileName);
|
|
183
|
+
}
|
|
184
|
+
function rejectDuplicateCommandPaths(files, options) {
|
|
185
|
+
const entryFileName = options.entryFileName ?? "index";
|
|
186
|
+
const seen = /* @__PURE__ */ new Map();
|
|
187
|
+
for (const file of files) {
|
|
188
|
+
const commandPath = commandPathFromModulePath(options.base, file.modulePath, options.extensions, entryFileName);
|
|
189
|
+
const key = commandPath.join("\0");
|
|
190
|
+
const previous = seen.get(key);
|
|
191
|
+
if (previous != null) throw new TypeError(`Duplicate command path "${displayCommandPath(commandPath)}" from ${previous} and ${file.modulePath}.`);
|
|
192
|
+
seen.set(key, file.modulePath);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function commandPathFromModulePath(base, modulePath, extensions, entryFileName) {
|
|
196
|
+
const withoutExtension = stripCommandExtension(modulePath, extensions);
|
|
197
|
+
const relativePath = relativeModulePath(base, withoutExtension, modulePath);
|
|
198
|
+
const path = relativePath.split("/").filter((segment) => segment.length > 0);
|
|
199
|
+
return commandPathFromSegments(path, modulePath, entryFileName);
|
|
200
|
+
}
|
|
201
|
+
function stripCommandExtension(path, extensions) {
|
|
202
|
+
const matchedExtension = extensions.find((ext) => path.endsWith(ext));
|
|
203
|
+
if (matchedExtension == null) throw new TypeError(`No configured extension matches ${path}.`);
|
|
204
|
+
return path.slice(0, -matchedExtension.length);
|
|
205
|
+
}
|
|
206
|
+
function relativeModulePath(base, modulePath, originalModulePath) {
|
|
207
|
+
const normalizedBase = normalizeDerivedModulePath(base);
|
|
208
|
+
const normalizedPath = normalizeDerivedModulePath(modulePath);
|
|
209
|
+
const relativePath = posix.relative(normalizedBase, normalizedPath);
|
|
210
|
+
if (relativePath.length < 1 || relativePath === ".." || relativePath.startsWith("../") || posix.isAbsolute(relativePath)) throw new TypeError(`Module path ${originalModulePath} is not under base path ${base}.`);
|
|
211
|
+
return relativePath;
|
|
212
|
+
}
|
|
213
|
+
function commandPathFromSegments(path, source, entryFileName) {
|
|
214
|
+
if (path.length < 1) throw new TypeError(`Command module ${source} does not define a path.`);
|
|
215
|
+
if (entryFileName !== false && path[path.length - 1] === entryFileName) return path.slice(0, -1);
|
|
216
|
+
return path;
|
|
217
|
+
}
|
|
218
|
+
function displayCommandPath(path) {
|
|
219
|
+
return path.length < 1 ? "<root>" : path.join(" ");
|
|
220
|
+
}
|
|
221
|
+
function relativeModuleSpecifier(fromDir, target) {
|
|
222
|
+
const relativePath = normalizeRelativePath(relative(fromDir, target));
|
|
223
|
+
return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
|
|
224
|
+
}
|
|
225
|
+
function relativeImportSpecifier(fromDir, target) {
|
|
226
|
+
const specifier = relativeModuleSpecifier(fromDir, target);
|
|
227
|
+
if (requiresEncodedImportSpecifier(specifier)) return encodeImportSpecifier(specifier);
|
|
228
|
+
return specifier;
|
|
229
|
+
}
|
|
230
|
+
function requiresEncodedImportSpecifier(specifier) {
|
|
231
|
+
return specifier.includes("#") || specifier.includes("?") || specifier.includes("%");
|
|
232
|
+
}
|
|
233
|
+
function encodeImportSpecifier(specifier) {
|
|
234
|
+
return specifier.split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
235
|
+
}
|
|
236
|
+
function normalizeRelativePath(path) {
|
|
237
|
+
return path.replaceAll("\\", "/");
|
|
238
|
+
}
|
|
239
|
+
function normalizeModulePath(path) {
|
|
240
|
+
return path.replaceAll("\\", "/");
|
|
241
|
+
}
|
|
242
|
+
function normalizeDerivedModulePath(path) {
|
|
243
|
+
return posix.normalize(normalizeModulePath(path));
|
|
244
|
+
}
|
|
245
|
+
function joinModulePath(base, relativePath) {
|
|
246
|
+
if (base === "." || base === "./") return `./${relativePath}`;
|
|
247
|
+
const prefix = base.endsWith("/") ? base.slice(0, -1) : base;
|
|
248
|
+
return `${prefix}/${relativePath}`;
|
|
249
|
+
}
|
|
250
|
+
function formatStringArray(values) {
|
|
251
|
+
return `[${values.map((value) => JSON.stringify(value)).join(", ")}]`;
|
|
252
|
+
}
|
|
253
|
+
function delay(ms, signal) {
|
|
254
|
+
if (signal?.aborted === true) return Promise.resolve();
|
|
255
|
+
return new Promise((resolve$1) => {
|
|
256
|
+
const onTimeout = () => {
|
|
257
|
+
signal?.removeEventListener("abort", onAbort);
|
|
258
|
+
resolve$1();
|
|
259
|
+
};
|
|
260
|
+
const onAbort = () => {
|
|
261
|
+
clearTimeout(timeout);
|
|
262
|
+
resolve$1();
|
|
263
|
+
};
|
|
264
|
+
const timeout = setTimeout(onTimeout, ms);
|
|
265
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
//#endregion
|
|
270
|
+
export { generateCommandsModule, watchCommandsModule, writeCommandsModule };
|