create-windy 0.2.29 → 0.2.30
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/cli.js +377 -7
- package/package.json +1 -1
- package/template/.windy-template.json +2 -2
- package/template/README.md +3 -0
- package/template/apps/server/src/work-order/drizzle-repository.ts +1 -2
- package/template/apps/web/src/router/shared-scaffold-neutrality.webtest.ts +7 -5
- package/template/docs/architecture/migration-bundle.md +151 -0
- package/template/module-schema.lineages.json +8 -0
- package/template/packages/database/drizzle/0033_rich_george_stacy.sql +2 -0
- package/template/packages/database/drizzle/meta/0033_snapshot.json +7239 -0
- package/template/packages/database/drizzle/meta/_journal.json +7 -0
- package/template/packages/database/src/schema/index.ts +0 -3
- package/template/packages/example-work-order/drizzle/0000_bitter_the_santerians.sql +2 -0
- package/template/packages/example-work-order/drizzle/meta/0000_snapshot.json +179 -0
- package/template/packages/example-work-order/drizzle/meta/_journal.json +13 -0
- package/template/packages/example-work-order/drizzle.config.ts +8 -0
- package/template/packages/example-work-order/index.ts +1 -0
- package/template/packages/example-work-order/package.json +4 -0
- package/template/packages/{database/src/schema/work-orders.ts → example-work-order/src/drizzle-schema.ts} +16 -2
package/dist/cli.js
CHANGED
|
@@ -394,8 +394,9 @@ var optionalModuleManifests = [
|
|
|
394
394
|
"apps/server/src/example-permissions.ts"
|
|
395
395
|
],
|
|
396
396
|
documentation: ["docs/development/example-work-order.md"],
|
|
397
|
-
schemaFiles: ["packages/
|
|
398
|
-
databaseTables: ["work_orders"]
|
|
397
|
+
schemaFiles: ["packages/example-work-order/src/drizzle-schema.ts"],
|
|
398
|
+
databaseTables: ["work_orders"],
|
|
399
|
+
databaseLineageOwner: "module"
|
|
399
400
|
}
|
|
400
401
|
];
|
|
401
402
|
function module(name, label, description, dependencies) {
|
|
@@ -414,6 +415,7 @@ function module(name, label, description, dependencies) {
|
|
|
414
415
|
documentation: [],
|
|
415
416
|
schemaFiles: [],
|
|
416
417
|
databaseTables: [],
|
|
418
|
+
databaseLineageOwner: "platform",
|
|
417
419
|
transientDatabaseTables: [],
|
|
418
420
|
databaseStatementTokens: [],
|
|
419
421
|
uninstallPolicy: "retain-data"
|
|
@@ -628,6 +630,7 @@ function module2(name, label, description, selection, dependencies = []) {
|
|
|
628
630
|
documentation: [],
|
|
629
631
|
schemaFiles: [],
|
|
630
632
|
databaseTables: [],
|
|
633
|
+
databaseLineageOwner: "platform",
|
|
631
634
|
transientDatabaseTables: [],
|
|
632
635
|
databaseStatementTokens: [],
|
|
633
636
|
uninstallPolicy: "retain-data"
|
|
@@ -735,6 +738,7 @@ var helpText = `create-windy <项目名> [选项]
|
|
|
735
738
|
create-windy update --check [--cwd <目录>]
|
|
736
739
|
create-windy update --dry-run [--to <版本>] [--cwd <目录>]
|
|
737
740
|
create-windy update [--to <版本>] [--cwd <目录>]
|
|
741
|
+
create-windy schema handoff [--config <JSON>] [--cwd <目录>]
|
|
738
742
|
|
|
739
743
|
选项:
|
|
740
744
|
--profile private-enterprise 单组织、单租户、私有部署 profile
|
|
@@ -1200,8 +1204,11 @@ var ignoredFiles = new Set([
|
|
|
1200
1204
|
".windy/update-state.json"
|
|
1201
1205
|
]);
|
|
1202
1206
|
var projectOwnedRoots = [
|
|
1207
|
+
"module-schema.lineages.json",
|
|
1208
|
+
"module-schema",
|
|
1203
1209
|
"apps/web/src/app",
|
|
1204
1210
|
"apps/server/src/installed-business-modules.ts",
|
|
1211
|
+
"apps/server/src/domain-schema",
|
|
1205
1212
|
"apps/server/src/work-order",
|
|
1206
1213
|
"packages/example-work-order"
|
|
1207
1214
|
];
|
|
@@ -1320,10 +1327,13 @@ async function materializeSelectedModules(targetDirectory, selectedNames) {
|
|
|
1320
1327
|
removedPackageNames.add(dependency.name);
|
|
1321
1328
|
}
|
|
1322
1329
|
await stripModuleBlocks(targetDirectory, selected);
|
|
1323
|
-
await pruneDatabaseArtifacts(targetDirectory,
|
|
1324
|
-
...databaseTables,
|
|
1325
|
-
|
|
1326
|
-
|
|
1330
|
+
await pruneDatabaseArtifacts(targetDirectory, [
|
|
1331
|
+
...removed.flatMap(({ databaseTables, transientDatabaseTables }) => [
|
|
1332
|
+
...databaseTables,
|
|
1333
|
+
...transientDatabaseTables
|
|
1334
|
+
]),
|
|
1335
|
+
...moduleDistributionCatalog.filter(({ databaseLineageOwner }) => databaseLineageOwner === "module").flatMap(({ databaseTables }) => databaseTables)
|
|
1336
|
+
], removed.flatMap(({ databaseStatementTokens }) => databaseStatementTokens));
|
|
1327
1337
|
const removedRoots = removed.flatMap((manifest) => [
|
|
1328
1338
|
...manifest.packages,
|
|
1329
1339
|
...manifest.fileRoots,
|
|
@@ -1624,6 +1634,7 @@ License 签发中心、独立签名服务、Vault、发布治理、内部任务
|
|
|
1624
1634
|
规划属于软件供应方控制面,不进入客户 Starter。
|
|
1625
1635
|
|
|
1626
1636
|
- 架构:从 [Module Manifest 组合](./architecture/module-manifest-composition.md) 开始。
|
|
1637
|
+
- 数据库:业务表使用 [Migration Bundle 与独立 Drizzle lineage](./architecture/migration-bundle.md)。
|
|
1627
1638
|
- 平台开发:从 [系统 CRUD API](./platform/system-crud-api.md) 和 [CRUD 生成器](./development/crud-generator.md) 开始。
|
|
1628
1639
|
${includeOfflineLicense ? "- License 消费端:[离线激活与安装协议](./license/license-offline-activation-code.md);运行时能力由 npm 依赖 `@southwind-ai/license` 提供。" : ""}
|
|
1629
1640
|
- 运维:[可观测性](./operations/observability.md) 与 [运维总览](./platform/operations-overview.md)。
|
|
@@ -1748,6 +1759,9 @@ create-windy 在本机 \`.env\` 中随机生成的 \`WINDY_BOOTSTRAP_ADMIN_PASSW
|
|
|
1748
1759
|
| ---------- | -------- | ------------ | ---- |
|
|
1749
1760
|
${moduleRows}
|
|
1750
1761
|
|
|
1762
|
+
项目自有业务模块以 \`apps/server/src/installed-business-modules.ts\` 装配的 Module Manifest
|
|
1763
|
+
为准;shared-scaffold README 不复制项目业务 Key,避免平台升级固化产品命名。
|
|
1764
|
+
|
|
1751
1765
|
精确生成来源和选择记录在 \`.windy/generation.json\`。当前版本已提供可回滚的
|
|
1752
1766
|
\`windy update\`;通用 \`module add/remove\` 尚未交付,不要把 Feature Flag 关闭当成
|
|
1753
1767
|
模块已经卸载。
|
|
@@ -1876,6 +1890,7 @@ var templateRootFiles = [
|
|
|
1876
1890
|
"bunfig.toml",
|
|
1877
1891
|
"docker-compose.yml",
|
|
1878
1892
|
"drizzle.config.ts",
|
|
1893
|
+
"module-schema.lineages.json",
|
|
1879
1894
|
"package.json"
|
|
1880
1895
|
];
|
|
1881
1896
|
var templateDirectories = [
|
|
@@ -1902,6 +1917,7 @@ var templateDocumentationFiles = [
|
|
|
1902
1917
|
"docs/architecture/data-scope-query-enforcement.md",
|
|
1903
1918
|
"docs/architecture/durable-jobs.md",
|
|
1904
1919
|
"docs/architecture/minimal-approval-workflow.md",
|
|
1920
|
+
"docs/architecture/migration-bundle.md",
|
|
1905
1921
|
"docs/architecture/module-manifest-composition.md",
|
|
1906
1922
|
"docs/architecture/object-storage.md",
|
|
1907
1923
|
"docs/architecture/search-provider-protocol.md",
|
|
@@ -4338,7 +4354,8 @@ var recipes = [
|
|
|
4338
4354
|
{ id: "starter-0.2.25-to-0.2.26", from: "0.2.25", to: "0.2.26" },
|
|
4339
4355
|
{ id: "starter-0.2.26-to-0.2.27", from: "0.2.26", to: "0.2.27" },
|
|
4340
4356
|
{ id: "starter-0.2.27-to-0.2.28", from: "0.2.27", to: "0.2.28" },
|
|
4341
|
-
{ id: "starter-0.2.28-to-0.2.29", from: "0.2.28", to: "0.2.29" }
|
|
4357
|
+
{ id: "starter-0.2.28-to-0.2.29", from: "0.2.28", to: "0.2.29" },
|
|
4358
|
+
{ id: "starter-0.2.29-to-0.2.30", from: "0.2.29", to: "0.2.30" }
|
|
4342
4359
|
];
|
|
4343
4360
|
function resolveRecipeChain(sourceVersion, targetVersion) {
|
|
4344
4361
|
if (sourceVersion === targetVersion)
|
|
@@ -4466,11 +4483,32 @@ var known023DownstreamHashes = new Map([
|
|
|
4466
4483
|
"81fb08f26e5baa3238606d62b9c46761a68c90f3967ea639b178c80c74c69fc1"
|
|
4467
4484
|
]
|
|
4468
4485
|
]);
|
|
4486
|
+
var known029DownstreamHashes = new Map([
|
|
4487
|
+
[
|
|
4488
|
+
"README.md",
|
|
4489
|
+
"a8b10907e151c28768678c016d102f98c3414981a65a314d75b2049fb1f9fe35"
|
|
4490
|
+
],
|
|
4491
|
+
[
|
|
4492
|
+
"drizzle.config.ts",
|
|
4493
|
+
"c765b611d68b3a42375ea3530048fc7f32b1c7161257fa16f9dd093b580963e3"
|
|
4494
|
+
],
|
|
4495
|
+
[
|
|
4496
|
+
"packages/database/src/schema/index.ts",
|
|
4497
|
+
"250682d5182b580a9e1fae27e7a596e6a503b41d6f5dca9e4322f5c6094d0976"
|
|
4498
|
+
],
|
|
4499
|
+
[
|
|
4500
|
+
"apps/web/src/router/shared-scaffold-neutrality.webtest.ts",
|
|
4501
|
+
"420f1093eefcccf2a321e12c403628854c9a4a0b459ee14e96883557cd71dcaa"
|
|
4502
|
+
]
|
|
4503
|
+
]);
|
|
4469
4504
|
var wrongExampleDescription = "创建时包含了 `work-order` 示例模块,用于演示 Module Manifest 接入;它不是生产工单产品,可以在正式开发前替换。";
|
|
4470
4505
|
var correctCoreDescription = "创建时未选择 `work-order` 示例模块,当前项目只包含平台模块。";
|
|
4471
4506
|
var wrongExampleRow = /^\|\s*示例工单\s*\|\s*已包含\s*\|$/m;
|
|
4472
4507
|
var correctCoreRow = /^\|\s*示例工单\s*\|\s*未包含\s*\|$/m;
|
|
4473
4508
|
function repairKnownDerivedFile(input) {
|
|
4509
|
+
const ownershipRepair = repairKnown029Downstream(input);
|
|
4510
|
+
if (ownershipRepair !== undefined)
|
|
4511
|
+
return ownershipRepair;
|
|
4474
4512
|
const neutralRouterRepair = repairKnown026Downstream(input);
|
|
4475
4513
|
if (neutralRouterRepair !== undefined)
|
|
4476
4514
|
return neutralRouterRepair;
|
|
@@ -4494,6 +4532,46 @@ function repairKnownDerivedFile(input) {
|
|
|
4494
4532
|
}
|
|
4495
4533
|
return input.content.replace(wrongExampleDescription, correctCoreDescription).replace(wrongExampleRow, targetRow);
|
|
4496
4534
|
}
|
|
4535
|
+
function repairKnown029Downstream(input) {
|
|
4536
|
+
if (input.sourceVersion !== "0.2.29" || known029DownstreamHashes.get(input.path) !== contentHash(input.content)) {
|
|
4537
|
+
return;
|
|
4538
|
+
}
|
|
4539
|
+
if (input.path === "drizzle.config.ts" || input.path === "apps/web/src/router/shared-scaffold-neutrality.webtest.ts") {
|
|
4540
|
+
return input.targetContent;
|
|
4541
|
+
}
|
|
4542
|
+
if (input.path === "packages/database/src/schema/index.ts") {
|
|
4543
|
+
return removeUnknownSchemaExports(input.content, input.targetContent);
|
|
4544
|
+
}
|
|
4545
|
+
if (input.path === "README.md") {
|
|
4546
|
+
return removeProjectModuleRows(input.content, input.targetContent);
|
|
4547
|
+
}
|
|
4548
|
+
}
|
|
4549
|
+
function removeUnknownSchemaExports(content, targetContent) {
|
|
4550
|
+
const targetExports = new Set(targetContent.split(`
|
|
4551
|
+
`).filter((line) => line.startsWith("export ") && line.includes(" from ")));
|
|
4552
|
+
return content.split(`
|
|
4553
|
+
`).filter((line) => !line.startsWith("export ") || !line.includes(" from ") || targetExports.has(line)).join(`
|
|
4554
|
+
`);
|
|
4555
|
+
}
|
|
4556
|
+
function removeProjectModuleRows(content, targetContent) {
|
|
4557
|
+
const neutralNote = extractProjectModuleNote(targetContent);
|
|
4558
|
+
const withoutProjectRows = content.split(`
|
|
4559
|
+
`).filter((line) => !/^\|.*\|\s*产品模块\s*\|.*\|$/.test(line)).join(`
|
|
4560
|
+
`);
|
|
4561
|
+
if (!neutralNote || withoutProjectRows.includes(neutralNote.trim())) {
|
|
4562
|
+
return withoutProjectRows;
|
|
4563
|
+
}
|
|
4564
|
+
return replaceKnown(withoutProjectRows, `
|
|
4565
|
+
精确生成来源和选择记录在`, `
|
|
4566
|
+
${neutralNote}
|
|
4567
|
+
精确生成来源和选择记录在`);
|
|
4568
|
+
}
|
|
4569
|
+
function extractProjectModuleNote(content) {
|
|
4570
|
+
const start = content.indexOf("项目自有业务模块以");
|
|
4571
|
+
const end = content.indexOf(`
|
|
4572
|
+
精确生成来源和选择记录在`, start);
|
|
4573
|
+
return start < 0 || end < 0 ? undefined : content.slice(start, end);
|
|
4574
|
+
}
|
|
4497
4575
|
function repairKnown026Downstream(input) {
|
|
4498
4576
|
if (input.sourceVersion !== "0.2.26")
|
|
4499
4577
|
return;
|
|
@@ -5458,9 +5536,301 @@ function compareVersions2(left, right) {
|
|
|
5458
5536
|
return 0;
|
|
5459
5537
|
}
|
|
5460
5538
|
|
|
5539
|
+
// src/module-schema/cli.ts
|
|
5540
|
+
import { resolve as resolve7 } from "node:path";
|
|
5541
|
+
|
|
5542
|
+
// src/module-schema/handoff.ts
|
|
5543
|
+
import {
|
|
5544
|
+
access as access4,
|
|
5545
|
+
cp,
|
|
5546
|
+
mkdir as mkdir8,
|
|
5547
|
+
mkdtemp as mkdtemp3,
|
|
5548
|
+
readFile as readFile18,
|
|
5549
|
+
readdir as readdir7,
|
|
5550
|
+
rename,
|
|
5551
|
+
rm as rm9,
|
|
5552
|
+
writeFile as writeFile13
|
|
5553
|
+
} from "node:fs/promises";
|
|
5554
|
+
import { dirname as dirname5, join as join18, relative as relative4 } from "node:path";
|
|
5555
|
+
|
|
5556
|
+
// src/module-schema/config.ts
|
|
5557
|
+
import { readFile as readFile17 } from "node:fs/promises";
|
|
5558
|
+
import { isAbsolute, relative as relative3, resolve as resolve6 } from "node:path";
|
|
5559
|
+
async function readModuleSchemaLineageConfig(projectDirectory, configPath) {
|
|
5560
|
+
const absolutePath = resolveProjectPath(projectDirectory, configPath);
|
|
5561
|
+
const parsed = JSON.parse(await readFile17(absolutePath, "utf8"));
|
|
5562
|
+
if (!isConfig(parsed)) {
|
|
5563
|
+
throw new Error("Module schema lineage 配置无效");
|
|
5564
|
+
}
|
|
5565
|
+
validatePaths(projectDirectory, parsed);
|
|
5566
|
+
validateUniqueness(parsed);
|
|
5567
|
+
if (parsed.modules.length === 0) {
|
|
5568
|
+
throw new Error("Module schema lineage 配置没有待交接的业务模块");
|
|
5569
|
+
}
|
|
5570
|
+
return parsed;
|
|
5571
|
+
}
|
|
5572
|
+
function resolveProjectPath(projectDirectory, path2) {
|
|
5573
|
+
if (isAbsolute(path2))
|
|
5574
|
+
throw new Error(`路径必须相对项目根目录:${path2}`);
|
|
5575
|
+
const resolved = resolve6(projectDirectory, path2);
|
|
5576
|
+
const projectRelative = relative3(projectDirectory, resolved);
|
|
5577
|
+
if (projectRelative.startsWith("..") || isAbsolute(projectRelative)) {
|
|
5578
|
+
throw new Error(`路径越出项目根目录:${path2}`);
|
|
5579
|
+
}
|
|
5580
|
+
return resolved;
|
|
5581
|
+
}
|
|
5582
|
+
function isConfig(value) {
|
|
5583
|
+
if (!value || typeof value !== "object")
|
|
5584
|
+
return false;
|
|
5585
|
+
const config = value;
|
|
5586
|
+
return config.schemaVersion === 1 && isPlatform(config.platform) && Array.isArray(config.modules) && config.modules.every(isModule);
|
|
5587
|
+
}
|
|
5588
|
+
function isPlatform(value) {
|
|
5589
|
+
if (!value || typeof value !== "object")
|
|
5590
|
+
return false;
|
|
5591
|
+
const platform = value;
|
|
5592
|
+
return isStringList(platform.schema) && typeof platform.out === "string";
|
|
5593
|
+
}
|
|
5594
|
+
function isModule(value) {
|
|
5595
|
+
if (!value || typeof value !== "object")
|
|
5596
|
+
return false;
|
|
5597
|
+
const module3 = value;
|
|
5598
|
+
return typeof module3.key === "string" && /^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/.test(module3.key) && isStringList(module3.schema) && typeof module3.out === "string" && isStringList(module3.legacyPlatformTables) && module3.legacyPlatformTables.every(isTableName);
|
|
5599
|
+
}
|
|
5600
|
+
function isStringList(value) {
|
|
5601
|
+
return Array.isArray(value) && value.length > 0 && value.every((item) => typeof item === "string" && item.length > 0);
|
|
5602
|
+
}
|
|
5603
|
+
function isTableName(value) {
|
|
5604
|
+
return /^[a-z_][a-z0-9_]*(?:\.[a-z_][a-z0-9_]*)?$/.test(value);
|
|
5605
|
+
}
|
|
5606
|
+
function validatePaths(projectDirectory, config) {
|
|
5607
|
+
for (const path2 of [
|
|
5608
|
+
...config.platform.schema,
|
|
5609
|
+
config.platform.out,
|
|
5610
|
+
...config.modules.flatMap(({ schema, out }) => [...schema, out])
|
|
5611
|
+
]) {
|
|
5612
|
+
resolveProjectPath(projectDirectory, path2);
|
|
5613
|
+
}
|
|
5614
|
+
const platformOut = resolveProjectPath(projectDirectory, config.platform.out);
|
|
5615
|
+
for (const module3 of config.modules) {
|
|
5616
|
+
const moduleOut = resolveProjectPath(projectDirectory, module3.out);
|
|
5617
|
+
if (moduleOut === platformOut || moduleOut.startsWith(`${platformOut}/`) || platformOut.startsWith(`${moduleOut}/`)) {
|
|
5618
|
+
throw new Error(`${module3.key} 的 lineage 输出不能与平台目录重叠`);
|
|
5619
|
+
}
|
|
5620
|
+
}
|
|
5621
|
+
}
|
|
5622
|
+
function validateUniqueness(config) {
|
|
5623
|
+
const keys = new Set;
|
|
5624
|
+
const outputs = new Set;
|
|
5625
|
+
const tables = new Set;
|
|
5626
|
+
for (const module3 of config.modules) {
|
|
5627
|
+
if (keys.has(module3.key))
|
|
5628
|
+
throw new Error(`Module key 重复:${module3.key}`);
|
|
5629
|
+
if (outputs.has(module3.out))
|
|
5630
|
+
throw new Error(`Module lineage 输出重复:${module3.out}`);
|
|
5631
|
+
keys.add(module3.key);
|
|
5632
|
+
outputs.add(module3.out);
|
|
5633
|
+
for (const table of module3.legacyPlatformTables) {
|
|
5634
|
+
if (tables.has(table))
|
|
5635
|
+
throw new Error(`旧平台表重复声明:${table}`);
|
|
5636
|
+
tables.add(table);
|
|
5637
|
+
}
|
|
5638
|
+
}
|
|
5639
|
+
}
|
|
5640
|
+
|
|
5641
|
+
// src/module-schema/sql.ts
|
|
5642
|
+
function validatePlatformHandoffSql(sql, allowedTables) {
|
|
5643
|
+
const allowed = new Set(allowedTables);
|
|
5644
|
+
const dropped = new Set;
|
|
5645
|
+
const statements = sql.replaceAll("--> statement-breakpoint", "").split(";").map((statement) => statement.trim()).filter(Boolean);
|
|
5646
|
+
for (const statement of statements) {
|
|
5647
|
+
const match = statement.match(/^DROP TABLE(?: IF EXISTS)? (?:(?:"([^"]+)")\.)?"([^"]+)"(?: CASCADE)?$/i);
|
|
5648
|
+
if (!match) {
|
|
5649
|
+
throw new Error(`交接差异包含非 DROP TABLE DDL:${statement}`);
|
|
5650
|
+
}
|
|
5651
|
+
const table = normalizeTableName(match[1], match[2]);
|
|
5652
|
+
if (!allowed.has(table)) {
|
|
5653
|
+
throw new Error(`交接差异试图删除未声明的表:${table}`);
|
|
5654
|
+
}
|
|
5655
|
+
dropped.add(table);
|
|
5656
|
+
}
|
|
5657
|
+
const missing = [...allowed].filter((table) => !dropped.has(table));
|
|
5658
|
+
if (missing.length > 0) {
|
|
5659
|
+
throw new Error(`平台 snapshot 未包含声明的旧表:${missing.join("、")}`);
|
|
5660
|
+
}
|
|
5661
|
+
}
|
|
5662
|
+
function metadataOnlySql(kind) {
|
|
5663
|
+
const owner = kind === "platform" ? "platform lineage release" : "module lineage baseline";
|
|
5664
|
+
return `-- Windy ${owner}.
|
|
5665
|
+
-- Metadata-only migration: intentionally no executable DDL.
|
|
5666
|
+
`;
|
|
5667
|
+
}
|
|
5668
|
+
function normalizeTableName(schema, table) {
|
|
5669
|
+
return schema && schema !== "public" ? `${schema}.${table}` : table;
|
|
5670
|
+
}
|
|
5671
|
+
|
|
5672
|
+
// src/module-schema/handoff.ts
|
|
5673
|
+
async function handoffModuleSchemaLineages(options) {
|
|
5674
|
+
const project = options.projectDirectory;
|
|
5675
|
+
await assertCleanGit(project);
|
|
5676
|
+
const config = await readModuleSchemaLineageConfig(project, options.configPath || "module-schema.lineages.json");
|
|
5677
|
+
await validateSchemaSources(project, config);
|
|
5678
|
+
const temporaryRoot = await createTemporaryRoot(project);
|
|
5679
|
+
const execute = options.execute || executeDrizzleGenerate;
|
|
5680
|
+
try {
|
|
5681
|
+
const stagedModules = await stageModuleLineages(project, temporaryRoot, config, execute);
|
|
5682
|
+
const stagedPlatform = await stagePlatformLineage(project, temporaryRoot, config, execute);
|
|
5683
|
+
await installStagedLineages(project, temporaryRoot, config, stagedModules, stagedPlatform);
|
|
5684
|
+
} finally {
|
|
5685
|
+
await rm9(temporaryRoot, { recursive: true, force: true });
|
|
5686
|
+
}
|
|
5687
|
+
return {
|
|
5688
|
+
moduleCount: config.modules.length,
|
|
5689
|
+
tableCount: config.modules.reduce((total, module3) => total + module3.legacyPlatformTables.length, 0)
|
|
5690
|
+
};
|
|
5691
|
+
}
|
|
5692
|
+
async function stageModuleLineages(project, temporaryRoot, config, execute) {
|
|
5693
|
+
const outputs = [];
|
|
5694
|
+
for (const [index, module3] of config.modules.entries()) {
|
|
5695
|
+
const output = join18(temporaryRoot, `module-${index}`);
|
|
5696
|
+
await mkdir8(output, { recursive: true });
|
|
5697
|
+
const generatedConfig = join18(temporaryRoot, `module-${index}.config.ts`);
|
|
5698
|
+
await writeDrizzleConfig(generatedConfig, project, module3.schema, relative4(temporaryRoot, output));
|
|
5699
|
+
await execute(generatedConfig, temporaryRoot);
|
|
5700
|
+
const sqlFiles = await listSqlFiles(output);
|
|
5701
|
+
if (sqlFiles.length !== 1) {
|
|
5702
|
+
throw new Error(`${module3.key} baseline 必须恰好生成一个 migration`);
|
|
5703
|
+
}
|
|
5704
|
+
await writeFile13(join18(output, sqlFiles[0]), metadataOnlySql("module"));
|
|
5705
|
+
await writeFile13(join18(output, "lineage.json"), `${JSON.stringify({
|
|
5706
|
+
schemaVersion: 1,
|
|
5707
|
+
moduleKey: module3.key,
|
|
5708
|
+
schema: module3.schema,
|
|
5709
|
+
legacyPlatformTables: module3.legacyPlatformTables,
|
|
5710
|
+
productionExecutor: "module-migration-bundle"
|
|
5711
|
+
}, null, 2)}
|
|
5712
|
+
`);
|
|
5713
|
+
outputs.push(output);
|
|
5714
|
+
}
|
|
5715
|
+
return outputs;
|
|
5716
|
+
}
|
|
5717
|
+
async function stagePlatformLineage(project, temporaryRoot, config, execute) {
|
|
5718
|
+
const source = resolveProjectPath(project, config.platform.out);
|
|
5719
|
+
const output = join18(temporaryRoot, "platform");
|
|
5720
|
+
await cp(source, output, { recursive: true });
|
|
5721
|
+
const before = new Set(await listSqlFiles(output));
|
|
5722
|
+
const generatedConfig = join18(temporaryRoot, "platform.config.ts");
|
|
5723
|
+
await writeDrizzleConfig(generatedConfig, project, config.platform.schema, relative4(temporaryRoot, output));
|
|
5724
|
+
await execute(generatedConfig, temporaryRoot);
|
|
5725
|
+
const created = (await listSqlFiles(output)).filter((path2) => !before.has(path2));
|
|
5726
|
+
if (created.length !== 1) {
|
|
5727
|
+
throw new Error("平台 lineage handoff 必须恰好生成一个 migration");
|
|
5728
|
+
}
|
|
5729
|
+
const sqlPath = join18(output, created[0]);
|
|
5730
|
+
const allowed = config.modules.flatMap(({ legacyPlatformTables }) => legacyPlatformTables);
|
|
5731
|
+
validatePlatformHandoffSql(await readFile18(sqlPath, "utf8"), allowed);
|
|
5732
|
+
await writeFile13(sqlPath, metadataOnlySql("platform"));
|
|
5733
|
+
return output;
|
|
5734
|
+
}
|
|
5735
|
+
async function installStagedLineages(project, temporaryRoot, config, stagedModules, stagedPlatform) {
|
|
5736
|
+
const platform = resolveProjectPath(project, config.platform.out);
|
|
5737
|
+
const backup = join18(temporaryRoot, "platform-backup");
|
|
5738
|
+
const installed = [];
|
|
5739
|
+
await rename(platform, backup);
|
|
5740
|
+
try {
|
|
5741
|
+
await rename(stagedPlatform, platform);
|
|
5742
|
+
for (const [index, module3] of config.modules.entries()) {
|
|
5743
|
+
const output = resolveProjectPath(project, module3.out);
|
|
5744
|
+
if (await exists2(output)) {
|
|
5745
|
+
throw new Error(`${module3.key} lineage 输出已存在,禁止覆盖`);
|
|
5746
|
+
}
|
|
5747
|
+
await mkdir8(dirname5(output), { recursive: true });
|
|
5748
|
+
await rename(stagedModules[index], output);
|
|
5749
|
+
installed.push(output);
|
|
5750
|
+
}
|
|
5751
|
+
} catch (error) {
|
|
5752
|
+
await rm9(platform, { recursive: true, force: true });
|
|
5753
|
+
await rename(backup, platform);
|
|
5754
|
+
for (const output of installed)
|
|
5755
|
+
await rm9(output, { recursive: true, force: true });
|
|
5756
|
+
throw error;
|
|
5757
|
+
}
|
|
5758
|
+
}
|
|
5759
|
+
async function writeDrizzleConfig(path2, project, schemas, out) {
|
|
5760
|
+
const absoluteSchemas = schemas.map((schema) => resolveProjectPath(project, schema));
|
|
5761
|
+
await writeFile13(path2, `import { defineConfig } from "drizzle-kit";
|
|
5762
|
+
export default defineConfig(${JSON.stringify({
|
|
5763
|
+
dialect: "postgresql",
|
|
5764
|
+
schema: absoluteSchemas,
|
|
5765
|
+
out,
|
|
5766
|
+
casing: "snake_case"
|
|
5767
|
+
})});
|
|
5768
|
+
`);
|
|
5769
|
+
}
|
|
5770
|
+
async function validateSchemaSources(project, config) {
|
|
5771
|
+
for (const schema of [
|
|
5772
|
+
...config.platform.schema,
|
|
5773
|
+
...config.modules.flatMap(({ schema: schema2 }) => schema2)
|
|
5774
|
+
]) {
|
|
5775
|
+
await access4(resolveProjectPath(project, schema));
|
|
5776
|
+
}
|
|
5777
|
+
for (const module3 of config.modules) {
|
|
5778
|
+
if (await exists2(resolveProjectPath(project, module3.out))) {
|
|
5779
|
+
throw new Error(`${module3.key} lineage 输出已存在,禁止覆盖`);
|
|
5780
|
+
}
|
|
5781
|
+
}
|
|
5782
|
+
}
|
|
5783
|
+
async function createTemporaryRoot(project) {
|
|
5784
|
+
const windy = join18(project, ".windy");
|
|
5785
|
+
await mkdir8(windy, { recursive: true });
|
|
5786
|
+
return await mkdtemp3(join18(windy, "module-schema-handoff-"));
|
|
5787
|
+
}
|
|
5788
|
+
async function listSqlFiles(root) {
|
|
5789
|
+
return (await readdir7(root)).filter((path2) => path2.endsWith(".sql")).sort();
|
|
5790
|
+
}
|
|
5791
|
+
async function executeDrizzleGenerate(configPath, cwd) {
|
|
5792
|
+
await assertProcess("bunx", ["--bun", "drizzle-kit", "generate", "--config", configPath], cwd, true);
|
|
5793
|
+
}
|
|
5794
|
+
async function assertCleanGit(project) {
|
|
5795
|
+
const result2 = await captureProcess("git", ["status", "--porcelain"], project);
|
|
5796
|
+
if (result2.code !== 0 || result2.stdout.trim()) {
|
|
5797
|
+
throw new Error("schema lineage handoff 前必须保持 Git 工作区干净");
|
|
5798
|
+
}
|
|
5799
|
+
}
|
|
5800
|
+
async function exists2(path2) {
|
|
5801
|
+
try {
|
|
5802
|
+
await access4(path2);
|
|
5803
|
+
return true;
|
|
5804
|
+
} catch {
|
|
5805
|
+
return false;
|
|
5806
|
+
}
|
|
5807
|
+
}
|
|
5808
|
+
|
|
5809
|
+
// src/module-schema/cli.ts
|
|
5810
|
+
async function runModuleSchemaCommand(args) {
|
|
5811
|
+
if (args[0] !== "schema")
|
|
5812
|
+
return false;
|
|
5813
|
+
if (args[1] !== "handoff") {
|
|
5814
|
+
throw new Error("当前 schema 命令只支持 handoff");
|
|
5815
|
+
}
|
|
5816
|
+
const projectDirectory = resolve7(readValue3(args, "--cwd") || process.cwd());
|
|
5817
|
+
const result2 = await handoffModuleSchemaLineages({
|
|
5818
|
+
projectDirectory,
|
|
5819
|
+
configPath: readValue3(args, "--config")
|
|
5820
|
+
});
|
|
5821
|
+
console.log(`Module schema lineage 交接完成:${result2.moduleCount} 个模块、${result2.tableCount} 张旧平台 snapshot 表`);
|
|
5822
|
+
return true;
|
|
5823
|
+
}
|
|
5824
|
+
function readValue3(args, flag) {
|
|
5825
|
+
const index = args.indexOf(flag);
|
|
5826
|
+
return index >= 0 ? args[index + 1] : undefined;
|
|
5827
|
+
}
|
|
5828
|
+
|
|
5461
5829
|
// src/cli.ts
|
|
5462
5830
|
async function main() {
|
|
5463
5831
|
const args = process.argv.slice(2);
|
|
5832
|
+
if (await runModuleSchemaCommand(args))
|
|
5833
|
+
return;
|
|
5464
5834
|
if (await runLifecycleCommand(args))
|
|
5465
5835
|
return;
|
|
5466
5836
|
const options = parseArguments(args);
|
package/package.json
CHANGED
package/template/README.md
CHANGED
|
@@ -107,6 +107,9 @@ create-windy 在本机 `.env` 中随机生成的 `WINDY_BOOTSTRAP_ADMIN_PASSWORD
|
|
|
107
107
|
| `system.data-governance` | 未包含 | H2 | 敏感字段、明文访问和治理导出。 |
|
|
108
108
|
| `work-order` | 未包含 | H2 | 可替换的业务模块接入示例。 |
|
|
109
109
|
|
|
110
|
+
项目自有业务模块以 `apps/server/src/installed-business-modules.ts` 装配的 Module Manifest
|
|
111
|
+
为准;shared-scaffold README 不复制项目业务 Key,避免平台升级固化产品命名。
|
|
112
|
+
|
|
110
113
|
精确生成来源和选择记录在 `.windy/generation.json`。当前版本已提供可回滚的
|
|
111
114
|
`windy update`;通用 `module add/remove` 尚未交付,不要把 Feature Flag 关闭当成
|
|
112
115
|
模块已经卸载。
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { workOrders } from "@southwind-ai/
|
|
2
|
-
import type { WorkOrder } from "@southwind-ai/example-work-order";
|
|
1
|
+
import { workOrders, type WorkOrder } from "@southwind-ai/example-work-order";
|
|
3
2
|
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
4
3
|
import {
|
|
5
4
|
DrizzleScopedEntityRepository,
|
|
@@ -5,16 +5,18 @@ import fixtureSource from "./router-test-fixtures.ts?raw";
|
|
|
5
5
|
import { collectRouteAccess } from "./router-test-fixtures";
|
|
6
6
|
|
|
7
7
|
describe("Router shared-scaffold 中性契约", () => {
|
|
8
|
-
test("
|
|
8
|
+
test("平台登录页使用中性共享页面", () => {
|
|
9
9
|
expect(routerSource).toContain(
|
|
10
10
|
'import LoginPage from "@/pages/auth/LoginPage.vue";',
|
|
11
11
|
);
|
|
12
|
-
expect(
|
|
13
|
-
/LijianLoginPage|lijian\./,
|
|
14
|
-
);
|
|
12
|
+
expect(fixtureSource).toContain("collectRouteAccess");
|
|
15
13
|
});
|
|
16
14
|
|
|
17
|
-
test("
|
|
15
|
+
test("测试快照只从项目注入路由递归派生访问键", () => {
|
|
16
|
+
expect(collectRouteAccess([])).toEqual({
|
|
17
|
+
permissionKeys: [],
|
|
18
|
+
featureKeys: [],
|
|
19
|
+
});
|
|
18
20
|
const projectRoutes: RouteRecordRaw[] = [
|
|
19
21
|
{
|
|
20
22
|
path: "/records",
|