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.
- package/README.md +4 -11
- package/bin/yida.js +4 -14
- package/lib/app/app-entry.js +34 -8
- package/lib/app/application-style.js +0 -2
- package/lib/app/canvas-compile.js +7 -1
- package/lib/app/canvas-theme-guard.js +60 -13
- package/lib/app/compile.js +12 -2
- package/lib/app/custom-theme.js +27 -24
- package/lib/app/fix-theme.js +18 -66
- package/lib/app/publish.js +33 -55
- package/lib/app/services/canvas-page-compiler.js +3 -1
- package/lib/app/theme-from-design.js +63 -99
- package/lib/app/theme-scope.js +76 -0
- package/lib/auth/token-auth.js +18 -36
- package/lib/core/cli-error.js +1 -0
- package/lib/core/command-manifest.js +38 -14
- package/lib/core/locales/en.js +18 -12
- package/lib/core/locales/zh.js +18 -12
- package/lib/core/source-repair.js +38 -0
- package/lib/core/theme-brand-scale.js +133 -0
- package/lib/design/check-design.js +16 -6
- package/lib/design/document.js +36 -15
- package/lib/design/icon-contrast.js +1 -1
- package/lib/design/palette-contrast.js +77 -0
- package/lib/design-plan/design-plan.js +22 -7
- package/lib/design-plan/init.js +1 -1
- package/lib/design-plan/materialize.js +2 -2
- package/package.json +3 -2
- package/yida-skills/SKILL.md +2 -0
- package/yida-skills/references/source-repair.md +10 -0
- package/yida-skills/skills/yida-app/workflow/plan/step-4-deliver.md +2 -0
- package/yida-skills/skills/yida-app/workflow/plan/workflow.md +13 -4
- package/yida-skills/skills/yida-app/workflow/step-2-design.md +1 -1
- package/yida-skills/skills/yida-app/workflow/step-3-create-or-reuse-app.md +1 -1
- package/yida-skills/skills/yida-app/workflow/step-9-output-finish.md +3 -1
- package/yida-skills/skills/yida-canvas-custom-page/references/canvas-theme-provider.md +3 -1
- package/yida-skills/skills/yida-canvas-custom-page/references/waf-safe-authoring.md +3 -3
- package/yida-skills/skills/yida-canvas-custom-page/scripts/build-canvas-theme.js +2 -1
- package/yida-skills/skills/yida-canvas-custom-page/scripts/canvas-theme-provider.template.jsx +7 -0
- package/yida-skills/skills/yida-design/SKILL.md +13 -7
- package/yida-skills/skills/yida-design/references/application-style-library.md +2 -2
- package/yida-skills/skills/yida-design/references/application-theme-consistency.md +55 -11
- package/yida-skills/skills/yida-design/references/navigation-decision.md +3 -3
- package/yida-skills/skills/yida-design/references/theme/app-custom-theme-template.css +69 -155
- package/yida-skills/skills/yida-design/scripts/validate_design_themes.py +3 -1
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/SKILL.md +2 -0
- package/yida-skills/skills/yida-design/templates/design-themes/README.md +3 -1
- package/yida-skills/skills/yida-design/templates/design-themes/app-amber/app_theme.css +85 -263
- package/yida-skills/skills/yida-design/templates/design-themes/app-amber/design.md +14 -50
- package/yida-skills/skills/yida-design/templates/design-themes/app-editorial/app_theme.css +85 -263
- package/yida-skills/skills/yida-design/templates/design-themes/app-editorial/design.md +16 -51
- package/yida-skills/skills/yida-design/templates/design-themes/app-executive/app_theme.css +87 -265
- package/yida-skills/skills/yida-design/templates/design-themes/app-executive/design.md +16 -51
- package/yida-skills/skills/yida-design/templates/design-themes/app-finance/app_theme.css +85 -263
- package/yida-skills/skills/yida-design/templates/design-themes/app-finance/design.md +14 -50
- package/yida-skills/skills/yida-design/templates/design-themes/app-glass/app_theme.css +88 -266
- package/yida-skills/skills/yida-design/templates/design-themes/app-glass/design.md +18 -53
- package/yida-skills/skills/yida-design/templates/design-themes/app-graphite/app_theme.css +85 -263
- package/yida-skills/skills/yida-design/templates/design-themes/app-graphite/design.md +14 -49
- package/yida-skills/skills/yida-design/templates/design-themes/app-line/app_theme.css +85 -263
- package/yida-skills/skills/yida-design/templates/design-themes/app-line/design.md +17 -52
- package/yida-skills/skills/yida-design/templates/design-themes/app-neon/app_theme.css +89 -267
- package/yida-skills/skills/yida-design/templates/design-themes/app-neon/design.md +19 -56
- package/yida-skills/skills/yida-design/templates/design-themes/app-nordic/app_theme.css +94 -278
- package/yida-skills/skills/yida-design/templates/design-themes/app-nordic/design.md +17 -57
- package/yida-skills/skills/yida-design/templates/design-themes/app-paper/app_theme.css +90 -268
- package/yida-skills/skills/yida-design/templates/design-themes/app-paper/design.md +18 -55
- package/yida-skills/skills/yida-design/templates/design-themes/app-platinum/app_theme.css +84 -262
- package/yida-skills/skills/yida-design/templates/design-themes/app-platinum/design.md +14 -49
- package/yida-skills/skills/yida-design/templates/design-themes/app-plum/app_theme.css +88 -266
- package/yida-skills/skills/yida-design/templates/design-themes/app-plum/design.md +18 -53
- package/yida-skills/skills/yida-design/templates/design-themes/app-pop/app_theme.css +99 -287
- package/yida-skills/skills/yida-design/templates/design-themes/app-pop/design.md +25 -67
- package/yida-skills/skills/yida-design/templates/design-themes/app-sage/app_theme.css +86 -264
- package/yida-skills/skills/yida-design/templates/design-themes/app-sage/design.md +18 -53
- package/yida-skills/skills/yida-design/templates/design-themes/app-teal/app_theme.css +85 -263
- package/yida-skills/skills/yida-design/templates/design-themes/app-teal/design.md +14 -49
- package/yida-skills/skills/yida-design/templates/design-themes/app-terminal/app_theme.css +90 -274
- package/yida-skills/skills/yida-design/templates/design-themes/app-terminal/design.md +14 -55
- package/yida-skills/skills/yida-design/templates/design-themes/app-ticket/app_theme.css +97 -279
- package/yida-skills/skills/yida-design/templates/design-themes/app-ticket/design.md +23 -60
- package/yida-skills/skills/yida-design/templates/design-themes/app-wire/app_theme.css +85 -263
- package/yida-skills/skills/yida-design/templates/design-themes/app-wire/design.md +14 -50
- package/yida-skills/skills/yida-design/templates/design-themes/basic-tokens.json +1 -8
- package/yida-skills/skills/yida-design/templates/design-themes/dark-inset-hairline/app_theme.css +101 -282
- package/yida-skills/skills/yida-design/templates/design-themes/dark-inset-hairline/design.md +36 -46
- package/yida-skills/skills/yida-design/templates/design-themes/dark-rail-fine-lines/app_theme.css +102 -276
- package/yida-skills/skills/yida-design/templates/design-themes/dark-rail-fine-lines/design.md +37 -46
- package/yida-skills/skills/yida-design/templates/design-themes/dark-rail-signal-panels/app_theme.css +101 -282
- package/yida-skills/skills/yida-design/templates/design-themes/dark-rail-signal-panels/design.md +38 -45
- package/yida-skills/skills/yida-design/templates/design-themes/free-creative/app_theme.css +79 -257
- package/yida-skills/skills/yida-design/templates/design-themes/free-creative/design.md +7 -43
- package/yida-skills/skills/yida-design/templates/design-themes/graphite-bevel-grid/app_theme.css +101 -282
- package/yida-skills/skills/yida-design/templates/design-themes/graphite-bevel-grid/design.md +35 -44
- package/yida-skills/skills/yida-design/templates/design-themes/hairline-shared-bands/app_theme.css +100 -281
- package/yida-skills/skills/yida-design/templates/design-themes/hairline-shared-bands/design.md +36 -45
- package/yida-skills/skills/yida-design/templates/design-themes/hairline-soft-blocks/app_theme.css +104 -285
- package/yida-skills/skills/yida-design/templates/design-themes/hairline-soft-blocks/design.md +38 -48
- package/yida-skills/skills/yida-design/templates/design-themes/index.json +18 -18
- package/yida-skills/skills/yida-design/templates/design-themes/ink-glow-soft-panels/app_theme.css +104 -285
- package/yida-skills/skills/yida-design/templates/design-themes/ink-glow-soft-panels/design.md +39 -49
- package/yida-skills/skills/yida-design/templates/design-themes/inset-frame-pixel-rhythm/app_theme.css +104 -285
- package/yida-skills/skills/yida-design/templates/design-themes/inset-frame-pixel-rhythm/design.md +37 -46
- package/yida-skills/skills/yida-design/templates/design-themes/outlined-texture-duotone/app_theme.css +103 -284
- package/yida-skills/skills/yida-design/templates/design-themes/outlined-texture-duotone/design.md +36 -46
- package/yida-skills/skills/yida-design/templates/design-themes/soft-inset-surfaces/app_theme.css +104 -285
- package/yida-skills/skills/yida-design/templates/design-themes/soft-inset-surfaces/design.md +38 -48
- package/yida-skills/skills/yida-design/templates/design-themes/soft-outline-rhythm/app_theme.css +105 -286
- package/yida-skills/skills/yida-design/templates/design-themes/soft-outline-rhythm/design.md +38 -48
- package/yida-skills/skills/yida-design/templates/design-themes/soft-rail-bold-band/app_theme.css +105 -286
- package/yida-skills/skills/yida-design/templates/design-themes/soft-rail-bold-band/design.md +40 -50
- package/yida-skills/skills/yida-design/templates/design-themes/soft-spectral-panels/app_theme.css +104 -285
- package/yida-skills/skills/yida-design/templates/design-themes/soft-spectral-panels/design.md +41 -49
- package/yida-skills/skills/yida-design/templates/design-themes/warm-canvas-contrast-panels/app_theme.css +104 -285
- package/yida-skills/skills/yida-design/templates/design-themes/warm-canvas-contrast-panels/design.md +39 -49
- package/yida-skills/skills/yida-design/templates/design-themes/warm-rail-muted-panels/app_theme.css +104 -285
- package/yida-skills/skills/yida-design/templates/design-themes/warm-rail-muted-panels/design.md +37 -47
- package/yida-skills/skills/yida-design/workflow/output-design.md +37 -4
- package/yida-skills/skills/yida-design/workflow/step-2-theme-system.md +7 -2
- package/yida-skills/skills/yida-design/workflow/step-6-handoff.md +2 -0
- package/yida-skills/skills/yida-export-conversation/SKILL.md +0 -1
- package/yida-skills/skills/yida-nav-shell/SKILL.md +5 -3
- package/yida-skills/skills/yida-nav-shell/references/nav-shell-patterns.md +3 -3
- package/yida-skills/skills/yida-prd/SKILL.md +1 -1
- package/yida-skills/skills/yida-publish-page/SKILL.md +21 -18
- package/yida-skills/skills/yida-requirement-analysis/references/handoff.md +1 -1
- package/yida-skills/skills-index.json +2 -2
- package/lib/auth/oauth-device.js +0 -278
- package/yida-skills/skills/yida-design/templates/application-styles.json +0 -398
- 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]
|
|
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:
|
|
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
|
-
|
|
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
|
});
|
package/lib/app/app-entry.js
CHANGED
|
@@ -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)
|
|
58
|
-
if (action !== 'set' || !match ||
|
|
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
|
|
62
|
-
|
|
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,
|
|
94
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
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) {
|
|
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
|
-
//
|
|
217
|
-
//
|
|
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), {
|
package/lib/app/compile.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 })) {
|
package/lib/app/custom-theme.js
CHANGED
|
@@ -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
|
|
141
|
-
if (!
|
|
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(
|
|
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
|
|
183
|
-
const
|
|
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
|
|
194
|
-
|
|
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,
|
package/lib/app/fix-theme.js
CHANGED
|
@@ -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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
34
|
+
module.exports = { transformFixedBrandTheme };
|