openyida 2026.5.25-beta.0 → 2026.5.26
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 +5 -3
- package/bin/yida.js +15 -3
- package/lib/app/check-page.js +2 -2
- package/lib/app/compile.js +3 -2
- package/lib/app/import-app.js +39 -11
- package/lib/app/page-compat.js +258 -2
- package/lib/app/page-compiler.js +4 -1
- package/lib/app/page-linter.js +281 -0
- package/lib/app/publish.js +3 -2
- package/lib/app/update-app.js +41 -3
- package/lib/auth/cdp-browser-login.js +7 -3
- package/lib/auth/login.js +2 -3
- package/lib/core/command-manifest.js +2 -1
- package/lib/core/env-manager.js +24 -16
- package/lib/core/locales/ar.js +11 -0
- package/lib/core/locales/de.js +11 -0
- package/lib/core/locales/en.js +13 -4
- package/lib/core/locales/es.js +11 -0
- package/lib/core/locales/fr.js +11 -0
- package/lib/core/locales/hi.js +11 -0
- package/lib/core/locales/ja.js +11 -0
- package/lib/core/locales/ko.js +11 -0
- package/lib/core/locales/pt.js +11 -0
- package/lib/core/locales/vi.js +11 -0
- package/lib/core/locales/zh-HK.js +11 -0
- package/lib/core/locales/zh.js +13 -4
- package/lib/core/query-data.js +184 -5
- package/lib/core/utils.js +34 -8
- package/lib/process/configure-process.js +552 -20
- package/package.json +1 -1
- package/project/pages/src/demo-agent-chatbox.oyd.jsx +78 -3
- package/scripts/e2e-real/full-runner.js +257 -8
- package/scripts/e2e-real/skill-coverage.js +3 -2
- package/yida-skills/SKILL.md +2 -1
- package/yida-skills/skills/yida-chart/SKILL.md +1 -1
- package/yida-skills/skills/yida-connector-safe-actions/SKILL.md +282 -0
- package/yida-skills/skills/yida-create-process/SKILL.md +3 -2
- package/yida-skills/skills/yida-custom-page/SKILL.md +7 -2
- package/yida-skills/skills/yida-custom-page/examples/attachment-upload.js +14 -12
- package/yida-skills/skills/yida-custom-page/references/attachment-upload-guide.md +3 -1
- package/yida-skills/skills/yida-custom-page/references/coding-guide.md +4 -0
- package/yida-skills/skills/yida-custom-page/references/component-jsx-guide.md +31 -22
- package/yida-skills/skills/yida-dashboard/SKILL.md +10 -9
- package/yida-skills/skills/yida-dashboard/references/interaction-patterns.md +2 -0
- package/yida-skills/skills/yida-dashboard/references/pitfalls.md +13 -4
- package/yida-skills/skills/yida-dashboard/references/structure-and-layout.md +1 -1
- package/yida-skills/skills/yida-ppt-slider/SKILL.md +47 -37
- package/yida-skills/skills/yida-ppt-slider/references/examples.md +5 -4
- package/yida-skills/skills/yida-process-rule/SKILL.md +93 -3
- package/yida-skills/skills/yida-process-rule/references/official-component-nodes.md +93 -0
- package/yida-skills/skills/yida-publish-page/SKILL.md +6 -4
package/README.md
CHANGED
|
@@ -228,12 +228,13 @@ Most checks should stay offline, but OpenYida also includes an explicit real-env
|
|
|
228
228
|
```bash
|
|
229
229
|
OPENYIDA_E2E=1 npm run test:e2e:real
|
|
230
230
|
OPENYIDA_E2E=1 npm run test:e2e:real:full
|
|
231
|
+
OPENYIDA_E2E=1 OPENYIDA_E2E_FULL_STAGES=auth,app,form,process npm run test:e2e:real:full
|
|
231
232
|
npm run test:e2e:real:skills
|
|
232
233
|
```
|
|
233
234
|
|
|
234
235
|
The runner creates a disposable app, form, and custom page with an `OY_E2E_*` prefix, then verifies login, app listing, schema fetch, data query, and page publish. It writes a registry to `project/.cache/e2e-real/` so created resources can be audited later. To inject CI cookies without relying on a local login cache, pass `OPENYIDA_E2E_COOKIES_BASE64` as a base64 encoded cookie array or `{ "cookies": [...] }` object.
|
|
235
236
|
|
|
236
|
-
`test:e2e:real:full` extends the smoke path into a broad deterministic feature matrix: auth/env, app update, form update and option mutation, page build/compile/generate/publish, data create/get/update/query, permission read, page config and short URL check, report create/append, dashboard skill verification, export/import, batch, task-center, formula/doctor/sample/CDN config, and local connector parsing/template generation. AI-backed commands such as `flash-to-prd` are available as the optional `ai` stage because they depend on remote model availability.
|
|
237
|
+
`test:e2e:real:full` extends the smoke path into a broad deterministic feature matrix: auth/env, app update, form update and option mutation, page build/compile/generate/publish, data create/get/update/query, permission read, page config and short URL check, report create/append, dashboard skill verification, export/import, batch, task-center, formula/doctor/sample/CDN config, and local connector parsing/template generation. AI-backed commands such as `flash-to-prd` are available as the optional `ai` stage because they depend on remote model availability. Workflow mutation is available as the opt-in `process` stage; it creates and republishes a workflow on the disposable E2E form and records advanced official-node fixtures for review.
|
|
237
238
|
|
|
238
239
|
`test:e2e:real:skills` enforces coverage for every directory under `yida-skills/skills/`. Each skill must be classified as real E2E, offline/unit, opt-in, or deprecated with an explicit reason. This prevents new skills from quietly bypassing the real-environment test plan.
|
|
239
240
|
|
|
@@ -296,7 +297,7 @@ For overseas apps, pass `--locale en_US` or `--locale ja_JP` on creation command
|
|
|
296
297
|
| `openyida app-list [--size N]` | List Yida applications |
|
|
297
298
|
| `openyida corp-efficiency [overview\|details\|detail\|groups\|notify] [options] [--open\|--no-open]` | Query enterprise efficiency metrics, detail report entries, and related notification actions |
|
|
298
299
|
| `openyida create-app "<name>"\|--name <name> [options] [--locale zh_CN\|en_US\|ja_JP] [--open\|--no-open]` | Create an application and output `appType` |
|
|
299
|
-
| `openyida update-app <appType> --name "..."` | Update application metadata |
|
|
300
|
+
| `openyida update-app <appType> [--name "..."] [--layout slide\|ver] [--theme deepBlue]` | Update application metadata and theme/layout fields |
|
|
300
301
|
| `openyida nav-group <list\|create\|rename\|delete\|move\|hide\|show> <appType> ...` | Manage sidebar navigation groups and move pages between groups |
|
|
301
302
|
| `openyida app-permission <get\|set\|add\|remove\|search-user> ...` | Manage app primary admins, data admins, and developer members |
|
|
302
303
|
| `openyida i18n <overview\|config\|languages\|list\|upsert\|delete\|translate\|translate-all\|upgrade> <appType> ...` | Manage app multilingual copy and language configuration |
|
|
@@ -329,7 +330,7 @@ For overseas apps, pass `--locale en_US` or `--locale ja_JP` on creation command
|
|
|
329
330
|
|
|
330
331
|
| Command | Description |
|
|
331
332
|
|---------|-------------|
|
|
332
|
-
| `openyida data <action> <resource> [args]` | Unified data management for forms, processes, tasks, and subforms |
|
|
333
|
+
| `openyida data <action> <resource> [args]` | Unified data management for forms, processes, tasks, and subforms; form queries support `--all` and `--form-uuid` subform hydration |
|
|
333
334
|
| `openyida data check <appType> <formUuid> <rules.json>` | Detect anomalous process-form records |
|
|
334
335
|
| `openyida task-center <type> [options]` | Query todo, created, processed, CC, or proxy-submitted tasks |
|
|
335
336
|
| `openyida agent-center <sub-command>` | Manage Yida process delegation and departure delegation |
|
|
@@ -371,6 +372,7 @@ For overseas apps, pass `--locale en_US` or `--locale ja_JP` on creation command
|
|
|
371
372
|
| `openyida sample [--list]` | Emit sample templates |
|
|
372
373
|
| `openyida bridge start [--token <pair-token>] [--port 6736] [--open]` | Start the local OpenYida web bridge for `https://demo.aliwork.com/s/openyida` |
|
|
373
374
|
| `openyida doctor [--fix]` | Diagnose and repair environment issues |
|
|
375
|
+
| `openyida db-seq-fix [--fix]` | Detect and repair PostgreSQL sequence drift |
|
|
374
376
|
| `openyida formula evaluate <formula\|file> [--schema file]` | Static-check formula syntax and field references |
|
|
375
377
|
| `openyida update` | Update OpenYida through npm |
|
|
376
378
|
| `openyida export-conversation [options]` | Export AI conversation history |
|
package/bin/yida.js
CHANGED
|
@@ -306,7 +306,7 @@ function shouldUsePlaywrightFallbackInAgentLogin() {
|
|
|
306
306
|
const { detectActiveTool } = require('../lib/core/utils');
|
|
307
307
|
const activeTool = detectActiveTool();
|
|
308
308
|
if (activeTool && activeTool.tool === 'wukong') {
|
|
309
|
-
return
|
|
309
|
+
return false;
|
|
310
310
|
}
|
|
311
311
|
return process.env.OPENYIDA_AGENT_PLAYWRIGHT_FALLBACK === '1';
|
|
312
312
|
}
|
|
@@ -477,6 +477,14 @@ async function main() {
|
|
|
477
477
|
if (cachedResult.status === 'ok') {
|
|
478
478
|
printLoginResult(cachedResult);
|
|
479
479
|
} else {
|
|
480
|
+
const { detectActiveTool } = require('../lib/core/utils');
|
|
481
|
+
const activeTool = detectActiveTool();
|
|
482
|
+
if (activeTool && activeTool.tool === 'wukong') {
|
|
483
|
+
const { codexLogin } = require('../lib/auth/codex-login');
|
|
484
|
+
const result = await codexLogin({ tool: 'wukong' });
|
|
485
|
+
printLoginResult(result);
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
480
488
|
const { interactiveLogin } = require('../lib/auth/login');
|
|
481
489
|
const browserResult = interactiveLogin({
|
|
482
490
|
playwrightFallback: shouldUsePlaywrightFallbackInAgentLogin(),
|
|
@@ -486,8 +494,6 @@ async function main() {
|
|
|
486
494
|
} else {
|
|
487
495
|
// CDP/Playwright 失败后的兜底策略:
|
|
488
496
|
// QoderWork 有 in-app browser,优先使用 browser handoff;其余走终端二维码
|
|
489
|
-
const { detectActiveTool } = require('../lib/core/utils');
|
|
490
|
-
const activeTool = detectActiveTool();
|
|
491
497
|
if (activeTool && activeTool.tool === 'qoderwork') {
|
|
492
498
|
const { codexLogin } = require('../lib/auth/codex-login');
|
|
493
499
|
const result = await codexLogin({ tool: 'qoderwork' });
|
|
@@ -1065,6 +1071,12 @@ async function main() {
|
|
|
1065
1071
|
break;
|
|
1066
1072
|
}
|
|
1067
1073
|
|
|
1074
|
+
case 'db-seq-fix': {
|
|
1075
|
+
const { run: runDbSeqFix } = require('../lib/db/db-seq-fix');
|
|
1076
|
+
await runDbSeqFix(args);
|
|
1077
|
+
break;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1068
1080
|
case 'update': {
|
|
1069
1081
|
const { runUpdate } = require('../lib/core/update');
|
|
1070
1082
|
await runUpdate(currentVersion);
|
package/lib/app/check-page.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { lintYidaSource, printLintResult } = require('./page-linter');
|
|
6
|
-
const { buildPageSource,
|
|
6
|
+
const { buildPageSource, shouldBuildPageSource } = require('./page-compat');
|
|
7
7
|
const { t } = require('../core/i18n');
|
|
8
8
|
const { warn, error, hint } = require('../core/chalk');
|
|
9
9
|
|
|
@@ -30,7 +30,7 @@ async function run(args) {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
const sourceCode = fs.readFileSync(sourcePath, 'utf-8');
|
|
33
|
-
const shouldBuild = options.compat
|
|
33
|
+
const shouldBuild = shouldBuildPageSource(sourceCode, sourcePath, { modern: options.compat });
|
|
34
34
|
const buildResult = shouldBuild
|
|
35
35
|
? buildPageSource(sourceCode, sourcePath, { modern: options.compat })
|
|
36
36
|
: null;
|
package/lib/app/compile.js
CHANGED
|
@@ -4,7 +4,7 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { compileSource } = require('./page-compiler');
|
|
6
6
|
const { runLintCheck } = require('./page-linter');
|
|
7
|
-
const { buildPageFile,
|
|
7
|
+
const { buildPageFile, shouldBuildPageSource } = require('./page-compat');
|
|
8
8
|
const { t } = require('../core/i18n');
|
|
9
9
|
const { warn, error, info, step } = require('../core/chalk');
|
|
10
10
|
|
|
@@ -27,7 +27,8 @@ async function run(args) {
|
|
|
27
27
|
error(t('publish.source_not_found', sourcePath));
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
const initialSourceCode = fs.readFileSync(sourcePath, 'utf-8');
|
|
31
|
+
if (shouldBuildPageSource(initialSourceCode, sourcePath, { modern: compat })) {
|
|
31
32
|
info(t('build_page.preparing'));
|
|
32
33
|
const buildResult = buildPageFile(sourcePath, { modern: compat });
|
|
33
34
|
if (!buildResult.ok) {
|
package/lib/app/import-app.js
CHANGED
|
@@ -52,15 +52,26 @@ async function createApp(appName, authRef) {
|
|
|
52
52
|
return result.content; // appType
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
function normalizeImportFormType(formType) {
|
|
56
|
+
const normalized = String(formType || '').trim().toLowerCase();
|
|
57
|
+
if (!normalized || normalized === 'form') {
|
|
58
|
+
return 'receipt';
|
|
59
|
+
}
|
|
60
|
+
return normalized;
|
|
61
|
+
}
|
|
56
62
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
_csrf_token:
|
|
60
|
-
formType:
|
|
63
|
+
function buildCreateFormPostData(csrfToken, formTitle, formType = 'receipt') {
|
|
64
|
+
return querystring.stringify({
|
|
65
|
+
_csrf_token: csrfToken,
|
|
66
|
+
formType: normalizeImportFormType(formType),
|
|
61
67
|
title: JSON.stringify(buildYidaI18n(formTitle, { en_US: formTitle, ja_JP: formTitle })),
|
|
62
68
|
});
|
|
69
|
+
}
|
|
63
70
|
|
|
71
|
+
// ── 创建空白表单/页面/报表 ──────────────────────────────
|
|
72
|
+
|
|
73
|
+
async function createBlankForm(appType, formTitle, authRef, formType = 'receipt') {
|
|
74
|
+
const postData = buildCreateFormPostData(authRef.csrfToken, formTitle, formType);
|
|
64
75
|
const result = await requestWithAutoLogin((auth) => {
|
|
65
76
|
return httpPost(
|
|
66
77
|
auth.baseUrl,
|
|
@@ -80,14 +91,18 @@ async function createBlankForm(appType, formTitle, authRef) {
|
|
|
80
91
|
|
|
81
92
|
// ── 保存表单 Schema ───────────────────────────────────
|
|
82
93
|
|
|
83
|
-
async function saveFormSchema(appType, formUuid, schema, authRef) {
|
|
84
|
-
const
|
|
94
|
+
async function saveFormSchema(appType, formUuid, schema, authRef, formType = 'receipt') {
|
|
95
|
+
const payload = {
|
|
85
96
|
_csrf_token: authRef.csrfToken,
|
|
86
97
|
appType,
|
|
87
98
|
formUuid,
|
|
88
99
|
content: JSON.stringify(schema),
|
|
89
100
|
schemaVersion: 'V5',
|
|
90
|
-
}
|
|
101
|
+
};
|
|
102
|
+
if (normalizeImportFormType(formType) === 'report') {
|
|
103
|
+
payload.importSchema = 'true';
|
|
104
|
+
}
|
|
105
|
+
const postData = querystring.stringify(payload);
|
|
91
106
|
|
|
92
107
|
const result = await requestWithAutoLogin((auth) => {
|
|
93
108
|
return httpPost(
|
|
@@ -234,12 +249,13 @@ async function run(args) {
|
|
|
234
249
|
|
|
235
250
|
for (const form of forms) {
|
|
236
251
|
const { formUuid: oldFormUuid, name: formName, schema: formSchemaResult } = form;
|
|
252
|
+
const formType = normalizeImportFormType(form.formType);
|
|
237
253
|
info(t('import.migrating', formName, oldFormUuid));
|
|
238
254
|
|
|
239
255
|
// 4.1 创建空白表单
|
|
240
256
|
let newFormUuid;
|
|
241
257
|
try {
|
|
242
|
-
newFormUuid = await createBlankForm(newAppType, formName, authRef);
|
|
258
|
+
newFormUuid = await createBlankForm(newAppType, formName, authRef, formType);
|
|
243
259
|
success(t('import.blank_form_created', newFormUuid));
|
|
244
260
|
} catch (err) {
|
|
245
261
|
fail(t('import.create_form_failed', err.message));
|
|
@@ -247,6 +263,7 @@ async function run(args) {
|
|
|
247
263
|
oldFormUuid,
|
|
248
264
|
newFormUuid: null,
|
|
249
265
|
name: formName,
|
|
266
|
+
formType,
|
|
250
267
|
status: 'failed',
|
|
251
268
|
error: err.message,
|
|
252
269
|
});
|
|
@@ -262,6 +279,7 @@ async function run(args) {
|
|
|
262
279
|
oldFormUuid,
|
|
263
280
|
newFormUuid,
|
|
264
281
|
name: formName,
|
|
282
|
+
formType,
|
|
265
283
|
status: 'skipped',
|
|
266
284
|
error: t('import.schema_empty_msg'),
|
|
267
285
|
});
|
|
@@ -279,7 +297,7 @@ async function run(args) {
|
|
|
279
297
|
);
|
|
280
298
|
|
|
281
299
|
// 4.3 保存 Schema
|
|
282
|
-
const saveResult = await saveFormSchema(newAppType, newFormUuid, adaptedSchema, authRef);
|
|
300
|
+
const saveResult = await saveFormSchema(newAppType, newFormUuid, adaptedSchema, authRef, formType);
|
|
283
301
|
if (!saveResult || !saveResult.success) {
|
|
284
302
|
const errorMsg = saveResult ? saveResult.errorMsg || t('common.unknown_error') : t('common.request_failed');
|
|
285
303
|
fail(t('import.save_schema_failed', errorMsg));
|
|
@@ -287,6 +305,7 @@ async function run(args) {
|
|
|
287
305
|
oldFormUuid,
|
|
288
306
|
newFormUuid,
|
|
289
307
|
name: formName,
|
|
308
|
+
formType,
|
|
290
309
|
status: 'failed',
|
|
291
310
|
error: t('import.save_schema_failed', errorMsg),
|
|
292
311
|
});
|
|
@@ -308,6 +327,7 @@ async function run(args) {
|
|
|
308
327
|
oldFormUuid,
|
|
309
328
|
newFormUuid,
|
|
310
329
|
name: formName,
|
|
330
|
+
formType,
|
|
311
331
|
status: 'success',
|
|
312
332
|
});
|
|
313
333
|
successCount++;
|
|
@@ -352,4 +372,12 @@ async function run(args) {
|
|
|
352
372
|
);
|
|
353
373
|
}
|
|
354
374
|
|
|
355
|
-
module.exports = {
|
|
375
|
+
module.exports = {
|
|
376
|
+
run,
|
|
377
|
+
__test__: {
|
|
378
|
+
normalizeImportFormType,
|
|
379
|
+
buildCreateFormPostData,
|
|
380
|
+
adaptSerialNumberFormulas,
|
|
381
|
+
extractSchemaContent,
|
|
382
|
+
},
|
|
383
|
+
};
|
package/lib/app/page-compat.js
CHANGED
|
@@ -30,6 +30,37 @@ const SUPPORTED_REMOVABLE_IMPORTS = new Set([
|
|
|
30
30
|
]);
|
|
31
31
|
|
|
32
32
|
const SUPPORTED_HOOKS = new Set(['useState', 'useEffect']);
|
|
33
|
+
const REQUIRED_RUNTIME_EXPORTS = {
|
|
34
|
+
getCustomState: [
|
|
35
|
+
'export function getCustomState(key) {',
|
|
36
|
+
' if (typeof _customState === "undefined") {',
|
|
37
|
+
' return key ? undefined : {};',
|
|
38
|
+
' }',
|
|
39
|
+
' if (key) {',
|
|
40
|
+
' return _customState[key];',
|
|
41
|
+
' }',
|
|
42
|
+
' return Object.assign({}, _customState);',
|
|
43
|
+
'}',
|
|
44
|
+
].join('\n'),
|
|
45
|
+
setCustomState: [
|
|
46
|
+
'export function setCustomState(newState) {',
|
|
47
|
+
' if (typeof _customState === "undefined") {',
|
|
48
|
+
' return;',
|
|
49
|
+
' }',
|
|
50
|
+
' Object.keys(newState || {}).forEach(function(key) {',
|
|
51
|
+
' _customState[key] = newState[key];',
|
|
52
|
+
' });',
|
|
53
|
+
' this.forceUpdate();',
|
|
54
|
+
'}',
|
|
55
|
+
].join('\n'),
|
|
56
|
+
forceUpdate: [
|
|
57
|
+
'export function forceUpdate() {',
|
|
58
|
+
' this.setState({ timestamp: new Date().getTime() });',
|
|
59
|
+
'}',
|
|
60
|
+
].join('\n'),
|
|
61
|
+
didMount: 'export function didMount() {}',
|
|
62
|
+
didUnmount: 'export function didUnmount() {}',
|
|
63
|
+
};
|
|
33
64
|
|
|
34
65
|
function parseSource(sourceCode) {
|
|
35
66
|
return parser.parse(sourceCode, PARSER_OPTIONS);
|
|
@@ -53,11 +84,21 @@ function hasYidaRenderExport(ast) {
|
|
|
53
84
|
});
|
|
54
85
|
}
|
|
55
86
|
|
|
87
|
+
function sourceHasYidaRenderExport(sourceCode) {
|
|
88
|
+
return /export\s+function\s+renderJsx\s*\(/.test(sourceCode || '');
|
|
89
|
+
}
|
|
90
|
+
|
|
56
91
|
function isAuthoringPath(sourcePath) {
|
|
57
92
|
return /\.oyd\.(jsx?|tsx?)$/i.test(sourcePath || '') ||
|
|
58
93
|
/\.openyida\.(jsx?|tsx?)$/i.test(sourcePath || '');
|
|
59
94
|
}
|
|
60
95
|
|
|
96
|
+
function shouldBuildPageSource(sourceCode, sourcePath = '', options = {}) {
|
|
97
|
+
return options.modern === true ||
|
|
98
|
+
isAuthoringPath(sourcePath) ||
|
|
99
|
+
(!sourceHasYidaRenderExport(sourceCode) && /export\s+default\b/.test(sourceCode || ''));
|
|
100
|
+
}
|
|
101
|
+
|
|
61
102
|
function isSupportedRemovableImport(sourceValue) {
|
|
62
103
|
return SUPPORTED_REMOVABLE_IMPORTS.has(String(sourceValue || '').toLowerCase());
|
|
63
104
|
}
|
|
@@ -324,6 +365,73 @@ function removeSupportedImports(ast, fixes, errors) {
|
|
|
324
365
|
});
|
|
325
366
|
}
|
|
326
367
|
|
|
368
|
+
function collectExportedFunctionNames(ast) {
|
|
369
|
+
const names = new Set();
|
|
370
|
+
ast.program.body.forEach((statement) => {
|
|
371
|
+
if (
|
|
372
|
+
statement.type === 'ExportNamedDeclaration' &&
|
|
373
|
+
statement.declaration &&
|
|
374
|
+
statement.declaration.type === 'FunctionDeclaration' &&
|
|
375
|
+
statement.declaration.id
|
|
376
|
+
) {
|
|
377
|
+
names.add(statement.declaration.id.name);
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
return names;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function hasTopLevelCustomState(ast) {
|
|
384
|
+
return ast.program.body.some((statement) => {
|
|
385
|
+
return statement.type === 'VariableDeclaration' &&
|
|
386
|
+
statement.declarations.some((declarator) => {
|
|
387
|
+
return declarator.id &&
|
|
388
|
+
declarator.id.type === 'Identifier' &&
|
|
389
|
+
declarator.id.name === '_customState';
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function ensureYidaRuntimeContract(sourceCode) {
|
|
395
|
+
const ast = parseSource(sourceCode);
|
|
396
|
+
const exportedNames = collectExportedFunctionNames(ast);
|
|
397
|
+
const fixes = [];
|
|
398
|
+
const prepend = [];
|
|
399
|
+
const append = [];
|
|
400
|
+
const needsStateHelper = !exportedNames.has('getCustomState') || !exportedNames.has('setCustomState');
|
|
401
|
+
|
|
402
|
+
if (needsStateHelper && !hasTopLevelCustomState(ast)) {
|
|
403
|
+
prepend.push('var _customState = {};');
|
|
404
|
+
fixes.push({
|
|
405
|
+
rule: 'runtime-custom-state',
|
|
406
|
+
message: 'Inserted default _customState store for Yida runtime helpers',
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
Object.keys(REQUIRED_RUNTIME_EXPORTS).forEach((name) => {
|
|
411
|
+
if (!exportedNames.has(name)) {
|
|
412
|
+
append.push(REQUIRED_RUNTIME_EXPORTS[name]);
|
|
413
|
+
fixes.push({
|
|
414
|
+
rule: 'runtime-export-' + name,
|
|
415
|
+
message: `Inserted missing export function ${name} for Yida runtime contract`,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
if (prepend.length === 0 && append.length === 0) {
|
|
421
|
+
return { code: sourceCode, fixes };
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
code: [
|
|
426
|
+
prepend.join('\n'),
|
|
427
|
+
sourceCode.trimEnd(),
|
|
428
|
+
append.join('\n\n'),
|
|
429
|
+
'',
|
|
430
|
+
].filter(Boolean).join('\n\n'),
|
|
431
|
+
fixes,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
327
435
|
function fixYidaSource(sourceCode) {
|
|
328
436
|
const ast = parseSource(sourceCode);
|
|
329
437
|
const fixes = [];
|
|
@@ -335,9 +443,11 @@ function fixYidaSource(sourceCode) {
|
|
|
335
443
|
fixVariableDeclarations(ast, fixes);
|
|
336
444
|
fixRenderTimestamp(ast, fixes);
|
|
337
445
|
|
|
446
|
+
const runtimeResult = ensureYidaRuntimeContract(generateCode(ast));
|
|
447
|
+
|
|
338
448
|
return {
|
|
339
|
-
code:
|
|
340
|
-
fixes,
|
|
449
|
+
code: runtimeResult.code,
|
|
450
|
+
fixes: fixes.concat(runtimeResult.fixes),
|
|
341
451
|
errors,
|
|
342
452
|
mode: 'yida-source',
|
|
343
453
|
};
|
|
@@ -529,6 +639,132 @@ function replaceStateSettersInStatement(statement, stateSetters) {
|
|
|
529
639
|
return wrapper.program.body[0];
|
|
530
640
|
}
|
|
531
641
|
|
|
642
|
+
function addBindingNames(pattern, names) {
|
|
643
|
+
if (!pattern) {
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
if (pattern.type === 'Identifier') {
|
|
647
|
+
names.add(pattern.name);
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
if (pattern.type === 'ArrayPattern') {
|
|
651
|
+
pattern.elements.forEach(element => addBindingNames(element, names));
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
if (pattern.type === 'ObjectPattern') {
|
|
655
|
+
pattern.properties.forEach((property) => {
|
|
656
|
+
addBindingNames(property.value || property.argument || property.key, names);
|
|
657
|
+
});
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
if (pattern.type === 'RestElement') {
|
|
661
|
+
addBindingNames(pattern.argument, names);
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
if (pattern.type === 'AssignmentPattern') {
|
|
665
|
+
addBindingNames(pattern.left, names);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function collectComponentBindingNames(componentBody) {
|
|
670
|
+
const names = new Set();
|
|
671
|
+
componentBody.forEach((statement) => {
|
|
672
|
+
if (statement.type === 'VariableDeclaration') {
|
|
673
|
+
statement.declarations.forEach(declarator => addBindingNames(declarator.id, names));
|
|
674
|
+
} else if ((statement.type === 'FunctionDeclaration' || statement.type === 'ClassDeclaration') && statement.id) {
|
|
675
|
+
names.add(statement.id.name);
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
return names;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function collectUseStateSetterNames(componentBody) {
|
|
682
|
+
const names = new Set();
|
|
683
|
+
componentBody.forEach((statement) => {
|
|
684
|
+
if (statement.type !== 'VariableDeclaration') {
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
statement.declarations.forEach((declarator) => {
|
|
688
|
+
if (isUseStateDeclarator(declarator)) {
|
|
689
|
+
names.add(declarator.id.elements[1].name);
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
});
|
|
693
|
+
return names;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function collectReferencedIdentifiersFromStatements(statements) {
|
|
697
|
+
const names = new Set();
|
|
698
|
+
if (!statements || statements.length === 0) {
|
|
699
|
+
return names;
|
|
700
|
+
}
|
|
701
|
+
const wrapper = t.file(t.program(statements.map(statement => t.cloneNode(statement))));
|
|
702
|
+
traverse(wrapper, {
|
|
703
|
+
Identifier(pathRef) {
|
|
704
|
+
if (typeof pathRef.isReferencedIdentifier === 'function') {
|
|
705
|
+
if (!pathRef.isReferencedIdentifier()) {
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
} else if (
|
|
709
|
+
pathRef.parent.type === 'MemberExpression' &&
|
|
710
|
+
pathRef.parent.property === pathRef.node &&
|
|
711
|
+
!pathRef.parent.computed
|
|
712
|
+
) {
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
names.add(pathRef.node.name);
|
|
716
|
+
},
|
|
717
|
+
});
|
|
718
|
+
return names;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function collectStatementBindingNames(statements) {
|
|
722
|
+
const names = new Set();
|
|
723
|
+
(statements || []).forEach((statement) => {
|
|
724
|
+
if (statement.type === 'VariableDeclaration') {
|
|
725
|
+
statement.declarations.forEach(declarator => addBindingNames(declarator.id, names));
|
|
726
|
+
} else if ((statement.type === 'FunctionDeclaration' || statement.type === 'ClassDeclaration') && statement.id) {
|
|
727
|
+
names.add(statement.id.name);
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
return names;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function addUnsupportedEffectLocalReferenceErrors(effect, componentBindings, allowedBindings, errors, reportedNames, line) {
|
|
734
|
+
const referencedNames = collectReferencedIdentifiersFromStatements(effect.mount.concat(effect.unmount));
|
|
735
|
+
referencedNames.forEach((name) => {
|
|
736
|
+
if (!componentBindings.has(name) || allowedBindings.has(name) || reportedNames.has(name)) {
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
reportedNames.add(name);
|
|
740
|
+
errors.push({
|
|
741
|
+
code: 'UNSUPPORTED_EFFECT_LOCAL_REFERENCE',
|
|
742
|
+
message: `useEffect(..., []) references local component binding "${name}", which would be undefined inside Yida didMount/didUnmount. Move the logic inline, use a supported state setter functional update, or write native export function didMount().`,
|
|
743
|
+
line,
|
|
744
|
+
});
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function addUnsupportedEffectCleanupReferenceErrors(effect, errors, reportedNames, line) {
|
|
749
|
+
const mountBindings = collectStatementBindingNames(effect.mount);
|
|
750
|
+
if (mountBindings.size === 0 || effect.unmount.length === 0) {
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const cleanupRefs = collectReferencedIdentifiersFromStatements(effect.unmount);
|
|
755
|
+
cleanupRefs.forEach((name) => {
|
|
756
|
+
if (!mountBindings.has(name) || reportedNames.has(name)) {
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
reportedNames.add(name);
|
|
760
|
+
errors.push({
|
|
761
|
+
code: 'UNSUPPORTED_EFFECT_CLEANUP_REFERENCE',
|
|
762
|
+
message: `useEffect cleanup references effect-local binding "${name}", which would be undefined inside Yida didUnmount. Store it on this (for native didMount/didUnmount) or avoid cleanup-local captures in authoring mode.`,
|
|
763
|
+
line,
|
|
764
|
+
});
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
|
|
532
768
|
function statementContainsUseEffect(statement) {
|
|
533
769
|
return statement.type === 'ExpressionStatement' &&
|
|
534
770
|
statement.expression &&
|
|
@@ -539,6 +775,10 @@ function statementContainsUseEffect(statement) {
|
|
|
539
775
|
function splitComponentBody(componentBody, errors) {
|
|
540
776
|
const stateItems = [];
|
|
541
777
|
const stateSetters = new Map();
|
|
778
|
+
const componentBindings = collectComponentBindingNames(componentBody);
|
|
779
|
+
const allowedEffectBindings = collectUseStateSetterNames(componentBody);
|
|
780
|
+
const reportedEffectRefs = new Set();
|
|
781
|
+
const reportedCleanupRefs = new Set();
|
|
542
782
|
const renderStatements = [];
|
|
543
783
|
const didMountStatements = [];
|
|
544
784
|
const didUnmountStatements = [];
|
|
@@ -573,6 +813,20 @@ function splitComponentBody(componentBody, errors) {
|
|
|
573
813
|
if (statementContainsUseEffect(statement)) {
|
|
574
814
|
const effect = extractEffect(statement.expression, errors);
|
|
575
815
|
if (effect) {
|
|
816
|
+
addUnsupportedEffectLocalReferenceErrors(
|
|
817
|
+
effect,
|
|
818
|
+
componentBindings,
|
|
819
|
+
allowedEffectBindings,
|
|
820
|
+
errors,
|
|
821
|
+
reportedEffectRefs,
|
|
822
|
+
statement.loc && statement.loc.start ? statement.loc.start.line : undefined
|
|
823
|
+
);
|
|
824
|
+
addUnsupportedEffectCleanupReferenceErrors(
|
|
825
|
+
effect,
|
|
826
|
+
errors,
|
|
827
|
+
reportedCleanupRefs,
|
|
828
|
+
statement.loc && statement.loc.start ? statement.loc.start.line : undefined
|
|
829
|
+
);
|
|
576
830
|
didMountStatements.push(...effect.mount);
|
|
577
831
|
didUnmountStatements.push(...effect.unmount);
|
|
578
832
|
}
|
|
@@ -801,7 +1055,9 @@ function buildPageFile(sourcePath, options = {}) {
|
|
|
801
1055
|
module.exports = {
|
|
802
1056
|
buildPageSource,
|
|
803
1057
|
buildPageFile,
|
|
1058
|
+
ensureYidaRuntimeContract,
|
|
804
1059
|
fixYidaSource,
|
|
805
1060
|
getCompatOutputPath,
|
|
806
1061
|
isAuthoringPath,
|
|
1062
|
+
shouldBuildPageSource,
|
|
807
1063
|
};
|
package/lib/app/page-compiler.js
CHANGED
|
@@ -7,6 +7,7 @@ const { default: babelTransform } = require('../core/babel-transform');
|
|
|
7
7
|
const { findProjectRoot } = require('../core/utils');
|
|
8
8
|
const { t } = require('../core/i18n');
|
|
9
9
|
const { info, success, error } = require('../core/chalk');
|
|
10
|
+
const { ensureYidaRuntimeContract } = require('./page-compat');
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* 编译宜搭自定义页面 JSX/JS 源码。
|
|
@@ -20,7 +21,9 @@ function compileSource(sourcePath) {
|
|
|
20
21
|
const compiledPath = path.join(findProjectRoot(), 'pages', 'dist', compiledFileName);
|
|
21
22
|
|
|
22
23
|
info(t('publish.reading_source', sourceFileName));
|
|
23
|
-
const
|
|
24
|
+
const rawSourceCode = fs.readFileSync(sourcePath, 'utf-8');
|
|
25
|
+
const runtimeResult = ensureYidaRuntimeContract(rawSourceCode);
|
|
26
|
+
const sourceCode = runtimeResult.code;
|
|
24
27
|
|
|
25
28
|
info(t('publish.compiling', sourceFileName));
|
|
26
29
|
const babelResult = babelTransform(sourceCode, {}, false, { RE_VERSION: '7.4.0' });
|