@xfe-repo/web-app 2.0.0-beta.2 → 2.0.0-beta.4

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
@@ -122,3 +122,35 @@ SSR `start` 使用与构建一致的隐藏工作目录,使官方 serve 正确
122
122
  构建会先加载 `react-app-polyfill/stable`,并在应用原有 browserslist 的基础上,补充 Edge 18、Chrome 64、Firefox 60、Safari/iOS 12 及以上的 JS 编译目标,业务代码和第三方依赖使用同一目标。
123
123
 
124
124
  这是 JS 兼容基线;具体组件样式和浏览器 Web API 仍需在目标浏览器验证。
125
+
126
+
127
+ ## 可选的项目部署模板
128
+
129
+ 已有项目保持默认 Node 22、集群选择、发布环境和无状态部署方式。设置 `deploy.managed: true` 后,`xfe-web config` 将框架模板生成到项目 `deploy/`,不修改 npm 包中的 Helm values;项目只维护配置,生成文件由框架管理。Drone 入口需提交仓库,安装后的流水线再通过同一命令生成模板。
130
+
131
+ ```json
132
+ {
133
+ "deploy": {
134
+ "managed": true,
135
+ "droneConfigPath": "deploy/.drone.yml",
136
+ "targetK8s": "infra",
137
+ "targetNamespace": "deployment-prod",
138
+ "nodeVersion": "24.22.1",
139
+ "environments": ["prod"],
140
+ "hostsProd": ["app.example.com"],
141
+ "buildCommand": "pnpm check && pnpm build && pnpm prune --prod",
142
+ "values": {
143
+ "replicas": 1,
144
+ "strategy": {"type": "Recreate"},
145
+ "persistence": {"enabled": true, "size": "20Gi", "mountPath": "/data"},
146
+ "podSecurityContext": {"fsGroup": 1000},
147
+ "env": [{"name": "DATA_DIR", "value": "/data"}],
148
+ "envFrom": [{"secretRef": {"name": "application-env"}}]
149
+ }
150
+ }
151
+ }
152
+ ```
153
+
154
+ `values` 递归覆盖默认 Helm values;列表整体替换。可配置实例数、更新策略、容器资源、Pod/容器安全上下文、环境变量及来源、启动/就绪/存活探针、终止宽限期和 PVC。PVC 默认关闭,可指定 `existingClaim` 或 `storageClass`。应用负责声明自己的 `DATA_DIR` 等业务变量,框架不包含 OSS 或认证逻辑。
155
+
156
+ `nodeVersion` 同时应用于安装、构建及运行镜像,省略时保留 22.22.0。`environments` 可选择 test/stage/prod/stable,省略时保持原流程;仅 prod 时不生成其他发布步骤。managed 正式发布使用 Helm 等待就绪,失败自动撤销资源变更。`virtualService.timeout` 可设置长连接超时,`corsPolicy: null` 可关闭默认跨域策略。密钥只通过运行环境提供,不放入 values 或构建命令。
package/deploy/Dockerfile CHANGED
@@ -1,4 +1,5 @@
1
- FROM node:22.22.0-bookworm-slim
1
+ ARG XFE_NODE_VERSION=22.22.0
2
+ FROM node:${XFE_NODE_VERSION}-bookworm-slim
2
3
 
3
4
  WORKDIR /app
4
5
 
@@ -0,0 +1,10 @@
1
+ # 构建产物和 deploy/modules 是运行镜像输入,不能排除。
2
+ node_modules/*
3
+ .env*
4
+ .git
5
+ .idea
6
+ .data
7
+ .impeccable
8
+ design
9
+ coverage
10
+ *.tsbuildinfo
@@ -1,4 +1,4 @@
1
- apiVersion: v1
1
+ apiVersion: v2
2
2
  name: deployment
3
3
  description: A Helm chart for xfe
4
4
 
@@ -7,11 +7,15 @@ metadata:
7
7
  kubernetes.io/change-cause: {{ or $.Values.tagContent $.Values.commitSHA }}
8
8
  spec:
9
9
  strategy:
10
+ {{- if .Values.strategy }}
11
+ {{- toYaml .Values.strategy | nindent 4 }}
12
+ {{- else }}
10
13
  type: RollingUpdate
11
14
  rollingUpdate:
12
15
  maxSurge: 2
13
16
  maxUnavailable: 50%
14
- replicas: {{ if eq $.Values.envType "prod" }} 2 {{ else }} 1 {{ end }}
17
+ {{- end }}
18
+ replicas: {{ if hasKey .Values "replicas" }}{{ .Values.replicas }}{{ else if eq $.Values.envType "prod" }} 2 {{ else }} 1 {{ end }}
15
19
  selector:
16
20
  matchLabels:
17
21
  app: {{ $.Values.app }}
@@ -20,6 +24,13 @@ spec:
20
24
  labels:
21
25
  app: {{ $.Values.app }}
22
26
  spec:
27
+ {{- with .Values.podSecurityContext }}
28
+ securityContext:
29
+ {{- toYaml . | nindent 8 }}
30
+ {{- end }}
31
+ {{- with .Values.terminationGracePeriodSeconds }}
32
+ terminationGracePeriodSeconds: {{ . }}
33
+ {{- end }}
23
34
  imagePullSecrets:
24
35
  - name: {{ $.Values.imagePullSecret }}
25
36
  containers:
@@ -27,9 +38,17 @@ spec:
27
38
  imagePullPolicy: Always
28
39
  image: "{{ $.Values.image }}"
29
40
  resources:
41
+ {{- if .Values.resources }}
42
+ {{- toYaml .Values.resources | nindent 12 }}
43
+ {{- else }}
30
44
  requests:
31
45
  cpu: "0.01"
32
46
  memory: "100Mi"
47
+ {{- end }}
48
+ {{- with .Values.containerSecurityContext }}
49
+ securityContext:
50
+ {{- toYaml . | nindent 12 }}
51
+ {{- end }}
33
52
  env:
34
53
  - name: HOST
35
54
  value: "0.0.0.0"
@@ -39,9 +58,37 @@ spec:
39
58
  - name: HOST_PREFIX
40
59
  value: "{{ $.Values.hostPrefix }}"
41
60
  {{end}}
42
- {{- if eq $.Values.targetK8s "saas" }}
61
+ {{- with .Values.env }}
62
+ {{- toYaml . | nindent 12 }}
63
+ {{- end }}
64
+ {{- if hasKey .Values "envFrom" }}
65
+ envFrom:
66
+ {{- toYaml .Values.envFrom | nindent 12 }}
67
+ {{- else if eq $.Values.targetK8s "saas" }}
43
68
  envFrom:
44
69
  - configMapRef:
45
70
  name: "app-common-config"
46
71
  {{- end }}
47
- ---
72
+ {{- with .Values.startupProbe }}
73
+ startupProbe:
74
+ {{- toYaml . | nindent 12 }}
75
+ {{- end }}
76
+ {{- with .Values.readinessProbe }}
77
+ readinessProbe:
78
+ {{- toYaml . | nindent 12 }}
79
+ {{- end }}
80
+ {{- with .Values.livenessProbe }}
81
+ livenessProbe:
82
+ {{- toYaml . | nindent 12 }}
83
+ {{- end }}
84
+ {{- if .Values.persistence.enabled }}
85
+ volumeMounts:
86
+ - name: data
87
+ mountPath: {{ .Values.persistence.mountPath | quote }}
88
+ {{- end }}
89
+ {{- if .Values.persistence.enabled }}
90
+ volumes:
91
+ - name: data
92
+ persistentVolumeClaim:
93
+ claimName: {{ default (printf "%s-data" .Values.app) .Values.persistence.existingClaim }}
94
+ {{- end }}
@@ -0,0 +1,17 @@
1
+ {{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) }}
2
+ apiVersion: v1
3
+ kind: PersistentVolumeClaim
4
+ metadata:
5
+ name: {{ .Values.app }}-data
6
+ namespace: {{ include "template.namespace" . }}
7
+ annotations:
8
+ helm.sh/resource-policy: keep
9
+ spec:
10
+ accessModes: [ReadWriteOnce]
11
+ {{- with .Values.persistence.storageClass }}
12
+ storageClassName: {{ . | quote }}
13
+ {{- end }}
14
+ resources:
15
+ requests:
16
+ storage: {{ .Values.persistence.size }}
17
+ {{- end }}
@@ -19,14 +19,16 @@ spec:
19
19
  - match:
20
20
  - uri:
21
21
  prefix: /
22
+ {{- with .Values.virtualService.timeout }}
23
+ timeout: {{ . }}
24
+ {{- end }}
22
25
  route:
23
26
  - destination:
24
27
  host: {{ .Values.app }}
25
28
  port:
26
29
  number: 80
30
+ {{- with .Values.virtualService.corsPolicy }}
27
31
  corsPolicy:
28
- allowOrigin:
29
- - "*"
30
- allowMethods:
31
- - "*"
32
+ {{- toYaml . | nindent 12 }}
33
+ {{- end }}
32
34
  {{- end }}
@@ -7,6 +7,9 @@ envType: '<<ENV_TYPE>>'
7
7
  hostPrefix: '<<HOST_PREFIX>>'
8
8
  namespace: ''
9
9
  virtualService:
10
+ corsPolicy:
11
+ allowOrigin: ['*']
12
+ allowMethods: ['*']
10
13
  enabled: true
11
14
  hosts:
12
15
  test:
@@ -18,3 +21,11 @@ virtualService:
18
21
  gateways:
19
22
  - 'common-gateway-eshetang'
20
23
  targetK8s: 'saas'
24
+
25
+ # 可选持久卷;省略时保持现有无状态 Web 应用的部署行为。
26
+ persistence:
27
+ enabled: false
28
+ mountPath: /data
29
+ size: 20Gi
30
+ existingClaim: ''
31
+ storageClass: ''
package/dist/cli.js CHANGED
@@ -30,7 +30,72 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
30
30
  ));
31
31
 
32
32
  // src/config.ts
33
- var import_node_fs, import_node_path, import_node_url, import_yaml, import_meta, DEFAULT_BUILD_DIRECTORY, BUILD_DIRECTORY_ARGUMENT, protectedDirectories, configDirectory, loadXfeConfig, getBuildDirectory, getServerEntry, configDeploy, getJwtEnv;
33
+ function mergeDeployValues(base, overrides) {
34
+ const entries = Object.entries({ ...base, ...overrides }).map(
35
+ /** 只递归普通对象,列表保持调用方声明的顺序与完整内容。 */
36
+ ([key, value]) => {
37
+ const previous = base[key];
38
+ const mergeable = value && previous && typeof value === "object" && typeof previous === "object" && !Array.isArray(value) && !Array.isArray(previous);
39
+ return [key, mergeable ? mergeDeployValues(previous, value) : value];
40
+ }
41
+ );
42
+ return Object.fromEntries(entries);
43
+ }
44
+ function configureDeployPipeline(template, deployment) {
45
+ if (!deployment.managed && !deployment.nodeVersion && !deployment.environments && !deployment.buildCommand) return template;
46
+ const documents = import_yaml.default.parseAllDocuments(template).map(
47
+ /** 保留镜像和集群 Secret 定义。 */
48
+ (document) => document.toJSON()
49
+ );
50
+ const pipeline = documents[0];
51
+ const environments = deployment.environments;
52
+ const known = ["test", "stage", "prod", "stable"];
53
+ if (environments && (!Array.isArray(environments) || !environments.length || environments.some(
54
+ /** 拒绝未声明的发布环境名称。 */
55
+ (environment) => typeof environment !== "string" || !known.includes(environment)
56
+ ))) throw new Error("deploy.environments \u5FC5\u987B\u662F test/stage/prod/stable \u7684\u975E\u7A7A\u6570\u7EC4");
57
+ if (deployment.nodeVersion && !/^\d+\.\d+\.\d+$/.test(deployment.nodeVersion)) throw new Error("deploy.nodeVersion \u5FC5\u987B\u662F\u5B8C\u6574 Node \u7248\u672C\u53F7");
58
+ if (deployment.managed && deployment.targetNamespace && !/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/.test(deployment.targetNamespace)) throw new Error("deploy.targetNamespace \u5FC5\u987B\u662F\u6709\u6548 Kubernetes \u547D\u540D\u7A7A\u95F4");
59
+ const selected = environments ? new Set(environments) : void 0;
60
+ const stepEnvironments = /* @__PURE__ */ new Map([["deploy-to-test", "test"], ["deploy-to-stage", "stage"], ["deploy-to-prod", "prod"], ["deploy-to-stable", "stable"], ["push-helm-chart", "prod"]]);
61
+ pipeline.steps = pipeline.steps.filter(
62
+ /** 通用安装和构建步骤保留,发布步骤按显式环境选择。 */
63
+ (step) => !selected || !stepEnvironments.has(step.name) || selected.has(stepEnvironments.get(step.name))
64
+ );
65
+ for (const step of pipeline.steps) {
66
+ if (deployment.nodeVersion && ["install", "build"].includes(step.name)) step.image = `node:${deployment.nodeVersion}-bookworm`;
67
+ if (deployment.nodeVersion && step.name === "image-build") step.settings.build_args.push(`XFE_NODE_VERSION=${deployment.nodeVersion}`);
68
+ if (step.name === "build") step.commands = step.commands.map(
69
+ /** managed 构建复用配置生成器;应用自定义检查仍由应用命令负责。 */
70
+ (command2) => {
71
+ if (deployment.managed && command2 === "cp -r node_modules/@xfe-repo/web-app/deploy deploy") return "pnpm exec xfe-web config";
72
+ return command2 === "pnpm build" ? deployment.buildCommand || command2 : command2;
73
+ }
74
+ );
75
+ if (deployment.managed && deployment.targetK8s === "infra" && step.name === "pre-deploy") delete step.environment.SAAS_K8S_CONFIG;
76
+ if (deployment.managed && step.name === "deploy-to-prod") step.commands = step.commands.map(
77
+ /** 正式 Helm 部署等待探针并在失败时撤销资源变更。 */
78
+ (command2) => command2.includes("| kubectl") ? `helm upgrade --install "\${DRONE_REPO//\\//-}" deploy/helm --values deploy/helm/values.prod.yaml --namespace ${deployment.targetNamespace || "deployment-prod"} --create-namespace --kubeconfig=kube_config --atomic --wait --timeout 5m` : command2
79
+ );
80
+ }
81
+ if (selected && !selected.has("test")) {
82
+ pipeline.trigger.event = ["tag"];
83
+ const tags = [...new Set([...selected].map(
84
+ /** prod 与 stable 共用正式标签,其余使用各自的标签。 */
85
+ (environment) => environment === "stage" ? "refs/tags/stage-*" : "refs/tags/release-*"
86
+ ))];
87
+ pipeline.trigger.ref = { include: tags };
88
+ }
89
+ const result = deployment.managed && deployment.targetK8s === "infra" ? documents.filter(
90
+ /** infra 独立生成的流水线不要求额外的 saas kubeconfig。 */
91
+ (document) => document.kind !== "secret" || document.name !== "saas-k8s-config"
92
+ ) : documents;
93
+ return result.map(
94
+ /** 序列化多文档流水线,保留 Kubernetes Runner 的 Secret 引用。 */
95
+ (document) => import_yaml.default.stringify(document)
96
+ ).join("---\n");
97
+ }
98
+ var import_node_fs, import_node_path, import_node_url, import_yaml, import_meta, DEFAULT_BUILD_DIRECTORY, BUILD_DIRECTORY_ARGUMENT, protectedDirectories, loadXfeConfig, getBuildDirectory, getServerEntry, configDeploy, getJwtEnv;
34
99
  var init_config = __esm({
35
100
  "src/config.ts"() {
36
101
  "use strict";
@@ -42,7 +107,6 @@ var init_config = __esm({
42
107
  DEFAULT_BUILD_DIRECTORY = "dist";
43
108
  BUILD_DIRECTORY_ARGUMENT = "XFE_BUILD_DIRECTORY";
44
109
  protectedDirectories = /* @__PURE__ */ new Set(["src", "types", "public", "node_modules", ".git"]);
45
- configDirectory = typeof __dirname === "string" ? __dirname : import_node_path.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
46
110
  loadXfeConfig = (root = process.cwd()) => {
47
111
  const xfeConfigPath = import_node_path.default.join(root, "xfe.json");
48
112
  if (!import_node_fs.default.existsSync(xfeConfigPath)) {
@@ -99,38 +163,33 @@ var init_config = __esm({
99
163
  };
100
164
  configDeploy = () => {
101
165
  const xfeConfig = loadXfeConfig();
102
- if (!xfeConfig?.deploy) return;
166
+ const deployment = xfeConfig.deploy;
167
+ if (!deployment) return;
168
+ const configDirectory = typeof __dirname === "string" ? __dirname : import_node_path.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
103
169
  const projectRoot = process.cwd();
170
+ const source = import_node_path.default.resolve(configDirectory, "../deploy");
171
+ const target = deployment.managed ? getBuildDirectory(projectRoot, { buildDirectory: "deploy" }) : source;
104
172
  const buildDirectory = import_node_path.default.relative(projectRoot, getBuildDirectory(projectRoot, xfeConfig.webapp)).replaceAll(import_node_path.default.sep, "/");
105
- const droneConfigFilePath = import_node_path.default.resolve(configDirectory, "../deploy/.drone.yml");
106
- const droneTarget = import_node_path.default.resolve(projectRoot, xfeConfig.deploy.droneConfigPath || "./.drone.yml");
107
- const defaultArgument = JSON.stringify(`${BUILD_DIRECTORY_ARGUMENT}=${DEFAULT_BUILD_DIRECTORY}`);
108
- const configuredArgument = JSON.stringify(`${BUILD_DIRECTORY_ARGUMENT}=${buildDirectory}`);
109
- const droneConfig = import_node_fs.default.readFileSync(droneConfigFilePath, "utf8").replace(defaultArgument, configuredArgument);
173
+ const droneTarget = import_node_path.default.resolve(projectRoot, deployment.droneConfigPath || "./.drone.yml");
174
+ const template = import_node_fs.default.readFileSync(import_node_path.default.join(source, ".drone.yml"), "utf8").replace(JSON.stringify(`${BUILD_DIRECTORY_ARGUMENT}=${DEFAULT_BUILD_DIRECTORY}`), JSON.stringify(`${BUILD_DIRECTORY_ARGUMENT}=${buildDirectory}`));
175
+ const drone = configureDeployPipeline(template, deployment);
176
+ const values = import_yaml.default.parse(import_node_fs.default.readFileSync(import_node_path.default.join(source, "helm/values.yaml"), "utf8"));
177
+ const { virtualServiceEnabled = true, hostsProd = [], hostsStage = [], hostsTest = [], gateways = ["common-gateway-eshetang"] } = deployment;
178
+ values.virtualService = { ...values.virtualService, enabled: virtualServiceEnabled, hosts: { prod: hostsProd, stage: hostsStage, test: hostsTest }, gateways };
179
+ values.targetK8s = deployment.targetK8s || "saas";
180
+ values.namespace = deployment.targetNamespace || "";
181
+ const configuredValues = mergeDeployValues(values, deployment.values || {});
182
+ if (deployment.managed && deployment.environments) configuredValues.virtualService.hosts = Object.fromEntries(Object.entries(configuredValues.virtualService.hosts).filter(
183
+ /** 托管部署仅保留已选择环境的域名。 */
184
+ ([environment]) => deployment.environments.includes(environment) || environment === "test" && deployment.environments.includes("stable")
185
+ ));
186
+ if (deployment.managed) {
187
+ if (target === source) throw new Error("managed \u90E8\u7F72\u8F93\u51FA\u4E0D\u80FD\u8986\u76D6\u6846\u67B6\u6A21\u677F\u76EE\u5F55");
188
+ import_node_fs.default.cpSync(source, target, { recursive: true });
189
+ }
110
190
  import_node_fs.default.mkdirSync(import_node_path.default.dirname(droneTarget), { recursive: true });
111
- import_node_fs.default.writeFileSync(droneTarget, droneConfig);
112
- const valuesYamlFilePath = import_node_path.default.resolve(configDirectory, "../deploy/helm/values.yaml");
113
- const valuesYamlFile = import_node_fs.default.readFileSync(valuesYamlFilePath, "utf8");
114
- const valuesYaml = import_yaml.default.parse(valuesYamlFile);
115
- if (!valuesYaml.virtualService) valuesYaml.virtualService = {};
116
- if (!valuesYaml.virtualService.hosts) valuesYaml.virtualService.hosts = { test: [], stage: [], prod: [] };
117
- const {
118
- virtualServiceEnabled = true,
119
- hostsProd = [],
120
- hostsStage = [],
121
- hostsTest = [],
122
- gateways = ["common-gateway-eshetang"]
123
- } = xfeConfig.deploy;
124
- const { targetK8s = "saas", targetNamespace = "" } = xfeConfig.deploy;
125
- valuesYaml.virtualService.enabled = virtualServiceEnabled;
126
- valuesYaml.virtualService.gateways = gateways;
127
- valuesYaml.virtualService.hosts.prod = hostsProd;
128
- valuesYaml.virtualService.hosts.stage = hostsStage;
129
- valuesYaml.virtualService.hosts.test = hostsTest;
130
- valuesYaml.targetK8s = targetK8s;
131
- valuesYaml.namespace = targetNamespace;
132
- const valuesYamlStr = import_yaml.default.stringify(valuesYaml, { defaultStringType: "QUOTE_SINGLE", defaultKeyType: "PLAIN" });
133
- import_node_fs.default.writeFileSync(valuesYamlFilePath, valuesYamlStr, "utf8");
191
+ import_node_fs.default.writeFileSync(import_node_path.default.join(target, "helm/values.yaml"), import_yaml.default.stringify(configuredValues, { defaultStringType: "QUOTE_SINGLE", defaultKeyType: "PLAIN" }));
192
+ import_node_fs.default.writeFileSync(droneTarget, drone);
134
193
  };
135
194
  getJwtEnv = async () => {
136
195
  const xfeConfig = loadXfeConfig();
package/dist/cli.mjs CHANGED
@@ -19,14 +19,78 @@ import fs from "fs";
19
19
  import path from "path";
20
20
  import { fileURLToPath } from "url";
21
21
  import yaml from "yaml";
22
- var DEFAULT_BUILD_DIRECTORY, BUILD_DIRECTORY_ARGUMENT, protectedDirectories, configDirectory, loadXfeConfig, getBuildDirectory, getServerEntry, configDeploy, getJwtEnv;
22
+ function mergeDeployValues(base, overrides) {
23
+ const entries = Object.entries({ ...base, ...overrides }).map(
24
+ /** 只递归普通对象,列表保持调用方声明的顺序与完整内容。 */
25
+ ([key, value]) => {
26
+ const previous = base[key];
27
+ const mergeable = value && previous && typeof value === "object" && typeof previous === "object" && !Array.isArray(value) && !Array.isArray(previous);
28
+ return [key, mergeable ? mergeDeployValues(previous, value) : value];
29
+ }
30
+ );
31
+ return Object.fromEntries(entries);
32
+ }
33
+ function configureDeployPipeline(template, deployment) {
34
+ if (!deployment.managed && !deployment.nodeVersion && !deployment.environments && !deployment.buildCommand) return template;
35
+ const documents = yaml.parseAllDocuments(template).map(
36
+ /** 保留镜像和集群 Secret 定义。 */
37
+ (document) => document.toJSON()
38
+ );
39
+ const pipeline = documents[0];
40
+ const environments = deployment.environments;
41
+ const known = ["test", "stage", "prod", "stable"];
42
+ if (environments && (!Array.isArray(environments) || !environments.length || environments.some(
43
+ /** 拒绝未声明的发布环境名称。 */
44
+ (environment) => typeof environment !== "string" || !known.includes(environment)
45
+ ))) throw new Error("deploy.environments \u5FC5\u987B\u662F test/stage/prod/stable \u7684\u975E\u7A7A\u6570\u7EC4");
46
+ if (deployment.nodeVersion && !/^\d+\.\d+\.\d+$/.test(deployment.nodeVersion)) throw new Error("deploy.nodeVersion \u5FC5\u987B\u662F\u5B8C\u6574 Node \u7248\u672C\u53F7");
47
+ if (deployment.managed && deployment.targetNamespace && !/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/.test(deployment.targetNamespace)) throw new Error("deploy.targetNamespace \u5FC5\u987B\u662F\u6709\u6548 Kubernetes \u547D\u540D\u7A7A\u95F4");
48
+ const selected = environments ? new Set(environments) : void 0;
49
+ const stepEnvironments = /* @__PURE__ */ new Map([["deploy-to-test", "test"], ["deploy-to-stage", "stage"], ["deploy-to-prod", "prod"], ["deploy-to-stable", "stable"], ["push-helm-chart", "prod"]]);
50
+ pipeline.steps = pipeline.steps.filter(
51
+ /** 通用安装和构建步骤保留,发布步骤按显式环境选择。 */
52
+ (step) => !selected || !stepEnvironments.has(step.name) || selected.has(stepEnvironments.get(step.name))
53
+ );
54
+ for (const step of pipeline.steps) {
55
+ if (deployment.nodeVersion && ["install", "build"].includes(step.name)) step.image = `node:${deployment.nodeVersion}-bookworm`;
56
+ if (deployment.nodeVersion && step.name === "image-build") step.settings.build_args.push(`XFE_NODE_VERSION=${deployment.nodeVersion}`);
57
+ if (step.name === "build") step.commands = step.commands.map(
58
+ /** managed 构建复用配置生成器;应用自定义检查仍由应用命令负责。 */
59
+ (command2) => {
60
+ if (deployment.managed && command2 === "cp -r node_modules/@xfe-repo/web-app/deploy deploy") return "pnpm exec xfe-web config";
61
+ return command2 === "pnpm build" ? deployment.buildCommand || command2 : command2;
62
+ }
63
+ );
64
+ if (deployment.managed && deployment.targetK8s === "infra" && step.name === "pre-deploy") delete step.environment.SAAS_K8S_CONFIG;
65
+ if (deployment.managed && step.name === "deploy-to-prod") step.commands = step.commands.map(
66
+ /** 正式 Helm 部署等待探针并在失败时撤销资源变更。 */
67
+ (command2) => command2.includes("| kubectl") ? `helm upgrade --install "\${DRONE_REPO//\\//-}" deploy/helm --values deploy/helm/values.prod.yaml --namespace ${deployment.targetNamespace || "deployment-prod"} --create-namespace --kubeconfig=kube_config --atomic --wait --timeout 5m` : command2
68
+ );
69
+ }
70
+ if (selected && !selected.has("test")) {
71
+ pipeline.trigger.event = ["tag"];
72
+ const tags = [...new Set([...selected].map(
73
+ /** prod 与 stable 共用正式标签,其余使用各自的标签。 */
74
+ (environment) => environment === "stage" ? "refs/tags/stage-*" : "refs/tags/release-*"
75
+ ))];
76
+ pipeline.trigger.ref = { include: tags };
77
+ }
78
+ const result = deployment.managed && deployment.targetK8s === "infra" ? documents.filter(
79
+ /** infra 独立生成的流水线不要求额外的 saas kubeconfig。 */
80
+ (document) => document.kind !== "secret" || document.name !== "saas-k8s-config"
81
+ ) : documents;
82
+ return result.map(
83
+ /** 序列化多文档流水线,保留 Kubernetes Runner 的 Secret 引用。 */
84
+ (document) => yaml.stringify(document)
85
+ ).join("---\n");
86
+ }
87
+ var DEFAULT_BUILD_DIRECTORY, BUILD_DIRECTORY_ARGUMENT, protectedDirectories, loadXfeConfig, getBuildDirectory, getServerEntry, configDeploy, getJwtEnv;
23
88
  var init_config = __esm({
24
89
  "src/config.ts"() {
25
90
  "use strict";
26
91
  DEFAULT_BUILD_DIRECTORY = "dist";
27
92
  BUILD_DIRECTORY_ARGUMENT = "XFE_BUILD_DIRECTORY";
28
93
  protectedDirectories = /* @__PURE__ */ new Set(["src", "types", "public", "node_modules", ".git"]);
29
- configDirectory = typeof __dirname === "string" ? __dirname : path.dirname(fileURLToPath(import.meta.url));
30
94
  loadXfeConfig = (root = process.cwd()) => {
31
95
  const xfeConfigPath = path.join(root, "xfe.json");
32
96
  if (!fs.existsSync(xfeConfigPath)) {
@@ -83,38 +147,33 @@ var init_config = __esm({
83
147
  };
84
148
  configDeploy = () => {
85
149
  const xfeConfig = loadXfeConfig();
86
- if (!xfeConfig?.deploy) return;
150
+ const deployment = xfeConfig.deploy;
151
+ if (!deployment) return;
152
+ const configDirectory = typeof __dirname === "string" ? __dirname : path.dirname(fileURLToPath(import.meta.url));
87
153
  const projectRoot = process.cwd();
154
+ const source = path.resolve(configDirectory, "../deploy");
155
+ const target = deployment.managed ? getBuildDirectory(projectRoot, { buildDirectory: "deploy" }) : source;
88
156
  const buildDirectory = path.relative(projectRoot, getBuildDirectory(projectRoot, xfeConfig.webapp)).replaceAll(path.sep, "/");
89
- const droneConfigFilePath = path.resolve(configDirectory, "../deploy/.drone.yml");
90
- const droneTarget = path.resolve(projectRoot, xfeConfig.deploy.droneConfigPath || "./.drone.yml");
91
- const defaultArgument = JSON.stringify(`${BUILD_DIRECTORY_ARGUMENT}=${DEFAULT_BUILD_DIRECTORY}`);
92
- const configuredArgument = JSON.stringify(`${BUILD_DIRECTORY_ARGUMENT}=${buildDirectory}`);
93
- const droneConfig = fs.readFileSync(droneConfigFilePath, "utf8").replace(defaultArgument, configuredArgument);
157
+ const droneTarget = path.resolve(projectRoot, deployment.droneConfigPath || "./.drone.yml");
158
+ const template = fs.readFileSync(path.join(source, ".drone.yml"), "utf8").replace(JSON.stringify(`${BUILD_DIRECTORY_ARGUMENT}=${DEFAULT_BUILD_DIRECTORY}`), JSON.stringify(`${BUILD_DIRECTORY_ARGUMENT}=${buildDirectory}`));
159
+ const drone = configureDeployPipeline(template, deployment);
160
+ const values = yaml.parse(fs.readFileSync(path.join(source, "helm/values.yaml"), "utf8"));
161
+ const { virtualServiceEnabled = true, hostsProd = [], hostsStage = [], hostsTest = [], gateways = ["common-gateway-eshetang"] } = deployment;
162
+ values.virtualService = { ...values.virtualService, enabled: virtualServiceEnabled, hosts: { prod: hostsProd, stage: hostsStage, test: hostsTest }, gateways };
163
+ values.targetK8s = deployment.targetK8s || "saas";
164
+ values.namespace = deployment.targetNamespace || "";
165
+ const configuredValues = mergeDeployValues(values, deployment.values || {});
166
+ if (deployment.managed && deployment.environments) configuredValues.virtualService.hosts = Object.fromEntries(Object.entries(configuredValues.virtualService.hosts).filter(
167
+ /** 托管部署仅保留已选择环境的域名。 */
168
+ ([environment]) => deployment.environments.includes(environment) || environment === "test" && deployment.environments.includes("stable")
169
+ ));
170
+ if (deployment.managed) {
171
+ if (target === source) throw new Error("managed \u90E8\u7F72\u8F93\u51FA\u4E0D\u80FD\u8986\u76D6\u6846\u67B6\u6A21\u677F\u76EE\u5F55");
172
+ fs.cpSync(source, target, { recursive: true });
173
+ }
94
174
  fs.mkdirSync(path.dirname(droneTarget), { recursive: true });
95
- fs.writeFileSync(droneTarget, droneConfig);
96
- const valuesYamlFilePath = path.resolve(configDirectory, "../deploy/helm/values.yaml");
97
- const valuesYamlFile = fs.readFileSync(valuesYamlFilePath, "utf8");
98
- const valuesYaml = yaml.parse(valuesYamlFile);
99
- if (!valuesYaml.virtualService) valuesYaml.virtualService = {};
100
- if (!valuesYaml.virtualService.hosts) valuesYaml.virtualService.hosts = { test: [], stage: [], prod: [] };
101
- const {
102
- virtualServiceEnabled = true,
103
- hostsProd = [],
104
- hostsStage = [],
105
- hostsTest = [],
106
- gateways = ["common-gateway-eshetang"]
107
- } = xfeConfig.deploy;
108
- const { targetK8s = "saas", targetNamespace = "" } = xfeConfig.deploy;
109
- valuesYaml.virtualService.enabled = virtualServiceEnabled;
110
- valuesYaml.virtualService.gateways = gateways;
111
- valuesYaml.virtualService.hosts.prod = hostsProd;
112
- valuesYaml.virtualService.hosts.stage = hostsStage;
113
- valuesYaml.virtualService.hosts.test = hostsTest;
114
- valuesYaml.targetK8s = targetK8s;
115
- valuesYaml.namespace = targetNamespace;
116
- const valuesYamlStr = yaml.stringify(valuesYaml, { defaultStringType: "QUOTE_SINGLE", defaultKeyType: "PLAIN" });
117
- fs.writeFileSync(valuesYamlFilePath, valuesYamlStr, "utf8");
175
+ fs.writeFileSync(path.join(target, "helm/values.yaml"), yaml.stringify(configuredValues, { defaultStringType: "QUOTE_SINGLE", defaultKeyType: "PLAIN" }));
176
+ fs.writeFileSync(droneTarget, drone);
118
177
  };
119
178
  getJwtEnv = async () => {
120
179
  const xfeConfig = loadXfeConfig();
package/dist/config.d.mts CHANGED
@@ -26,7 +26,7 @@ declare const loadXfeConfig: (root?: string) => XfeConfig;
26
26
  declare const getBuildDirectory: (projectRoot: string, webapp?: WebAppConfig) => string;
27
27
  /** 生产仅校验入口约定而不读取源码,构建阶段由编译器检查文件存在。 */
28
28
  declare const getServerEntry: (projectRoot: string, webapp?: WebAppConfig) => string | undefined;
29
- /** 将输出目录传给 Docker 构建,并沿用现有的集群、域名与 Helm 配置。 */
29
+ /** 默认保留既有部署;managed 模式将公共模板和覆盖 values 生成到项目 deploy 目录。 */
30
30
  declare const configDeploy: () => void;
31
31
  declare const getJwtEnv: () => Promise<Partial<JwtEnv>>;
32
32
 
package/dist/config.d.ts CHANGED
@@ -26,7 +26,7 @@ declare const loadXfeConfig: (root?: string) => XfeConfig;
26
26
  declare const getBuildDirectory: (projectRoot: string, webapp?: WebAppConfig) => string;
27
27
  /** 生产仅校验入口约定而不读取源码,构建阶段由编译器检查文件存在。 */
28
28
  declare const getServerEntry: (projectRoot: string, webapp?: WebAppConfig) => string | undefined;
29
- /** 将输出目录传给 Docker 构建,并沿用现有的集群、域名与 Helm 配置。 */
29
+ /** 默认保留既有部署;managed 模式将公共模板和覆盖 values 生成到项目 deploy 目录。 */
30
30
  declare const configDeploy: () => void;
31
31
  declare const getJwtEnv: () => Promise<Partial<JwtEnv>>;
32
32
 
package/dist/config.js CHANGED
@@ -45,7 +45,6 @@ var import_meta = {};
45
45
  var DEFAULT_BUILD_DIRECTORY = "dist";
46
46
  var BUILD_DIRECTORY_ARGUMENT = "XFE_BUILD_DIRECTORY";
47
47
  var protectedDirectories = /* @__PURE__ */ new Set(["src", "types", "public", "node_modules", ".git"]);
48
- var configDirectory = typeof __dirname === "string" ? __dirname : import_node_path.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
49
48
  var loadXfeConfig = (root = process.cwd()) => {
50
49
  const xfeConfigPath = import_node_path.default.join(root, "xfe.json");
51
50
  if (!import_node_fs.default.existsSync(xfeConfigPath)) {
@@ -102,39 +101,99 @@ var getServerEntry = (projectRoot, webapp = {}) => {
102
101
  };
103
102
  var configDeploy = () => {
104
103
  const xfeConfig = loadXfeConfig();
105
- if (!xfeConfig?.deploy) return;
104
+ const deployment = xfeConfig.deploy;
105
+ if (!deployment) return;
106
+ const configDirectory = typeof __dirname === "string" ? __dirname : import_node_path.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
106
107
  const projectRoot = process.cwd();
108
+ const source = import_node_path.default.resolve(configDirectory, "../deploy");
109
+ const target = deployment.managed ? getBuildDirectory(projectRoot, { buildDirectory: "deploy" }) : source;
107
110
  const buildDirectory = import_node_path.default.relative(projectRoot, getBuildDirectory(projectRoot, xfeConfig.webapp)).replaceAll(import_node_path.default.sep, "/");
108
- const droneConfigFilePath = import_node_path.default.resolve(configDirectory, "../deploy/.drone.yml");
109
- const droneTarget = import_node_path.default.resolve(projectRoot, xfeConfig.deploy.droneConfigPath || "./.drone.yml");
110
- const defaultArgument = JSON.stringify(`${BUILD_DIRECTORY_ARGUMENT}=${DEFAULT_BUILD_DIRECTORY}`);
111
- const configuredArgument = JSON.stringify(`${BUILD_DIRECTORY_ARGUMENT}=${buildDirectory}`);
112
- const droneConfig = import_node_fs.default.readFileSync(droneConfigFilePath, "utf8").replace(defaultArgument, configuredArgument);
111
+ const droneTarget = import_node_path.default.resolve(projectRoot, deployment.droneConfigPath || "./.drone.yml");
112
+ const template = import_node_fs.default.readFileSync(import_node_path.default.join(source, ".drone.yml"), "utf8").replace(JSON.stringify(`${BUILD_DIRECTORY_ARGUMENT}=${DEFAULT_BUILD_DIRECTORY}`), JSON.stringify(`${BUILD_DIRECTORY_ARGUMENT}=${buildDirectory}`));
113
+ const drone = configureDeployPipeline(template, deployment);
114
+ const values = import_yaml.default.parse(import_node_fs.default.readFileSync(import_node_path.default.join(source, "helm/values.yaml"), "utf8"));
115
+ const { virtualServiceEnabled = true, hostsProd = [], hostsStage = [], hostsTest = [], gateways = ["common-gateway-eshetang"] } = deployment;
116
+ values.virtualService = { ...values.virtualService, enabled: virtualServiceEnabled, hosts: { prod: hostsProd, stage: hostsStage, test: hostsTest }, gateways };
117
+ values.targetK8s = deployment.targetK8s || "saas";
118
+ values.namespace = deployment.targetNamespace || "";
119
+ const configuredValues = mergeDeployValues(values, deployment.values || {});
120
+ if (deployment.managed && deployment.environments) configuredValues.virtualService.hosts = Object.fromEntries(Object.entries(configuredValues.virtualService.hosts).filter(
121
+ /** 托管部署仅保留已选择环境的域名。 */
122
+ ([environment]) => deployment.environments.includes(environment) || environment === "test" && deployment.environments.includes("stable")
123
+ ));
124
+ if (deployment.managed) {
125
+ if (target === source) throw new Error("managed \u90E8\u7F72\u8F93\u51FA\u4E0D\u80FD\u8986\u76D6\u6846\u67B6\u6A21\u677F\u76EE\u5F55");
126
+ import_node_fs.default.cpSync(source, target, { recursive: true });
127
+ }
113
128
  import_node_fs.default.mkdirSync(import_node_path.default.dirname(droneTarget), { recursive: true });
114
- import_node_fs.default.writeFileSync(droneTarget, droneConfig);
115
- const valuesYamlFilePath = import_node_path.default.resolve(configDirectory, "../deploy/helm/values.yaml");
116
- const valuesYamlFile = import_node_fs.default.readFileSync(valuesYamlFilePath, "utf8");
117
- const valuesYaml = import_yaml.default.parse(valuesYamlFile);
118
- if (!valuesYaml.virtualService) valuesYaml.virtualService = {};
119
- if (!valuesYaml.virtualService.hosts) valuesYaml.virtualService.hosts = { test: [], stage: [], prod: [] };
120
- const {
121
- virtualServiceEnabled = true,
122
- hostsProd = [],
123
- hostsStage = [],
124
- hostsTest = [],
125
- gateways = ["common-gateway-eshetang"]
126
- } = xfeConfig.deploy;
127
- const { targetK8s = "saas", targetNamespace = "" } = xfeConfig.deploy;
128
- valuesYaml.virtualService.enabled = virtualServiceEnabled;
129
- valuesYaml.virtualService.gateways = gateways;
130
- valuesYaml.virtualService.hosts.prod = hostsProd;
131
- valuesYaml.virtualService.hosts.stage = hostsStage;
132
- valuesYaml.virtualService.hosts.test = hostsTest;
133
- valuesYaml.targetK8s = targetK8s;
134
- valuesYaml.namespace = targetNamespace;
135
- const valuesYamlStr = import_yaml.default.stringify(valuesYaml, { defaultStringType: "QUOTE_SINGLE", defaultKeyType: "PLAIN" });
136
- import_node_fs.default.writeFileSync(valuesYamlFilePath, valuesYamlStr, "utf8");
129
+ import_node_fs.default.writeFileSync(import_node_path.default.join(target, "helm/values.yaml"), import_yaml.default.stringify(configuredValues, { defaultStringType: "QUOTE_SINGLE", defaultKeyType: "PLAIN" }));
130
+ import_node_fs.default.writeFileSync(droneTarget, drone);
137
131
  };
132
+ function mergeDeployValues(base, overrides) {
133
+ const entries = Object.entries({ ...base, ...overrides }).map(
134
+ /** 只递归普通对象,列表保持调用方声明的顺序与完整内容。 */
135
+ ([key, value]) => {
136
+ const previous = base[key];
137
+ const mergeable = value && previous && typeof value === "object" && typeof previous === "object" && !Array.isArray(value) && !Array.isArray(previous);
138
+ return [key, mergeable ? mergeDeployValues(previous, value) : value];
139
+ }
140
+ );
141
+ return Object.fromEntries(entries);
142
+ }
143
+ function configureDeployPipeline(template, deployment) {
144
+ if (!deployment.managed && !deployment.nodeVersion && !deployment.environments && !deployment.buildCommand) return template;
145
+ const documents = import_yaml.default.parseAllDocuments(template).map(
146
+ /** 保留镜像和集群 Secret 定义。 */
147
+ (document) => document.toJSON()
148
+ );
149
+ const pipeline = documents[0];
150
+ const environments = deployment.environments;
151
+ const known = ["test", "stage", "prod", "stable"];
152
+ if (environments && (!Array.isArray(environments) || !environments.length || environments.some(
153
+ /** 拒绝未声明的发布环境名称。 */
154
+ (environment) => typeof environment !== "string" || !known.includes(environment)
155
+ ))) throw new Error("deploy.environments \u5FC5\u987B\u662F test/stage/prod/stable \u7684\u975E\u7A7A\u6570\u7EC4");
156
+ if (deployment.nodeVersion && !/^\d+\.\d+\.\d+$/.test(deployment.nodeVersion)) throw new Error("deploy.nodeVersion \u5FC5\u987B\u662F\u5B8C\u6574 Node \u7248\u672C\u53F7");
157
+ if (deployment.managed && deployment.targetNamespace && !/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/.test(deployment.targetNamespace)) throw new Error("deploy.targetNamespace \u5FC5\u987B\u662F\u6709\u6548 Kubernetes \u547D\u540D\u7A7A\u95F4");
158
+ const selected = environments ? new Set(environments) : void 0;
159
+ const stepEnvironments = /* @__PURE__ */ new Map([["deploy-to-test", "test"], ["deploy-to-stage", "stage"], ["deploy-to-prod", "prod"], ["deploy-to-stable", "stable"], ["push-helm-chart", "prod"]]);
160
+ pipeline.steps = pipeline.steps.filter(
161
+ /** 通用安装和构建步骤保留,发布步骤按显式环境选择。 */
162
+ (step) => !selected || !stepEnvironments.has(step.name) || selected.has(stepEnvironments.get(step.name))
163
+ );
164
+ for (const step of pipeline.steps) {
165
+ if (deployment.nodeVersion && ["install", "build"].includes(step.name)) step.image = `node:${deployment.nodeVersion}-bookworm`;
166
+ if (deployment.nodeVersion && step.name === "image-build") step.settings.build_args.push(`XFE_NODE_VERSION=${deployment.nodeVersion}`);
167
+ if (step.name === "build") step.commands = step.commands.map(
168
+ /** managed 构建复用配置生成器;应用自定义检查仍由应用命令负责。 */
169
+ (command) => {
170
+ if (deployment.managed && command === "cp -r node_modules/@xfe-repo/web-app/deploy deploy") return "pnpm exec xfe-web config";
171
+ return command === "pnpm build" ? deployment.buildCommand || command : command;
172
+ }
173
+ );
174
+ if (deployment.managed && deployment.targetK8s === "infra" && step.name === "pre-deploy") delete step.environment.SAAS_K8S_CONFIG;
175
+ if (deployment.managed && step.name === "deploy-to-prod") step.commands = step.commands.map(
176
+ /** 正式 Helm 部署等待探针并在失败时撤销资源变更。 */
177
+ (command) => command.includes("| kubectl") ? `helm upgrade --install "\${DRONE_REPO//\\//-}" deploy/helm --values deploy/helm/values.prod.yaml --namespace ${deployment.targetNamespace || "deployment-prod"} --create-namespace --kubeconfig=kube_config --atomic --wait --timeout 5m` : command
178
+ );
179
+ }
180
+ if (selected && !selected.has("test")) {
181
+ pipeline.trigger.event = ["tag"];
182
+ const tags = [...new Set([...selected].map(
183
+ /** prod 与 stable 共用正式标签,其余使用各自的标签。 */
184
+ (environment) => environment === "stage" ? "refs/tags/stage-*" : "refs/tags/release-*"
185
+ ))];
186
+ pipeline.trigger.ref = { include: tags };
187
+ }
188
+ const result = deployment.managed && deployment.targetK8s === "infra" ? documents.filter(
189
+ /** infra 独立生成的流水线不要求额外的 saas kubeconfig。 */
190
+ (document) => document.kind !== "secret" || document.name !== "saas-k8s-config"
191
+ ) : documents;
192
+ return result.map(
193
+ /** 序列化多文档流水线,保留 Kubernetes Runner 的 Secret 引用。 */
194
+ (document) => import_yaml.default.stringify(document)
195
+ ).join("---\n");
196
+ }
138
197
  var getJwtEnv = async () => {
139
198
  const xfeConfig = loadXfeConfig();
140
199
  if (!xfeConfig?.jwt) return {};
package/dist/config.mjs CHANGED
@@ -6,7 +6,6 @@ import yaml from "yaml";
6
6
  var DEFAULT_BUILD_DIRECTORY = "dist";
7
7
  var BUILD_DIRECTORY_ARGUMENT = "XFE_BUILD_DIRECTORY";
8
8
  var protectedDirectories = /* @__PURE__ */ new Set(["src", "types", "public", "node_modules", ".git"]);
9
- var configDirectory = typeof __dirname === "string" ? __dirname : path.dirname(fileURLToPath(import.meta.url));
10
9
  var loadXfeConfig = (root = process.cwd()) => {
11
10
  const xfeConfigPath = path.join(root, "xfe.json");
12
11
  if (!fs.existsSync(xfeConfigPath)) {
@@ -63,39 +62,99 @@ var getServerEntry = (projectRoot, webapp = {}) => {
63
62
  };
64
63
  var configDeploy = () => {
65
64
  const xfeConfig = loadXfeConfig();
66
- if (!xfeConfig?.deploy) return;
65
+ const deployment = xfeConfig.deploy;
66
+ if (!deployment) return;
67
+ const configDirectory = typeof __dirname === "string" ? __dirname : path.dirname(fileURLToPath(import.meta.url));
67
68
  const projectRoot = process.cwd();
69
+ const source = path.resolve(configDirectory, "../deploy");
70
+ const target = deployment.managed ? getBuildDirectory(projectRoot, { buildDirectory: "deploy" }) : source;
68
71
  const buildDirectory = path.relative(projectRoot, getBuildDirectory(projectRoot, xfeConfig.webapp)).replaceAll(path.sep, "/");
69
- const droneConfigFilePath = path.resolve(configDirectory, "../deploy/.drone.yml");
70
- const droneTarget = path.resolve(projectRoot, xfeConfig.deploy.droneConfigPath || "./.drone.yml");
71
- const defaultArgument = JSON.stringify(`${BUILD_DIRECTORY_ARGUMENT}=${DEFAULT_BUILD_DIRECTORY}`);
72
- const configuredArgument = JSON.stringify(`${BUILD_DIRECTORY_ARGUMENT}=${buildDirectory}`);
73
- const droneConfig = fs.readFileSync(droneConfigFilePath, "utf8").replace(defaultArgument, configuredArgument);
72
+ const droneTarget = path.resolve(projectRoot, deployment.droneConfigPath || "./.drone.yml");
73
+ const template = fs.readFileSync(path.join(source, ".drone.yml"), "utf8").replace(JSON.stringify(`${BUILD_DIRECTORY_ARGUMENT}=${DEFAULT_BUILD_DIRECTORY}`), JSON.stringify(`${BUILD_DIRECTORY_ARGUMENT}=${buildDirectory}`));
74
+ const drone = configureDeployPipeline(template, deployment);
75
+ const values = yaml.parse(fs.readFileSync(path.join(source, "helm/values.yaml"), "utf8"));
76
+ const { virtualServiceEnabled = true, hostsProd = [], hostsStage = [], hostsTest = [], gateways = ["common-gateway-eshetang"] } = deployment;
77
+ values.virtualService = { ...values.virtualService, enabled: virtualServiceEnabled, hosts: { prod: hostsProd, stage: hostsStage, test: hostsTest }, gateways };
78
+ values.targetK8s = deployment.targetK8s || "saas";
79
+ values.namespace = deployment.targetNamespace || "";
80
+ const configuredValues = mergeDeployValues(values, deployment.values || {});
81
+ if (deployment.managed && deployment.environments) configuredValues.virtualService.hosts = Object.fromEntries(Object.entries(configuredValues.virtualService.hosts).filter(
82
+ /** 托管部署仅保留已选择环境的域名。 */
83
+ ([environment]) => deployment.environments.includes(environment) || environment === "test" && deployment.environments.includes("stable")
84
+ ));
85
+ if (deployment.managed) {
86
+ if (target === source) throw new Error("managed \u90E8\u7F72\u8F93\u51FA\u4E0D\u80FD\u8986\u76D6\u6846\u67B6\u6A21\u677F\u76EE\u5F55");
87
+ fs.cpSync(source, target, { recursive: true });
88
+ }
74
89
  fs.mkdirSync(path.dirname(droneTarget), { recursive: true });
75
- fs.writeFileSync(droneTarget, droneConfig);
76
- const valuesYamlFilePath = path.resolve(configDirectory, "../deploy/helm/values.yaml");
77
- const valuesYamlFile = fs.readFileSync(valuesYamlFilePath, "utf8");
78
- const valuesYaml = yaml.parse(valuesYamlFile);
79
- if (!valuesYaml.virtualService) valuesYaml.virtualService = {};
80
- if (!valuesYaml.virtualService.hosts) valuesYaml.virtualService.hosts = { test: [], stage: [], prod: [] };
81
- const {
82
- virtualServiceEnabled = true,
83
- hostsProd = [],
84
- hostsStage = [],
85
- hostsTest = [],
86
- gateways = ["common-gateway-eshetang"]
87
- } = xfeConfig.deploy;
88
- const { targetK8s = "saas", targetNamespace = "" } = xfeConfig.deploy;
89
- valuesYaml.virtualService.enabled = virtualServiceEnabled;
90
- valuesYaml.virtualService.gateways = gateways;
91
- valuesYaml.virtualService.hosts.prod = hostsProd;
92
- valuesYaml.virtualService.hosts.stage = hostsStage;
93
- valuesYaml.virtualService.hosts.test = hostsTest;
94
- valuesYaml.targetK8s = targetK8s;
95
- valuesYaml.namespace = targetNamespace;
96
- const valuesYamlStr = yaml.stringify(valuesYaml, { defaultStringType: "QUOTE_SINGLE", defaultKeyType: "PLAIN" });
97
- fs.writeFileSync(valuesYamlFilePath, valuesYamlStr, "utf8");
90
+ fs.writeFileSync(path.join(target, "helm/values.yaml"), yaml.stringify(configuredValues, { defaultStringType: "QUOTE_SINGLE", defaultKeyType: "PLAIN" }));
91
+ fs.writeFileSync(droneTarget, drone);
98
92
  };
93
+ function mergeDeployValues(base, overrides) {
94
+ const entries = Object.entries({ ...base, ...overrides }).map(
95
+ /** 只递归普通对象,列表保持调用方声明的顺序与完整内容。 */
96
+ ([key, value]) => {
97
+ const previous = base[key];
98
+ const mergeable = value && previous && typeof value === "object" && typeof previous === "object" && !Array.isArray(value) && !Array.isArray(previous);
99
+ return [key, mergeable ? mergeDeployValues(previous, value) : value];
100
+ }
101
+ );
102
+ return Object.fromEntries(entries);
103
+ }
104
+ function configureDeployPipeline(template, deployment) {
105
+ if (!deployment.managed && !deployment.nodeVersion && !deployment.environments && !deployment.buildCommand) return template;
106
+ const documents = yaml.parseAllDocuments(template).map(
107
+ /** 保留镜像和集群 Secret 定义。 */
108
+ (document) => document.toJSON()
109
+ );
110
+ const pipeline = documents[0];
111
+ const environments = deployment.environments;
112
+ const known = ["test", "stage", "prod", "stable"];
113
+ if (environments && (!Array.isArray(environments) || !environments.length || environments.some(
114
+ /** 拒绝未声明的发布环境名称。 */
115
+ (environment) => typeof environment !== "string" || !known.includes(environment)
116
+ ))) throw new Error("deploy.environments \u5FC5\u987B\u662F test/stage/prod/stable \u7684\u975E\u7A7A\u6570\u7EC4");
117
+ if (deployment.nodeVersion && !/^\d+\.\d+\.\d+$/.test(deployment.nodeVersion)) throw new Error("deploy.nodeVersion \u5FC5\u987B\u662F\u5B8C\u6574 Node \u7248\u672C\u53F7");
118
+ if (deployment.managed && deployment.targetNamespace && !/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/.test(deployment.targetNamespace)) throw new Error("deploy.targetNamespace \u5FC5\u987B\u662F\u6709\u6548 Kubernetes \u547D\u540D\u7A7A\u95F4");
119
+ const selected = environments ? new Set(environments) : void 0;
120
+ const stepEnvironments = /* @__PURE__ */ new Map([["deploy-to-test", "test"], ["deploy-to-stage", "stage"], ["deploy-to-prod", "prod"], ["deploy-to-stable", "stable"], ["push-helm-chart", "prod"]]);
121
+ pipeline.steps = pipeline.steps.filter(
122
+ /** 通用安装和构建步骤保留,发布步骤按显式环境选择。 */
123
+ (step) => !selected || !stepEnvironments.has(step.name) || selected.has(stepEnvironments.get(step.name))
124
+ );
125
+ for (const step of pipeline.steps) {
126
+ if (deployment.nodeVersion && ["install", "build"].includes(step.name)) step.image = `node:${deployment.nodeVersion}-bookworm`;
127
+ if (deployment.nodeVersion && step.name === "image-build") step.settings.build_args.push(`XFE_NODE_VERSION=${deployment.nodeVersion}`);
128
+ if (step.name === "build") step.commands = step.commands.map(
129
+ /** managed 构建复用配置生成器;应用自定义检查仍由应用命令负责。 */
130
+ (command) => {
131
+ if (deployment.managed && command === "cp -r node_modules/@xfe-repo/web-app/deploy deploy") return "pnpm exec xfe-web config";
132
+ return command === "pnpm build" ? deployment.buildCommand || command : command;
133
+ }
134
+ );
135
+ if (deployment.managed && deployment.targetK8s === "infra" && step.name === "pre-deploy") delete step.environment.SAAS_K8S_CONFIG;
136
+ if (deployment.managed && step.name === "deploy-to-prod") step.commands = step.commands.map(
137
+ /** 正式 Helm 部署等待探针并在失败时撤销资源变更。 */
138
+ (command) => command.includes("| kubectl") ? `helm upgrade --install "\${DRONE_REPO//\\//-}" deploy/helm --values deploy/helm/values.prod.yaml --namespace ${deployment.targetNamespace || "deployment-prod"} --create-namespace --kubeconfig=kube_config --atomic --wait --timeout 5m` : command
139
+ );
140
+ }
141
+ if (selected && !selected.has("test")) {
142
+ pipeline.trigger.event = ["tag"];
143
+ const tags = [...new Set([...selected].map(
144
+ /** prod 与 stable 共用正式标签,其余使用各自的标签。 */
145
+ (environment) => environment === "stage" ? "refs/tags/stage-*" : "refs/tags/release-*"
146
+ ))];
147
+ pipeline.trigger.ref = { include: tags };
148
+ }
149
+ const result = deployment.managed && deployment.targetK8s === "infra" ? documents.filter(
150
+ /** infra 独立生成的流水线不要求额外的 saas kubeconfig。 */
151
+ (document) => document.kind !== "secret" || document.name !== "saas-k8s-config"
152
+ ) : documents;
153
+ return result.map(
154
+ /** 序列化多文档流水线,保留 Kubernetes Runner 的 Secret 引用。 */
155
+ (document) => yaml.stringify(document)
156
+ ).join("---\n");
157
+ }
99
158
  var getJwtEnv = async () => {
100
159
  const xfeConfig = loadXfeConfig();
101
160
  if (!xfeConfig?.jwt) return {};
package/dist/framework.js CHANGED
@@ -44,10 +44,8 @@ var import_node_fs = __toESM(require("fs"));
44
44
  var import_node_path = __toESM(require("path"));
45
45
  var import_node_url = require("url");
46
46
  var import_yaml = __toESM(require("yaml"));
47
- var import_meta = {};
48
47
  var DEFAULT_BUILD_DIRECTORY = "dist";
49
48
  var protectedDirectories = /* @__PURE__ */ new Set(["src", "types", "public", "node_modules", ".git"]);
50
- var configDirectory = typeof __dirname === "string" ? __dirname : import_node_path.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
51
49
  var loadXfeConfig = (root = process.cwd()) => {
52
50
  const xfeConfigPath = import_node_path.default.join(root, "xfe.json");
53
51
  if (!import_node_fs.default.existsSync(xfeConfigPath)) {
@@ -104,8 +102,8 @@ var getServerEntry = (projectRoot, webapp = {}) => {
104
102
  };
105
103
 
106
104
  // src/framework.ts
107
- var import_meta2 = {};
108
- var frameworkDirectory = typeof __dirname === "string" ? __dirname : import_node_path2.default.dirname((0, import_node_url2.fileURLToPath)(import_meta2.url));
105
+ var import_meta = {};
106
+ var frameworkDirectory = typeof __dirname === "string" ? __dirname : import_node_path2.default.dirname((0, import_node_url2.fileURLToPath)(import_meta.url));
109
107
  var requireFromFramework = (0, import_node_module.createRequire)(import_node_path2.default.join(frameworkDirectory, "framework.js"));
110
108
  var extensions = [".tsx", ".ts", ".jsx", ".js"];
111
109
  var findSource = (directory, name) => extensions.map((extension) => import_node_path2.default.join(directory, `${name}${extension}`)).find(import_node_fs2.default.existsSync);
@@ -11,7 +11,6 @@ import { fileURLToPath } from "url";
11
11
  import yaml from "yaml";
12
12
  var DEFAULT_BUILD_DIRECTORY = "dist";
13
13
  var protectedDirectories = /* @__PURE__ */ new Set(["src", "types", "public", "node_modules", ".git"]);
14
- var configDirectory = typeof __dirname === "string" ? __dirname : path.dirname(fileURLToPath(import.meta.url));
15
14
  var loadXfeConfig = (root = process.cwd()) => {
16
15
  const xfeConfigPath = path.join(root, "xfe.json");
17
16
  if (!fs.existsSync(xfeConfigPath)) {
package/dist/node-dev.js CHANGED
@@ -27,10 +27,8 @@ var import_node_fs = __toESM(require("fs"));
27
27
  var import_node_path = __toESM(require("path"));
28
28
  var import_node_url = require("url");
29
29
  var import_yaml = __toESM(require("yaml"));
30
- var import_meta = {};
31
30
  var DEFAULT_BUILD_DIRECTORY = "dist";
32
31
  var protectedDirectories = /* @__PURE__ */ new Set(["src", "types", "public", "node_modules", ".git"]);
33
- var configDirectory = typeof __dirname === "string" ? __dirname : import_node_path.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
34
32
  var loadXfeConfig = (root = process.cwd()) => {
35
33
  const xfeConfigPath = import_node_path.default.join(root, "xfe.json");
36
34
  if (!import_node_fs.default.existsSync(xfeConfigPath)) {
@@ -99,8 +97,8 @@ var import_node_fs2 = __toESM(require("fs"));
99
97
  var import_node_module = require("module");
100
98
  var import_node_path2 = __toESM(require("path"));
101
99
  var import_node_url2 = require("url");
102
- var import_meta2 = {};
103
- var frameworkDirectory = typeof __dirname === "string" ? __dirname : import_node_path2.default.dirname((0, import_node_url2.fileURLToPath)(import_meta2.url));
100
+ var import_meta = {};
101
+ var frameworkDirectory = typeof __dirname === "string" ? __dirname : import_node_path2.default.dirname((0, import_node_url2.fileURLToPath)(import_meta.url));
104
102
  var requireFromFramework = (0, import_node_module.createRequire)(import_node_path2.default.join(frameworkDirectory, "framework.js"));
105
103
  var extensions = [".tsx", ".ts", ".jsx", ".js"];
106
104
  var findSource = (directory, name) => extensions.map((extension) => import_node_path2.default.join(directory, `${name}${extension}`)).find(import_node_fs2.default.existsSync);
package/dist/node-dev.mjs CHANGED
@@ -5,7 +5,6 @@ import { fileURLToPath } from "url";
5
5
  import yaml from "yaml";
6
6
  var DEFAULT_BUILD_DIRECTORY = "dist";
7
7
  var protectedDirectories = /* @__PURE__ */ new Set(["src", "types", "public", "node_modules", ".git"]);
8
- var configDirectory = typeof __dirname === "string" ? __dirname : path.dirname(fileURLToPath(import.meta.url));
9
8
  var loadXfeConfig = (root = process.cwd()) => {
10
9
  const xfeConfigPath = path.join(root, "xfe.json");
11
10
  if (!fs.existsSync(xfeConfigPath)) {
package/dist/node.js CHANGED
@@ -46,10 +46,8 @@ var import_node_fs = __toESM(require("fs"));
46
46
  var import_node_path = __toESM(require("path"));
47
47
  var import_node_url = require("url");
48
48
  var import_yaml = __toESM(require("yaml"));
49
- var import_meta = {};
50
49
  var DEFAULT_BUILD_DIRECTORY = "dist";
51
50
  var protectedDirectories = /* @__PURE__ */ new Set(["src", "types", "public", "node_modules", ".git"]);
52
- var configDirectory = typeof __dirname === "string" ? __dirname : import_node_path.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
53
51
  var loadXfeConfig = (root = process.cwd()) => {
54
52
  const xfeConfigPath = import_node_path.default.join(root, "xfe.json");
55
53
  if (!import_node_fs.default.existsSync(xfeConfigPath)) {
@@ -110,8 +108,8 @@ var import_node_fs2 = __toESM(require("fs"));
110
108
  var import_node_module = require("module");
111
109
  var import_node_path2 = __toESM(require("path"));
112
110
  var import_node_url2 = require("url");
113
- var import_meta2 = {};
114
- var frameworkDirectory = typeof __dirname === "string" ? __dirname : import_node_path2.default.dirname((0, import_node_url2.fileURLToPath)(import_meta2.url));
111
+ var import_meta = {};
112
+ var frameworkDirectory = typeof __dirname === "string" ? __dirname : import_node_path2.default.dirname((0, import_node_url2.fileURLToPath)(import_meta.url));
115
113
  var requireFromFramework = (0, import_node_module.createRequire)(import_node_path2.default.join(frameworkDirectory, "framework.js"));
116
114
  var extensions = [".tsx", ".ts", ".jsx", ".js"];
117
115
  var findSource = (directory, name) => extensions.map((extension) => import_node_path2.default.join(directory, `${name}${extension}`)).find(import_node_fs2.default.existsSync);
package/dist/node.mjs CHANGED
@@ -13,7 +13,6 @@ import { fileURLToPath } from "url";
13
13
  import yaml from "yaml";
14
14
  var DEFAULT_BUILD_DIRECTORY = "dist";
15
15
  var protectedDirectories = /* @__PURE__ */ new Set(["src", "types", "public", "node_modules", ".git"]);
16
- var configDirectory = typeof __dirname === "string" ? __dirname : path.dirname(fileURLToPath(import.meta.url));
17
16
  var loadXfeConfig = (root = process.cwd()) => {
18
17
  const xfeConfigPath = path.join(root, "xfe.json");
19
18
  if (!fs.existsSync(xfeConfigPath)) {
@@ -39,10 +39,8 @@ var import_node_fs = __toESM(require("fs"));
39
39
  var import_node_path = __toESM(require("path"));
40
40
  var import_node_url = require("url");
41
41
  var import_yaml = __toESM(require("yaml"));
42
- var import_meta = {};
43
42
  var DEFAULT_BUILD_DIRECTORY = "dist";
44
43
  var protectedDirectories = /* @__PURE__ */ new Set(["src", "types", "public", "node_modules", ".git"]);
45
- var configDirectory = typeof __dirname === "string" ? __dirname : import_node_path.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
46
44
  var loadXfeConfig = (root = process.cwd()) => {
47
45
  const xfeConfigPath = import_node_path.default.join(root, "xfe.json");
48
46
  if (!import_node_fs.default.existsSync(xfeConfigPath)) {
@@ -5,7 +5,6 @@ import { fileURLToPath } from "url";
5
5
  import yaml from "yaml";
6
6
  var DEFAULT_BUILD_DIRECTORY = "dist";
7
7
  var protectedDirectories = /* @__PURE__ */ new Set(["src", "types", "public", "node_modules", ".git"]);
8
- var configDirectory = typeof __dirname === "string" ? __dirname : path.dirname(fileURLToPath(import.meta.url));
9
8
  var loadXfeConfig = (root = process.cwd()) => {
10
9
  const xfeConfigPath = path.join(root, "xfe.json");
11
10
  if (!fs.existsSync(xfeConfigPath)) {
package/dist/vite.js CHANGED
@@ -44,10 +44,8 @@ var import_node_fs = __toESM(require("fs"));
44
44
  var import_node_path = __toESM(require("path"));
45
45
  var import_node_url = require("url");
46
46
  var import_yaml = __toESM(require("yaml"));
47
- var import_meta = {};
48
47
  var DEFAULT_BUILD_DIRECTORY = "dist";
49
48
  var protectedDirectories = /* @__PURE__ */ new Set(["src", "types", "public", "node_modules", ".git"]);
50
- var configDirectory = typeof __dirname === "string" ? __dirname : import_node_path.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
51
49
  var loadXfeConfig = (root = process.cwd()) => {
52
50
  const xfeConfigPath = import_node_path.default.join(root, "xfe.json");
53
51
  if (!import_node_fs.default.existsSync(xfeConfigPath)) {
package/dist/vite.mjs CHANGED
@@ -11,7 +11,6 @@ import { fileURLToPath } from "url";
11
11
  import yaml from "yaml";
12
12
  var DEFAULT_BUILD_DIRECTORY = "dist";
13
13
  var protectedDirectories = /* @__PURE__ */ new Set(["src", "types", "public", "node_modules", ".git"]);
14
- var configDirectory = typeof __dirname === "string" ? __dirname : path.dirname(fileURLToPath(import.meta.url));
15
14
  var loadXfeConfig = (root = process.cwd()) => {
16
15
  const xfeConfigPath = path.join(root, "xfe.json");
17
16
  if (!fs.existsSync(xfeConfigPath)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xfe-repo/web-app",
3
- "version": "2.0.0-beta.2",
3
+ "version": "2.0.0-beta.4",
4
4
  "bin": {
5
5
  "xfe-web": "./bin/index.js"
6
6
  },
@@ -30,8 +30,8 @@
30
30
  "yaml": "^2.4.2",
31
31
  "tsx": "4.23.15",
32
32
  "@xfe-repo/eslint-config": "2.0.0-beta.1",
33
- "@xfe-repo/web-router": "2.0.0-beta.1",
34
- "@xfe-repo/typescript-config": "2.0.0-beta.1"
33
+ "@xfe-repo/typescript-config": "2.0.0-beta.1",
34
+ "@xfe-repo/web-router": "2.0.0-beta.1"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/express": "5.0.6"