@zhin.js/command 1.0.3 → 1.0.5

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.
@@ -133,11 +133,18 @@ export function isCommandIndex(value) {
133
133
  return !!value && typeof value === 'object'
134
134
  && value.$projection === 'zhin.command-index/1';
135
135
  }
136
+ /**
137
+ * 命令运行时名 = 插件树路径段(instanceKey,去掉 root)以 `.` 连接后,再与命令
138
+ * 文件路径首段以 `.` 连接;命令内部嵌套段仍为空格分隔。Root 插件无前缀。
139
+ * 例:`root/qq` + `endpoint/list` → `qq.endpoint list`;
140
+ * `root/b/a` + `foo` → `b.a.foo`;root + `foo` → `foo`。
141
+ */
136
142
  function runtimeSegments(owner, localName) {
137
- const ownerSegments = owner === 'root'
138
- ? []
139
- : owner.slice('root/'.length).split('/');
140
- return [...ownerSegments, ...localName.split('/')];
143
+ const localSegments = localName.split('/');
144
+ if (owner === 'root')
145
+ return localSegments;
146
+ const prefix = owner.slice('root/'.length).split('/').join('.');
147
+ return [`${prefix}.${localSegments[0]}`, ...localSegments.slice(1)];
141
148
  }
142
149
  function assertParameterSegment(segments, parameter, source) {
143
150
  const dynamicSegments = segments.filter((segment) => segment.startsWith('$'));
package/lib/definition.js CHANGED
@@ -140,14 +140,19 @@ function parseTarget(target) {
140
140
  const parts = target.split(':').filter(Boolean);
141
141
  if (parts.length < 2)
142
142
  return undefined;
143
- const kind = parts[0];
143
+ const [kind, ...rest] = parts;
144
+ if (!kind)
145
+ return undefined;
146
+ const lastPart = parts.at(-1);
147
+ if (!lastPart)
148
+ return undefined;
144
149
  if (kind === 'channel' && parts.length >= 3) {
145
- return { type: 'channel', id: parts[parts.length - 1] };
150
+ return { type: 'channel', id: lastPart };
146
151
  }
147
152
  if (kind === 'temp' && parts.length >= 3) {
148
- return { type: 'private', id: parts[parts.length - 1] };
153
+ return { type: 'private', id: lastPart };
149
154
  }
150
- return { type: kind, id: parts.slice(1).join(':') };
155
+ return { type: kind, id: rest.join(':') };
151
156
  }
152
157
  function isCommandScene(value) {
153
158
  if (!value || typeof value !== 'object')
package/lib/provider.js CHANGED
@@ -1,4 +1,4 @@
1
- import { basename, join, parse } from 'node:path';
1
+ import { basename, join, parse, sep } from 'node:path';
2
2
  import { featureId } from '@zhin.js/plugin-runtime';
3
3
  import { defineFeatureProvider, } from '@zhin.js/feature-kit';
4
4
  import { CommandIndex } from './command-index.js';
@@ -20,6 +20,23 @@ const commandFiles = {
20
20
  async function* discoverCommandDirectory(context, directory, ancestors) {
21
21
  const entries = [...await context.host.list(directory)]
22
22
  .sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
23
+ const files = entries.flatMap((entry) => {
24
+ if (entry.kind !== 'file')
25
+ return [];
26
+ const parsed = parseCommandFile(entry.name);
27
+ return parsed ? [{ entry, parsed }] : [];
28
+ });
29
+ const preferJavaScript = context.packageRoot
30
+ .split(sep)
31
+ .includes('node_modules');
32
+ const preferredFiles = new Map();
33
+ for (const { entry, parsed } of files) {
34
+ const current = preferredFiles.get(parsed.localSegment);
35
+ if (!current || commandFilePriority(entry.name, preferJavaScript)
36
+ < commandFilePriority(current, preferJavaScript)) {
37
+ preferredFiles.set(parsed.localSegment, entry.name);
38
+ }
39
+ }
23
40
  for (const entry of entries) {
24
41
  if (entry.kind === 'directory' && isCommandSegment(entry.name)) {
25
42
  yield* discoverCommandDirectory(context, join(directory, entry.name), [...ancestors, entry.name]);
@@ -30,6 +47,8 @@ async function* discoverCommandDirectory(context, directory, ancestors) {
30
47
  const file = parseCommandFile(entry.name);
31
48
  if (!file)
32
49
  continue;
50
+ if (preferredFiles.get(file.localSegment) !== entry.name)
51
+ continue;
33
52
  yield {
34
53
  localName: [...ancestors, file.localSegment].join('/'),
35
54
  source: join(directory, entry.name),
@@ -40,7 +59,7 @@ async function* discoverCommandDirectory(context, directory, ancestors) {
40
59
  function isCommandSegment(value) {
41
60
  return /^[a-z0-9][a-z0-9-]*$/.test(value);
42
61
  }
43
- const dynamicCommandFilePattern = /^\[([a-z][a-zA-Z0-9]*):([a-z][a-z0-9-]*)(?:=([^\]]*))?\]\.tsx?$/;
62
+ const dynamicCommandFilePattern = /^\[([a-z][a-zA-Z0-9]*):([a-z][a-z0-9-]*)(?:=([^\]]*))?\]\.(?:tsx?|[cm]?js)$/;
44
63
  const commandParameterTypes = new Set([
45
64
  'string',
46
65
  'number',
@@ -58,7 +77,7 @@ const commandParameterTypes = new Set([
58
77
  'rps',
59
78
  ]);
60
79
  function parseCommandFile(value) {
61
- if (/^[a-z0-9][a-z0-9-]*\.tsx?$/.test(value)) {
80
+ if (/^[a-z0-9][a-z0-9-]*\.(?:tsx?|[cm]?js)$/.test(value)) {
62
81
  return { localSegment: parse(value).name };
63
82
  }
64
83
  const match = dynamicCommandFilePattern.exec(value);
@@ -79,6 +98,14 @@ function parseCommandFile(value) {
79
98
  }
80
99
  return undefined;
81
100
  }
101
+ function commandFilePriority(value, preferJavaScript) {
102
+ const extension = value.slice(value.lastIndexOf('.') + 1);
103
+ const order = preferJavaScript
104
+ ? ['js', 'mjs', 'cjs', 'ts', 'tsx']
105
+ : ['ts', 'tsx', 'js', 'mjs', 'cjs'];
106
+ const priority = order.indexOf(extension);
107
+ return priority < 0 ? Number.MAX_SAFE_INTEGER : priority;
108
+ }
82
109
  function parseParameterValue(name, type, value, source) {
83
110
  if (type === 'string' || type === 'word' || type === 'text')
84
111
  return value;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/command",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Convention-based Command Feature for Zhin Plugin Runtime",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -18,11 +18,11 @@
18
18
  ],
19
19
  "dependencies": {
20
20
  "segment-matcher": "^1.0.5",
21
- "@zhin.js/feature-kit": "1.0.3",
21
+ "@zhin.js/feature-kit": "1.0.4",
22
22
  "@zhin.js/plugin-runtime": "1.1.1"
23
23
  },
24
24
  "devDependencies": {
25
- "@types/node": "^26.1.0",
25
+ "@types/node": "^26.1.2",
26
26
  "typescript": "^6.0.3"
27
27
  },
28
28
  "zhin": {
@@ -207,11 +207,17 @@ export function isCommandIndex(value: unknown): value is CommandIndex {
207
207
  && (value as { readonly $projection?: unknown }).$projection === 'zhin.command-index/1';
208
208
  }
209
209
 
210
+ /**
211
+ * 命令运行时名 = 插件树路径段(instanceKey,去掉 root)以 `.` 连接后,再与命令
212
+ * 文件路径首段以 `.` 连接;命令内部嵌套段仍为空格分隔。Root 插件无前缀。
213
+ * 例:`root/qq` + `endpoint/list` → `qq.endpoint list`;
214
+ * `root/b/a` + `foo` → `b.a.foo`;root + `foo` → `foo`。
215
+ */
210
216
  function runtimeSegments(owner: string, localName: string): string[] {
211
- const ownerSegments = owner === 'root'
212
- ? []
213
- : owner.slice('root/'.length).split('/');
214
- return [...ownerSegments, ...localName.split('/')];
217
+ const localSegments = localName.split('/');
218
+ if (owner === 'root') return localSegments;
219
+ const prefix = owner.slice('root/'.length).split('/').join('.');
220
+ return [`${prefix}.${localSegments[0]}`, ...localSegments.slice(1)];
215
221
  }
216
222
 
217
223
  function assertParameterSegment(
package/src/definition.ts CHANGED
@@ -312,14 +312,17 @@ function resolveRoles(
312
312
  function parseTarget(target: string): { readonly type: string; readonly id: string } | undefined {
313
313
  const parts = target.split(':').filter(Boolean);
314
314
  if (parts.length < 2) return undefined;
315
- const kind = parts[0];
315
+ const [kind, ...rest] = parts;
316
+ if (!kind) return undefined;
317
+ const lastPart = parts.at(-1);
318
+ if (!lastPart) return undefined;
316
319
  if (kind === 'channel' && parts.length >= 3) {
317
- return { type: 'channel', id: parts[parts.length - 1]! };
320
+ return { type: 'channel', id: lastPart };
318
321
  }
319
322
  if (kind === 'temp' && parts.length >= 3) {
320
- return { type: 'private', id: parts[parts.length - 1]! };
323
+ return { type: 'private', id: lastPart };
321
324
  }
322
- return { type: kind!, id: parts.slice(1).join(':') };
325
+ return { type: kind, id: rest.join(':') };
323
326
  }
324
327
 
325
328
  function isCommandScene(value: unknown): value is CommandScene {
package/src/provider.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { basename, join, parse } from 'node:path';
1
+ import { basename, join, parse, sep } from 'node:path';
2
2
  import { featureId } from '@zhin.js/plugin-runtime';
3
3
  import {
4
4
  defineFeatureProvider,
@@ -38,6 +38,22 @@ async function* discoverCommandDirectory(
38
38
  ): AsyncIterable<DiscoveredSource> {
39
39
  const entries = [...await context.host.list(directory)]
40
40
  .sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
41
+ const files = entries.flatMap((entry) => {
42
+ if (entry.kind !== 'file') return [];
43
+ const parsed = parseCommandFile(entry.name);
44
+ return parsed ? [{ entry, parsed }] : [];
45
+ });
46
+ const preferJavaScript = context.packageRoot
47
+ .split(sep)
48
+ .includes('node_modules');
49
+ const preferredFiles = new Map<string, string>();
50
+ for (const { entry, parsed } of files) {
51
+ const current = preferredFiles.get(parsed.localSegment);
52
+ if (!current || commandFilePriority(entry.name, preferJavaScript)
53
+ < commandFilePriority(current, preferJavaScript)) {
54
+ preferredFiles.set(parsed.localSegment, entry.name);
55
+ }
56
+ }
41
57
  for (const entry of entries) {
42
58
  if (entry.kind === 'directory' && isCommandSegment(entry.name)) {
43
59
  yield* discoverCommandDirectory(
@@ -50,6 +66,7 @@ async function* discoverCommandDirectory(
50
66
  if (entry.kind !== 'file') continue;
51
67
  const file = parseCommandFile(entry.name);
52
68
  if (!file) continue;
69
+ if (preferredFiles.get(file.localSegment) !== entry.name) continue;
53
70
  yield {
54
71
  localName: [...ancestors, file.localSegment].join('/'),
55
72
  source: join(directory, entry.name),
@@ -68,7 +85,7 @@ interface ParsedCommandFile {
68
85
  }
69
86
 
70
87
  const dynamicCommandFilePattern =
71
- /^\[([a-z][a-zA-Z0-9]*):([a-z][a-z0-9-]*)(?:=([^\]]*))?\]\.tsx?$/;
88
+ /^\[([a-z][a-zA-Z0-9]*):([a-z][a-z0-9-]*)(?:=([^\]]*))?\]\.(?:tsx?|[cm]?js)$/;
72
89
 
73
90
  const commandParameterTypes = new Set<CommandParameterType>([
74
91
  'string',
@@ -88,7 +105,7 @@ const commandParameterTypes = new Set<CommandParameterType>([
88
105
  ]);
89
106
 
90
107
  function parseCommandFile(value: string): ParsedCommandFile | undefined {
91
- if (/^[a-z0-9][a-z0-9-]*\.tsx?$/.test(value)) {
108
+ if (/^[a-z0-9][a-z0-9-]*\.(?:tsx?|[cm]?js)$/.test(value)) {
92
109
  return { localSegment: parse(value).name };
93
110
  }
94
111
  const match = dynamicCommandFilePattern.exec(value);
@@ -110,6 +127,15 @@ function parseCommandFile(value: string): ParsedCommandFile | undefined {
110
127
  return undefined;
111
128
  }
112
129
 
130
+ function commandFilePriority(value: string, preferJavaScript: boolean): number {
131
+ const extension = value.slice(value.lastIndexOf('.') + 1);
132
+ const order = preferJavaScript
133
+ ? ['js', 'mjs', 'cjs', 'ts', 'tsx']
134
+ : ['ts', 'tsx', 'js', 'mjs', 'cjs'];
135
+ const priority = order.indexOf(extension);
136
+ return priority < 0 ? Number.MAX_SAFE_INTEGER : priority;
137
+ }
138
+
113
139
  function parseParameterValue(
114
140
  name: string,
115
141
  type: CommandParameterType,