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.
Files changed (51) hide show
  1. package/README.md +5 -3
  2. package/bin/yida.js +15 -3
  3. package/lib/app/check-page.js +2 -2
  4. package/lib/app/compile.js +3 -2
  5. package/lib/app/import-app.js +39 -11
  6. package/lib/app/page-compat.js +258 -2
  7. package/lib/app/page-compiler.js +4 -1
  8. package/lib/app/page-linter.js +281 -0
  9. package/lib/app/publish.js +3 -2
  10. package/lib/app/update-app.js +41 -3
  11. package/lib/auth/cdp-browser-login.js +7 -3
  12. package/lib/auth/login.js +2 -3
  13. package/lib/core/command-manifest.js +2 -1
  14. package/lib/core/env-manager.js +24 -16
  15. package/lib/core/locales/ar.js +11 -0
  16. package/lib/core/locales/de.js +11 -0
  17. package/lib/core/locales/en.js +13 -4
  18. package/lib/core/locales/es.js +11 -0
  19. package/lib/core/locales/fr.js +11 -0
  20. package/lib/core/locales/hi.js +11 -0
  21. package/lib/core/locales/ja.js +11 -0
  22. package/lib/core/locales/ko.js +11 -0
  23. package/lib/core/locales/pt.js +11 -0
  24. package/lib/core/locales/vi.js +11 -0
  25. package/lib/core/locales/zh-HK.js +11 -0
  26. package/lib/core/locales/zh.js +13 -4
  27. package/lib/core/query-data.js +184 -5
  28. package/lib/core/utils.js +34 -8
  29. package/lib/process/configure-process.js +552 -20
  30. package/package.json +1 -1
  31. package/project/pages/src/demo-agent-chatbox.oyd.jsx +78 -3
  32. package/scripts/e2e-real/full-runner.js +257 -8
  33. package/scripts/e2e-real/skill-coverage.js +3 -2
  34. package/yida-skills/SKILL.md +2 -1
  35. package/yida-skills/skills/yida-chart/SKILL.md +1 -1
  36. package/yida-skills/skills/yida-connector-safe-actions/SKILL.md +282 -0
  37. package/yida-skills/skills/yida-create-process/SKILL.md +3 -2
  38. package/yida-skills/skills/yida-custom-page/SKILL.md +7 -2
  39. package/yida-skills/skills/yida-custom-page/examples/attachment-upload.js +14 -12
  40. package/yida-skills/skills/yida-custom-page/references/attachment-upload-guide.md +3 -1
  41. package/yida-skills/skills/yida-custom-page/references/coding-guide.md +4 -0
  42. package/yida-skills/skills/yida-custom-page/references/component-jsx-guide.md +31 -22
  43. package/yida-skills/skills/yida-dashboard/SKILL.md +10 -9
  44. package/yida-skills/skills/yida-dashboard/references/interaction-patterns.md +2 -0
  45. package/yida-skills/skills/yida-dashboard/references/pitfalls.md +13 -4
  46. package/yida-skills/skills/yida-dashboard/references/structure-and-layout.md +1 -1
  47. package/yida-skills/skills/yida-ppt-slider/SKILL.md +47 -37
  48. package/yida-skills/skills/yida-ppt-slider/references/examples.md +5 -4
  49. package/yida-skills/skills/yida-process-rule/SKILL.md +93 -3
  50. package/yida-skills/skills/yida-process-rule/references/official-component-nodes.md +93 -0
  51. 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 true;
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);
@@ -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, isAuthoringPath } = require('./page-compat');
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 || isAuthoringPath(sourcePath);
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;
@@ -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, isAuthoringPath } = require('./page-compat');
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
- if (compat || isAuthoringPath(sourcePath)) {
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) {
@@ -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
- async function createBlankForm(appType, formTitle, authRef) {
58
- const postData = querystring.stringify({
59
- _csrf_token: authRef.csrfToken,
60
- formType: 'receipt',
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 postData = querystring.stringify({
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 = { run };
375
+ module.exports = {
376
+ run,
377
+ __test__: {
378
+ normalizeImportFormType,
379
+ buildCreateFormPostData,
380
+ adaptSerialNumberFormulas,
381
+ extractSchemaContent,
382
+ },
383
+ };
@@ -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: generateCode(ast),
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
  };
@@ -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 sourceCode = fs.readFileSync(sourcePath, 'utf-8');
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' });