@liujitcn/kratos-taro-app-cli 0.0.36 → 0.0.38

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,3 +25,14 @@ JavaScript 调用方也可以直接使用 `scaffoldKratosTaroApp(target, options
25
25
  在 Taro workspace 根目录执行 `pnpm test` 会实际打包、解压并运行包内 CLI,核对生成的
26
26
  图标内容和本地业务模块,避免只测试源码而遗漏 npm 发布资源。打包产物和生成项目均位于
27
27
  临时目录,测试结束后自动清理。
28
+
29
+
30
+ ### CLI 生成边界
31
+
32
+ CLI 直接生成完整宿主、本地业务模块、四种语言源文件与注册入口、类型检查、lint 和打包配置。
33
+ 支持本地 `system` 与其他模块一起创建;管理端本地 System 继承内置能力,运行时只注册一次,
34
+ 构建仍扫描内置源码。应用端本地模块使用独立导入别名,保留内置 System。
35
+
36
+ 通过 `--kratos-project` 生成与 Go 后端配套的前端:H5 输出到 `backend/data/<terminal>`,
37
+ 管理端 CLI 同时在 workspace 父目录创建共享 `Makefile` 与 `scripts`。Go 调用方仅传参执行 CLI。
38
+ 新增语言或修改语言文件后运行 `pnpm i18n:sync`;`pnpm i18n:check` 校验注册文件是否同步。
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "type": "git",
8
8
  "url": "git@github.com:liujitcn/kratos-admin.git"
9
9
  },
10
- "version": "0.0.36",
10
+ "version": "0.0.38",
11
11
  "type": "module",
12
12
  "bin": {
13
13
  "kratos-taro-app": "./bin/kratos-taro-app.mjs"
@@ -20,7 +20,8 @@
20
20
  "assets",
21
21
  "bin",
22
22
  "src",
23
- "README.md"
23
+ "README.md",
24
+ "templates"
24
25
  ],
25
26
  "publishConfig": {
26
27
  "access": "public"
package/src/index.mjs CHANGED
@@ -1,4 +1,5 @@
1
- import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
1
+ import { execFileSync } from 'node:child_process'
2
+ import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
2
3
  import { basename, resolve } from 'node:path'
3
4
 
4
5
  const cliPackage = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
@@ -33,44 +34,6 @@ export function scaffoldKratosTaroApp(targetPath, options = {}) {
33
34
  'node_modules\ndist\n.kratos-taro-app-pages-state.json\n.kratos-taro-app-pages-state.json.*\napps/taro-app/src/pages/*\n!apps/taro-app/src/pages/bootstrap/\n!apps/taro-app/src/pages/bootstrap/**\napps/taro-app/src/pages?*/\n',
34
35
  )
35
36
  write(target, 'pnpm-workspace.yaml', 'packages:\n - apps/*\n - packages/modules/*\n')
36
- write(
37
- target,
38
- 'package.json',
39
- json({
40
- name: projectName,
41
- description: `Independent pnpm workspace for ${projectName}.`,
42
- private: true,
43
- packageManager: 'pnpm@10.13.1',
44
- scripts: {
45
- 'prepare:modules':
46
- "pnpm --recursive --filter './packages/modules/**' --if-present run build:entries",
47
- 'dev:h5': 'pnpm prepare:modules && pnpm --filter @local/kratos-taro-app dev:h5',
48
- 'dev:mp-weixin':
49
- 'pnpm prepare:modules && pnpm --filter @local/kratos-taro-app dev:mp-weixin',
50
- 'build:h5': 'pnpm prepare:modules && pnpm --filter @local/kratos-taro-app build:h5',
51
- 'build:mp-weixin':
52
- 'pnpm prepare:modules && pnpm --filter @local/kratos-taro-app build:mp-weixin',
53
- tsc: 'pnpm --recursive --if-present run tsc',
54
- },
55
- devDependencies: {
56
- '@babel/core': '7.28.4',
57
- '@babel/preset-react': '7.28.5',
58
- '@types/node': '20.19.9',
59
- '@types/react': '18.3.23',
60
- '@types/react-dom': '18.3.7',
61
- 'babel-preset-taro': taroVersion,
62
- 'cross-env': '7.0.3',
63
- esbuild: '0.25.8',
64
- sass: '1.89.2',
65
- 'tsconfig-paths-webpack-plugin': '4.2.0',
66
- typescript: '5.8.3',
67
- webpack: '5.91.0',
68
- },
69
- pnpm: {
70
- onlyBuiltDependencies: ['@swc/core', '@tarojs/binding', 'esbuild'],
71
- },
72
- }),
73
- )
74
37
  write(
75
38
  target,
76
39
  'tsconfig.json',
@@ -91,14 +54,17 @@ export function scaffoldKratosTaroApp(targetPath, options = {}) {
91
54
  }),
92
55
  )
93
56
  writeWorkspaceReadme(target, projectName)
57
+ renderWorkspaceTemplates(new URL('../templates/workspace/', import.meta.url), target, { __PROJECT_NAME__: projectName })
94
58
  writeEnvironmentFiles(target)
95
- writeHost(target, projectName, modules, packages)
59
+ writeHost(target, projectName, modules, packages, options)
96
60
  write(target, 'apps/taro-app/src/static/h5-root-font.js', `${h5RootFontSource}\n`)
97
61
  copyFileSync(
98
62
  new URL('../assets/favicon.ico', import.meta.url),
99
63
  resolve(target, 'apps/taro-app/src/static/favicon.ico'),
100
64
  )
101
65
  modules.forEach((name) => writeLocalModule(target, projectName, name))
66
+ modules.forEach((name) => writeModuleLocales(target, name))
67
+ execFileSync(process.execPath, [resolve(target, 'scripts/sync-locales.mjs'), '--write'], { stdio: 'inherit' })
102
68
  return target
103
69
  }
104
70
 
@@ -115,7 +81,7 @@ export async function run(args = process.argv.slice(2)) {
115
81
  }
116
82
  const modules = readOptions(args.slice(2), '--module')
117
83
  const packages = readOptions(args.slice(2), '--with')
118
- const target = scaffoldKratosTaroApp(targetPath, { modules, packages })
84
+ const target = scaffoldKratosTaroApp(targetPath, { modules, packages, kratosProject: args.includes('--kratos-project') })
119
85
  process.stdout.write(`已创建 Taro workspace:${target}\n`)
120
86
  }
121
87
 
@@ -183,7 +149,8 @@ function writeEnvironmentFiles(target) {
183
149
  )
184
150
  }
185
151
 
186
- function writeHost(target, projectName, modules, packages) {
152
+ /** 生成 Taro 宿主与平台构建配置。 */
153
+ function writeHost(target, projectName, modules, packages, options) {
187
154
  const localDependencies = Object.fromEntries(
188
155
  modules.map((name) => [`@local/${name}`, 'workspace:*']),
189
156
  )
@@ -202,7 +169,7 @@ function writeHost(target, projectName, modules, packages) {
202
169
  'dev:mp-weixin':
203
170
  'cross-env NODE_ENV=development node scripts/run-taro.mjs --type weapp --watch --mode development',
204
171
  'build:h5':
205
- 'cross-env KRATOS_TARO_OUTPUT_ROOT=dist/build/h5 node scripts/run-taro.mjs --type h5 --mode production',
172
+ `cross-env KRATOS_TARO_OUTPUT_ROOT=${options.kratosProject ? '../../../../backend/data/taro-app' : 'dist/build/h5'} node scripts/run-taro.mjs --type h5 --mode production`,
206
173
  'build:mp-weixin':
207
174
  'cross-env KRATOS_TARO_OUTPUT_ROOT=dist/build/mp-weixin node scripts/run-taro.mjs --type weapp --mode production',
208
175
  tsc: 'tsc --noEmit -p tsconfig.json',
@@ -260,19 +227,6 @@ function writeHost(target, projectName, modules, packages) {
260
227
  }
261
228
  `,
262
229
  )
263
- write(
264
- target,
265
- 'apps/taro-app/tsconfig.json',
266
- json({
267
- extends: '../../tsconfig.json',
268
- compilerOptions: {
269
- baseUrl: '.',
270
- paths: { '@/*': ['src/*'] },
271
- types: ['node', '@tarojs/taro', '@tarojs/components'],
272
- },
273
- include: ['src', 'config', 'types'],
274
- }),
275
- )
276
230
  write(target, 'apps/taro-app/config/dev.ts', platformConfig(true))
277
231
  write(target, 'apps/taro-app/config/prod.ts', platformConfig(false))
278
232
  const modulePackages = [
@@ -343,22 +297,6 @@ function writeHost(target, projectName, modules, packages) {
343
297
  'apps/taro-app/src/app.scss',
344
298
  "@use '@liujitcn/kratos-taro-app-ui/styles/theme.scss';\n@use '@liujitcn/kratos-taro-app-core/styles/base.scss';\n",
345
299
  )
346
- write(
347
- target,
348
- 'apps/taro-app/src/app.tsx',
349
- `import type { PropsWithChildren } from 'react'
350
- import { useLaunch } from '@tarojs/taro'
351
- import { bootstrapKratosTaroApp } from '@liujitcn/kratos-taro-app-core'
352
- import { moduleManifest } from './module-manifest'
353
- import './app.scss'
354
-
355
- /** Taro 宿主根组件。 */
356
- export default function App({ children }: PropsWithChildren) {
357
- useLaunch(() => bootstrapKratosTaroApp({ modules: moduleManifest }))
358
- return children
359
- }
360
- `,
361
- )
362
300
  write(target, 'apps/taro-app/src/module-manifest.ts', moduleManifest(modules, packages))
363
301
  write(
364
302
  target,
@@ -387,6 +325,7 @@ export default function BootstrapPage() {
387
325
  )
388
326
  }
389
327
 
328
+ /** 生成本地 Taro 模块与独立构建入口。 */
390
329
  function writeLocalModule(target, projectName, name) {
391
330
  const identifier = `${toCamelCase(name)}Module`
392
331
  write(
@@ -409,6 +348,7 @@ function writeLocalModule(target, projectName, name) {
409
348
  './package.json': './package.json',
410
349
  },
411
350
  scripts: {
351
+ build: 'pnpm build:entries && pnpm pack --pack-destination ../../../dist/npm',
412
352
  'build:entries':
413
353
  'esbuild src/build.ts --bundle --platform=node --format=esm --packages=external --outfile=dist/build.mjs',
414
354
  tsc: 'tsc --noEmit -p tsconfig.json',
@@ -457,12 +397,14 @@ export const ${toCamelCase(name)}Pages: Record<string, KratosTaroPageConfig> = {
457
397
  `packages/modules/${name}/src/index.ts`,
458
398
  `import { defineKratosTaroModule } from '@liujitcn/kratos-taro-app-core'
459
399
  import { ${toCamelCase(name)}Pages } from './pages'
400
+ import { LOCALE_MESSAGES } from './locales/generated'
460
401
 
461
402
  /** ${name} 业务模块。 */
462
403
  export const ${identifier} = defineKratosTaroModule({
463
404
  name: '@local/${name}',
464
405
  pages: ${toCamelCase(name)}Pages,
465
406
  views: {},
407
+ messages: LOCALE_MESSAGES,
466
408
  })
467
409
  `,
468
410
  )
@@ -486,17 +428,18 @@ export const buildModule = defineKratosTaroBuildModule({
486
428
  )
487
429
  }
488
430
 
431
+ /** 生成模块清单,导入别名避免本地 system 与内置模块重名。 */
489
432
  function moduleManifest(modules, packages) {
490
433
  const imports = [
491
434
  "import { coreModule } from '@liujitcn/kratos-taro-app-core'",
492
435
  "import { systemModule } from '@liujitcn/kratos-taro-app-system'",
493
- ...modules.map((name) => `import { ${toCamelCase(name)}Module } from '@local/${name}'`),
436
+ ...modules.map((name, index) => `import { ${toCamelCase(name)}Module as localModule${index} } from '@local/${name}'`),
494
437
  ...packages.map((name, index) => `import packageModule${index} from '${name}'`),
495
438
  ]
496
439
  const members = [
497
440
  'coreModule',
498
441
  'systemModule',
499
- ...modules.map((name) => `${toCamelCase(name)}Module`),
442
+ ...modules.map((_, index) => `localModule${index}`),
500
443
  ...packages.map((_, index) => `packageModule${index}`),
501
444
  ]
502
445
  return `${imports.join('\n')}\n\n/** 宿主唯一模块清单,顺序决定静态视图覆盖优先级。 */\nexport const moduleManifest = [${members.join(', ')}]\n`
@@ -649,10 +592,12 @@ export default ${JSON.stringify(
649
592
  `
650
593
  }
651
594
 
595
+ /** 读取重复模块参数并识别项目集成开关。 */
652
596
  function readOptions(args, option) {
653
597
  const values = []
654
598
  for (let index = 0; index < args.length; index += 1) {
655
599
  const argument = args[index]
600
+ if (argument === '--kratos-project') continue
656
601
  if (argument !== '--module' && argument !== '--with') {
657
602
  throw new Error(`未知参数:${argument}`)
658
603
  }
@@ -674,9 +619,9 @@ function validateProjectName(name) {
674
619
  if (!/^[a-z][a-z0-9-]*$/.test(name)) throw new Error(`项目名必须使用 kebab-case:${name}`)
675
620
  }
676
621
 
622
+ /** 校验本地业务模块名称。 */
677
623
  function validateModuleName(name) {
678
624
  if (!/^[a-z][a-z0-9-]*$/.test(name)) throw new Error(`模块名无效:${name}`)
679
- if (name === 'system') throw new Error('模块名不能使用保留名称:system')
680
625
  }
681
626
 
682
627
  function validatePackageName(name) {
@@ -710,3 +655,28 @@ function printHelp() {
710
655
  ].join('\n'),
711
656
  )
712
657
  }
658
+
659
+ /** 渲染随 CLI 发布的宿主与工具链模板。 */
660
+ function renderWorkspaceTemplates(source, target, tokens) {
661
+ for (const entry of readdirSync(source, { withFileTypes: true })) {
662
+ const input = new URL(entry.name + (entry.isDirectory() ? '/' : ''), source)
663
+ const output = resolve(target, entry.name)
664
+ if (entry.isDirectory()) {
665
+ mkdirSync(output, { recursive: true })
666
+ renderWorkspaceTemplates(input, output, tokens)
667
+ } else {
668
+ const content = Object.entries(tokens).reduce((value, [key, replacement]) => value.replaceAll(key, replacement), readFileSync(input, 'utf8'))
669
+ writeFileSync(output, content)
670
+ }
671
+ }
672
+ }
673
+
674
+ /** 创建业务模块语言源文件及 API、RPC、测试目录,注册产物由同步命令生成。 */
675
+ function writeModuleLocales(target, name) {
676
+ for (const directory of ['src/api', 'src/rpc', 'test']) {
677
+ write(target, `packages/modules/${name}/${directory}/.gitkeep`, '')
678
+ }
679
+ for (const locale of ['zh-CN', 'en-US', 'zh-TW', 'ja-JP']) {
680
+ write(target, `packages/modules/${name}/src/locales/${locale}.json`, '{}\n')
681
+ }
682
+ }
@@ -0,0 +1,19 @@
1
+ module.exports = {
2
+ root: true,
3
+ extends: ['taro/react'],
4
+ parserOptions: {
5
+ project: false,
6
+ requireConfigFile: false,
7
+ },
8
+ rules: {
9
+ 'react/react-in-jsx-scope': 'off',
10
+ },
11
+ ignorePatterns: [
12
+ '**/dist/**',
13
+ '**/src/rpc/**',
14
+ 'node_modules/**',
15
+ 'apps/taro-app/src/pages/**',
16
+ '!apps/taro-app/src/pages/bootstrap/**',
17
+ 'apps/taro-app/src/pagesMember/**',
18
+ ],
19
+ }
@@ -0,0 +1,19 @@
1
+ import type { PropsWithChildren } from 'react'
2
+ import { useDidHide, useDidShow, useLaunch } from '@tarojs/taro'
3
+ import { bootstrapKratosTaroApp } from '@liujitcn/kratos-taro-app-core'
4
+ import {
5
+ pauseNotificationPolling,
6
+ resumeNotificationPolling,
7
+ } from '@liujitcn/kratos-taro-app-system'
8
+ import { moduleManifest } from './module-manifest'
9
+ import './app.scss'
10
+
11
+ /** Taro 宿主根组件。 */
12
+ export default function App({ children }: PropsWithChildren) {
13
+ useLaunch(() => {
14
+ bootstrapKratosTaroApp({ modules: moduleManifest })
15
+ })
16
+ useDidShow(() => resumeNotificationPolling())
17
+ useDidHide(() => pauseNotificationPolling())
18
+ return children
19
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "baseUrl": ".",
5
+ "paths": { "@/*": ["src/*"] },
6
+ "types": ["node", "@tarojs/taro", "@tarojs/components"],
7
+ "allowImportingTsExtensions": true,
8
+ "noEmit": true
9
+ },
10
+ "include": ["src", "config", "types"]
11
+ }
@@ -0,0 +1,5 @@
1
+ // Taro 构建器将 PNG 资源导入转换为可访问的图片地址。
2
+ declare module '*.png' {
3
+ const url: string;
4
+ export default url;
5
+ }
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "__PROJECT_NAME__",
3
+ "description": "Independent pnpm workspace for __PROJECT_NAME__.",
4
+ "private": true,
5
+ "packageManager": "pnpm@10.13.1",
6
+ "scripts": {
7
+ "prepare:modules": "pnpm --recursive --filter './packages/modules/**' --if-present run build:entries",
8
+ "dev:h5": "pnpm prepare:modules && pnpm --filter @local/kratos-taro-app dev:h5",
9
+ "dev:mp-weixin": "pnpm prepare:modules && pnpm --filter @local/kratos-taro-app dev:mp-weixin",
10
+ "build:h5": "pnpm prepare:modules && pnpm --filter @local/kratos-taro-app build:h5",
11
+ "build:mp-weixin": "pnpm prepare:modules && pnpm --filter @local/kratos-taro-app build:mp-weixin",
12
+ "tsc": "pnpm --recursive --if-present run tsc",
13
+ "test": "node --test",
14
+ "build:packages": "pnpm --recursive --filter './packages/modules/**' run build",
15
+ "check:exports": "pnpm run prepare:modules && node scripts/check-package-exports.mjs",
16
+ "lint": "eslint . --fix --ext .js,.mjs,.ts,.tsx",
17
+ "i18n:sync": "node scripts/sync-locales.mjs --write",
18
+ "i18n:check": "node scripts/sync-locales.mjs"
19
+ },
20
+ "devDependencies": {
21
+ "@babel/core": "7.28.4",
22
+ "@babel/preset-react": "7.28.5",
23
+ "@types/node": "20.19.9",
24
+ "@types/react": "18.3.23",
25
+ "@types/react-dom": "18.3.7",
26
+ "babel-preset-taro": "4.2.1",
27
+ "cross-env": "7.0.3",
28
+ "esbuild": "0.25.8",
29
+ "sass": "1.89.2",
30
+ "tsconfig-paths-webpack-plugin": "4.2.0",
31
+ "typescript": "5.8.3",
32
+ "webpack": "5.91.0",
33
+ "@tarojs/plugin-platform-h5": "4.2.1",
34
+ "@tarojs/plugin-platform-weapp": "4.2.1",
35
+ "eslint": "8.57.1",
36
+ "eslint-config-taro": "4.2.1",
37
+ "eslint-plugin-react": "7.37.5",
38
+ "eslint-plugin-react-hooks": "4.6.2",
39
+ "less": "4.8.1",
40
+ "prettier": "3.6.2",
41
+ "ts-proto": "2.7.7",
42
+ "turbo": "2.5.5"
43
+ },
44
+ "pnpm": {
45
+ "overrides": {
46
+ "swiper": "12.1.2",
47
+ "tmp": "0.2.6"
48
+ },
49
+ "onlyBuiltDependencies": [
50
+ "@swc/core",
51
+ "@tarojs/binding",
52
+ "@tarojs/cli",
53
+ "esbuild"
54
+ ],
55
+ "ignoredBuiltDependencies": [
56
+ "@nutui/nutui-react-taro",
57
+ "@parcel/watcher",
58
+ "core-js",
59
+ "core-js-pure"
60
+ ],
61
+ "auditConfig": {
62
+ "ignoreCves": [
63
+ "CVE-2022-25900",
64
+ "CVE-2024-29415",
65
+ "CVE-2025-25975",
66
+ "CVE-2022-37620",
67
+ "CVE-2026-53486",
68
+ "CVE-2026-10732",
69
+ "CVE-2025-71330",
70
+ "CVE-2025-71329",
71
+ "CVE-2026-39243"
72
+ ]
73
+ }
74
+ }
75
+ }
@@ -0,0 +1,152 @@
1
+ import { readdir, readFile, stat } from 'node:fs/promises'
2
+ import { dirname, extname, relative, resolve, sep } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+ import ts from 'typescript'
5
+
6
+ const workspaceRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
7
+ const coreRoot = resolve(workspaceRoot, 'packages/core')
8
+ const uiRoot = resolve(workspaceRoot, 'packages/ui')
9
+ const systemRoot = resolve(workspaceRoot, 'packages/modules/system')
10
+ const ignoredDirectories = new Set(['.git', '.turbo', 'dist', 'node_modules'])
11
+ const sourceExtensions = new Set(['.js', '.mjs', '.ts', '.tsx'])
12
+ const allFiles = await collectFiles(workspaceRoot)
13
+ const packageFiles = allFiles.filter((file) => file.endsWith(`${sep}package.json`))
14
+ const packages = new Map()
15
+ const publicPackageVersions = new Map()
16
+ const violations = []
17
+
18
+ for (const packageFile of packageFiles) {
19
+ const metadata = JSON.parse(await readFile(packageFile, 'utf8'))
20
+ if (!metadata.name || !metadata.exports) continue
21
+ if (!metadata.private) {
22
+ publicPackageVersions.set(metadata.name, metadata.version)
23
+
24
+ }
25
+ const packageRoot = dirname(packageFile)
26
+ const exportEntries = Object.entries(metadata.exports)
27
+ packages.set(metadata.name, { packageRoot, exportEntries })
28
+ for (const [, value] of exportEntries) {
29
+ for (const target of collectTargets(value)) {
30
+ const prefix = target.includes('*') ? target.slice(0, target.indexOf('*')) : target
31
+ const checkPath = resolve(packageRoot, prefix)
32
+ if (!(await pathExists(checkPath))) {
33
+ throw new Error(`${metadata.name} 导出目标不存在:${target}`)
34
+ }
35
+ }
36
+ }
37
+ }
38
+
39
+ const distinctPublicVersions = new Set(publicPackageVersions.values())
40
+ if (distinctPublicVersions.size > 1 || [...distinctPublicVersions].some((version) => !version)) {
41
+ const versions = [...publicPackageVersions]
42
+ .map(([name, version]) => `${name}@${version}`)
43
+ .join('、')
44
+ throw new Error(`Taro 公开包版本必须保持一致:${versions}`)
45
+ }
46
+
47
+ for (const sourceFile of allFiles.filter((file) => sourceExtensions.has(extname(file)))) {
48
+ if (sourceFile.includes(`${sep}src${sep}rpc${sep}`)) continue
49
+ const source = await readFile(sourceFile, 'utf8')
50
+ for (const imported of ts.preProcessFile(source, true, true).importedFiles) {
51
+ const specifier = imported.fileName
52
+ const line = source.slice(0, imported.pos).split('\n').length
53
+ if ((specifier === '@' || specifier.startsWith('@/')) && !isWithin(sourceFile, coreRoot)) {
54
+ violations.push(
55
+ `${format(sourceFile, line)}: 非 core 代码不得使用 core 私有别名 ${specifier}`,
56
+ )
57
+ continue
58
+ }
59
+ if (specifier.startsWith('@system/') && !isWithin(sourceFile, systemRoot)) {
60
+ violations.push(
61
+ `${format(sourceFile, line)}: 非 system 代码不得使用 system 私有别名 ${specifier}`,
62
+ )
63
+ continue
64
+ }
65
+ if (specifier.startsWith('.')) {
66
+ const sourcePackage = findPackageByPath(sourceFile)
67
+ const targetPackage = findPackageByPath(resolve(dirname(sourceFile), specifier))
68
+ if (sourcePackage && targetPackage && sourcePackage !== targetPackage) {
69
+ violations.push(`${format(sourceFile, line)}: 禁止通过相对路径跨包引用 ${specifier}`)
70
+ }
71
+ continue
72
+ }
73
+ const packageName = [...packages.keys()].find(
74
+ (name) => specifier === name || specifier.startsWith(`${name}/`),
75
+ )
76
+ if (!packageName) continue
77
+ const subpath = specifier === packageName ? '.' : `./${specifier.slice(packageName.length + 1)}`
78
+ const exported = packages
79
+ .get(packageName)
80
+ .exportEntries.some(([pattern]) => matchExportPattern(pattern, subpath))
81
+ if (!exported) {
82
+ violations.push(
83
+ `${format(sourceFile, line)}: ${specifier} 未在 ${packageName} exports 中公开`,
84
+ )
85
+ }
86
+ }
87
+ }
88
+
89
+ for (const [packageName, { packageRoot }] of packages) {
90
+ if (packageName === '@liujitcn/kratos-taro-app-core' && packageRoot !== coreRoot) {
91
+ violations.push(`${packageName}: core 包目录异常`)
92
+ }
93
+ if (packageName === '@liujitcn/kratos-taro-app-ui' && packageRoot !== uiRoot) {
94
+ violations.push(`${packageName}: UI 包目录异常`)
95
+ }
96
+ if (packageName === '@liujitcn/kratos-taro-app-system' && packageRoot !== systemRoot) {
97
+ violations.push(`${packageName}: system 包目录异常`)
98
+ }
99
+ }
100
+
101
+ if (violations.length) {
102
+ console.error(
103
+ `Taro package exports 检查失败:\n${violations.map((item) => ` ${item}`).join('\n')}`,
104
+ )
105
+ process.exit(1)
106
+ }
107
+ console.log(`Taro package exports 检查通过(${[...packages.keys()].join('、')})`)
108
+
109
+ async function collectFiles(directory) {
110
+ const files = []
111
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
112
+ if (entry.isDirectory() && ignoredDirectories.has(entry.name)) continue
113
+ const path = resolve(directory, entry.name)
114
+ if (entry.isDirectory()) files.push(...(await collectFiles(path)))
115
+ else files.push(path)
116
+ }
117
+ return files
118
+ }
119
+
120
+ function collectTargets(value) {
121
+ if (typeof value === 'string') return [value]
122
+ if (!value || typeof value !== 'object') return []
123
+ return Object.values(value).flatMap(collectTargets)
124
+ }
125
+
126
+ async function pathExists(path) {
127
+ try {
128
+ await stat(path)
129
+ return true
130
+ } catch {
131
+ return false
132
+ }
133
+ }
134
+
135
+ function matchExportPattern(pattern, subpath) {
136
+ if (!pattern.includes('*')) return pattern === subpath
137
+ const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace('\\*', '.+')
138
+ return new RegExp(`^${escaped}$`).test(subpath)
139
+ }
140
+
141
+ function findPackageByPath(file) {
142
+ return [...packages.values()].find(({ packageRoot }) => isWithin(file, packageRoot))
143
+ }
144
+
145
+ function isWithin(file, directory) {
146
+ const path = relative(directory, file)
147
+ return path === '' || (!path.startsWith('..') && !path.startsWith(sep))
148
+ }
149
+
150
+ function format(file, line) {
151
+ return `${relative(workspaceRoot, file)}:${line}`
152
+ }
@@ -0,0 +1,38 @@
1
+ import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
2
+ import { dirname, resolve } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+
5
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
6
+ const modulesRoot = resolve(root, 'packages/modules')
7
+ const write = process.argv.includes('--write')
8
+ let expectedLocales
9
+ for (const entry of existsSync(modulesRoot) ? readdirSync(modulesRoot, { withFileTypes: true }) : []) {
10
+ if (!entry.isDirectory()) continue
11
+ const directory = resolve(modulesRoot, entry.name, 'src/locales')
12
+ const locales = readdirSync(directory).filter(file => file.endsWith('.json')).map(file => file.slice(0, -5))
13
+ .sort((a, b) => a === 'zh-CN' ? -1 : b === 'zh-CN' ? 1 : a.localeCompare(b))
14
+ if (!locales.includes('zh-CN')) throw new Error(`${entry.name} 缺少 zh-CN 语言包`)
15
+ if (expectedLocales && locales.join(',') !== expectedLocales) throw new Error(`${entry.name} 语言集合不一致`)
16
+ expectedLocales = locales.join(',')
17
+ const messages = Object.fromEntries(locales.map(locale => {
18
+ if (!/^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/.test(locale)) throw new Error(`语言代码无效:${locale}`)
19
+ return [locale, JSON.parse(readFileSync(resolve(directory, `${locale}.json`), 'utf8'))]
20
+ }))
21
+ const keys = Object.keys(messages['zh-CN']).sort().join('\0')
22
+ for (const locale of locales) {
23
+ if (Object.keys(messages[locale]).sort().join('\0') !== keys) throw new Error(`${entry.name}/${locale} 语言键集合不一致`)
24
+ }
25
+ const imports = locales.map((locale, index) => `import locale${index} from './${locale}.json'`)
26
+ const content = [
27
+ '/* 此文件由 scripts/sync-locales.mjs 生成,请勿手工修改。 */',
28
+ ...imports, '', 'export const LOCALE_MESSAGES = {',
29
+ ...locales.map((locale, index) => ` '${locale}': locale${index},`),
30
+ '} as const satisfies Record<string, Record<string, string>>', '',
31
+ 'export type GeneratedLocale = keyof typeof LOCALE_MESSAGES',
32
+ "export const DEFAULT_LOCALE: GeneratedLocale = 'zh-CN'",
33
+ 'export const SUPPORTED_LOCALES = Object.keys(LOCALE_MESSAGES) as GeneratedLocale[]', '',
34
+ ].join('\n')
35
+ const file = resolve(directory, 'generated.ts')
36
+ if (write) writeFileSync(file, content)
37
+ else if (!existsSync(file) || readFileSync(file, 'utf8') !== content) throw new Error(`语言注册产物过期:${file},请执行 pnpm i18n:sync`)
38
+ }