create-halo-plugin-template 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.
package/README.md CHANGED
@@ -25,11 +25,13 @@
25
25
 
26
26
  ```bash
27
27
  node scripts/create-project.mjs \
28
- --plugin-name hello-world \
28
+ --plugin-name todo \
29
29
  --base-package com.example.helloworld \
30
- --display-name "Hello World" \
30
+ --display-name "Todo" \
31
31
  --author-name "Your Name" \
32
- --target-dir ../hello-world \
32
+ --route-prefix /plugin-todo \
33
+ --permission-prefix plugin:plugin-todo \
34
+ --target-dir ../plugin-todo \
33
35
  --install \
34
36
  --build
35
37
  ```
@@ -48,9 +50,9 @@ node scripts/create-project.mjs \
48
50
 
49
51
  ```bash
50
52
  npm create halo-plugin-template@latest -- \
51
- --plugin-name hello-world \
53
+ --plugin-name todo \
52
54
  --base-package com.example.helloworld \
53
- --display-name "Hello World" \
55
+ --display-name "Todo" \
54
56
  --author-name "Your Name"
55
57
  ```
56
58
 
@@ -93,13 +95,15 @@ npm run release:prepare -- --bump patch --push
93
95
 
94
96
  ```bash
95
97
  node scripts/init-template.mjs \
96
- --plugin-name hello-world \
98
+ --plugin-name todo \
97
99
  --base-package com.example.helloworld \
98
- --display-name "Hello World" \
100
+ --display-name "Todo" \
99
101
  --author-name "Your Name" \
102
+ --route-prefix /plugin-todo \
103
+ --permission-prefix plugin:plugin-todo \
100
104
  --author-website "https://github.com/your-name" \
101
105
  --repo-owner your-name \
102
- --description "Hello World - Halo 插件"
106
+ --description "Todo - Halo 插件"
103
107
  ```
104
108
 
105
109
  这个脚本是一次性的。执行后会连同自身模板常量一起改写,确保生成出来的项目里不再残留模板占位符。
@@ -108,10 +112,12 @@ node scripts/init-template.mjs \
108
112
 
109
113
  ```bash
110
114
  node scripts/verify-template.mjs \
111
- --plugin-name hello-world \
115
+ --plugin-name todo \
112
116
  --base-package com.example.helloworld \
113
- --display-name "Hello World" \
114
- --author-name "Your Name"
117
+ --display-name "Todo" \
118
+ --author-name "Your Name" \
119
+ --route-prefix /plugin-todo \
120
+ --permission-prefix plugin:plugin-todo
115
121
  ```
116
122
 
117
123
  然后安装前端依赖并启动开发环境:
package/build.gradle CHANGED
@@ -58,14 +58,16 @@ tasks.named('generatePluginComponentsIdx') {
58
58
  notCompatibleWithConfigurationCache('This task invokes "Task.project" at execution time, which is not supported with configuration cache.')
59
59
  }
60
60
 
61
- tasks.register('verifyTemplateConsistency', Exec) {
62
- group = 'verification'
63
- description = 'Verify the starter repository or initialized plugin stays internally consistent'
64
- commandLine 'node', 'scripts/verify-template.mjs'
65
- }
61
+ if (file('scripts/verify-template.mjs').exists()) {
62
+ tasks.register('verifyTemplateConsistency', Exec) {
63
+ group = 'verification'
64
+ description = 'Verify the starter repository or initialized plugin stays internally consistent'
65
+ commandLine 'node', 'scripts/verify-template.mjs'
66
+ }
66
67
 
67
- tasks.named('check') {
68
- dependsOn tasks.named('verifyTemplateConsistency')
68
+ tasks.named('check') {
69
+ dependsOn tasks.named('verifyTemplateConsistency')
70
+ }
69
71
  }
70
72
 
71
73
  halo {
@@ -76,16 +78,17 @@ haloPlugin {
76
78
  openApi {
77
79
  outputDir = file("$rootDir/api-docs/openapi/v3_0")
78
80
  groupingRules {
79
- templateApis {
81
+ plugintemplateApis {
80
82
  displayName = 'Extension API for halo-plugin-template'
81
83
  pathsToMatch = [
82
- '/apis/console.halo-plugin-template.halo.run/v1alpha1/**',
83
- '/apis/uc.halo-plugin-template.halo.run/v1alpha1/**'
84
+ '/apis/console.plugintemplate.halo.run/v1alpha1/**',
85
+ '/apis/uc.plugintemplate.halo.run/v1alpha1/**',
86
+ '/apis/public.plugintemplate.halo.run/v1alpha1/**'
84
87
  ]
85
88
  }
86
89
  }
87
90
  groupedApiMappings = [
88
- '/v3/api-docs/templateApis': 'templateApis.json'
91
+ '/v3/api-docs/plugintemplateApis': 'plugintemplateApis.json'
89
92
  ]
90
93
  generator {
91
94
  outputDir = file("${projectDir}/ui/src/api/generated")
@@ -51,8 +51,8 @@ npm view create-halo-plugin-template version
51
51
 
52
52
  ```bash
53
53
  npm create halo-plugin-template@latest -- \
54
- --plugin-name hello-world \
54
+ --plugin-name todo \
55
55
  --base-package com.example.helloworld \
56
- --display-name "Hello World" \
56
+ --display-name "Todo" \
57
57
  --author-name "Your Name"
58
58
  ```
@@ -15,9 +15,9 @@
15
15
 
16
16
  ```bash
17
17
  npm create halo-plugin-template@latest -- \
18
- --plugin-name hello-world \
18
+ --plugin-name todo \
19
19
  --base-package com.example.helloworld \
20
- --display-name "Hello World" \
20
+ --display-name "Todo" \
21
21
  --author-name "Your Name"
22
22
  ```
23
23
 
@@ -31,9 +31,9 @@ npm create halo-plugin-template@latest -- \
31
31
 
32
32
  ```bash
33
33
  npm init @your-scope/halo-plugin-template@latest -- \
34
- --plugin-name hello-world \
34
+ --plugin-name todo \
35
35
  --base-package com.example.helloworld \
36
- --display-name "Hello World" \
36
+ --display-name "Todo" \
37
37
  --author-name "Your Name"
38
38
  ```
39
39
 
@@ -31,10 +31,12 @@
31
31
 
32
32
  ```bash
33
33
  node scripts/verify-template.mjs \
34
- --plugin-name hello-world \
34
+ --plugin-name todo \
35
35
  --base-package com.example.helloworld \
36
- --display-name "Hello World" \
37
- --author-name "Your Name"
36
+ --display-name "Todo" \
37
+ --author-name "Your Name" \
38
+ --route-prefix /plugin-todo \
39
+ --permission-prefix plugin:plugin-todo
38
40
 
39
41
  ./gradlew build
40
42
  cd ui && pnpm test:unit
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-halo-plugin-template",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Create and initialize reusable Halo plugin starter projects.",
5
5
  "type": "module",
6
6
  "license": "GPL-3.0",
@@ -5,6 +5,11 @@ import path from 'node:path'
5
5
  import process from 'node:process'
6
6
  import { spawn } from 'node:child_process'
7
7
  import { fileURLToPath } from 'node:url'
8
+ import {
9
+ normalizePermissionPrefix,
10
+ normalizePluginName,
11
+ normalizeRoutePrefix,
12
+ } from './lib/template-meta.mjs'
8
13
 
9
14
  const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url))
10
15
  const TEMPLATE_ROOT = path.resolve(SCRIPT_DIR, '..')
@@ -22,38 +27,33 @@ const SKIP_RELATIVE_PATHS = new Set([
22
27
  '.npmignore',
23
28
  '.github/workflows/publish-npm.yaml',
24
29
  'bin',
30
+ 'docs',
25
31
  'docs/publish-template.md',
26
32
  'package.json',
27
33
  'package-lock.json',
28
34
  'pnpm-lock.yaml',
29
- 'scripts/create-project.mjs',
30
- 'scripts/publish-check.mjs',
35
+ 'scripts',
31
36
  'ui/build',
32
37
  ])
33
38
 
34
39
  const usage = `
35
40
  Usage:
36
41
  node scripts/create-project.mjs \\
37
- --plugin-name hello-world \\
42
+ --plugin-name todo \\
38
43
  --base-package com.example.helloworld \\
39
- --display-name "Hello World" \\
44
+ --display-name "Todo" \\
40
45
  --author-name "Your Name" \\
41
- [--target-dir ../hello-world] \\
46
+ [--route-prefix /plugin-todo] \\
47
+ [--permission-prefix plugin:plugin-todo] \\
48
+ [--target-dir ../plugin-todo] \\
42
49
  [--author-website "https://github.com/your-name"] \\
43
50
  [--repo-owner your-name] \\
44
- [--description "Hello World - Halo 插件"] \\
51
+ [--description "Todo - Halo 插件"] \\
45
52
  [--install] \\
46
53
  [--build] \\
47
54
  [--halo-server]
48
55
  `.trim()
49
56
 
50
- const slugify = (value) =>
51
- value
52
- .trim()
53
- .toLowerCase()
54
- .replace(/[^a-z0-9]+/g, '-')
55
- .replace(/^-+|-+$/g, '')
56
-
57
57
  const parseArgs = (argv) => {
58
58
  const parsed = {
59
59
  install: false,
@@ -121,7 +121,9 @@ const parseArgs = (argv) => {
121
121
  }
122
122
  }
123
123
 
124
- const pluginName = slugify(parsed['plugin-name'])
124
+ const pluginName = normalizePluginName(parsed['plugin-name'])
125
+ const routePrefix = normalizeRoutePrefix(parsed['route-prefix'], pluginName)
126
+ const permissionPrefix = normalizePermissionPrefix(parsed['permission-prefix'], pluginName)
125
127
 
126
128
  return {
127
129
  pluginName,
@@ -131,6 +133,8 @@ const parseArgs = (argv) => {
131
133
  authorWebsite: parsed['author-website'] || '',
132
134
  repoOwner: parsed['repo-owner'] || '',
133
135
  description: parsed.description || '',
136
+ routePrefix,
137
+ permissionPrefix,
134
138
  targetDir: path.resolve(parsed['target-dir'] || path.resolve(process.cwd(), pluginName)),
135
139
  install: parsed.install,
136
140
  build: parsed.build,
@@ -148,6 +152,12 @@ const shouldCopy = (sourcePath) => {
148
152
  return true
149
153
  }
150
154
 
155
+ for (const skippedPath of SKIP_RELATIVE_PATHS) {
156
+ if (relativePath === skippedPath || relativePath.startsWith(`${skippedPath}/`)) {
157
+ return false
158
+ }
159
+ }
160
+
151
161
  if (SKIP_RELATIVE_PATHS.has(relativePath)) {
152
162
  return false
153
163
  }
@@ -200,8 +210,8 @@ const buildGeneratedReadme = (options) => `# ${options.displayName}
200
210
 
201
211
  - 插件名:\`${options.pluginName}\`
202
212
  - Java 包名:\`${options.basePackage}\`
203
- - UI 路由前缀:\`/${options.pluginName}\`
204
- - 权限前缀:\`plugin:${options.pluginName}:*\`
213
+ - UI 路由前缀:\`${options.routePrefix}\`
214
+ - 权限前缀:\`${options.permissionPrefix}:*\`
205
215
 
206
216
  ## 环境要求
207
217
 
@@ -234,9 +244,6 @@ pnpm dev
234
244
  # 后端单测
235
245
  ./gradlew test
236
246
 
237
- # 一致性检查
238
- node scripts/verify-template.mjs
239
-
240
247
  # 前端检查
241
248
  cd ui
242
249
  pnpm verify
@@ -253,7 +260,8 @@ cd ..
253
260
 
254
261
  - 后端先补接口和 Springdoc 注解,再执行 \`./gradlew generateApiClient\`。
255
262
  - 前端统一从 \`ui/src/api/index.ts\` 暴露 API,不要在页面里写裸 URL。
256
- - 如果这个插件不需要 UC、附件扩展或仪表盘部件,尽早裁剪,避免模板示例残留。
263
+ - 通过 \`--route-prefix\` 和 \`--permission-prefix\` 传入的规则已经写入项目,不需要再手工全局替换。
264
+ - 生成项目默认不再附带模板仓库的 \`docs/\` 和 \`scripts/\` 目录,保持业务仓库更轻。
257
265
 
258
266
  ## 目录说明
259
267
 
@@ -264,12 +272,10 @@ cd ..
264
272
  - \`ui/src/components/\`:业务级共享组件
265
273
  - \`ui/src/api/index.ts\`:前端唯一 API 包装出口
266
274
  - \`ui/src/api/generated/\`:由 \`generateApiClient\` 生成并已接入的客户端代码
267
- - \`docs/rsbuild-switch.md\`:从当前模板切换到 Rsbuild 的最小差异说明
268
- - \`docs/template-pruning.md\`:初始化后如何裁剪模板能力的操作建议
269
275
 
270
276
  ## 裁剪模板
271
277
 
272
- 如果你只做 Console 页面,或不需要附件扩展、UC 页面,可以参考 [docs/template-pruning.md](./docs/template-pruning.md) 做删减。
278
+ 如果你只做 Console 页面,或不需要附件扩展、UC 页面,直接删除对应的 \`ucRoutes\`、扩展点和角色模板即可。
273
279
 
274
280
  ## 许可证
275
281
 
@@ -286,7 +292,7 @@ const writeGeneratedReadme = async (options) => {
286
292
 
287
293
  const buildInitArgs = (options) => {
288
294
  const args = [
289
- path.join('scripts', 'init-template.mjs'),
295
+ path.join(SCRIPT_DIR, 'init-template.mjs'),
290
296
  '--plugin-name',
291
297
  options.pluginName,
292
298
  '--base-package',
@@ -295,6 +301,10 @@ const buildInitArgs = (options) => {
295
301
  options.displayName,
296
302
  '--author-name',
297
303
  options.authorName,
304
+ '--route-prefix',
305
+ options.routePrefix,
306
+ '--permission-prefix',
307
+ options.permissionPrefix,
298
308
  ]
299
309
 
300
310
  if (options.authorWebsite) {
@@ -312,7 +322,7 @@ const buildInitArgs = (options) => {
312
322
 
313
323
  const buildVerifyArgs = (options) => {
314
324
  const args = [
315
- path.join('scripts', 'verify-template.mjs'),
325
+ path.join(SCRIPT_DIR, 'verify-template.mjs'),
316
326
  '--plugin-name',
317
327
  options.pluginName,
318
328
  '--base-package',
@@ -321,6 +331,10 @@ const buildVerifyArgs = (options) => {
321
331
  options.displayName,
322
332
  '--author-name',
323
333
  options.authorName,
334
+ '--route-prefix',
335
+ options.routePrefix,
336
+ '--permission-prefix',
337
+ options.permissionPrefix,
324
338
  ]
325
339
 
326
340
  if (options.authorWebsite) {
@@ -3,11 +3,23 @@
3
3
  import fs from 'node:fs/promises'
4
4
  import path from 'node:path'
5
5
  import process from 'node:process'
6
+ import {
7
+ getApiGroupKey,
8
+ normalizePermissionPrefix,
9
+ normalizePluginName,
10
+ normalizeRoutePrefix,
11
+ reverseBasePackage,
12
+ slugify,
13
+ toClassPrefix,
14
+ toKebabToken,
15
+ } from './lib/template-meta.mjs'
6
16
 
7
17
  const TEMPLATE = {
8
18
  pluginName: 'halo-plugin-template',
9
19
  basePackage: 'run.halo.plugintemplate',
10
20
  classPrefix: 'PluginTemplate',
21
+ routePrefix: '/halo-plugin-template',
22
+ permissionPrefix: 'plugin:halo-plugin-template',
11
23
  displayName: 'Halo Plugin Template',
12
24
  authorName: 'Template Author',
13
25
  authorWebsite: 'https://github.com/example',
@@ -48,36 +60,17 @@ const SKIP_DIRS = new Set([
48
60
  const usage = `
49
61
  Usage:
50
62
  node scripts/init-template.mjs \\
51
- --plugin-name hello-world \\
63
+ --plugin-name todo \\
52
64
  --base-package com.example.helloworld \\
53
- --display-name "Hello World" \\
65
+ --display-name "Todo" \\
54
66
  --author-name "Your Name" \\
67
+ [--route-prefix /plugin-todo] \\
68
+ [--permission-prefix plugin:plugin-todo] \\
55
69
  [--author-website "https://github.com/your-name"] \\
56
70
  [--repo-owner your-name] \\
57
- [--description "Hello World - Halo 插件"]
71
+ [--description "Todo - Halo 插件"]
58
72
  `.trim()
59
73
 
60
- const slugify = (value) =>
61
- value
62
- .trim()
63
- .toLowerCase()
64
- .replace(/[^a-z0-9]+/g, '-')
65
- .replace(/^-+|-+$/g, '')
66
-
67
- const toClassPrefix = (pluginName) => {
68
- const parts = pluginName.split(/[^a-zA-Z0-9]+/).filter(Boolean)
69
- if (!parts.length) {
70
- return 'PluginTemplate'
71
- }
72
- return parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join('')
73
- }
74
-
75
- const toKebabToken = (value) =>
76
- value
77
- .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
78
- .replace(/[^a-zA-Z0-9]+/g, '-')
79
- .toLowerCase()
80
-
81
74
  const parseArgs = (argv) => {
82
75
  const parsed = {}
83
76
 
@@ -102,10 +95,12 @@ const parseArgs = (argv) => {
102
95
  }
103
96
 
104
97
  return {
105
- pluginName: slugify(parsed['plugin-name']),
98
+ pluginName: normalizePluginName(parsed['plugin-name']),
106
99
  basePackage: parsed['base-package'].trim(),
107
100
  displayName: parsed['display-name'].trim(),
108
101
  authorName: parsed['author-name'].trim(),
102
+ routePrefix: normalizeRoutePrefix(parsed['route-prefix'], normalizePluginName(parsed['plugin-name'])),
103
+ permissionPrefix: normalizePermissionPrefix(parsed['permission-prefix'], normalizePluginName(parsed['plugin-name'])),
109
104
  authorWebsite: parsed['author-website']?.trim() || '',
110
105
  repoOwner: parsed['repo-owner']?.trim() || '',
111
106
  description: parsed.description?.trim() || '',
@@ -168,7 +163,33 @@ const pruneEmptyDirectories = async (startPath, stopPath) => {
168
163
  }
169
164
  }
170
165
 
171
- const renamePackageDirectory = async (sourceRoot, targetRoot) => {
166
+ const pruneEmptyDirectoriesRecursively = async (directory, stopPath) => {
167
+ let entries
168
+
169
+ try {
170
+ entries = await fs.readdir(directory, { withFileTypes: true })
171
+ } catch {
172
+ return
173
+ }
174
+
175
+ for (const entry of entries) {
176
+ if (!entry.isDirectory()) {
177
+ continue
178
+ }
179
+ await pruneEmptyDirectoriesRecursively(path.join(directory, entry.name), stopPath)
180
+ }
181
+
182
+ if (directory === stopPath) {
183
+ return
184
+ }
185
+
186
+ const remainingEntries = await fs.readdir(directory)
187
+ if (remainingEntries.length === 0) {
188
+ await fs.rmdir(directory)
189
+ }
190
+ }
191
+
192
+ const renamePackageDirectory = async (sourceRoot, targetRoot, pruneStopPath) => {
172
193
  try {
173
194
  await fs.access(sourceRoot)
174
195
  } catch {
@@ -177,7 +198,7 @@ const renamePackageDirectory = async (sourceRoot, targetRoot) => {
177
198
 
178
199
  await fs.mkdir(path.dirname(targetRoot), { recursive: true })
179
200
  await fs.rename(sourceRoot, targetRoot)
180
- await pruneEmptyDirectories(path.dirname(sourceRoot), path.dirname(path.dirname(sourceRoot)))
201
+ await pruneEmptyDirectories(path.dirname(sourceRoot), pruneStopPath)
181
202
  }
182
203
 
183
204
  const collectRenameTargets = async (directory, token, result = []) => {
@@ -221,6 +242,10 @@ const main = async () => {
221
242
  const authorWebsite = options.authorWebsite || `https://github.com/${repoOwner}`
222
243
  const description = options.description || `${options.displayName} - Halo 插件`
223
244
  const nextPackagePath = options.basePackage.split('.').join('/')
245
+ const templateApiGroup = reverseBasePackage(TEMPLATE.basePackage)
246
+ const nextApiGroup = reverseBasePackage(options.basePackage)
247
+ const templateApiGroupKey = getApiGroupKey(TEMPLATE.basePackage)
248
+ const nextApiGroupKey = getApiGroupKey(options.basePackage)
224
249
 
225
250
  const replacements = [
226
251
  [
@@ -242,6 +267,11 @@ const main = async () => {
242
267
  [TEMPLATE.authorWebsite, authorWebsite],
243
268
  [TEMPLATE_PACKAGE_PATH, nextPackagePath],
244
269
  [TEMPLATE.basePackage, options.basePackage],
270
+ [TEMPLATE.routePrefix, options.routePrefix],
271
+ [TEMPLATE.permissionPrefix, options.permissionPrefix],
272
+ [templateApiGroup, nextApiGroup],
273
+ [`${templateApiGroupKey}Apis`, `${nextApiGroupKey}Apis`],
274
+ [`"${templateApiGroupKey}"`, `"${nextApiGroupKey}"`],
245
275
  [TEMPLATE.classPrefix, classPrefix],
246
276
  [TEMPLATE.displayName, options.displayName],
247
277
  [TEMPLATE.description, description],
@@ -258,17 +288,23 @@ const main = async () => {
258
288
  await renamePackageDirectory(
259
289
  path.join(ROOT, 'src/main/java', TEMPLATE_PACKAGE_PATH),
260
290
  path.join(ROOT, 'src/main/java', nextPackagePath),
291
+ path.join(ROOT, 'src/main/java'),
261
292
  )
262
293
  await renamePackageDirectory(
263
294
  path.join(ROOT, 'src/test/java', TEMPLATE_PACKAGE_PATH),
264
295
  path.join(ROOT, 'src/test/java', nextPackagePath),
296
+ path.join(ROOT, 'src/test/java'),
265
297
  )
298
+ await pruneEmptyDirectoriesRecursively(path.join(ROOT, 'src/main/java'), path.join(ROOT, 'src/main/java'))
299
+ await pruneEmptyDirectoriesRecursively(path.join(ROOT, 'src/test/java'), path.join(ROOT, 'src/test/java'))
266
300
  await renameClassPrefixPaths(ROOT, TEMPLATE.classPrefix, classPrefix)
267
301
  await renameClassPrefixPaths(ROOT, templateClassPrefixKebab, classPrefixKebab)
268
302
 
269
303
  console.log(`Initialized template as ${options.pluginName}`)
270
304
  console.log(`Base package: ${options.basePackage}`)
271
305
  console.log(`Class prefix: ${classPrefix}`)
306
+ console.log(`Route prefix: ${options.routePrefix}`)
307
+ console.log(`Permission prefix: ${options.permissionPrefix}`)
272
308
  console.log(`Repository owner: ${repoOwner}`)
273
309
  } catch (error) {
274
310
  console.error(error instanceof Error ? error.message : String(error))
@@ -0,0 +1,64 @@
1
+ export const slugify = (value) =>
2
+ value
3
+ .trim()
4
+ .toLowerCase()
5
+ .replace(/[^a-z0-9]+/g, '-')
6
+ .replace(/^-+|-+$/g, '')
7
+
8
+ export const normalizePluginName = (value) => {
9
+ const slug = slugify(value)
10
+
11
+ if (!slug) {
12
+ return ''
13
+ }
14
+
15
+ if (slug.startsWith('halo-plugin-')) {
16
+ return `plugin-${slug.slice('halo-plugin-'.length)}`
17
+ }
18
+
19
+ if (slug.startsWith('plugin-')) {
20
+ return slug
21
+ }
22
+
23
+ return `plugin-${slug}`
24
+ }
25
+
26
+ export const normalizeRoutePrefix = (value, pluginName) => {
27
+ const raw = (value?.trim() || `/${pluginName}`).replace(/^\/+/, '')
28
+ if (!raw) {
29
+ return '/'
30
+ }
31
+ return `/${raw.replace(/\/+$/, '')}`
32
+ }
33
+
34
+ export const normalizePermissionPrefix = (value, pluginName) =>
35
+ (value?.trim() || `plugin:${pluginName}`).replace(/:+$/, '')
36
+
37
+ export const toClassPrefix = (pluginName) => {
38
+ const parts = pluginName.split(/[^a-zA-Z0-9]+/).filter(Boolean)
39
+ if (!parts.length) {
40
+ return 'PluginTemplate'
41
+ }
42
+ return parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join('')
43
+ }
44
+
45
+ export const toKebabToken = (value) =>
46
+ value
47
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
48
+ .replace(/[^a-zA-Z0-9]+/g, '-')
49
+ .toLowerCase()
50
+
51
+ export const reverseBasePackage = (basePackage) =>
52
+ basePackage
53
+ .split('.')
54
+ .map((segment) => segment.trim())
55
+ .filter(Boolean)
56
+ .reverse()
57
+ .join('.')
58
+
59
+ export const getApiGroupKey = (basePackage) =>
60
+ basePackage
61
+ .split('.')
62
+ .map((segment) => segment.trim())
63
+ .filter(Boolean)
64
+ .at(-1) || 'plugin'
@@ -9,7 +9,6 @@ const REQUIRED_FILES = [
9
9
  'README.md',
10
10
  'build.gradle',
11
11
  'settings.gradle',
12
- 'scripts/init-template.mjs',
13
12
  'src/main/resources/plugin.yaml',
14
13
  'src/main/resources/extensions/settings.yaml',
15
14
  'src/main/resources/extensions/roleTemplate-console.yaml',
@@ -17,8 +16,6 @@ const REQUIRED_FILES = [
17
16
  'ui/src/index.ts',
18
17
  'ui/src/api/index.ts',
19
18
  'ui/src/api/generated/index.ts',
20
- 'docs/rsbuild-switch.md',
21
- 'docs/template-pruning.md',
22
19
  ]
23
20
  const TEXT_EXTENSIONS = new Set([
24
21
  '.md',
@@ -59,13 +56,15 @@ Usage:
59
56
  node scripts/verify-template.mjs
60
57
 
61
58
  node scripts/verify-template.mjs \\
62
- --plugin-name hello-world \\
59
+ --plugin-name todo \\
63
60
  --base-package com.example.helloworld \\
64
- --display-name "Hello World" \\
61
+ --display-name "Todo" \\
65
62
  --author-name "Your Name" \\
63
+ [--route-prefix /plugin-todo] \\
64
+ [--permission-prefix plugin:plugin-todo] \\
66
65
  [--author-website "https://github.com/your-name"] \\
67
66
  [--repo-owner your-name] \\
68
- [--description "Hello World - Halo 插件"]
67
+ [--description "Todo - Halo 插件"]
69
68
  `.trim()
70
69
 
71
70
  const parseArgs = (argv) => {
@@ -252,10 +251,16 @@ const verifyRepo = async (args) => {
252
251
  const consolePath = extractJavaConstant(settingKeysContent, 'CONSOLE_PATH')
253
252
  const ucPath = extractJavaConstant(settingKeysContent, 'UC_PATH')
254
253
  const generatedClientPath = extractJavaConstant(settingKeysContent, 'GENERATED_CLIENT_PATH')
254
+ const routePrefix = extractJavaConstant(settingKeysContent, 'ROUTE_PREFIX')
255
+ const configuredPermissionPrefix = extractJavaConstant(settingKeysContent, 'PERMISSION_PREFIX')
256
+ const apiGroupSuffix = extractJavaConstant(settingKeysContent, 'API_GROUP_SUFFIX')
257
+ const apiGroupKey = extractJavaConstant(settingKeysContent, 'API_GROUP_KEY')
255
258
  const basePackage = extractGradleGroup(buildGradleContent)
256
259
  const rootProjectName = extractRootProjectName(settingsGradleContent)
257
260
  const packageDirectory = path.posix.join('src/main/java', ...basePackage.split('.'))
258
- const permissionPrefix = `plugin:${pluginName}:`
261
+ const permissionPrefix = `${configuredPermissionPrefix}:`
262
+ const resolvedConsolePath = consolePath || routePrefix
263
+ const resolvedUcPath = ucPath || routePrefix
259
264
 
260
265
  if (!pluginName || !displayName || !basePackage) {
261
266
  fail(results, 'Unable to resolve plugin constants from SettingKeys.java or build.gradle')
@@ -280,10 +285,12 @@ const verifyRepo = async (args) => {
280
285
  assertContains(results, pluginYamlContent, `displayName: "${displayName}"`, 'plugin.yaml displayName is consistent')
281
286
  assertContains(results, settingsYamlContent, `name: ${settingName}`, 'settings.yaml metadata name is consistent')
282
287
 
283
- assertContains(results, buildGradleContent, `/apis/console.${pluginName}.halo.run/v1alpha1/**`, 'OpenAPI console path matches plugin name')
284
- assertContains(results, buildGradleContent, `/apis/uc.${pluginName}.halo.run/v1alpha1/**`, 'OpenAPI UC path matches plugin name')
285
- assertContains(results, uiIndexContent, `path: '${consolePath}'`, 'Console route path matches setting constants')
286
- assertContains(results, uiIndexContent, `path: '${ucPath}'`, 'UC route path matches setting constants')
288
+ assertContains(results, buildGradleContent, `/apis/console.${apiGroupSuffix}/v1alpha1/**`, 'OpenAPI console path matches API group suffix')
289
+ assertContains(results, buildGradleContent, `/apis/uc.${apiGroupSuffix}/v1alpha1/**`, 'OpenAPI UC path matches API group suffix')
290
+ assertContains(results, buildGradleContent, `/apis/public.${apiGroupSuffix}/v1alpha1/**`, 'OpenAPI public path matches API group suffix')
291
+ assertContains(results, buildGradleContent, `${apiGroupKey}Apis`, 'OpenAPI group key matches API group key')
292
+ assertContains(results, uiIndexContent, `path: '${resolvedConsolePath}'`, 'Console route path matches setting constants')
293
+ assertContains(results, uiIndexContent, `path: '${resolvedUcPath}'`, 'UC route path matches setting constants')
287
294
  assertContains(results, uiIndexContent, permissionPrefix, 'UI permissions use the current plugin prefix')
288
295
  assertContains(results, providerContent, `${pluginName}-admin-shell`, 'UI provider namespace follows the plugin prefix')
289
296
  assertContains(results, uiApiContent, `from '@/api/generated'`, 'UI API wrapper imports from the generated client')
@@ -296,8 +303,15 @@ const verifyRepo = async (args) => {
296
303
 
297
304
  assertContains(results, consoleRoleTemplateContent, permissionPrefix, 'Console role template uses the plugin permission prefix')
298
305
  assertContains(results, ucRoleTemplateContent, permissionPrefix, 'UC role template uses the plugin permission prefix')
299
- assertContains(results, readmeContent, 'node scripts/verify-template.mjs', 'README documents the verification command')
300
- assertContains(results, readmeContent, 'docs/template-pruning.md', 'README links to the pruning guide')
306
+ if (readmeContent.includes('node scripts/verify-template.mjs')) {
307
+ pass(results, 'README documents the verification command')
308
+ }
309
+
310
+ if (!(await exists('docs/template-pruning.md')) || readmeContent.includes('docs/template-pruning.md')) {
311
+ pass(results, 'README/doc pruning guidance is consistent')
312
+ } else {
313
+ fail(results, 'README references are inconsistent with the generated docs layout')
314
+ }
301
315
 
302
316
  if (await exists(path.posix.join(generatedClientPath, 'api'))) {
303
317
  pass(results, `Generated API directory exists: ${generatedClientPath}/api`)
@@ -329,6 +343,18 @@ const verifyRepo = async (args) => {
329
343
  pass(results, `Expected display name matches: ${displayName}`)
330
344
  }
331
345
 
346
+ if (args['route-prefix'] && args['route-prefix'] !== routePrefix) {
347
+ fail(results, `Expected route prefix ${args['route-prefix']}, got ${routePrefix}`)
348
+ } else if (args['route-prefix']) {
349
+ pass(results, `Expected route prefix matches: ${routePrefix}`)
350
+ }
351
+
352
+ if (args['permission-prefix'] && args['permission-prefix'] !== configuredPermissionPrefix) {
353
+ fail(results, `Expected permission prefix ${args['permission-prefix']}, got ${configuredPermissionPrefix}`)
354
+ } else if (args['permission-prefix']) {
355
+ pass(results, `Expected permission prefix matches: ${configuredPermissionPrefix}`)
356
+ }
357
+
332
358
  if (args['author-name']) {
333
359
  assertContains(
334
360
  results,
@@ -9,12 +9,14 @@ import org.springframework.web.reactive.function.server.ServerResponse;
9
9
  import run.halo.app.core.extension.endpoint.CustomEndpoint;
10
10
  import run.halo.app.extension.GroupVersion;
11
11
  import run.halo.plugintemplate.endpoint.routes.PluginTemplateOverviewRoutes;
12
+ import run.halo.plugintemplate.setting.PluginTemplateSettingKeys;
12
13
 
13
14
  @Component
14
15
  @RequiredArgsConstructor
15
16
  public class PluginTemplateConsoleEndpoint implements CustomEndpoint {
16
17
 
17
- public static final String CONSOLE_GROUP_VERSION = "console.halo-plugin-template.halo.run/v1alpha1";
18
+ public static final String CONSOLE_GROUP_VERSION = "console."
19
+ + PluginTemplateSettingKeys.API_GROUP_SUFFIX + "/v1alpha1";
18
20
  public static final String CONSOLE_TAG = "PluginTemplateConsole";
19
21
 
20
22
  private final PluginTemplateOverviewRoutes overviewRoutes;
@@ -5,6 +5,7 @@ import org.springframework.web.reactive.function.server.RouterFunction;
5
5
  import org.springframework.web.reactive.function.server.ServerResponse;
6
6
  import run.halo.app.core.extension.endpoint.CustomEndpoint;
7
7
  import run.halo.app.extension.GroupVersion;
8
+ import run.halo.plugintemplate.setting.PluginTemplateSettingKeys;
8
9
 
9
10
  /**
10
11
  * Public endpoint placeholder.
@@ -12,7 +13,8 @@ import run.halo.app.extension.GroupVersion;
12
13
  */
13
14
  public class PluginTemplatePublicEndpoint implements CustomEndpoint {
14
15
 
15
- public static final String PUBLIC_GROUP_VERSION = "public.halo-plugin-template.halo.run/v1alpha1";
16
+ public static final String PUBLIC_GROUP_VERSION = "public."
17
+ + PluginTemplateSettingKeys.API_GROUP_SUFFIX + "/v1alpha1";
16
18
 
17
19
  @Override
18
20
  public RouterFunction<ServerResponse> endpoint() {
@@ -9,12 +9,14 @@ import org.springframework.web.reactive.function.server.ServerResponse;
9
9
  import run.halo.app.core.extension.endpoint.CustomEndpoint;
10
10
  import run.halo.app.extension.GroupVersion;
11
11
  import run.halo.plugintemplate.endpoint.routes.PluginTemplateOverviewRoutes;
12
+ import run.halo.plugintemplate.setting.PluginTemplateSettingKeys;
12
13
 
13
14
  @Component
14
15
  @RequiredArgsConstructor
15
16
  public class PluginTemplateUcEndpoint implements CustomEndpoint {
16
17
 
17
- public static final String UC_GROUP_VERSION = "uc.halo-plugin-template.halo.run/v1alpha1";
18
+ public static final String UC_GROUP_VERSION = "uc."
19
+ + PluginTemplateSettingKeys.API_GROUP_SUFFIX + "/v1alpha1";
18
20
  public static final String UC_TAG = "PluginTemplateUc";
19
21
 
20
22
  private final PluginTemplateOverviewRoutes overviewRoutes;
@@ -8,12 +8,13 @@ import lombok.EqualsAndHashCode;
8
8
  import lombok.ToString;
9
9
  import run.halo.app.extension.AbstractExtension;
10
10
  import run.halo.app.extension.GVK;
11
+ import run.halo.plugintemplate.setting.PluginTemplateSettingKeys;
11
12
 
12
13
  @Data
13
14
  @EqualsAndHashCode(callSuper = true)
14
15
  @ToString(callSuper = true)
15
16
  @GVK(
16
- group = "halo-plugin-template.halo.run",
17
+ group = PluginTemplateSettingKeys.API_GROUP_SUFFIX,
17
18
  version = "v1alpha1",
18
19
  kind = "PluginTemplateRecord",
19
20
  plural = "pluginTemplateRecords",
@@ -6,8 +6,12 @@ public final class PluginTemplateSettingKeys {
6
6
  public static final String DISPLAY_NAME = "Halo Plugin Template";
7
7
  public static final String SETTING_NAME = "halo-plugin-template-settings";
8
8
  public static final String CONFIG_MAP_NAME = "halo-plugin-template-configmap";
9
- public static final String CONSOLE_PATH = "/halo-plugin-template";
10
- public static final String UC_PATH = "/halo-plugin-template";
9
+ public static final String ROUTE_PREFIX = "/halo-plugin-template";
10
+ public static final String PERMISSION_PREFIX = "plugin:halo-plugin-template";
11
+ public static final String API_GROUP_SUFFIX = "plugintemplate.halo.run";
12
+ public static final String API_GROUP_KEY = "plugintemplate";
13
+ public static final String CONSOLE_PATH = ROUTE_PREFIX;
14
+ public static final String UC_PATH = ROUTE_PREFIX;
11
15
  public static final String GENERATED_CLIENT_PATH = "ui/src/api/generated";
12
16
  public static final String GENERAL_GROUP = "general";
13
17
  public static final String UI_GROUP = "ui";
@@ -1,7 +1,7 @@
1
1
  kind: Role
2
2
  apiVersion: v1alpha1
3
3
  metadata:
4
- name: rt-console.halo-plugin-template.halo.run-view
4
+ name: rt-console.plugintemplate.halo.run-view
5
5
  labels:
6
6
  halo.run/role-template: "true"
7
7
  annotations:
@@ -11,7 +11,7 @@ metadata:
11
11
  rbac.authorization.halo.run/module: "Halo 插件模板"
12
12
  rules:
13
13
  - apiGroups:
14
- - console.halo-plugin-template.halo.run
14
+ - console.plugintemplate.halo.run
15
15
  resources:
16
16
  - template-overview
17
17
  verbs:
@@ -21,7 +21,7 @@ rules:
21
21
  kind: Role
22
22
  apiVersion: v1alpha1
23
23
  metadata:
24
- name: rt-console.halo-plugin-template.halo.run-manage
24
+ name: rt-console.plugintemplate.halo.run-manage
25
25
  labels:
26
26
  halo.run/role-template: "true"
27
27
  annotations:
@@ -31,7 +31,7 @@ metadata:
31
31
  rbac.authorization.halo.run/module: "Halo 插件模板"
32
32
  rules:
33
33
  - apiGroups:
34
- - console.halo-plugin-template.halo.run
34
+ - console.plugintemplate.halo.run
35
35
  resources:
36
36
  - template-overview
37
37
  verbs:
@@ -1,7 +1,7 @@
1
1
  kind: Role
2
2
  apiVersion: v1alpha1
3
3
  metadata:
4
- name: rt-uc.halo-plugin-template.halo.run-personal
4
+ name: rt-uc.plugintemplate.halo.run-personal
5
5
  labels:
6
6
  halo.run/role-template: "true"
7
7
  annotations:
@@ -11,7 +11,7 @@ metadata:
11
11
  rbac.authorization.halo.run/module: "Halo 插件模板"
12
12
  rules:
13
13
  - apiGroups:
14
- - uc.halo-plugin-template.halo.run
14
+ - uc.plugintemplate.halo.run
15
15
  resources:
16
16
  - template-overview
17
17
  verbs: