openyida 2026.9.23-beta.1 → 2026.9.23

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.
Files changed (130) hide show
  1. package/README.md +4 -11
  2. package/bin/yida.js +4 -14
  3. package/lib/app/app-entry.js +34 -8
  4. package/lib/app/application-style.js +0 -2
  5. package/lib/app/canvas-compile.js +7 -1
  6. package/lib/app/canvas-theme-guard.js +60 -13
  7. package/lib/app/compile.js +12 -2
  8. package/lib/app/custom-theme.js +27 -24
  9. package/lib/app/fix-theme.js +18 -66
  10. package/lib/app/publish.js +33 -55
  11. package/lib/app/services/canvas-page-compiler.js +3 -1
  12. package/lib/app/theme-from-design.js +63 -99
  13. package/lib/app/theme-scope.js +76 -0
  14. package/lib/auth/token-auth.js +18 -36
  15. package/lib/core/cli-error.js +1 -0
  16. package/lib/core/command-manifest.js +38 -14
  17. package/lib/core/locales/en.js +18 -12
  18. package/lib/core/locales/zh.js +18 -12
  19. package/lib/core/source-repair.js +38 -0
  20. package/lib/core/theme-brand-scale.js +133 -0
  21. package/lib/design/check-design.js +16 -6
  22. package/lib/design/document.js +36 -15
  23. package/lib/design/icon-contrast.js +1 -1
  24. package/lib/design/palette-contrast.js +77 -0
  25. package/lib/design-plan/design-plan.js +22 -7
  26. package/lib/design-plan/init.js +1 -1
  27. package/lib/design-plan/materialize.js +2 -2
  28. package/package.json +3 -2
  29. package/yida-skills/SKILL.md +2 -0
  30. package/yida-skills/references/source-repair.md +10 -0
  31. package/yida-skills/skills/yida-app/workflow/plan/step-4-deliver.md +2 -0
  32. package/yida-skills/skills/yida-app/workflow/plan/workflow.md +13 -4
  33. package/yida-skills/skills/yida-app/workflow/step-2-design.md +1 -1
  34. package/yida-skills/skills/yida-app/workflow/step-3-create-or-reuse-app.md +1 -1
  35. package/yida-skills/skills/yida-app/workflow/step-9-output-finish.md +3 -1
  36. package/yida-skills/skills/yida-canvas-custom-page/references/canvas-theme-provider.md +3 -1
  37. package/yida-skills/skills/yida-canvas-custom-page/references/waf-safe-authoring.md +3 -3
  38. package/yida-skills/skills/yida-canvas-custom-page/scripts/build-canvas-theme.js +2 -1
  39. package/yida-skills/skills/yida-canvas-custom-page/scripts/canvas-theme-provider.template.jsx +7 -0
  40. package/yida-skills/skills/yida-design/SKILL.md +13 -7
  41. package/yida-skills/skills/yida-design/references/application-style-library.md +2 -2
  42. package/yida-skills/skills/yida-design/references/application-theme-consistency.md +55 -11
  43. package/yida-skills/skills/yida-design/references/navigation-decision.md +3 -3
  44. package/yida-skills/skills/yida-design/references/theme/app-custom-theme-template.css +69 -155
  45. package/yida-skills/skills/yida-design/scripts/validate_design_themes.py +3 -1
  46. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/SKILL.md +2 -0
  47. package/yida-skills/skills/yida-design/templates/design-themes/README.md +3 -1
  48. package/yida-skills/skills/yida-design/templates/design-themes/app-amber/app_theme.css +85 -263
  49. package/yida-skills/skills/yida-design/templates/design-themes/app-amber/design.md +14 -50
  50. package/yida-skills/skills/yida-design/templates/design-themes/app-editorial/app_theme.css +85 -263
  51. package/yida-skills/skills/yida-design/templates/design-themes/app-editorial/design.md +16 -51
  52. package/yida-skills/skills/yida-design/templates/design-themes/app-executive/app_theme.css +87 -265
  53. package/yida-skills/skills/yida-design/templates/design-themes/app-executive/design.md +16 -51
  54. package/yida-skills/skills/yida-design/templates/design-themes/app-finance/app_theme.css +85 -263
  55. package/yida-skills/skills/yida-design/templates/design-themes/app-finance/design.md +14 -50
  56. package/yida-skills/skills/yida-design/templates/design-themes/app-glass/app_theme.css +88 -266
  57. package/yida-skills/skills/yida-design/templates/design-themes/app-glass/design.md +18 -53
  58. package/yida-skills/skills/yida-design/templates/design-themes/app-graphite/app_theme.css +85 -263
  59. package/yida-skills/skills/yida-design/templates/design-themes/app-graphite/design.md +14 -49
  60. package/yida-skills/skills/yida-design/templates/design-themes/app-line/app_theme.css +85 -263
  61. package/yida-skills/skills/yida-design/templates/design-themes/app-line/design.md +17 -52
  62. package/yida-skills/skills/yida-design/templates/design-themes/app-neon/app_theme.css +89 -267
  63. package/yida-skills/skills/yida-design/templates/design-themes/app-neon/design.md +19 -56
  64. package/yida-skills/skills/yida-design/templates/design-themes/app-nordic/app_theme.css +94 -278
  65. package/yida-skills/skills/yida-design/templates/design-themes/app-nordic/design.md +17 -57
  66. package/yida-skills/skills/yida-design/templates/design-themes/app-paper/app_theme.css +90 -268
  67. package/yida-skills/skills/yida-design/templates/design-themes/app-paper/design.md +18 -55
  68. package/yida-skills/skills/yida-design/templates/design-themes/app-platinum/app_theme.css +84 -262
  69. package/yida-skills/skills/yida-design/templates/design-themes/app-platinum/design.md +14 -49
  70. package/yida-skills/skills/yida-design/templates/design-themes/app-plum/app_theme.css +88 -266
  71. package/yida-skills/skills/yida-design/templates/design-themes/app-plum/design.md +18 -53
  72. package/yida-skills/skills/yida-design/templates/design-themes/app-pop/app_theme.css +99 -287
  73. package/yida-skills/skills/yida-design/templates/design-themes/app-pop/design.md +25 -67
  74. package/yida-skills/skills/yida-design/templates/design-themes/app-sage/app_theme.css +86 -264
  75. package/yida-skills/skills/yida-design/templates/design-themes/app-sage/design.md +18 -53
  76. package/yida-skills/skills/yida-design/templates/design-themes/app-teal/app_theme.css +85 -263
  77. package/yida-skills/skills/yida-design/templates/design-themes/app-teal/design.md +14 -49
  78. package/yida-skills/skills/yida-design/templates/design-themes/app-terminal/app_theme.css +90 -274
  79. package/yida-skills/skills/yida-design/templates/design-themes/app-terminal/design.md +14 -55
  80. package/yida-skills/skills/yida-design/templates/design-themes/app-ticket/app_theme.css +97 -279
  81. package/yida-skills/skills/yida-design/templates/design-themes/app-ticket/design.md +23 -60
  82. package/yida-skills/skills/yida-design/templates/design-themes/app-wire/app_theme.css +85 -263
  83. package/yida-skills/skills/yida-design/templates/design-themes/app-wire/design.md +14 -50
  84. package/yida-skills/skills/yida-design/templates/design-themes/basic-tokens.json +1 -8
  85. package/yida-skills/skills/yida-design/templates/design-themes/dark-inset-hairline/app_theme.css +101 -282
  86. package/yida-skills/skills/yida-design/templates/design-themes/dark-inset-hairline/design.md +36 -46
  87. package/yida-skills/skills/yida-design/templates/design-themes/dark-rail-fine-lines/app_theme.css +102 -276
  88. package/yida-skills/skills/yida-design/templates/design-themes/dark-rail-fine-lines/design.md +37 -46
  89. package/yida-skills/skills/yida-design/templates/design-themes/dark-rail-signal-panels/app_theme.css +101 -282
  90. package/yida-skills/skills/yida-design/templates/design-themes/dark-rail-signal-panels/design.md +38 -45
  91. package/yida-skills/skills/yida-design/templates/design-themes/free-creative/app_theme.css +79 -257
  92. package/yida-skills/skills/yida-design/templates/design-themes/free-creative/design.md +7 -43
  93. package/yida-skills/skills/yida-design/templates/design-themes/graphite-bevel-grid/app_theme.css +101 -282
  94. package/yida-skills/skills/yida-design/templates/design-themes/graphite-bevel-grid/design.md +35 -44
  95. package/yida-skills/skills/yida-design/templates/design-themes/hairline-shared-bands/app_theme.css +100 -281
  96. package/yida-skills/skills/yida-design/templates/design-themes/hairline-shared-bands/design.md +36 -45
  97. package/yida-skills/skills/yida-design/templates/design-themes/hairline-soft-blocks/app_theme.css +104 -285
  98. package/yida-skills/skills/yida-design/templates/design-themes/hairline-soft-blocks/design.md +38 -48
  99. package/yida-skills/skills/yida-design/templates/design-themes/index.json +18 -18
  100. package/yida-skills/skills/yida-design/templates/design-themes/ink-glow-soft-panels/app_theme.css +104 -285
  101. package/yida-skills/skills/yida-design/templates/design-themes/ink-glow-soft-panels/design.md +39 -49
  102. package/yida-skills/skills/yida-design/templates/design-themes/inset-frame-pixel-rhythm/app_theme.css +104 -285
  103. package/yida-skills/skills/yida-design/templates/design-themes/inset-frame-pixel-rhythm/design.md +37 -46
  104. package/yida-skills/skills/yida-design/templates/design-themes/outlined-texture-duotone/app_theme.css +103 -284
  105. package/yida-skills/skills/yida-design/templates/design-themes/outlined-texture-duotone/design.md +36 -46
  106. package/yida-skills/skills/yida-design/templates/design-themes/soft-inset-surfaces/app_theme.css +104 -285
  107. package/yida-skills/skills/yida-design/templates/design-themes/soft-inset-surfaces/design.md +38 -48
  108. package/yida-skills/skills/yida-design/templates/design-themes/soft-outline-rhythm/app_theme.css +105 -286
  109. package/yida-skills/skills/yida-design/templates/design-themes/soft-outline-rhythm/design.md +38 -48
  110. package/yida-skills/skills/yida-design/templates/design-themes/soft-rail-bold-band/app_theme.css +105 -286
  111. package/yida-skills/skills/yida-design/templates/design-themes/soft-rail-bold-band/design.md +40 -50
  112. package/yida-skills/skills/yida-design/templates/design-themes/soft-spectral-panels/app_theme.css +104 -285
  113. package/yida-skills/skills/yida-design/templates/design-themes/soft-spectral-panels/design.md +41 -49
  114. package/yida-skills/skills/yida-design/templates/design-themes/warm-canvas-contrast-panels/app_theme.css +104 -285
  115. package/yida-skills/skills/yida-design/templates/design-themes/warm-canvas-contrast-panels/design.md +39 -49
  116. package/yida-skills/skills/yida-design/templates/design-themes/warm-rail-muted-panels/app_theme.css +104 -285
  117. package/yida-skills/skills/yida-design/templates/design-themes/warm-rail-muted-panels/design.md +37 -47
  118. package/yida-skills/skills/yida-design/workflow/output-design.md +37 -4
  119. package/yida-skills/skills/yida-design/workflow/step-2-theme-system.md +7 -2
  120. package/yida-skills/skills/yida-design/workflow/step-6-handoff.md +2 -0
  121. package/yida-skills/skills/yida-export-conversation/SKILL.md +0 -1
  122. package/yida-skills/skills/yida-nav-shell/SKILL.md +5 -3
  123. package/yida-skills/skills/yida-nav-shell/references/nav-shell-patterns.md +3 -3
  124. package/yida-skills/skills/yida-prd/SKILL.md +1 -1
  125. package/yida-skills/skills/yida-publish-page/SKILL.md +21 -18
  126. package/yida-skills/skills/yida-requirement-analysis/references/handoff.md +1 -1
  127. package/yida-skills/skills-index.json +2 -2
  128. package/lib/auth/oauth-device.js +0 -278
  129. package/yida-skills/skills/yida-design/templates/application-styles.json +0 -398
  130. package/yida-skills/skills/yida-design/templates/navigation-styles.json +0 -769
package/README.md CHANGED
@@ -74,13 +74,6 @@ openyida login
74
74
 
75
75
  OpenYida login defaults to OAuth token mode. It opens the DingTalk OAuth authorization page, receives the local loopback callback, exchanges `code` / `authCode` with the Yida server, and stores `access_token` / `refresh_token` in the user auth store when available. If the user auth store is not writable, OpenYida explicitly falls back to the current project cache and reports that narrower persistence scope.
76
76
 
77
- In a sandbox, headless shell, or CI dashboard where the loopback callback cannot complete, use the device authorization flow. Open the displayed verification URL on any device and enter the user code. `--env-hint` optionally pins the intended environment profile at authorization time:
78
-
79
- ```bash
80
- openyida login --device
81
- openyida login --device --env-hint pre
82
- ```
83
-
84
77
  When the user names a target Yida entry URL, pass it to the login command so OpenYida can select the matching environment and auth profile. For example:
85
78
 
86
79
  ```bash
@@ -390,7 +383,7 @@ Run `openyida --help` or `openyida <command> --help` for detailed usage.
390
383
 
391
384
  | Command | Description |
392
385
  |---------|-------------|
393
- | `openyida login [target-url] [--env <name>\|--intl\|--overseas\|--global\|--yidaapps\|--alibaba] [--client-id <clientId>] [--endpoint <url>] [--no-browser] [--device] [--env-hint <name>]` | Login with OAuth token mode |
386
+ | `openyida login [target-url] [--env <name>\|--intl\|--overseas\|--global\|--yidaapps\|--alibaba] [--client-id <clientId>] [--endpoint <url>] [--no-browser]` | Login with OAuth token mode |
394
387
  | `openyida logout` | Logout / unbind current project auth |
395
388
  | `openyida auth <status\|login\|refresh\|logout\|profiles\|profile switch>` | Token login state and profile management |
396
389
  | `openyida org <list\|switch> [--json] [--corp-id <corpId>]` | Organization management (list / switch existing profiles first) |
@@ -401,7 +394,7 @@ Run `openyida --help` or `openyida <command> --help` for detailed usage.
401
394
  | Command | Description |
402
395
  |---------|-------------|
403
396
  | `openyida app-entry get <appType> [--json]` | Read frontend and management runtime entries |
404
- | `openyida app-entry set <appType> [--frontend <url>] [--management <url>] [--clear-frontend] [--clear-management] [--json]` | Register application entries with partial update and readback |
397
+ | `openyida app-entry set <appType> [--frontend <url>\|--frontend-page <formUuid>] [--management <url>\|--management-page <formUuid>] [--clear-frontend] [--clear-management] [--json]` | Register application entries with partial update and readback |
405
398
  | `openyida app-list [--type managed\|created] [--page N] [--size N]` | Page through apps I manage or created |
406
399
  | `openyida corp-efficiency [overview\|details\|detail\|groups\|notify] [options] [--open\|--no-open]` | Query enterprise efficiency overview and detail reports |
407
400
  | `openyida create-app "<name>"\|--name <name> [options] [--locale zh_CN\|en_US\|ja_JP] [--open\|--no-open]` | Create a Yida app |
@@ -444,8 +437,8 @@ Run `openyida --help` or `openyida <command> --help` for detailed usage.
444
437
  | `openyida create-page <appType> "<name>" [--mode dashboard] [--hide-nav] [--locale zh_CN\|en_US\|ja_JP] [--open\|--no-open]` | Create a custom display page |
445
438
  | `openyida build-page <sourceFile> [--output file\|--write]` | Build Yida-compatible page source |
446
439
  | `openyida check-page <src> [--compat] [--json]` | Check custom page standards |
447
- | `openyida compile <src> [--canvas] [--compat] [--skip-lint] [--json]` | Compile custom page locally |
448
- | `openyida publish <src> <appType> <formUuid> [--health-check] [--force] [--canvas] [--compat] [--skip-lint] [--auto-nav-order] [--open\|--no-open] [--json]` | Compile and publish custom page |
440
+ | `openyida compile <src> [--canvas] [--compat] [--skip-lint] [--strict-theme\|--allow-fixed-brand] [--json]` | Compile custom page locally |
441
+ | `openyida publish <src> <appType> <formUuid> [--health-check] [--force] [--canvas] [--compat] [--skip-lint] [--fix-theme] [--strict-theme] [--allow-fixed-brand] [--auto-nav-order] [--open\|--no-open] [--json]` | Compile and publish custom page |
449
442
  | `openyida update-form-config <appType> <formUuid> <true\|false\|keep> "<title>" [--locale zh_CN\|en_US\|ja_JP]` | Update form configuration |
450
443
  | `openyida get-form-config <appType> <formUuid> [--json]` | Query form configuration |
451
444
 
package/bin/yida.js CHANGED
@@ -279,7 +279,6 @@ function getFirstPositionalArg(cliArgs, startIndex = 0) {
279
279
  '--login-url',
280
280
  '--profile',
281
281
  '--user-id',
282
- '--env-hint',
283
282
  ]);
284
283
  for (let index = startIndex; index < cliArgs.length; index++) {
285
284
  const arg = cliArgs[index];
@@ -387,7 +386,6 @@ function applyLoginEnvironmentFlags(cliArgs, options = {}) {
387
386
  '--client-id',
388
387
  '--profile',
389
388
  '--user-id',
390
- '--env-hint',
391
389
  ]);
392
390
  const targetUrlFlags = new Set([
393
391
  '--endpoint',
@@ -539,23 +537,13 @@ function printLoginHelp() {
539
537
  }
540
538
 
541
539
  function buildTokenLoginOptions(loginArgs) {
542
- const device = loginArgs.includes('--device');
543
540
  return {
544
541
  clientId: getArgValue(loginArgs, '--client-id'),
545
542
  corpId: getArgValue(loginArgs, '--corp-id'),
546
543
  userId: getArgValue(loginArgs, '--user-id'),
547
544
  authProfile: getArgValue(loginArgs, '--profile'),
548
545
  quiet: process.env.YIDA_QUIET === '1' || loginArgs.includes('--quiet'),
549
- noBrowser: device || loginArgs.includes('--no-browser'),
550
- device,
551
- envHint: getArgValue(loginArgs, '--env-hint'),
552
- // Machine-readable progress events for agent runtimes (sandbox browsers,
553
- // CI dashboards). Emits one JSON line per state transition.
554
- onDeviceState: (state) => {
555
- if (device) {
556
- process.stdout.write(JSON.stringify({ type: 'device_login_state', ...state }) + '\n');
557
- }
558
- },
546
+ noBrowser: loginArgs.includes('--no-browser'),
559
547
  };
560
548
  }
561
549
 
@@ -1304,5 +1292,7 @@ main()
1304
1292
  } else {
1305
1293
  warn(t('cli.exec_failed', err.message));
1306
1294
  }
1307
- process.exit(err && err.exitCode ? err.exitCode : 1);
1295
+ // Let pending stdout/stderr writes drain, especially large JSON diagnostics
1296
+ // sent through pipes on macOS/Linux. process.exit() can truncate them.
1297
+ process.exitCode = err && err.exitCode ? err.exitCode : 1;
1308
1298
  });
@@ -3,6 +3,7 @@
3
3
  const { createAuthRef, createYidaClient, unwrapYidaResponse } = require('../core/yida-client');
4
4
  const { t } = require('../core/i18n');
5
5
  const { throwUsage } = require('../core/command-errors');
6
+ const { fetchFormPageList } = require('./form-navigation');
6
7
 
7
8
  /** Keep complete runtime URLs; the server verifies tenant and resource ownership. */
8
9
  function normalizeUrl(value, appType) {
@@ -42,6 +43,22 @@ function normalizeUrl(value, appType) {
42
43
  return input;
43
44
  }
44
45
 
46
+ /** Resolve a page identifier into the canonical entry URL the platform expects. */
47
+ async function resolveEntryUrl(authRef, appType, key, formUuid) {
48
+ if (!/^[A-Za-z0-9_-]+$/.test(String(formUuid || ''))) {
49
+ throw new Error(t('app_entry.invalid_url'));
50
+ }
51
+ const forms = await fetchFormPageList(appType, authRef);
52
+ const form = Array.isArray(forms) ? forms.find((item) => item.formUuid === formUuid) : null;
53
+ if (!form) { throw new Error(t('app_entry.page_not_in_app', formUuid)); }
54
+ if (key === 'frontend') {
55
+ if (String(form.formType).toLowerCase() !== 'display') { throw new Error(t('app_entry.frontend_display_required', formUuid)); }
56
+ if (!form.pathName) { throw new Error(t('app_entry.page_offline', formUuid)); }
57
+ return normalizeUrl(`${authRef.baseUrl}/${appType}/custom/${formUuid}`, appType);
58
+ }
59
+ return normalizeUrl(`${authRef.baseUrl}/${appType}/workbench/${formUuid}`, appType);
60
+ }
61
+
45
62
  /** Reject duplicate or ambiguous flags; omissions never clear stored entries. */
46
63
  function parseArgs(args) {
47
64
  const [action, appType, ...flags] = args;
@@ -54,15 +71,17 @@ function parseArgs(args) {
54
71
  if (flag === '--json') {
55
72
  continue;
56
73
  }
57
- const match = /^--(clear-)?(frontend|management)$/.exec(flag);
58
- if (action !== 'set' || !match || Object.prototype.hasOwnProperty.call(values, match[2])) {
74
+ const match = /^--(clear-)?(frontend|management)(-page)?$/.exec(flag);
75
+ if (action !== 'set' || !match || (match[1] && match[3]) ||
76
+ Object.prototype.hasOwnProperty.call(values, match[2])) {
59
77
  throwUsage(t('app_entry.usage'));
60
78
  }
61
- const value = match[1] ? null : flags[++index];
62
- if (value !== null && (!value || value.startsWith('--'))) {
79
+ const kind = match[1] ? 'clear' : (match[3] ? 'page' : 'url');
80
+ const value = kind === 'clear' ? null : flags[++index];
81
+ if (kind !== 'clear' && (!value || value.startsWith('--'))) {
63
82
  throwUsage(t('app_entry.usage'));
64
83
  }
65
- values[match[2]] = value;
84
+ values[match[2]] = { kind, value };
66
85
  }
67
86
  if (action === 'set' && !Object.keys(values).length) {
68
87
  throwUsage(t('app_entry.usage'));
@@ -90,8 +109,15 @@ async function run(args = []) {
90
109
  const authRef = createAuthRef();
91
110
  const client = createYidaClient({ authRef });
92
111
  const patch = {};
93
- for (const [key, value] of Object.entries(values)) {
94
- patch[key] = value === null ? null : { url: normalizeUrl(value, appType) };
112
+ for (const [key, spec] of Object.entries(values)) {
113
+ if (spec.kind === 'clear') {
114
+ patch[key] = null;
115
+ } else if (spec.kind === 'page') {
116
+ patch[key] = { url: await resolveEntryUrl(authRef, appType, key, spec.value) };
117
+ } else {
118
+ const url = normalizeUrl(spec.value, appType);
119
+ patch[key] = { url };
120
+ }
95
121
  }
96
122
  let result = await readEntries(client, appType);
97
123
  if (action === 'set') {
@@ -115,4 +141,4 @@ async function run(args = []) {
115
141
  return result;
116
142
  }
117
143
 
118
- module.exports = { run, parseArgs, normalizeUrl };
144
+ module.exports = { run, parseArgs, normalizeUrl, resolveEntryUrl };
@@ -13,8 +13,6 @@ const CREATIVE_TOKENS = [
13
13
  '--pod-page-footer-bg-color', '--pod-sticky-footer-box-shadow',
14
14
  '--oyd-content-width', '--oyd-content-padding', '--oyd-field-gap',
15
15
  '--oyd-heading-font', '--oyd-heading-size', '--oyd-rule-style',
16
- '--pod-shell-theme-bg-color', '--pod-nav-item-text-color', '--pod-nav-item-text-hover-color',
17
- '--pod-nav-item-text-selected-color', '--pod-nav-menu-bg-hover-color', '--pod-nav-menu-bg-selected-color',
18
16
  ];
19
17
 
20
18
  function validateCreativeDirection(direction, tokens) {
@@ -976,7 +976,12 @@ function compileCanvasLocal(source, options = {}) {
976
976
  assertCanvasNavigationStructure(source, options);
977
977
  assertCanvasNavigationPaths(source, options);
978
978
  assertInlineCssStructure(source, options);
979
- assertCanvasThemeStructure(source, options);
979
+ const warnings = [];
980
+ assertCanvasThemeStructure(source, { ...options, onThemeWarning(message, details) {
981
+ warnings.push({ ...details, message });
982
+ if (options.onThemeWarning) { options.onThemeWarning(message, details); }
983
+ else { require('../core/chalk').warn(message); }
984
+ } });
980
985
 
981
986
  const importedModules = extractImportedModules(source);
982
987
 
@@ -1036,6 +1041,7 @@ function compileCanvasLocal(source, options = {}) {
1036
1041
  return {
1037
1042
  runtimeCode,
1038
1043
  importedModules: JSON.stringify(importedModules),
1044
+ warnings,
1039
1045
  };
1040
1046
  }
1041
1047
 
@@ -6,8 +6,7 @@ const { warn } = require('../core/chalk');
6
6
  const { t } = require('../core/i18n');
7
7
 
8
8
  // Whether provably fixed brand overrides should be downgraded from a hard
9
- // failure to a warning. Mirrors OPENYIDA_CANVAS_ALLOW_UNSUPPORTED_IMPORTS so
10
- // legacy pages can ship first and migrate to canvas-theme later.
9
+ // failure to a warning while preserving the source override.
11
10
  function isTruthy(value) {
12
11
  return ['1', 'true', 'yes', 'on'].includes(String(value || '').trim().toLowerCase());
13
12
  }
@@ -17,14 +16,14 @@ function allowFixedBrand(options = {}, env = process.env) {
17
16
  }
18
17
 
19
18
  // Whether fixed brand overrides should hard-block publishing. Off by default so
20
- // users are never stuck: the guard warns and lets the app theme apply. Teams
19
+ // the guard warns and retains fixed overrides. Teams
21
20
  // that want CI enforcement opt in via --strict-theme / OPENYIDA_CANVAS_STRICT_THEME.
22
21
  function strictFixedBrand(options = {}, env = process.env) {
23
22
  return options.strictBrand === true || isTruthy(env.OPENYIDA_CANVAS_STRICT_THEME);
24
23
  }
25
24
 
26
- function emitThemeWarning(options, message) {
27
- if (typeof options.onThemeWarning === 'function') { options.onThemeWarning(message); return; }
25
+ function emitThemeWarning(options, message, details) {
26
+ if (typeof options.onThemeWarning === 'function') { options.onThemeWarning(message, details); return; }
28
27
  warn(message);
29
28
  }
30
29
 
@@ -48,6 +47,10 @@ function assertCanvasThemeStructure(source, options = {}) {
48
47
  const traverse = Babel.packages.traverse.default || Babel.packages.traverse;
49
48
  assertBrandOverrides(ast, traverse, options);
50
49
  const functions = new Map();
50
+ const migrationSites = [];
51
+ const migrationEdges = [];
52
+ const migrationUses = options.requireThemeProvider
53
+ ? new Set(collectBrandOverrides(ast, traverse).map(item => item.usageStart)) : null;
51
54
  let program;
52
55
  let defaultExport;
53
56
  const fail = (kind, node) => {
@@ -89,6 +92,17 @@ function assertCanvasThemeStructure(source, options = {}) {
89
92
  return null;
90
93
  }
91
94
 
95
+ function insideProvider(p) {
96
+ let parent = p.isJSXOpeningElement() ? p.parentPath.parentPath : p.parentPath;
97
+ for (; parent && !parent.isFunction(); parent = parent.parentPath) {
98
+ if (parent.isJSXElement()) {
99
+ const component = resolveFunction(parent.get('openingElement.name'));
100
+ if (functions.get(component)?.name === 'CanvasThemeProvider') { return true; }
101
+ }
102
+ }
103
+ return false;
104
+ }
105
+
92
106
  traverse(ast, {
93
107
  CallExpression(p) {
94
108
  const owner = functions.get(p.getFunctionParent()?.node);
@@ -100,6 +114,9 @@ function assertCanvasThemeStructure(source, options = {}) {
100
114
  }
101
115
  if (!owner) { return; }
102
116
  if (target) { owner.calls.push(target); }
117
+ if (migrationUses && target) {
118
+ migrationEdges.push({ from: p.getFunctionParent().node, to: target, covered: insideProvider(p) });
119
+ }
103
120
  const callee = p.node.callee;
104
121
  if (callee.type === 'MemberExpression' && callee.object.name === 'React' && callee.property.name === 'createElement') {
105
122
  const component = resolveFunction(p.get('arguments.0'));
@@ -110,11 +127,19 @@ function assertCanvasThemeStructure(source, options = {}) {
110
127
  const owner = functions.get(p.getFunctionParent()?.node);
111
128
  const component = resolveFunction(p.get('name'));
112
129
  if (owner && component) { owner.renders.push(component); }
130
+ if (migrationUses) {
131
+ const from = p.getFunctionParent()?.node;
132
+ if (component && from) { migrationEdges.push({ from, to: component, covered: insideProvider(p) }); }
133
+ if (migrationUses.has(p.node.start)) { migrationSites.push({ owner: from, covered: insideProvider(p), node: p.node }); }
134
+ }
113
135
  },
114
136
  });
115
137
 
116
138
  const entry = resolveFunction(program.scope.getBinding('YidaComp')?.path) || resolveFunction(defaultExport);
117
- if (!entry) { return; }
139
+ if (!entry) {
140
+ if (options.requireThemeProvider) { fail('provider_missing', defaultExport?.node); }
141
+ return;
142
+ }
118
143
 
119
144
  // Calling a hook before returning a Provider cannot read that Provider.
120
145
  const directSeen = new Set();
@@ -143,9 +168,27 @@ function assertCanvasThemeStructure(source, options = {}) {
143
168
  }
144
169
  visit(entry);
145
170
  const hasProvider = [...functions.values()].some(info => info.name === 'CanvasThemeProvider');
146
- if (!providerMounted && (hasProvider || firstHook)) {
171
+ if (!providerMounted && (options.requireThemeProvider || hasProvider || firstHook)) {
147
172
  fail('provider_missing', firstHook || entry);
148
173
  }
174
+ if (migrationUses) {
175
+ // A sibling provider does not supply context to the migrated controls.
176
+ // Unknown/dynamic render paths are left for manual migration.
177
+ const checked = new Map();
178
+ const checkScope = (node, covered) => {
179
+ const states = checked.get(node) || new Set();
180
+ if (states.has(covered)) { return; }
181
+ states.add(covered); checked.set(node, states);
182
+ for (const site of migrationSites.filter(site => site.owner === node)) {
183
+ if (!covered && !site.covered) { fail('provider_missing', site.node); }
184
+ }
185
+ migrationEdges.filter(edge => edge.from === node).forEach(edge => checkScope(edge.to, covered || edge.covered));
186
+ };
187
+ checkScope(entry, false);
188
+ for (const site of migrationSites) {
189
+ if (!checked.has(site.owner)) { fail('provider_missing', site.node); }
190
+ }
191
+ }
149
192
  }
150
193
 
151
194
  // Collect every provably fixed brand color passed to antd's ConfigProvider.
@@ -154,6 +197,7 @@ function assertCanvasThemeStructure(source, options = {}) {
154
197
  // can surgically remove it while the guard reuses the same detection to fail.
155
198
  function collectBrandOverrides(ast, traverse) {
156
199
  const violations = [];
200
+ let usageStart;
157
201
  const resolve = (p, seen = new Set()) => {
158
202
  if (!p?.node || seen.has(p.node)) { return null; }
159
203
  seen.add(p.node);
@@ -178,7 +222,7 @@ function collectBrandOverrides(ast, traverse) {
178
222
  // Remove the override at its ConfigProvider usage site, not the resolved literal.
179
223
  const property = p?.parentPath?.isObjectProperty() ? p.parentPath.node : null;
180
224
  violations.push({
181
- field, color, line,
225
+ field, color, line, usageStart,
182
226
  start: property ? property.start : val.node.start,
183
227
  end: property ? property.end : val.node.end,
184
228
  });
@@ -197,6 +241,7 @@ function collectBrandOverrides(ast, traverse) {
197
241
  if (!(imported.isImportSpecifier() && imported.node.imported.name === 'ConfigProvider')
198
242
  && !(imported.isImportNamespaceSpecifier() && name.node.property?.name === 'ConfigProvider')) { return; }
199
243
  const attribute = p.get('attributes').find(attr => attr.isJSXAttribute() && attr.node.name.name === 'theme');
244
+ usageStart = p.node.start;
200
245
  const theme = attribute?.get('value.expression');
201
246
  tokens(prop(theme, 'token'));
202
247
  const components = prop(theme, 'components');
@@ -213,19 +258,21 @@ function collectBrandOverrides(ast, traverse) {
213
258
  }
214
259
 
215
260
  // Fixed brand colors passed to antd's ConfigProvider are relaxed by default:
216
- // warn and continue so users are never blocked (the app theme still applies at
217
- // runtime). --strict-theme / OPENYIDA_CANVAS_STRICT_THEME restores the hard fail
218
- // for CI enforcement; --allow-fixed-brand keeps its explicit ship-as-is warning.
261
+ // Warn and preserve the literal override; this does not install an app theme.
262
+ // Strict mode blocks these overrides; allow-fixed-brand explicitly preserves them.
219
263
  function assertBrandOverrides(ast, traverse, options) {
220
264
  const explicitlyAllowed = allowFixedBrand(options);
221
265
  const strict = strictFixedBrand(options);
222
266
  for (const { field, color, line } of collectBrandOverrides(ast, traverse)) {
267
+ const details = { code: 'OPENYIDA_CANVAS_THEME_FIXED_BRAND', sourcePath: options.sourcePath || '',
268
+ line, field, value: color, themeConsistency: 'fixed_override',
269
+ nextAction: { type: 'review_theme_override', sample: 'openyida sample openyida-page-template canvas-theme' } };
223
270
  if (explicitlyAllowed) {
224
- emitThemeWarning(options, t('publish.canvas_theme_fixed_brand_allowed', line, field));
271
+ emitThemeWarning(options, t('publish.canvas_theme_fixed_brand_allowed', line, field), details);
225
272
  continue;
226
273
  }
227
274
  if (!strict) {
228
- emitThemeWarning(options, t('publish.canvas_theme_fixed_brand_relaxed', line, field));
275
+ emitThemeWarning(options, t('publish.canvas_theme_fixed_brand_relaxed', line, field), details);
229
276
  continue;
230
277
  }
231
278
  throw new CliError(t('publish.canvas_theme_fixed_brand', line), {
@@ -27,14 +27,18 @@ async function run(args) {
27
27
  const compat = args.includes('--compat') || args.includes('--modern');
28
28
  const canvas = args.includes('--canvas');
29
29
  const json = args.includes('--json');
30
+ const strictBrand = args.includes('--strict-theme');
31
+ const allowFixedBrand = args.includes('--allow-fixed-brand');
30
32
  const filteredArgs = args.filter(arg => ![
31
33
  '--skip-lint',
32
34
  '--compat',
33
35
  '--modern',
34
36
  '--canvas',
35
37
  '--json',
38
+ '--strict-theme',
39
+ '--allow-fixed-brand',
36
40
  ].includes(arg));
37
- if (filteredArgs.length < 1) {
41
+ if (filteredArgs.length !== 1 || filteredArgs[0].startsWith('--') || (strictBrand && allowFixedBrand)) {
38
42
  throwUsage(t('cli.compile_usage'), t('cli.compile_example'), {
39
43
  code: 'COMPILE_INVALID_ARGUMENTS',
40
44
  });
@@ -51,7 +55,9 @@ async function run(args) {
51
55
  const workspaceRoot = process.cwd();
52
56
  const relativeSourcePath = path.relative(workspaceRoot, sourcePath);
53
57
  const loadedSource = loadPageSource(relativeSourcePath, { workspaceRoot });
54
- const compiled = compileCanvasPageSource(loadedSource);
58
+ let compiled;
59
+ try { compiled = compileCanvasPageSource(loadedSource, { strictBrand, allowFixedBrand }); }
60
+ catch (error) { throw require('../core/source-repair').withSourceRepair(error, [sourcePath], args); }
55
61
  const payload = {
56
62
  ok: true,
57
63
  pageType: 'canvas',
@@ -61,6 +67,7 @@ async function run(args) {
61
67
  sourceHash: compiled.sourceHash,
62
68
  compiledHash: compiled.compiledHash,
63
69
  importedModules: JSON.parse(compiled.importedModules),
70
+ warnings: compiled.warnings,
64
71
  };
65
72
  if (json) {
66
73
  console.log(JSON.stringify(payload));
@@ -70,6 +77,9 @@ async function run(args) {
70
77
  return payload;
71
78
  }
72
79
 
80
+ if (strictBrand || allowFixedBrand) {
81
+ throwUsage(t('publish.theme_canvas_only'), undefined, { code: 'COMPILE_INVALID_ARGUMENTS' });
82
+ }
73
83
  const initialSourceCode = fs.readFileSync(sourcePath, 'utf-8');
74
84
  warnLargePageSource(initialSourceCode, sourcePath);
75
85
  if (shouldBuildPageSource(initialSourceCode, sourcePath, { modern: compat })) {
@@ -6,17 +6,9 @@ const { getAccessToken } = require('../auth/token-auth');
6
6
  const { CliError } = require('../core/cli-error');
7
7
  const { t } = require('../core/i18n');
8
8
  const { findCssStructureIssue } = require('../core/css-structure');
9
+ const { REQUIRED_BRAND_SCALE_TOKENS, REQUIRED_APPLICATION_BRAND_TOKENS, rootDeclarations, invalidBrandValues } = require('../core/theme-brand-scale');
9
10
 
10
11
  const MAX_THEME_FILE_BYTES = 20 * 1024 * 1024;
11
- const REQUIRED_BRAND_SCALE_TOKENS = Object.freeze([
12
- '--color-brand1-1',
13
- '--color-brand1-2',
14
- '--color-brand1-3',
15
- '--color-brand1-5',
16
- '--color-brand1-6',
17
- '--color-brand1-9',
18
- '--color-brand1-10',
19
- ]);
20
12
  const UNSAFE_THEME_PATTERNS = [
21
13
  { pattern: /<\/?script\b/i, message: '主题 CSS 不能包含 script 标签' },
22
14
  { pattern: /<\/?style\b/i, message: '主题 CSS 不能包含 style 标签' },
@@ -137,12 +129,12 @@ function normalizeThemeColor(value) {
137
129
 
138
130
  function extractThemeColor(cssText) {
139
131
  const source = String(cssText || '').replace(/\/\*[\s\S]*?\*\//g, '');
140
- const match = source.match(/(?:^|[;{])\s*--color-brand1-6\s*:\s*([^;}\n]+)/i);
141
- if (!match) {
132
+ const value = rootDeclarations(source)['--color-brand1-6'];
133
+ if (!value) {
142
134
  throw new Error('自定义主题文件必须声明 --color-brand1-6,CLI 会用它保存应用 themeColor');
143
135
  }
144
136
  try {
145
- return normalizeThemeColor(match[1]);
137
+ return normalizeThemeColor(value);
146
138
  } catch (error) {
147
139
  throw new Error(`--color-brand1-6 必须是可直接保存的颜色值: ${error.message}`);
148
140
  }
@@ -159,6 +151,17 @@ function validateThemeCssContent(cssText) {
159
151
  });
160
152
  }
161
153
  const sourceWithoutComments = source.replace(/\/\*[\s\S]*?\*\//g, '');
154
+ // Custom properties accept arbitrary text. An authoring formula can therefore
155
+ // override a valid platform color yet invalidate every property consuming it;
156
+ // var() fallbacks do not rescue that case. Reject it before any upload.
157
+ for (const declaration of sourceWithoutComments.matchAll(/(?:^|[;{])\s*(--[\w-]+)\s*:\s*([^;]+)/g)) {
158
+ if (/\{\{[^}]+\}\}|<生成实际色值[::]/.test(declaration[2])) {
159
+ throw new CliError(t('design_document.theme_css_unresolved', declaration[1]), {
160
+ code: 'THEME_CSS_UNRESOLVED_TOKEN',
161
+ details: { token: declaration[1], value: declaration[2].trim() },
162
+ });
163
+ }
164
+ }
162
165
  for (const rule of UNSAFE_THEME_PATTERNS) {
163
166
  if (rule.pattern.test(sourceWithoutComments)) {
164
167
  throw new Error(rule.message);
@@ -179,21 +182,20 @@ function validateThemeCssContent(cssText) {
179
182
  match = urlPattern.exec(sourceWithoutComments);
180
183
  }
181
184
 
182
- const declaredTokens = new Set();
183
- const declarationPattern = /(?:^|[;{])\s*(--color-brand1-(?:10|[1-9]))\s*:\s*([^;}\n]+)/gi;
184
- let declaration = declarationPattern.exec(sourceWithoutComments);
185
- while (declaration) {
186
- if (declaration[2].trim()) {
187
- declaredTokens.add(declaration[1].toLowerCase());
188
- }
189
- declaration = declarationPattern.exec(sourceWithoutComments);
190
- }
191
- const missingTokens = REQUIRED_BRAND_SCALE_TOKENS.filter((token) => !declaredTokens.has(token));
185
+ const tokens = rootDeclarations(sourceWithoutComments);
186
+ const missingTokens = REQUIRED_APPLICATION_BRAND_TOKENS.filter((token) => !(token in tokens));
192
187
  if (missingTokens.length > 0) {
193
- throw new Error(
194
- `自定义主题文件必须完整声明平台品牌色阶 ${REQUIRED_BRAND_SCALE_TOKENS.join('、')},缺少: ${missingTokens.join(', ')}`
188
+ throw new CliError(
189
+ `自定义主题文件必须在顶层 :root 完整声明平台品牌色阶 ${REQUIRED_APPLICATION_BRAND_TOKENS.join('、')},缺少: ${missingTokens.join(', ')}`,
190
+ { code: 'THEME_BRAND_SCALE_INCOMPLETE', details: { missingTokens, scope: ':root' } }
195
191
  );
196
192
  }
193
+ const issues = invalidBrandValues(tokens, normalizeThemeColor);
194
+ if (issues.length) {
195
+ throw new CliError(t('design_document.invalid', issues[0].token, issues[0].reason), {
196
+ code: 'THEME_BRAND_SCALE_INVALID', details: { issues, scope: ':root' },
197
+ });
198
+ }
197
199
  }
198
200
 
199
201
  function readThemeCssFile(filePath) {
@@ -300,6 +302,7 @@ async function uploadCustomThemeFile(appType, filePath, authRef, options = {}) {
300
302
  module.exports = {
301
303
  MAX_THEME_FILE_BYTES,
302
304
  REQUIRED_BRAND_SCALE_TOKENS,
305
+ REQUIRED_APPLICATION_BRAND_TOKENS,
303
306
  validateThemeCssContent,
304
307
  normalizeThemeColor,
305
308
  normalizeCssColorToHex,
@@ -1,82 +1,34 @@
1
1
  'use strict';
2
2
 
3
- /**
4
- * fix-theme.js - Canvas 主题迁移 codemod(纯函数)
5
- *
6
- * 把写死在 antd `ConfigProvider` 里的品牌交互色覆盖(如 `colorPrimary: '#1677ff'`)
7
- * 从源码中移除,交由外层应用主题(CanvasThemeProvider / 平台主题)解析。这是
8
- * "un-pin" 迁移:移除硬编码覆盖后,品牌色由应用主题接管,正好满足主题守卫
9
- * `OPENYIDA_CANVAS_THEME_FIXED_BRAND` 的要求。
10
- *
11
- * 本模块不做任何 IO,只接收源码字符串、返回改写后的源码,便于单测与复用。
12
- */
13
-
3
+ const Babel = require('@babel/standalone');
14
4
  const { collectFixedBrandViolations } = require('./canvas-theme-guard');
15
5
 
16
- /**
17
- * 计算某个对象属性的删除区间:优先连同其后的逗号一起删;若是最后一个属性,
18
- * 则回退删除其前的逗号。属性独占一行时顺带清理该行缩进与换行,避免留下空行。
19
- * @param {string} source
20
- * @param {number} start - 属性节点起始偏移
21
- * @param {number} end - 属性节点结束偏移
22
- * @returns {{ start: number, end: number }}
23
- */
24
- function expandRemovalSpan(source, start, end) {
25
- let removeStart = start;
26
- let removeEnd = end;
27
- const trailing = source.slice(removeEnd).match(/^[ \t]*,/);
28
- if (trailing) {
29
- removeEnd += trailing[0].length;
30
- const lineStart = source.lastIndexOf('\n', removeStart - 1) + 1;
31
- const indent = source.slice(lineStart, removeStart);
32
- if (/^[ \t]*$/.test(indent)) {
33
- removeStart = lineStart;
34
- const newline = source.slice(removeEnd).match(/^[ \t]*\r?\n/);
35
- if (newline) { removeEnd += newline[0].length; }
36
- }
37
- return { start: removeStart, end: removeEnd };
38
- }
39
- // Last property in its object: drop the preceding comma instead.
40
- const leading = source.slice(0, removeStart).match(/,[ \t\r\n]*$/);
41
- if (leading) { removeStart -= leading[0].length; }
42
- return { start: removeStart, end: removeEnd };
43
- }
44
-
45
- /**
46
- * 移除源码中写死的品牌色覆盖。改写按偏移从后往前应用,避免前面的偏移失效。
47
- * @param {string} source - canvas 源码
48
- * @returns {{ changed: boolean, output: string, violations: Array<{field:string,color:string,line:number}> }}
49
- */
6
+ // Use parser tokens to remove separators without treating comments as syntax.
7
+ // Preserve all other source text, including CSS and the maintained drawer/provider.
50
8
  function transformFixedBrandTheme(source) {
51
9
  const violations = collectFixedBrandViolations(source)
52
10
  .filter(item => Number.isInteger(item.start) && Number.isInteger(item.end) && item.end > item.start);
53
- if (!violations.length) {
54
- return { changed: false, output: source, violations: [] };
55
- }
56
- const spans = violations
57
- .map(item => expandRemovalSpan(source, item.start, item.end))
58
- .sort((a, b) => a.start - b.start);
59
-
60
- // Adjacent overrides can claim the same separating comma (one as trailing,
61
- // the next as leading), so merge overlapping spans instead of dropping one.
11
+ if (!violations.length) { return { changed: false, output: source, violations: [] }; }
12
+ const parse = value => Babel.packages.parser.parse(value, {
13
+ sourceType: 'module', plugins: ['jsx', 'typescript'], tokens: true,
14
+ });
15
+ const tokens = parse(source).tokens.filter(token => typeof token.type !== 'string');
16
+ const spans = violations.flatMap(({ start, end }) => {
17
+ const next = tokens.find(token => token.start >= end);
18
+ const previous = tokens.filter(token => token.end <= start).pop();
19
+ const comma = next?.type.label === ',' ? next : previous?.type.label === ',' ? previous : null;
20
+ return [{ start, end }, ...(comma ? [{ start: comma.start, end: comma.end }] : [])];
21
+ }).sort((a, b) => a.start - b.start);
62
22
  const merged = [];
63
23
  for (const span of spans) {
64
24
  const last = merged[merged.length - 1];
65
25
  if (last && span.start <= last.end) { last.end = Math.max(last.end, span.end); }
66
26
  else { merged.push({ ...span }); }
67
27
  }
68
-
69
28
  let output = source;
70
- // Apply from the back so earlier offsets stay valid.
71
- for (let i = merged.length - 1; i >= 0; i -= 1) {
72
- output = output.slice(0, merged[i].start) + output.slice(merged[i].end);
73
- }
74
-
75
- return {
76
- changed: output !== source,
77
- output,
78
- violations: violations.map(({ field, color, line }) => ({ field, color, line })),
79
- };
29
+ for (const span of merged.reverse()) { output = output.slice(0, span.start) + output.slice(span.end); }
30
+ parse(output);
31
+ return { changed: output !== source, output, violations: violations.map(({ field, color, line }) => ({ field, color, line })) };
80
32
  }
81
33
 
82
- module.exports = { transformFixedBrandTheme, expandRemovalSpan };
34
+ module.exports = { transformFixedBrandTheme };