fix: 修复代码节点补全与参数同步边界
- 保持 main 参数运行协议并在失焦或销毁前刷新输出分析 - 按输入名称生成 JavaScript 和 Python 补全且隐藏旧版 _result - 补充复杂签名与命名参数补全测试
This commit is contained in:
@@ -19,7 +19,6 @@
|
|||||||
import {genShortId} from '../utils/IdGen';
|
import {genShortId} from '../utils/IdGen';
|
||||||
import {createCodeNodeScaffold} from '../utils/codeNodeScaffold';
|
import {createCodeNodeScaffold} from '../utils/codeNodeScaffold';
|
||||||
import {
|
import {
|
||||||
analyzeCodeNodeMainArgsMode,
|
|
||||||
analyzeCodeNodeOutputs,
|
analyzeCodeNodeOutputs,
|
||||||
reconcileInferredCodeNodeOutputs,
|
reconcileInferredCodeNodeOutputs,
|
||||||
syncCodeNodeInputParameters,
|
syncCodeNodeInputParameters,
|
||||||
@@ -144,39 +143,54 @@ Python 示例
|
|||||||
};
|
};
|
||||||
|
|
||||||
let analysisTimer: ReturnType<typeof setTimeout> | undefined;
|
let analysisTimer: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
let pendingAnalysisCode: string | undefined;
|
||||||
|
|
||||||
|
// mainArgsMode 是持久化运行契约,人工编辑代码时只分析输出,禁止按形参文本切换协议。
|
||||||
|
const applyOutputAnalysis = (code: string) => {
|
||||||
|
updateNodeData(currentNodeId, (node) => {
|
||||||
|
if (String(node.data.code || '') !== code) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const outputDefs = reconcileOutputs(
|
||||||
|
code,
|
||||||
|
String(node.data.engine || defaultEngine),
|
||||||
|
(node.data.parameters as Parameter[]) || [],
|
||||||
|
(node.data.outputDefs as Parameter[]) || [],
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
JSON.stringify(outputDefs) ===
|
||||||
|
JSON.stringify(node.data.outputDefs || [])
|
||||||
|
) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return { outputDefs };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const flushOutputAnalysis = () => {
|
||||||
|
if (analysisTimer) {
|
||||||
|
clearTimeout(analysisTimer);
|
||||||
|
analysisTimer = undefined;
|
||||||
|
}
|
||||||
|
const code = pendingAnalysisCode;
|
||||||
|
pendingAnalysisCode = undefined;
|
||||||
|
if (code !== undefined) {
|
||||||
|
applyOutputAnalysis(code);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const scheduleOutputAnalysis = (code: string) => {
|
const scheduleOutputAnalysis = (code: string) => {
|
||||||
if (analysisTimer) {
|
if (analysisTimer) {
|
||||||
clearTimeout(analysisTimer);
|
clearTimeout(analysisTimer);
|
||||||
}
|
}
|
||||||
|
pendingAnalysisCode = code;
|
||||||
analysisTimer = setTimeout(() => {
|
analysisTimer = setTimeout(() => {
|
||||||
updateNodeData(currentNodeId, (node) => {
|
analysisTimer = undefined;
|
||||||
if (String(node.data.code || '') !== code) {
|
const pendingCode = pendingAnalysisCode;
|
||||||
return {};
|
pendingAnalysisCode = undefined;
|
||||||
}
|
if (pendingCode !== undefined) {
|
||||||
const outputDefs = reconcileOutputs(
|
applyOutputAnalysis(pendingCode);
|
||||||
code,
|
}
|
||||||
String(node.data.engine || defaultEngine),
|
|
||||||
(node.data.parameters as Parameter[]) || [],
|
|
||||||
(node.data.outputDefs as Parameter[]) || [],
|
|
||||||
);
|
|
||||||
const mainArgsMode = analyzeCodeNodeMainArgsMode(
|
|
||||||
code,
|
|
||||||
String(node.data.engine || defaultEngine),
|
|
||||||
(node.data.parameters as Parameter[]) || [],
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
JSON.stringify(outputDefs) ===
|
|
||||||
JSON.stringify(node.data.outputDefs || []) &&
|
|
||||||
(!mainArgsMode ||
|
|
||||||
mainArgsMode === node.data.mainArgsMode)
|
|
||||||
) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
outputDefs,
|
|
||||||
...(mainArgsMode ? { mainArgsMode } : {}),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}, 300);
|
}, 300);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -211,9 +225,8 @@ Python 示例
|
|||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
if (analysisTimer) {
|
// 页面切换前刷新最后一次分析,避免代码与输出参数持久化状态不一致。
|
||||||
clearTimeout(analysisTimer);
|
flushOutputAnalysis();
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
@@ -272,6 +285,7 @@ Python 示例
|
|||||||
});
|
});
|
||||||
scheduleOutputAnalysis(code);
|
scheduleOutputAnalysis(code);
|
||||||
}}
|
}}
|
||||||
|
onchange={flushOutputAnalysis}
|
||||||
value={nodeData.code as string||""}
|
value={nodeData.code as string||""}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -25,9 +25,12 @@ describe('codeCompletion utils', () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
expect(completions[0]).toMatchObject({
|
expect(completions[0]).toMatchObject({
|
||||||
label: '_result',
|
label: 'main',
|
||||||
type: 'variable',
|
type: 'snippet',
|
||||||
|
boost: 1000,
|
||||||
});
|
});
|
||||||
|
expect(String(completions[0].apply)).toContain('def main(question):');
|
||||||
|
expect(completions.some((item) => item.label === '_result')).toBe(false);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
completions.filter((item) => item.label === 'input.text').length,
|
completions.filter((item) => item.label === 'input.text').length,
|
||||||
@@ -53,7 +56,10 @@ describe('codeCompletion utils', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should prefer main return snippets for javascript output', () => {
|
it('should prefer main return snippets for javascript output', () => {
|
||||||
const completions = createBusinessCompletions('javascript', []);
|
const completions = createBusinessCompletions('javascript', [
|
||||||
|
{ name: 'data', resolved: true },
|
||||||
|
{ name: 'question', resolved: true },
|
||||||
|
]);
|
||||||
const mainCompletion = completions.find((item) => item.label === 'main');
|
const mainCompletion = completions.find((item) => item.label === 'main');
|
||||||
const resultCompletion = completions.find(
|
const resultCompletion = completions.find(
|
||||||
(item) => item.label === 'result-object',
|
(item) => item.label === 'result-object',
|
||||||
@@ -64,15 +70,21 @@ describe('codeCompletion utils', () => {
|
|||||||
detail: '代码节点入口',
|
detail: '代码节点入口',
|
||||||
boost: 1000,
|
boost: 1000,
|
||||||
});
|
});
|
||||||
expect(String(mainCompletion?.apply)).toContain('function main');
|
expect(String(mainCompletion?.apply)).toContain(
|
||||||
|
'function main(data, question)',
|
||||||
|
);
|
||||||
expect(String(mainCompletion?.apply)).toContain('return {');
|
expect(String(mainCompletion?.apply)).toContain('return {');
|
||||||
|
expect(String(mainCompletion?.apply)).not.toContain('_result');
|
||||||
expect(String(resultCompletion?.apply)).toBe(
|
expect(String(resultCompletion?.apply)).toBe(
|
||||||
"return { message: 'ok', data };",
|
"return { message: 'ok', data };",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should prefer main return snippets for python output', () => {
|
it('should prefer main return snippets for python output', () => {
|
||||||
const completions = createBusinessCompletions('python', []);
|
const completions = createBusinessCompletions('python', [
|
||||||
|
{ name: 'data', resolved: true },
|
||||||
|
{ name: 'question', resolved: true },
|
||||||
|
]);
|
||||||
const mainCompletion = completions.find((item) => item.label === 'main');
|
const mainCompletion = completions.find((item) => item.label === 'main');
|
||||||
const resultCompletion = completions.find(
|
const resultCompletion = completions.find(
|
||||||
(item) => item.label === 'result-object',
|
(item) => item.label === 'result-object',
|
||||||
@@ -83,21 +95,27 @@ describe('codeCompletion utils', () => {
|
|||||||
detail: '代码节点入口',
|
detail: '代码节点入口',
|
||||||
boost: 1000,
|
boost: 1000,
|
||||||
});
|
});
|
||||||
expect(String(mainCompletion?.apply)).toContain('def main(inputs):');
|
expect(String(mainCompletion?.apply)).toContain(
|
||||||
|
'def main(data, question):',
|
||||||
|
);
|
||||||
expect(String(mainCompletion?.apply)).toContain('return {');
|
expect(String(mainCompletion?.apply)).toContain('return {');
|
||||||
expect(String(resultCompletion?.apply)).toContain('def main(inputs):');
|
expect(String(mainCompletion?.apply)).not.toContain('_result');
|
||||||
expect(String(resultCompletion?.apply)).toContain(
|
expect(String(resultCompletion?.apply)).toContain(
|
||||||
"return {'message': 'ok', 'data': inputs.get('data')}",
|
'def main(data, question):',
|
||||||
|
);
|
||||||
|
expect(String(resultCompletion?.apply)).toContain(
|
||||||
|
"return {'message': 'ok', 'data': data}",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should provide standalone python snippets', () => {
|
it('should not expose the legacy object-input contract in python snippets', () => {
|
||||||
const completions = createBusinessCompletions('python', []);
|
const completions = createBusinessCompletions('python', []);
|
||||||
const snippets = completions.filter((item) => item.type === 'snippet');
|
const snippets = completions.filter((item) => item.type === 'snippet');
|
||||||
|
|
||||||
expect(snippets).toHaveLength(4);
|
expect(snippets).toHaveLength(4);
|
||||||
for (const snippet of snippets) {
|
for (const snippet of snippets) {
|
||||||
expect(String(snippet.apply)).toMatch(/^def main\(inputs\):\n /);
|
expect(String(snippet.apply)).not.toContain('inputs');
|
||||||
|
expect(String(snippet.apply)).not.toContain('_result');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import type {
|
|||||||
CompletionSource,
|
CompletionSource,
|
||||||
} from '@codemirror/autocomplete';
|
} from '@codemirror/autocomplete';
|
||||||
import { syntaxTree } from '@codemirror/language';
|
import { syntaxTree } from '@codemirror/language';
|
||||||
|
import {
|
||||||
|
createCodeNodeScaffold,
|
||||||
|
getCodeNodeParameterNames,
|
||||||
|
} from './codeNodeScaffold';
|
||||||
import type { ParameterCandidate } from './paramToken';
|
import type { ParameterCandidate } from './paramToken';
|
||||||
|
|
||||||
export type CodeEngine = 'javascript' | 'python';
|
export type CodeEngine = 'javascript' | 'python';
|
||||||
@@ -30,12 +34,6 @@ const BLOCKED_NODE_NAMES = new Set([
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const JS_SNIPPETS: Array<{ label: string; insert: string; detail: string }> = [
|
const JS_SNIPPETS: Array<{ label: string; insert: string; detail: string }> = [
|
||||||
{
|
|
||||||
label: 'main',
|
|
||||||
detail: '代码节点入口',
|
|
||||||
insert:
|
|
||||||
'function main({ input }) {\n return {\n output: input,\n };\n}',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: 'if-else',
|
label: 'if-else',
|
||||||
detail: '条件分支',
|
detail: '条件分支',
|
||||||
@@ -55,36 +53,45 @@ const JS_SNIPPETS: Array<{ label: string; insert: string; detail: string }> = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const PYTHON_SNIPPETS: Array<{
|
function createPythonSnippets(parameterNames: string[]) {
|
||||||
label: string;
|
const argumentsText = parameterNames.join(', ');
|
||||||
insert: string;
|
const conditionExpression = parameterNames[0] || 'False';
|
||||||
detail: string;
|
const valueExpression = parameterNames[1] || parameterNames[0] || 'None';
|
||||||
}> = [
|
const itemsExpression = parameterNames[0] || '[]';
|
||||||
{
|
const dataExpression = parameterNames[0] || 'None';
|
||||||
label: 'main',
|
|
||||||
detail: '代码节点入口',
|
return [
|
||||||
insert:
|
{
|
||||||
"def main(inputs):\n return {\n 'output': inputs.get('input'),\n }",
|
label: 'if-else',
|
||||||
},
|
detail: '条件分支',
|
||||||
{
|
insert: [
|
||||||
label: 'if-else',
|
`def main(${argumentsText}):`,
|
||||||
detail: '条件分支',
|
` if ${conditionExpression}:`,
|
||||||
insert:
|
` return {'value': ${valueExpression}}`,
|
||||||
"def main(inputs):\n condition = inputs.get('condition')\n value = inputs.get('value')\n if condition:\n return {'value': value}\n return {'value': None}",
|
" return {'value': None}",
|
||||||
},
|
].join('\n'),
|
||||||
{
|
},
|
||||||
label: 'for-loop',
|
{
|
||||||
detail: '遍历数组',
|
label: 'for-loop',
|
||||||
insert:
|
detail: '遍历数组',
|
||||||
"def main(inputs):\n result = []\n for item in inputs.get('items', []):\n result.append(item)\n return {'result': result}",
|
insert: [
|
||||||
},
|
`def main(${argumentsText}):`,
|
||||||
{
|
' result = []',
|
||||||
label: 'result-object',
|
` for item in ${itemsExpression}:`,
|
||||||
detail: '返回对象',
|
' result.append(item)',
|
||||||
insert:
|
" return {'result': result}",
|
||||||
"def main(inputs):\n return {'message': 'ok', 'data': inputs.get('data')}",
|
].join('\n'),
|
||||||
},
|
},
|
||||||
];
|
{
|
||||||
|
label: 'result-object',
|
||||||
|
detail: '返回对象',
|
||||||
|
insert: [
|
||||||
|
`def main(${argumentsText}):`,
|
||||||
|
` return {'message': 'ok', 'data': ${dataExpression}}`,
|
||||||
|
].join('\n'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
export function normalizeCodeEngine(rawEngine?: string): CodeEngine {
|
export function normalizeCodeEngine(rawEngine?: string): CodeEngine {
|
||||||
const normalized = (rawEngine || 'js').trim().toLowerCase();
|
const normalized = (rawEngine || 'js').trim().toLowerCase();
|
||||||
@@ -111,11 +118,13 @@ export function createBusinessCompletions(
|
|||||||
targetEngine: CodeEngine,
|
targetEngine: CodeEngine,
|
||||||
candidates: ParameterCandidate[],
|
candidates: ParameterCandidate[],
|
||||||
): Completion[] {
|
): Completion[] {
|
||||||
const resultCompletion: Completion = {
|
const parameterNames = getCodeNodeParameterNames(targetEngine, candidates);
|
||||||
label: '_result',
|
const mainCompletion: Completion = {
|
||||||
type: 'variable',
|
label: 'main',
|
||||||
detail: '兼容旧版输出对象',
|
type: 'snippet',
|
||||||
boost: 900,
|
detail: '代码节点入口',
|
||||||
|
apply: createCodeNodeScaffold(targetEngine, candidates),
|
||||||
|
boost: 1000,
|
||||||
};
|
};
|
||||||
|
|
||||||
const parameterCompletions: Completion[] = candidates.map((candidate) => ({
|
const parameterCompletions: Completion[] = candidates.map((candidate) => ({
|
||||||
@@ -127,17 +136,19 @@ export function createBusinessCompletions(
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const snippetCompletions: Completion[] = (
|
const snippetCompletions: Completion[] = (
|
||||||
targetEngine === 'python' ? PYTHON_SNIPPETS : JS_SNIPPETS
|
targetEngine === 'python'
|
||||||
|
? createPythonSnippets(parameterNames)
|
||||||
|
: JS_SNIPPETS
|
||||||
).map((snippet) => ({
|
).map((snippet) => ({
|
||||||
label: snippet.label,
|
label: snippet.label,
|
||||||
type: 'snippet',
|
type: 'snippet',
|
||||||
detail: snippet.detail,
|
detail: snippet.detail,
|
||||||
apply: snippet.insert,
|
apply: snippet.insert,
|
||||||
boost: snippet.label === 'main' ? 1000 : 500,
|
boost: 500,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return dedupeCompletions([
|
return dedupeCompletions([
|
||||||
resultCompletion,
|
mainCompletion,
|
||||||
...parameterCompletions,
|
...parameterCompletions,
|
||||||
...snippetCompletions,
|
...snippetCompletions,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -112,6 +112,23 @@ export function isValidCodeNodeParameterName(name: string, rawEngine?: string) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按配置顺序提取可安全写入函数签名的唯一参数名称。
|
||||||
|
*/
|
||||||
|
export function getCodeNodeParameterNames(
|
||||||
|
rawEngine: string | undefined,
|
||||||
|
parameters: Array<Pick<Parameter, 'name'>>,
|
||||||
|
) {
|
||||||
|
const engine = normalizeCodeNodeEngine(rawEngine);
|
||||||
|
return [
|
||||||
|
...new Set(
|
||||||
|
parameters
|
||||||
|
.map((parameter) => String(parameter.name || '').trim())
|
||||||
|
.filter((name) => isValidCodeNodeParameterName(name, engine)),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 为新代码节点生成与当前运行时兼容的 main 脚手架。
|
* 为新代码节点生成与当前运行时兼容的 main 脚手架。
|
||||||
*/
|
*/
|
||||||
@@ -121,13 +138,7 @@ export function createCodeNodeScaffold(
|
|||||||
outputDefs: Parameter[] = [],
|
outputDefs: Parameter[] = [],
|
||||||
) {
|
) {
|
||||||
const engine = normalizeCodeNodeEngine(rawEngine);
|
const engine = normalizeCodeNodeEngine(rawEngine);
|
||||||
const parameterNames = [
|
const parameterNames = getCodeNodeParameterNames(engine, parameters);
|
||||||
...new Set(
|
|
||||||
parameters
|
|
||||||
.map((parameter) => String(parameter.name || '').trim())
|
|
||||||
.filter((name) => isValidCodeNodeParameterName(name, engine)),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
const outputNames = [
|
const outputNames = [
|
||||||
...new Set(
|
...new Set(
|
||||||
outputDefs
|
outputDefs
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import type { Parameter } from '#types';
|
|||||||
import { createCodeNodeScaffold } from './codeNodeScaffold';
|
import { createCodeNodeScaffold } from './codeNodeScaffold';
|
||||||
import { getAvailableNodes } from './nodePalette';
|
import { getAvailableNodes } from './nodePalette';
|
||||||
import {
|
import {
|
||||||
analyzeCodeNodeMainArgsMode,
|
|
||||||
analyzeCodeNodeOutputs,
|
analyzeCodeNodeOutputs,
|
||||||
reconcileInferredCodeNodeOutputs,
|
reconcileInferredCodeNodeOutputs,
|
||||||
syncCodeNodeInputParameters,
|
syncCodeNodeInputParameters,
|
||||||
@@ -76,6 +75,7 @@ describe.each([
|
|||||||
emptyCode: createCodeNodeScaffold('js'),
|
emptyCode: createCodeNodeScaffold('js'),
|
||||||
mainWithData: 'function main(data)',
|
mainWithData: 'function main(data)',
|
||||||
mainWithQuery: 'function main(query)',
|
mainWithQuery: 'function main(query)',
|
||||||
|
mainWithDefault: 'function main(data = null)',
|
||||||
legacyCallWithData: '_result = main(data);',
|
legacyCallWithData: '_result = main(data);',
|
||||||
legacyCallWithQuery: '_result = main(query);',
|
legacyCallWithQuery: '_result = main(query);',
|
||||||
},
|
},
|
||||||
@@ -84,6 +84,7 @@ describe.each([
|
|||||||
emptyCode: createCodeNodeScaffold('python'),
|
emptyCode: createCodeNodeScaffold('python'),
|
||||||
mainWithData: 'def main(data):',
|
mainWithData: 'def main(data):',
|
||||||
mainWithQuery: 'def main(query):',
|
mainWithQuery: 'def main(query):',
|
||||||
|
mainWithDefault: 'def main(data=None):',
|
||||||
legacyCallWithData: '_result = main(data)',
|
legacyCallWithData: '_result = main(data)',
|
||||||
legacyCallWithQuery: '_result = main(query)',
|
legacyCallWithQuery: '_result = main(query)',
|
||||||
},
|
},
|
||||||
@@ -168,6 +169,28 @@ describe.each([
|
|||||||
expect(result.code).toBe(manualCode);
|
expect(result.code).toBe(manualCode);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not overwrite a main signature with default values', () => {
|
||||||
|
const initial = syncCodeNodeInputParameters(
|
||||||
|
fixture.emptyCode,
|
||||||
|
fixture.engine,
|
||||||
|
[],
|
||||||
|
[{ id: 'input-1', name: 'data' }],
|
||||||
|
);
|
||||||
|
const manualCode = initial.code.replace(
|
||||||
|
fixture.mainWithData,
|
||||||
|
fixture.mainWithDefault,
|
||||||
|
);
|
||||||
|
const result = syncCodeNodeInputParameters(
|
||||||
|
manualCode,
|
||||||
|
fixture.engine,
|
||||||
|
initial.parameters,
|
||||||
|
[{ ...initial.parameters[0], name: 'query' }],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.synced).toBe(false);
|
||||||
|
expect(result.code).toBe(manualCode);
|
||||||
|
});
|
||||||
|
|
||||||
it('keeps a compatible legacy explicit main call synchronized', () => {
|
it('keeps a compatible legacy explicit main call synchronized', () => {
|
||||||
const initial = syncCodeNodeInputParameters(
|
const initial = syncCodeNodeInputParameters(
|
||||||
fixture.emptyCode,
|
fixture.emptyCode,
|
||||||
@@ -240,33 +263,6 @@ describe.each([
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('code node main argument mode', () => {
|
|
||||||
it.each([
|
|
||||||
{
|
|
||||||
engine: 'js',
|
|
||||||
namedCode: 'function main(data) { return { data }; }',
|
|
||||||
objectCode: 'function main(inputs) { return { data: inputs.data }; }',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
engine: 'python',
|
|
||||||
namedCode: 'def main(data):\n return {"data": data}',
|
|
||||||
objectCode: 'def main(inputs):\n return {"data": inputs.get("data")}',
|
|
||||||
},
|
|
||||||
])(
|
|
||||||
'recognizes named and legacy object contracts for $engine',
|
|
||||||
({ engine, namedCode, objectCode }) => {
|
|
||||||
const parameters = [{ name: 'data', dataType: 'String' }];
|
|
||||||
|
|
||||||
expect(analyzeCodeNodeMainArgsMode(namedCode, engine, parameters)).toBe(
|
|
||||||
'named',
|
|
||||||
);
|
|
||||||
expect(analyzeCodeNodeMainArgsMode(objectCode, engine, parameters)).toBe(
|
|
||||||
'object',
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe.each([
|
describe.each([
|
||||||
{
|
{
|
||||||
engine: 'js',
|
engine: 'js',
|
||||||
|
|||||||
@@ -27,8 +27,6 @@ export type CodeNodeSyncResult = {
|
|||||||
synced: boolean;
|
synced: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CodeNodeMainArgsMode = 'named' | 'object';
|
|
||||||
|
|
||||||
export type InferredCodeNodeOutput = {
|
export type InferredCodeNodeOutput = {
|
||||||
dataType: CodeNodeDataType;
|
dataType: CodeNodeDataType;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -407,34 +405,6 @@ export function syncCodeNodeInputParameters(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据 main 简单形参与节点输入名称判断运行时调用方式。
|
|
||||||
*/
|
|
||||||
export function analyzeCodeNodeMainArgsMode(
|
|
||||||
source: string,
|
|
||||||
rawEngine: string | undefined,
|
|
||||||
parameters: Parameter[],
|
|
||||||
): CodeNodeMainArgsMode | null {
|
|
||||||
const engine = normalizeCodeNodeEngine(rawEngine);
|
|
||||||
const document = parseDocument(source, engine);
|
|
||||||
const mainFunction = document && findMainFunction(document);
|
|
||||||
if (!document || !mainFunction) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const configuredNames = parameterNames(parameters, engine, 'name');
|
|
||||||
const currentParams = readSimpleNames(
|
|
||||||
mainFunction.paramList,
|
|
||||||
source,
|
|
||||||
new Set(['VariableDefinition', 'VariableName']),
|
|
||||||
);
|
|
||||||
return configuredNames &&
|
|
||||||
currentParams &&
|
|
||||||
arraysEqual(currentParams, configuredNames)
|
|
||||||
? 'named'
|
|
||||||
: 'object';
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseQuotedString(rawValue: string) {
|
function parseQuotedString(rawValue: string) {
|
||||||
const raw = rawValue.trim();
|
const raw = rawValue.trim();
|
||||||
if (raw.length < 2) {
|
if (raw.length < 2) {
|
||||||
|
|||||||
Reference in New Issue
Block a user