@liujitcn/kratos-uni-app-core 0.0.18 → 0.0.19

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/dist/vite.mjs CHANGED
@@ -16,15 +16,16 @@ import { dirname, isAbsolute, relative, resolve } from "node:path";
16
16
  import uniPlugin from "@dcloudio/vite-plugin-uni";
17
17
  import { defineConfig, loadEnv } from "vite";
18
18
  var sourceVersionCounter = 0;
19
+ var PAGE_ASSEMBLY_WAIT_TIMEOUT_MS = 3e4;
20
+ var PAGE_ASSEMBLY_POLL_INTERVAL_MS = 25;
19
21
  function createKratosUniPlugin() {
22
+ recoverStalePageTransaction();
20
23
  const createPlugin = typeof uniPlugin === "function" ? uniPlugin : uniPlugin.default;
21
24
  return createPlugin();
22
25
  }
23
26
  function kratosApp(options) {
24
27
  const sourceVersion = `${process.pid}-${Date.now()}-${++sourceVersionCounter}`;
25
28
  const state = {
26
- generatedFiles: [],
27
- generatedStaticFiles: [],
28
29
  cleaned: false,
29
30
  persistent: false,
30
31
  moduleRoots: /* @__PURE__ */ new Map()
@@ -36,20 +37,11 @@ function kratosApp(options) {
36
37
  process.removeListener("exit", cleanup);
37
38
  process.removeListener("SIGINT", handleSigint);
38
39
  process.removeListener("SIGTERM", handleSigterm);
39
- for (const file of [...state.generatedFiles].reverse()) {
40
- if (existsSync(file))
41
- unlinkSync(file);
42
- removeEmptyParents(dirname(file), state.inputDir ?? "");
43
- }
44
- if (state.inputDir) {
45
- removeGeneratedStaticFiles(state.generatedStaticFiles, resolve(state.inputDir, "static"));
46
- }
47
- if (state.pagesFile && state.originalPages !== void 0) {
40
+ if (state.transactionFile && state.leaseFile) {
41
+ releasePageAssembly(state.transactionFile, state.leaseFile);
42
+ } else if (state.pagesFile && state.originalPages !== void 0) {
48
43
  writeFileSync(state.pagesFile, state.originalPages);
49
44
  }
50
- if (state.transactionFile && existsSync(state.transactionFile)) {
51
- unlinkSync(state.transactionFile);
52
- }
53
45
  };
54
46
  const handleSigint = () => {
55
47
  cleanup();
@@ -129,11 +121,8 @@ function kratosApp(options) {
129
121
  const inputDir = process.env.UNI_INPUT_DIR || resolve(process.cwd(), "src");
130
122
  const pagesFile = resolve(inputDir, "pages.json");
131
123
  const transactionFile = resolve(inputDir, "../.kratos-uni-app-pages-state.json");
132
- recoverBuildTransaction(transactionFile, inputDir, pagesFile);
133
124
  const hostRequire = createRequire(resolve(inputDir, "../package.json"));
134
125
  const originalPages = readFileSync(pagesFile, "utf8");
135
- const manifest = JSON.parse(originalPages);
136
- manifest.pages ??= [];
137
126
  const pageMap = /* @__PURE__ */ new Map();
138
127
  const moduleRoots = options.modules.map((module) => {
139
128
  const linkedRoot = resolve(inputDir, "../node_modules", ...module.name.split("/"));
@@ -154,48 +143,19 @@ function kratosApp(options) {
154
143
  });
155
144
  });
156
145
  });
157
- state.inputDir = inputDir;
158
146
  state.pagesFile = pagesFile;
159
- state.originalPages = originalPages;
160
147
  state.transactionFile = transactionFile;
161
- state.generatedFiles = [...pageMap.values()].map((page) => resolve(inputDir, `${page.route}.vue`)).filter((target) => !existsSync(target));
162
- const staticTarget = resolve(inputDir, "static");
163
- const staticFiles = collectModuleStaticFiles(
164
- moduleRoots.map(({ root }) => resolve(root, "src/static")),
165
- staticTarget
166
- );
167
- state.generatedStaticFiles = staticFiles.map(({ target }) => target);
168
- writeFileSync(
148
+ const pageAssembly = preparePageAssembly({
149
+ inputDir,
150
+ pagesFile,
169
151
  transactionFile,
170
- `${JSON.stringify(
171
- {
172
- inputDir,
173
- pagesFile,
174
- originalPages,
175
- generatedFiles: state.generatedFiles,
176
- generatedStaticFiles: state.generatedStaticFiles,
177
- ownerPid: process.pid
178
- },
179
- null,
180
- 2
181
- )}
182
- `,
183
- { flag: "wx" }
184
- );
185
- pageMap.forEach((page) => {
186
- const target = resolve(inputDir, `${page.route}.vue`);
187
- if (existsSync(target))
188
- return;
189
- mkdirSync(dirname(target), { recursive: true });
190
- writeFileSync(target, createPageWrapper(page));
191
- appendPage(manifest, page);
192
- });
193
- staticFiles.forEach(({ source, target }) => {
194
- mkdirSync(dirname(target), { recursive: true });
195
- copyFileSync(source, target);
152
+ originalPages,
153
+ moduleRoots,
154
+ pageMap: [...pageMap.values()],
155
+ sourceVersion
196
156
  });
197
- writeFileSync(pagesFile, `${JSON.stringify(manifest, null, 2)}
198
- `);
157
+ state.originalPages = pageAssembly.transaction.originalPages;
158
+ state.leaseFile = pageAssembly.leaseFile;
199
159
  process.once("exit", cleanup);
200
160
  process.prependOnceListener("SIGINT", handleSigint);
201
161
  process.prependOnceListener("SIGTERM", handleSigterm);
@@ -233,29 +193,233 @@ function kratosApp(options) {
233
193
  closeWatcher: cleanup
234
194
  };
235
195
  }
236
- function recoverBuildTransaction(transactionFile, inputDir, pagesFile) {
196
+ function preparePageAssembly(options) {
197
+ const lockFile = `${options.transactionFile}.lock`;
198
+ acquirePageAssemblyLock(lockFile);
199
+ try {
200
+ let transaction = readBuildTransaction(
201
+ options.transactionFile,
202
+ options.inputDir,
203
+ options.pagesFile
204
+ );
205
+ if (transaction && isPageAssemblyActive(transaction)) {
206
+ return {
207
+ transaction,
208
+ leaseFile: createPageAssemblyLease(options.transactionFile, options.sourceVersion)
209
+ };
210
+ }
211
+ let originalPages = options.originalPages;
212
+ if (transaction) {
213
+ originalPages = transaction.originalPages;
214
+ restoreBuildTransaction(transaction, options.transactionFile);
215
+ }
216
+ const manifest = JSON.parse(originalPages);
217
+ manifest.pages ??= [];
218
+ const generatedFiles = options.pageMap.map((page) => resolve(options.inputDir, `${page.route}.vue`)).filter((target) => !existsSync(target));
219
+ const staticTarget = resolve(options.inputDir, "static");
220
+ const staticFiles = collectModuleStaticFiles(
221
+ options.moduleRoots.map(({ root }) => resolve(root, "src/static")),
222
+ staticTarget
223
+ );
224
+ const generatedStaticFiles = staticFiles.map(({ target }) => target);
225
+ transaction = {
226
+ inputDir: options.inputDir,
227
+ pagesFile: options.pagesFile,
228
+ originalPages,
229
+ generatedFiles,
230
+ generatedStaticFiles,
231
+ ownerPid: process.pid,
232
+ ready: false
233
+ };
234
+ writeBuildTransaction(options.transactionFile, transaction, "wx");
235
+ const leaseFile = createPageAssemblyLease(options.transactionFile, options.sourceVersion);
236
+ try {
237
+ options.pageMap.forEach((page) => {
238
+ const target = resolve(options.inputDir, `${page.route}.vue`);
239
+ if (existsSync(target))
240
+ return;
241
+ mkdirSync(dirname(target), { recursive: true });
242
+ writeFileSync(target, createPageWrapper(page));
243
+ appendPage(manifest, page);
244
+ });
245
+ staticFiles.forEach(({ source, target }) => {
246
+ mkdirSync(dirname(target), { recursive: true });
247
+ copyFileSync(source, target);
248
+ });
249
+ writeFileSync(options.pagesFile, `${JSON.stringify(manifest, null, 2)}
250
+ `);
251
+ transaction.ready = true;
252
+ writeBuildTransaction(options.transactionFile, transaction);
253
+ return { transaction, leaseFile };
254
+ } catch (error) {
255
+ releasePageAssemblyLease(leaseFile);
256
+ restoreBuildTransaction(transaction, options.transactionFile);
257
+ throw error;
258
+ }
259
+ } finally {
260
+ releasePageAssemblyLock(lockFile);
261
+ }
262
+ }
263
+ function readBuildTransaction(transactionFile, inputDir, pagesFile) {
237
264
  if (!existsSync(transactionFile))
238
265
  return;
239
266
  const transaction = JSON.parse(readFileSync(transactionFile, "utf8"));
240
267
  if (transaction.inputDir !== inputDir || transaction.pagesFile !== pagesFile) {
241
268
  throw new Error(`uni-app \u6784\u5EFA\u4E8B\u52A1\u76EE\u5F55\u4E0D\u5339\u914D\uFF1A${transactionFile}`);
242
269
  }
243
- if (transaction.ownerPid !== void 0 && transaction.ownerPid !== process.pid && isProcessRunning(transaction.ownerPid)) {
244
- throw new Error(
245
- `\u9875\u9762\u88C5\u914D\u5DF2\u7531\u8FDB\u7A0B ${transaction.ownerPid} \u4F7F\u7528\uFF0C\u8BF7\u5148\u505C\u6B62\u53E6\u4E00\u4E2A H5 \u6216\u5C0F\u7A0B\u5E8F\u5F00\u53D1\u8FDB\u7A0B`
246
- );
247
- }
270
+ return transaction;
271
+ }
272
+ function writeBuildTransaction(transactionFile, transaction, flag) {
273
+ writeFileSync(
274
+ transactionFile,
275
+ `${JSON.stringify(transaction, null, 2)}
276
+ `,
277
+ flag ? { flag } : void 0
278
+ );
279
+ }
280
+ function restoreBuildTransaction(transaction, transactionFile) {
248
281
  for (const file of [...transaction.generatedFiles].reverse()) {
249
282
  if (existsSync(file))
250
283
  unlinkSync(file);
251
- removeEmptyParents(dirname(file), inputDir);
284
+ removeEmptyParents(dirname(file), transaction.inputDir);
252
285
  }
253
- removeGeneratedStaticFiles(transaction.generatedStaticFiles ?? [], resolve(inputDir, "static"));
286
+ removeGeneratedStaticFiles(
287
+ transaction.generatedStaticFiles ?? [],
288
+ resolve(transaction.inputDir, "static")
289
+ );
254
290
  if (transaction.generatedStatic && transaction.generatedStaticFiles === void 0) {
255
- rmSync(resolve(inputDir, "static"), { recursive: true, force: true });
291
+ rmSync(resolve(transaction.inputDir, "static"), { recursive: true, force: true });
292
+ }
293
+ writeFileSync(transaction.pagesFile, transaction.originalPages);
294
+ if (existsSync(transactionFile))
295
+ unlinkSync(transactionFile);
296
+ }
297
+ function releasePageAssembly(transactionFile, leaseFile) {
298
+ releasePageAssemblyLease(leaseFile);
299
+ const lockFile = `${transactionFile}.lock`;
300
+ acquirePageAssemblyLock(lockFile);
301
+ try {
302
+ if (!existsSync(transactionFile))
303
+ return;
304
+ const transaction = JSON.parse(readFileSync(transactionFile, "utf8"));
305
+ const current = readBuildTransaction(
306
+ transactionFile,
307
+ transaction.inputDir,
308
+ transaction.pagesFile
309
+ );
310
+ if (!current || isPageAssemblyActive(current))
311
+ return;
312
+ restoreBuildTransaction(current, transactionFile);
313
+ } finally {
314
+ releasePageAssemblyLock(lockFile);
315
+ }
316
+ }
317
+ function createPageAssemblyLease(transactionFile, sourceVersion) {
318
+ const leaseDirectory = `${transactionFile}.leases`;
319
+ mkdirSync(leaseDirectory, { recursive: true });
320
+ const leaseFile = resolve(leaseDirectory, `${process.pid}-${sourceVersion}.lease`);
321
+ writeFileSync(leaseFile, `${JSON.stringify({ ownerPid: process.pid })}
322
+ `, { flag: "wx" });
323
+ return leaseFile;
324
+ }
325
+ function releasePageAssemblyLease(leaseFile) {
326
+ if (existsSync(leaseFile))
327
+ unlinkSync(leaseFile);
328
+ const leaseDirectory = dirname(leaseFile);
329
+ if (existsSync(leaseDirectory) && !readdirSync(leaseDirectory).length) {
330
+ try {
331
+ rmdirSync(leaseDirectory);
332
+ } catch (error) {
333
+ const code = error.code;
334
+ if (code !== "ENOTEMPTY" && code !== "EEXIST")
335
+ throw error;
336
+ }
337
+ }
338
+ }
339
+ function isPageAssemblyActive(transaction) {
340
+ const leaseDirectory = resolve(
341
+ dirname(transaction.pagesFile),
342
+ "../.kratos-uni-app-pages-state.json.leases"
343
+ );
344
+ if (listActivePageLeases(leaseDirectory).length)
345
+ return true;
346
+ return transaction.ownerPid !== void 0 && transaction.ownerPid !== process.pid && isProcessRunning(transaction.ownerPid);
347
+ }
348
+ function listActivePageLeases(leaseDirectory) {
349
+ if (!existsSync(leaseDirectory))
350
+ return [];
351
+ const activePids = [];
352
+ for (const entry of readdirSync(leaseDirectory)) {
353
+ const leaseFile = resolve(leaseDirectory, entry);
354
+ try {
355
+ const lease = JSON.parse(readFileSync(leaseFile, "utf8"));
356
+ if (lease.ownerPid && isProcessRunning(lease.ownerPid)) {
357
+ activePids.push(lease.ownerPid);
358
+ } else {
359
+ unlinkSync(leaseFile);
360
+ }
361
+ } catch {
362
+ unlinkSync(leaseFile);
363
+ }
256
364
  }
257
- writeFileSync(pagesFile, transaction.originalPages);
258
- unlinkSync(transactionFile);
365
+ return activePids;
366
+ }
367
+ function acquirePageAssemblyLock(lockFile) {
368
+ const deadline = Date.now() + PAGE_ASSEMBLY_WAIT_TIMEOUT_MS;
369
+ while (true) {
370
+ try {
371
+ writeFileSync(lockFile, `${JSON.stringify({ ownerPid: process.pid })}
372
+ `, { flag: "wx" });
373
+ return;
374
+ } catch (error) {
375
+ if (error.code !== "EEXIST")
376
+ throw error;
377
+ const ownerPid = readPageAssemblyLockOwner(lockFile);
378
+ if (ownerPid !== void 0 && !isProcessRunning(ownerPid)) {
379
+ unlinkSync(lockFile);
380
+ continue;
381
+ }
382
+ if (Date.now() >= deadline) {
383
+ throw new Error(`\u7B49\u5F85 uni-app \u9875\u9762\u88C5\u914D\u9501\u8D85\u65F6\uFF1A${lockFile}`);
384
+ }
385
+ waitForPageAssembly(PAGE_ASSEMBLY_POLL_INTERVAL_MS);
386
+ }
387
+ }
388
+ }
389
+ function releasePageAssemblyLock(lockFile) {
390
+ if (!existsSync(lockFile))
391
+ return;
392
+ if (readPageAssemblyLockOwner(lockFile) === process.pid)
393
+ unlinkSync(lockFile);
394
+ }
395
+ function readPageAssemblyLockOwner(lockFile) {
396
+ try {
397
+ return JSON.parse(readFileSync(lockFile, "utf8")).ownerPid;
398
+ } catch {
399
+ return;
400
+ }
401
+ }
402
+ function waitForPageAssembly(milliseconds) {
403
+ const buffer = new SharedArrayBuffer(4);
404
+ Atomics.wait(new Int32Array(buffer), 0, 0, milliseconds);
405
+ }
406
+ function recoverStalePageTransaction() {
407
+ const inputDir = process.env.UNI_INPUT_DIR || resolve(process.cwd(), "src");
408
+ const transactionFile = resolve(inputDir, "../.kratos-uni-app-pages-state.json");
409
+ const pagesFile = resolve(inputDir, "pages.json");
410
+ const lockFile = `${transactionFile}.lock`;
411
+ acquirePageAssemblyLock(lockFile);
412
+ try {
413
+ recoverBuildTransaction(transactionFile, inputDir, pagesFile);
414
+ } finally {
415
+ releasePageAssemblyLock(lockFile);
416
+ }
417
+ }
418
+ function recoverBuildTransaction(transactionFile, inputDir, pagesFile) {
419
+ const transaction = readBuildTransaction(transactionFile, inputDir, pagesFile);
420
+ if (!transaction || isPageAssemblyActive(transaction))
421
+ return;
422
+ restoreBuildTransaction(transaction, transactionFile);
259
423
  }
260
424
  function isProcessRunning(pid) {
261
425
  try {
@@ -337,14 +501,16 @@ function appendPage(manifest, page) {
337
501
  function createPageWrapper(page) {
338
502
  const source = page.source.replace(/\\/g, "/");
339
503
  return `<script setup lang="ts">
504
+ import type { Component } from 'vue'
340
505
  import KratosPage from ${JSON.stringify(source)}
341
506
  import KratosTabBar from '@liujitcn/kratos-uni-app-core/components/KratosTabBar.vue'
342
507
 
343
508
  defineOptions({ inheritAttrs: false })
509
+ const Page = KratosPage as Component
344
510
  </script>
345
511
 
346
512
  <template>
347
- <KratosPage />
513
+ <Page v-bind="$attrs" />
348
514
  <KratosTabBar route=${JSON.stringify(page.route)} />
349
515
  </template>
350
516
  `;
@@ -401,5 +567,6 @@ export {
401
567
  createKratosUniPlugin,
402
568
  defineConfig,
403
569
  kratosApp,
404
- loadEnv
570
+ loadEnv,
571
+ recoverStalePageTransaction
405
572
  };
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.18",
10
+ "version": "0.0.19",
11
11
  "type": "module",
12
12
  "files": [
13
13
  "dist",
@@ -0,0 +1,14 @@
1
+ export interface MenuNode<T extends { id: number; parentId?: number }> extends T {
2
+ children: MenuNode<T>[]
3
+ }
4
+
5
+ export function buildMenuTree<T extends { id: number; parentId?: number }>(
6
+ menus: readonly T[],
7
+ rootId: number,
8
+ ): MenuNode<T>[]
9
+
10
+ export function resolveRootMenuId(
11
+ menus: readonly { id: number; parentId?: number }[],
12
+ menuId: number,
13
+ rootId: number,
14
+ ): number | undefined
@@ -5,7 +5,7 @@
5
5
  // source: base/v1/config.proto
6
6
 
7
7
  /* eslint-disable */
8
- import type { BaseConfigSite } from "../../common/v1/enum";
8
+ import type { BaseConfigSite } from "./enum";
9
9
 
10
10
  /** 获取系统配置条件 */
11
11
  export interface GetConfigRequest {
@@ -2,17 +2,30 @@
2
2
  // versions:
3
3
  // protoc-gen-ts_proto v2.11.8
4
4
  // protoc unknown
5
- // source: common/v1/enum.proto
5
+ // source: base/v1/enum.proto
6
6
 
7
7
  /* eslint-disable */
8
8
 
9
- /** 状态 */
10
- export enum Status {
11
- UNKNOWN = 0,
12
- /** ENABLE - 启用 */
13
- ENABLE = 1,
14
- /** DISABLE - 禁用 */
15
- DISABLE = 2,
9
+ /** AI 助手消息状态 */
10
+ export enum AiMessageStatus {
11
+ /** UNKNOWN_AMS - 未知AI助手消息状态 */
12
+ UNKNOWN_AMS = 0,
13
+ /** GENERATING_AMS - 生成中 */
14
+ GENERATING_AMS = 1,
15
+ /** SUCCESS_AMS - 成功 */
16
+ SUCCESS_AMS = 2,
17
+ /** FAILED_AMS - 失败 */
18
+ FAILED_AMS = 3,
19
+ }
20
+
21
+ /** 终端类型 */
22
+ export enum Terminal {
23
+ /** UNKNOWN_TERMINAL - 未知终端 */
24
+ UNKNOWN_TERMINAL = 0,
25
+ /** TERMINAL_APP - 移动端 */
26
+ TERMINAL_APP = 1,
27
+ /** TERMINAL_ADMIN - 管理端 */
28
+ TERMINAL_ADMIN = 2,
16
29
  }
17
30
 
18
31
  /** 系统配置位置 */
@@ -39,33 +52,3 @@ export enum PasswordCryptoScene {
39
52
  /** UPDATE_USER_PASSWORD - 个人修改密码加密场景 */
40
53
  UPDATE_USER_PASSWORD = 4,
41
54
  }
42
-
43
- /** AI 助手消息状态 */
44
- export enum AiMessageStatus {
45
- /** UNKNOWN_AAMS - 未知AI助手消息状态 */
46
- UNKNOWN_AAMS = 0,
47
- /** GENERATING_AAMS - 生成中 */
48
- GENERATING_AAMS = 1,
49
- /** SUCCESS_AAMS - 成功 */
50
- SUCCESS_AAMS = 2,
51
- /** FAILED_AAMS - 失败 */
52
- FAILED_AAMS = 3,
53
- }
54
-
55
- /** 支付通知资源类型 */
56
- export enum ResourceType {
57
- /** TRANSACTION - 支付 */
58
- TRANSACTION = 0,
59
- /** REFUND - 退款 */
60
- REFUND = 1,
61
- }
62
-
63
- /** 终端类型 */
64
- export enum Terminal {
65
- /** TERMINAL_UNSPECIFIED - 未知终端 */
66
- TERMINAL_UNSPECIFIED = 0,
67
- /** TERMINAL_APP - 移动端 */
68
- TERMINAL_APP = 1,
69
- /** TERMINAL_ADMIN - 管理端 */
70
- TERMINAL_ADMIN = 2,
71
- }
@@ -5,9 +5,9 @@
5
5
  // source: base/v1/login.proto
6
6
 
7
7
  /* eslint-disable */
8
- import type { PasswordCryptoScene } from "../../common/v1/enum";
9
8
  import type { PasswordCrypto } from "../../common/v1/types";
10
9
  import type { Empty } from "../../google/protobuf/empty";
10
+ import type { PasswordCryptoScene } from "./enum";
11
11
 
12
12
  /** 验证码获取条件 */
13
13
  export interface CaptchaRequest {
@@ -1,7 +1,7 @@
1
1
  import { defConfigService } from '../../api/base/config'
2
2
  import { defineStore } from 'pinia'
3
3
  import { ref } from 'vue'
4
- import { BaseConfigSite } from '../../rpc/common/v1/enum'
4
+ import { BaseConfigSite } from '../../rpc/base/v1/enum'
5
5
 
6
6
  const REQUIRED_APP_CONFIGS = [
7
7
  { key: 'serviceProtocol', name: '服务条款' },
@@ -1,5 +1,5 @@
1
1
  import { defLoginService } from '../api/base/login'
2
- import { PasswordCryptoScene } from '../rpc/common/v1/enum'
2
+ import { PasswordCryptoScene } from '../rpc/base/v1/enum'
3
3
  import type { PasswordCrypto } from '../rpc/common/v1/types'
4
4
 
5
5
  // #ifdef MP-WEIXIN
package/src/vite.ts CHANGED
@@ -41,8 +41,17 @@ type BuildTransaction = {
41
41
  generatedStaticFiles: string[]
42
42
  generatedStatic?: boolean
43
43
  ownerPid?: number
44
+ ready?: boolean
44
45
  }
45
46
 
47
+ type PageAssembly = {
48
+ transaction: BuildTransaction
49
+ leaseFile: string
50
+ }
51
+
52
+ const PAGE_ASSEMBLY_WAIT_TIMEOUT_MS = 30_000
53
+ const PAGE_ASSEMBLY_POLL_INTERVAL_MS = 25
54
+
46
55
  /** uni-app Vite 插件参数。 */
47
56
  export interface KratosAppViteOptions {
48
57
  /** 宿主模块清单,顺序同时决定覆盖优先级。 */
@@ -51,6 +60,7 @@ export interface KratosAppViteOptions {
51
60
 
52
61
  /** 创建 uni-app 官方 Vite 插件。 */
53
62
  export function createKratosUniPlugin() {
63
+ recoverStalePageTransaction()
54
64
  const createPlugin =
55
65
  typeof uniPlugin === 'function'
56
66
  ? uniPlugin
@@ -62,18 +72,14 @@ export function createKratosUniPlugin() {
62
72
  export function kratosApp(options: KratosAppViteOptions): Plugin {
63
73
  const sourceVersion = `${process.pid}-${Date.now()}-${++sourceVersionCounter}`
64
74
  const state: {
65
- inputDir?: string
66
75
  pagesFile?: string
67
76
  originalPages?: string
68
- generatedFiles: string[]
69
- generatedStaticFiles: string[]
70
77
  cleaned: boolean
71
78
  persistent: boolean
72
79
  transactionFile?: string
80
+ leaseFile?: string
73
81
  moduleRoots: Map<string, string>
74
82
  } = {
75
- generatedFiles: [],
76
- generatedStaticFiles: [],
77
83
  cleaned: false,
78
84
  persistent: false,
79
85
  moduleRoots: new Map(),
@@ -85,19 +91,11 @@ export function kratosApp(options: KratosAppViteOptions): Plugin {
85
91
  process.removeListener('exit', cleanup)
86
92
  process.removeListener('SIGINT', handleSigint)
87
93
  process.removeListener('SIGTERM', handleSigterm)
88
- for (const file of [...state.generatedFiles].reverse()) {
89
- if (existsSync(file)) unlinkSync(file)
90
- removeEmptyParents(dirname(file), state.inputDir ?? '')
91
- }
92
- if (state.inputDir) {
93
- removeGeneratedStaticFiles(state.generatedStaticFiles, resolve(state.inputDir, 'static'))
94
- }
95
- if (state.pagesFile && state.originalPages !== undefined) {
94
+ if (state.transactionFile && state.leaseFile) {
95
+ releasePageAssembly(state.transactionFile, state.leaseFile)
96
+ } else if (state.pagesFile && state.originalPages !== undefined) {
96
97
  writeFileSync(state.pagesFile, state.originalPages)
97
98
  }
98
- if (state.transactionFile && existsSync(state.transactionFile)) {
99
- unlinkSync(state.transactionFile)
100
- }
101
99
  }
102
100
  const handleSigint = () => {
103
101
  cleanup()
@@ -184,11 +182,8 @@ export function kratosApp(options: KratosAppViteOptions): Plugin {
184
182
  const inputDir = process.env.UNI_INPUT_DIR || resolve(process.cwd(), 'src')
185
183
  const pagesFile = resolve(inputDir, 'pages.json')
186
184
  const transactionFile = resolve(inputDir, '../.kratos-uni-app-pages-state.json')
187
- recoverBuildTransaction(transactionFile, inputDir, pagesFile)
188
185
  const hostRequire = createRequire(resolve(inputDir, '../package.json'))
189
186
  const originalPages = readFileSync(pagesFile, 'utf8')
190
- const manifest = JSON.parse(originalPages) as PagesManifest
191
- manifest.pages ??= []
192
187
  const pageMap = new Map<string, ScannedPage>()
193
188
  const moduleRoots = options.modules.map((module) => {
194
189
  const linkedRoot = resolve(inputDir, '../node_modules', ...module.name.split('/'))
@@ -213,48 +208,19 @@ export function kratosApp(options: KratosAppViteOptions): Plugin {
213
208
  })
214
209
  })
215
210
 
216
- state.inputDir = inputDir
217
211
  state.pagesFile = pagesFile
218
- state.originalPages = originalPages
219
212
  state.transactionFile = transactionFile
220
- state.generatedFiles = [...pageMap.values()]
221
- .map((page) => resolve(inputDir, `${page.route}.vue`))
222
- .filter((target) => !existsSync(target))
223
- const staticTarget = resolve(inputDir, 'static')
224
- const staticFiles = collectModuleStaticFiles(
225
- moduleRoots.map(({ root }) => resolve(root, 'src/static')),
226
- staticTarget,
227
- )
228
- state.generatedStaticFiles = staticFiles.map(({ target }) => target)
229
- writeFileSync(
213
+ const pageAssembly = preparePageAssembly({
214
+ inputDir,
215
+ pagesFile,
230
216
  transactionFile,
231
- `${JSON.stringify(
232
- {
233
- inputDir,
234
- pagesFile,
235
- originalPages,
236
- generatedFiles: state.generatedFiles,
237
- generatedStaticFiles: state.generatedStaticFiles,
238
- ownerPid: process.pid,
239
- } satisfies BuildTransaction,
240
- null,
241
- 2,
242
- )}\n`,
243
- { flag: 'wx' },
244
- )
245
- pageMap.forEach((page) => {
246
- const target = resolve(inputDir, `${page.route}.vue`)
247
- if (existsSync(target)) return
248
- mkdirSync(dirname(target), { recursive: true })
249
- writeFileSync(target, createPageWrapper(page))
250
- appendPage(manifest, page)
251
- })
252
-
253
- staticFiles.forEach(({ source, target }) => {
254
- mkdirSync(dirname(target), { recursive: true })
255
- copyFileSync(source, target)
217
+ originalPages,
218
+ moduleRoots,
219
+ pageMap: [...pageMap.values()],
220
+ sourceVersion,
256
221
  })
257
- writeFileSync(pagesFile, `${JSON.stringify(manifest, null, 2)}\n`)
222
+ state.originalPages = pageAssembly.transaction.originalPages
223
+ state.leaseFile = pageAssembly.leaseFile
258
224
  process.once('exit', cleanup)
259
225
  process.prependOnceListener('SIGINT', handleSigint)
260
226
  process.prependOnceListener('SIGTERM', handleSigterm)
@@ -292,36 +258,260 @@ export function kratosApp(options: KratosAppViteOptions): Plugin {
292
258
  }
293
259
  }
294
260
 
295
- function recoverBuildTransaction(
261
+ function preparePageAssembly(options: {
262
+ inputDir: string
263
+ pagesFile: string
264
+ transactionFile: string
265
+ originalPages: string
266
+ moduleRoots: Array<{ root: string }>
267
+ pageMap: ScannedPage[]
268
+ sourceVersion: string
269
+ }): PageAssembly {
270
+ const lockFile = `${options.transactionFile}.lock`
271
+ acquirePageAssemblyLock(lockFile)
272
+ try {
273
+ let transaction = readBuildTransaction(
274
+ options.transactionFile,
275
+ options.inputDir,
276
+ options.pagesFile,
277
+ )
278
+ if (transaction && isPageAssemblyActive(transaction)) {
279
+ return {
280
+ transaction,
281
+ leaseFile: createPageAssemblyLease(options.transactionFile, options.sourceVersion),
282
+ }
283
+ }
284
+
285
+ let originalPages = options.originalPages
286
+ if (transaction) {
287
+ originalPages = transaction.originalPages
288
+ restoreBuildTransaction(transaction, options.transactionFile)
289
+ }
290
+
291
+ const manifest = JSON.parse(originalPages) as PagesManifest
292
+ manifest.pages ??= []
293
+ const generatedFiles = options.pageMap
294
+ .map((page) => resolve(options.inputDir, `${page.route}.vue`))
295
+ .filter((target) => !existsSync(target))
296
+ const staticTarget = resolve(options.inputDir, 'static')
297
+ const staticFiles = collectModuleStaticFiles(
298
+ options.moduleRoots.map(({ root }) => resolve(root, 'src/static')),
299
+ staticTarget,
300
+ )
301
+ const generatedStaticFiles = staticFiles.map(({ target }) => target)
302
+ transaction = {
303
+ inputDir: options.inputDir,
304
+ pagesFile: options.pagesFile,
305
+ originalPages,
306
+ generatedFiles,
307
+ generatedStaticFiles,
308
+ ownerPid: process.pid,
309
+ ready: false,
310
+ }
311
+ writeBuildTransaction(options.transactionFile, transaction, 'wx')
312
+ const leaseFile = createPageAssemblyLease(options.transactionFile, options.sourceVersion)
313
+ try {
314
+ options.pageMap.forEach((page) => {
315
+ const target = resolve(options.inputDir, `${page.route}.vue`)
316
+ if (existsSync(target)) return
317
+ mkdirSync(dirname(target), { recursive: true })
318
+ writeFileSync(target, createPageWrapper(page))
319
+ appendPage(manifest, page)
320
+ })
321
+
322
+ staticFiles.forEach(({ source, target }) => {
323
+ mkdirSync(dirname(target), { recursive: true })
324
+ copyFileSync(source, target)
325
+ })
326
+ writeFileSync(options.pagesFile, `${JSON.stringify(manifest, null, 2)}\n`)
327
+ transaction.ready = true
328
+ writeBuildTransaction(options.transactionFile, transaction)
329
+ return { transaction, leaseFile }
330
+ } catch (error) {
331
+ releasePageAssemblyLease(leaseFile)
332
+ restoreBuildTransaction(transaction, options.transactionFile)
333
+ throw error
334
+ }
335
+ } finally {
336
+ releasePageAssemblyLock(lockFile)
337
+ }
338
+ }
339
+
340
+ function readBuildTransaction(
296
341
  transactionFile: string,
297
342
  inputDir: string,
298
343
  pagesFile: string,
299
- ): void {
344
+ ): BuildTransaction | undefined {
300
345
  if (!existsSync(transactionFile)) return
301
346
  const transaction = JSON.parse(readFileSync(transactionFile, 'utf8')) as BuildTransaction
302
347
  if (transaction.inputDir !== inputDir || transaction.pagesFile !== pagesFile) {
303
348
  throw new Error(`uni-app 构建事务目录不匹配:${transactionFile}`)
304
349
  }
305
- if (
306
- transaction.ownerPid !== undefined &&
307
- transaction.ownerPid !== process.pid &&
308
- isProcessRunning(transaction.ownerPid)
309
- ) {
310
- throw new Error(
311
- `页面装配已由进程 ${transaction.ownerPid} 使用,请先停止另一个 H5 或小程序开发进程`,
312
- )
313
- }
350
+ return transaction
351
+ }
352
+
353
+ function writeBuildTransaction(
354
+ transactionFile: string,
355
+ transaction: BuildTransaction,
356
+ flag?: 'wx',
357
+ ): void {
358
+ writeFileSync(
359
+ transactionFile,
360
+ `${JSON.stringify(transaction, null, 2)}\n`,
361
+ flag ? { flag } : undefined,
362
+ )
363
+ }
364
+
365
+ function restoreBuildTransaction(transaction: BuildTransaction, transactionFile: string): void {
314
366
  for (const file of [...transaction.generatedFiles].reverse()) {
315
367
  if (existsSync(file)) unlinkSync(file)
316
- removeEmptyParents(dirname(file), inputDir)
368
+ removeEmptyParents(dirname(file), transaction.inputDir)
317
369
  }
318
- removeGeneratedStaticFiles(transaction.generatedStaticFiles ?? [], resolve(inputDir, 'static'))
370
+ removeGeneratedStaticFiles(
371
+ transaction.generatedStaticFiles ?? [],
372
+ resolve(transaction.inputDir, 'static'),
373
+ )
319
374
  // 兼容旧事务:旧格式仅在 static 原本不存在时记录整个目录由插件生成。
320
375
  if (transaction.generatedStatic && transaction.generatedStaticFiles === undefined) {
321
- rmSync(resolve(inputDir, 'static'), { recursive: true, force: true })
376
+ rmSync(resolve(transaction.inputDir, 'static'), { recursive: true, force: true })
377
+ }
378
+ writeFileSync(transaction.pagesFile, transaction.originalPages)
379
+ if (existsSync(transactionFile)) unlinkSync(transactionFile)
380
+ }
381
+
382
+ function releasePageAssembly(transactionFile: string, leaseFile: string): void {
383
+ releasePageAssemblyLease(leaseFile)
384
+ const lockFile = `${transactionFile}.lock`
385
+ acquirePageAssemblyLock(lockFile)
386
+ try {
387
+ if (!existsSync(transactionFile)) return
388
+ const transaction = JSON.parse(readFileSync(transactionFile, 'utf8')) as BuildTransaction
389
+ const current = readBuildTransaction(
390
+ transactionFile,
391
+ transaction.inputDir,
392
+ transaction.pagesFile,
393
+ )
394
+ if (!current || isPageAssemblyActive(current)) return
395
+ restoreBuildTransaction(current, transactionFile)
396
+ } finally {
397
+ releasePageAssemblyLock(lockFile)
398
+ }
399
+ }
400
+
401
+ function createPageAssemblyLease(transactionFile: string, sourceVersion: string): string {
402
+ const leaseDirectory = `${transactionFile}.leases`
403
+ mkdirSync(leaseDirectory, { recursive: true })
404
+ const leaseFile = resolve(leaseDirectory, `${process.pid}-${sourceVersion}.lease`)
405
+ writeFileSync(leaseFile, `${JSON.stringify({ ownerPid: process.pid })}\n`, { flag: 'wx' })
406
+ return leaseFile
407
+ }
408
+
409
+ function releasePageAssemblyLease(leaseFile: string): void {
410
+ if (existsSync(leaseFile)) unlinkSync(leaseFile)
411
+ const leaseDirectory = dirname(leaseFile)
412
+ if (existsSync(leaseDirectory) && !readdirSync(leaseDirectory).length) {
413
+ try {
414
+ rmdirSync(leaseDirectory)
415
+ } catch (error) {
416
+ const code = (error as NodeJS.ErrnoException).code
417
+ if (code !== 'ENOTEMPTY' && code !== 'EEXIST') throw error
418
+ }
419
+ }
420
+ }
421
+
422
+ function isPageAssemblyActive(transaction: BuildTransaction): boolean {
423
+ const leaseDirectory = resolve(
424
+ dirname(transaction.pagesFile),
425
+ '../.kratos-uni-app-pages-state.json.leases',
426
+ )
427
+ if (listActivePageLeases(leaseDirectory).length) return true
428
+ return (
429
+ transaction.ownerPid !== undefined &&
430
+ transaction.ownerPid !== process.pid &&
431
+ isProcessRunning(transaction.ownerPid)
432
+ )
433
+ }
434
+
435
+ function listActivePageLeases(leaseDirectory: string): number[] {
436
+ if (!existsSync(leaseDirectory)) return []
437
+ const activePids: number[] = []
438
+ for (const entry of readdirSync(leaseDirectory)) {
439
+ const leaseFile = resolve(leaseDirectory, entry)
440
+ try {
441
+ const lease = JSON.parse(readFileSync(leaseFile, 'utf8')) as { ownerPid?: number }
442
+ if (lease.ownerPid && isProcessRunning(lease.ownerPid)) {
443
+ activePids.push(lease.ownerPid)
444
+ } else {
445
+ unlinkSync(leaseFile)
446
+ }
447
+ } catch {
448
+ unlinkSync(leaseFile)
449
+ }
322
450
  }
323
- writeFileSync(pagesFile, transaction.originalPages)
324
- unlinkSync(transactionFile)
451
+ return activePids
452
+ }
453
+
454
+ function acquirePageAssemblyLock(lockFile: string): void {
455
+ const deadline = Date.now() + PAGE_ASSEMBLY_WAIT_TIMEOUT_MS
456
+ while (true) {
457
+ try {
458
+ writeFileSync(lockFile, `${JSON.stringify({ ownerPid: process.pid })}\n`, { flag: 'wx' })
459
+ return
460
+ } catch (error) {
461
+ if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error
462
+ const ownerPid = readPageAssemblyLockOwner(lockFile)
463
+ if (ownerPid !== undefined && !isProcessRunning(ownerPid)) {
464
+ unlinkSync(lockFile)
465
+ continue
466
+ }
467
+ if (Date.now() >= deadline) {
468
+ throw new Error(`等待 uni-app 页面装配锁超时:${lockFile}`)
469
+ }
470
+ waitForPageAssembly(PAGE_ASSEMBLY_POLL_INTERVAL_MS)
471
+ }
472
+ }
473
+ }
474
+
475
+ function releasePageAssemblyLock(lockFile: string): void {
476
+ if (!existsSync(lockFile)) return
477
+ if (readPageAssemblyLockOwner(lockFile) === process.pid) unlinkSync(lockFile)
478
+ }
479
+
480
+ function readPageAssemblyLockOwner(lockFile: string): number | undefined {
481
+ try {
482
+ return (JSON.parse(readFileSync(lockFile, 'utf8')) as { ownerPid?: number }).ownerPid
483
+ } catch {
484
+ return
485
+ }
486
+ }
487
+
488
+ function waitForPageAssembly(milliseconds: number): void {
489
+ const buffer = new SharedArrayBuffer(4)
490
+ Atomics.wait(new Int32Array(buffer), 0, 0, milliseconds)
491
+ }
492
+
493
+ // recoverStalePageTransaction 在官方 uni 插件创建前恢复页面事务,避免官方插件先读取到空 pages.json。
494
+ export function recoverStalePageTransaction(): void {
495
+ const inputDir = process.env.UNI_INPUT_DIR || resolve(process.cwd(), 'src')
496
+ const transactionFile = resolve(inputDir, '../.kratos-uni-app-pages-state.json')
497
+ const pagesFile = resolve(inputDir, 'pages.json')
498
+ const lockFile = `${transactionFile}.lock`
499
+ acquirePageAssemblyLock(lockFile)
500
+ try {
501
+ recoverBuildTransaction(transactionFile, inputDir, pagesFile)
502
+ } finally {
503
+ releasePageAssemblyLock(lockFile)
504
+ }
505
+ }
506
+
507
+ function recoverBuildTransaction(
508
+ transactionFile: string,
509
+ inputDir: string,
510
+ pagesFile: string,
511
+ ): void {
512
+ const transaction = readBuildTransaction(transactionFile, inputDir, pagesFile)
513
+ if (!transaction || isPageAssemblyActive(transaction)) return
514
+ restoreBuildTransaction(transaction, transactionFile)
325
515
  }
326
516
 
327
517
  // isProcessRunning 判断页面事务的所属进程是否仍然存活。
@@ -407,14 +597,16 @@ function appendPage(manifest: PagesManifest, page: ScannedPage): void {
407
597
  function createPageWrapper(page: ScannedPage): string {
408
598
  const source = page.source.replace(/\\/g, '/')
409
599
  return `<script setup lang="ts">
600
+ import type { Component } from 'vue'
410
601
  import KratosPage from ${JSON.stringify(source)}
411
602
  import KratosTabBar from '@liujitcn/kratos-uni-app-core/components/KratosTabBar.vue'
412
603
 
413
604
  defineOptions({ inheritAttrs: false })
605
+ const Page = KratosPage as Component
414
606
  </script>
415
607
 
416
608
  <template>
417
- <KratosPage />
609
+ <Page v-bind="$attrs" />
418
610
  <KratosTabBar route=${JSON.stringify(page.route)} />
419
611
  </template>
420
612
  `