Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
b6ee69a
feat: add 'replace_output' props to plugin, allowing plugins to modif…
zxypro1 Apr 7, 2025
55ce65f
pub: engine@0.1.6-beta.4, parse-spec@0.0.30-beta.1
zxypro1 Apr 7, 2025
6e5ae63
rm replaceOutput
zxypro1 Apr 7, 2025
4840471
pub: engine@0.1.6-beta.5
zxypro1 Apr 21, 2025
1a5b82d
publish: registry@0.0.12-beta.6
zxypro1 May 19, 2025
4b6f9de
feat: resources.info support plugins
zxypro1 May 21, 2025
0663cdd
fix: s.yaml not found when info
zxypro1 May 23, 2025
46ff797
build(engine|parse-spec): update dependencies and fix env variable pa…
zxypro1 May 28, 2025
41a1157
build(deps): update @serverless-devs/art-template to 4.13.16-beta.39
zxypro1 Jun 11, 2025
f5fe096
feat(load-application): add support for number type prompts
zxypro1 Jun 18, 2025
a54c20c
feat(engine): enhance error handling and hook context in action execu…
zxypro1 Nov 26, 2025
d58c51c
chore(deps): downgrade pnpm lockfile version and update package depen…
zxypro1 Nov 26, 2025
43bff3c
chore(engine): bump version to 0.1.6-beta.11 and enhance app name han…
zxypro1 Nov 26, 2025
e84029c
fix(engine): downgrade version to 0.1.6-beta.10 and update error hand…
zxypro1 Nov 26, 2025
3140cfc
chore(engine): bump version to 0.1.6-beta.11 in package.json
zxypro1 Nov 27, 2025
437086b
chore(engine): bump version to 0.1.6-beta.12 in package.json
zxypro1 Nov 28, 2025
12ff9e9
fix(load-component): improve error handling in getEntryFile function
zxypro1 Jan 1, 2026
d11d7fc
fix(load-component): throw error if no valid entry file found
zxypro1 Jan 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/engine/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@serverless-devs/engine",
"version": "0.1.6-beta.3",
"version": "0.1.6-beta.13",
"description": "a engine lib for serverless-devs",
"main": "lib/index.js",
"scripts": {
Expand Down
28 changes: 25 additions & 3 deletions packages/engine/src/actions/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { IAction, IActionLevel, IActionType, IAllowFailure, IComponentAction, IHookType, IPluginAction, IRunAction, getInputs } from '@serverless-devs/parse-spec';
import { isEmpty, filter, includes, set, get } from 'lodash';
import { isEmpty, filter, includes, set, get, cloneDeep } from 'lodash';
import * as utils from '@serverless-devs/utils';
import { DevsError, ETrackerType } from '@serverless-devs/utils';
import fs from 'fs-extra';
Expand Down Expand Up @@ -27,7 +27,8 @@ interface IRecord {

interface IOptions {
hookLevel: `${IActionLevel}`;
projectName?: string;
projectName?: string; // 资源名称(resources下的key)
appName?: string; // 项目名称(s.yaml的name字段)
logger: ILoggerInstance;
skipActions?: boolean;
}
Expand Down Expand Up @@ -246,8 +247,29 @@ You can still use them now, but we suggest to modify them.`)
const instance = await loadComponent(hook.value, { logger: this.logger });
// Determine the inputs for the plugin based on the record's pluginOutput.
const inputs = isEmpty(this.record.pluginOutput) ? this.inputs : this.record.pluginOutput;
// 添加hook上下文信息
// level: 'resource' 表示资源级别hook, 'project' 表示项目级别hook(全局)
const isResourceLevel = this.option.hookLevel === IActionLevel.PROJECT;
const command = this.record.command || '';
const hookType = hook.hookType || '';
const inputsWithHookContext = {
...inputs,
hookContext: {
hookType: hookType, // 例如: 'pre', 'fail', 'success', 'complete'
hookName: command ? `${hookType}-${command}` : hookType, // 例如: 'fail-deploy', 'complete-deploy'
command: command, // 例如: 'deploy', 'remove'
actionType: hook.actionType || 'plugin', // 'plugin'
level: isResourceLevel ? 'resource' : 'project', // 'resource'=资源级别, 'project'=项目级别(全局)
projectName: this.option.appName || undefined, // 项目名称(s.yaml的name字段)
resourceName: isResourceLevel ? (this.option.projectName || undefined) : undefined, // 资源名称(仅resource级别有值)
},
};
// Execute the plugin with the determined inputs and provided arguments.
this.record.pluginOutput = await instance(inputs, hook.args, this.logger);
this.record.pluginOutput = await instance(inputsWithHookContext, hook.args, this.logger);
// If prop 'replace_output' is true, replace the record's step output with the plugin output.
if (hook.replace_output) {
this.record.step.output = cloneDeep(this.record.pluginOutput);
}
} catch (e) {
const error = e as Error;
// Check if the failure is allowed based on the record's allowFailure setting.
Expand Down
68 changes: 55 additions & 13 deletions packages/engine/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ class Engine {
// 初始化全局的 action
this.globalActionInstance = new Actions(yaml.actions, {
hookLevel: IActionLevel.GLOBAL,
appName: get(this.spec, 'yaml.appName'), // 项目名称
logger: this.logger,
skipActions: this.spec.skipActions,
});
Expand Down Expand Up @@ -393,19 +394,51 @@ class Engine {
const projectInfo = get(this.info, item.projectName);
if (!projectInfo || isEmpty(projectInfo) || isNil(projectInfo)) {
this.logger.write(`${chalk.gray(`execute info command of [${item.projectName}]...`)}`);
let args = cloneDeep(this.options.args);
if (args) {
args[0] = 'info';
}
// 20250520: support action for ${resources.xx.info.xx}
const newParseSpecInstance = new ParseSpec(get(this.spec, 'yaml.path'), {
argv: args,
logger: this.logger,
});
newParseSpecInstance.start();
const newAction = newParseSpecInstance.parseActions(item.actions, IActionLevel.PROJECT);
const newActionInstance = new Actions(newAction, {
hookLevel: IActionLevel.PROJECT,
projectName: item.projectName, // 资源名称
appName: get(this.spec, 'yaml.appName'), // 项目名称
logger: item.logger,
skipActions: this.spec.skipActions,
});
newActionInstance.setValue('magic', await this.getFilterContext(item));
newActionInstance.setValue('step', item);
newActionInstance.setValue('command', 'info');
const newInputs = await this.getProps(item);
newActionInstance.setValue('componentProps', newInputs);
const pluginResult = await this.actionInstance?.start(IHookType.PRE, newInputs) || {};
const spec = cloneDeep(this.spec);
spec['command'] = 'info';
// 20240912 when -f, don't throw error
const { f, force } = parseArgv(this.options.args);
let res = {}
if (f || force) {
try {
res = await this.doSrc(item, {}, spec);
} catch(e) {
this.logger.warn(get(e, 'data'));
// 20250521: remove previous logic
let res: any = {}
try {
res = await this.doSrc(item, pluginResult, spec);
set(newInputs, 'output', res);
const pluginSuccessResult = await newActionInstance?.start(IHookType.SUCCESS, newInputs);
if (!isEmpty(pluginSuccessResult)) {
res = get(pluginSuccessResult, 'step.output');
}
} else {
res = await this.doSrc(item, {}, spec);
} catch (e) {
this.logger.warn(get(e, 'data'));
// 将error信息添加到inputs中传递给fail hook
set(newInputs, 'errorContext', e);
res = get(await newActionInstance?.start(IHookType.FAIL, newInputs), 'step.output') || {};
}
const pluginCompleteResult = await newActionInstance?.start(IHookType.COMPLETE, newInputs);
if (!isEmpty(pluginCompleteResult)) {
res = get(pluginCompleteResult, 'step.output');
}
set(this.info, item.projectName, res);
this.logger.write(`${chalk.gray(`[${item.projectName}] info command executed.`)}`);
Expand Down Expand Up @@ -501,7 +534,12 @@ class Engine {
} catch (error) {
// On error, attempt to trigger the project's fail hook and update the recorded context.
try {
const res = await this.actionInstance?.start(IHookType.FAIL, this.record.componentProps);
// 将error信息添加到inputs中传递给fail hook
const failInputs = {
...this.record.componentProps,
errorContext: error,
};
const res = await this.actionInstance?.start(IHookType.FAIL, failInputs);
this.recordContext(item, get(res, 'pluginOutput', {}));
} catch (error) {
this.record.status = STEP_STATUS.FAILURE;
Expand Down Expand Up @@ -529,10 +567,13 @@ class Engine {

// Attempt to trigger the project's complete hook regardless of status.
try {
const res = await this.actionInstance?.start(IHookType.COMPLETE, {
// complete hook需要包含error信息(如果有的话)
const completeInputs = {
...this.record.componentProps,
output: get(item, 'output', {}),
});
errorContext: get(item, 'error'),
};
const res = await this.actionInstance?.start(IHookType.COMPLETE, completeInputs);
this.recordContext(item, get(res, 'pluginOutput', {}));
} catch (error) {
this.record.status = STEP_STATUS.FAILURE;
Expand Down Expand Up @@ -578,7 +619,8 @@ class Engine {
debug(`project actions: ${JSON.stringify(newAction)}`);
this.actionInstance = new Actions(newAction, {
hookLevel: IActionLevel.PROJECT,
projectName: item.projectName,
projectName: item.projectName, // 资源名称
appName: get(this.spec, 'yaml.appName'), // 项目名称
logger: item.logger,
skipActions: this.spec.skipActions,
});
Expand Down
4 changes: 2 additions & 2 deletions packages/load-application/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@serverless-devs/load-application",
"version": "0.0.16-beta.1",
"version": "0.0.16-beta.3",
"description": "load application for serverless-devs",
"main": "lib/index.js",
"scripts": {
Expand All @@ -18,7 +18,7 @@
},
"dependencies": {
"@serverless-cd/debug": "^4.3.4",
"@serverless-devs/art-template": "^4.13.16-beta.24",
"@serverless-devs/art-template": "^4.13.16-beta.39",
"@serverless-devs/credential": "workspace:^",
"@serverless-devs/downloads": "workspace:^",
"@serverless-devs/secret": "workspace:^",
Expand Down
5 changes: 5 additions & 0 deletions packages/load-application/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ export const getDefaultValue = (value: any) => {
return replace(value, RANDOM_PATTERN, randomId());
};

export const getNumberDefaultValue = (value: any) => {
if (typeof value !== 'number') return;
return value;
};

export const getSecretManager = () => {
const secretManager = SecretManager.getInstance();
return secretManager;
Expand Down
12 changes: 11 additions & 1 deletion packages/load-application/src/v3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { isEmpty, includes, split, get, has, set, sortBy, map, concat, keys, sta
import axios from 'axios';
import parse from './parse';
import { IOptions } from './types';
import { getInputs, getUrlWithLatest, getUrlWithVersion, getAllCredential, getDefaultValue, getSecretManager } from './utils';
import { getInputs, getUrlWithLatest, getUrlWithVersion, getAllCredential, getDefaultValue, getSecretManager, getNumberDefaultValue } from './utils';
import YAML from 'yaml';
import inquirer from 'inquirer';
import chalk from 'chalk';
Expand Down Expand Up @@ -338,6 +338,16 @@ class LoadApplication {
default: getDefaultValue(item.default),
validate,
});
} else if (item.type === 'number') {
// number类型
promptList.push({
type: 'input',
message: item.title,
name,
prefix,
default: getNumberDefaultValue(item.default),
validate,
});
}
}
return { promptList, tmpResult };
Expand Down
2 changes: 1 addition & 1 deletion packages/load-component/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@serverless-devs/load-component",
"version": "0.0.9",
"version": "0.0.10-beta.1",
"description": "request for serverless-devs",
"main": "lib/index.js",
"scripts": {
Expand Down
49 changes: 28 additions & 21 deletions packages/load-component/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,30 +18,37 @@ export function readJsonFile(filePath: string) {
}
}

const getEntryFile = async (componentPath: string) => {
const fsStat = await fs.stat(componentPath);
if (fsStat.isFile() || !fsStat.isDirectory()) return componentPath;
const packageInfo: any = readJsonFile(path.resolve(componentPath, 'package.json'));
// First look for main under the package json file
let entry = get(packageInfo, 'main');
if (entry) return path.resolve(componentPath, entry);
// Second check the out dir under the tsconfig json file
const tsconfigPath = path.resolve(componentPath, 'tsconfig.json');
const tsconfigInfo = readJsonFile(tsconfigPath);
entry = get(tsconfigInfo, 'compilerOptions.outDir');
if (entry) return path.resolve(componentPath, entry);
// Third look for src index js
const srcIndexPath = path.resolve(componentPath, './src/index.js');
if (fs.existsSync(srcIndexPath)) return srcIndexPath;
const indexPath = path.resolve(componentPath, './index.js');
if (fs.existsSync(indexPath)) return indexPath;
throw new Error(
'The component cannot be required. Please check whether the setting of the component entry file is correct. In the current directory, first look for main under the package json file, secondly look for compiler options out dir under the tsconfig json file, thirdly look for src index js, and finally look for index js',
);
const getEntryFile = async (componentPath: string, logger: any) => {
try {
const fsStat = await fs.stat(componentPath);
if (fsStat.isFile() || !fsStat.isDirectory()) return componentPath;
const packageInfo: any = readJsonFile(path.resolve(componentPath, 'package.json'));
// First look for main under the package json file
let entry = get(packageInfo, 'main');
if (entry) return path.resolve(componentPath, entry);
// Second check the out dir under the tsconfig json file
const tsconfigPath = path.resolve(componentPath, 'tsconfig.json');
const tsconfigInfo = readJsonFile(tsconfigPath);
entry = get(tsconfigInfo, 'compilerOptions.outDir');
if (entry) return path.resolve(componentPath, entry);
// Third look for src index js
const srcIndexPath = path.resolve(componentPath, './src/index.js');
if (fs.existsSync(srcIndexPath)) return srcIndexPath;
const indexPath = path.resolve(componentPath, './index.js');
if (fs.existsSync(indexPath)) return indexPath;
// No valid entry file found
throw new Error('No valid entry file found');
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
logger.debug(errorMessage);
throw new Error(
'The component cannot be required. Please check whether the setting of the component entry file is correct. In the current directory, first look for main under the package json file, secondly look for compiler options out dir under the tsconfig json file, thirdly look for src index js, and finally look for index js',
);
}
};

export const buildComponentInstance = async (componentPath: string, params?: any, cleanCache: boolean = false) => {
const requirePath = await getEntryFile(componentPath);
const requirePath = await getEntryFile(componentPath, params.logger || console);
// bug: `- component: fc invoke` timeout. Delete require cache
if (cleanCache && require.cache[requirePath]) {
try {
Expand Down
4 changes: 2 additions & 2 deletions packages/parse-spec/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@serverless-devs/parse-spec",
"version": "0.0.29",
"version": "0.0.30-beta.3",
"description": "a parse yaml spec lib for serverless-devs",
"main": "lib/index.js",
"scripts": {
Expand All @@ -22,7 +22,7 @@
},
"dependencies": {
"@serverless-cd/debug": "^4.3.4",
"@serverless-devs/art-template": "^4.13.16-beta.24",
"@serverless-devs/art-template": "^4.13.16-beta.39",
"@serverless-devs/credential": "workspace:^",
"@serverless-devs/utils": "workspace:^",
"chalk": "^4.1.2",
Expand Down
4 changes: 2 additions & 2 deletions packages/parse-spec/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,11 @@ class ParseSpec {
const code = get(projects, `${i}.props.code`);
if (code && isString(code)) {
const codePath = utils.getAbsolutePath(get(projects, `${i}.props.code`, ''));
expand(dotenv.config({ path: path.join(codePath, '.env') }));
expand(dotenv.config({ path: path.join(codePath, '.env'), override: true }));
}
}

expand(dotenv.config({ path: path.join(path.dirname(this.yaml.path), '.env') }));
expand(dotenv.config({ path: path.join(path.dirname(this.yaml.path), '.env'), override: true }));
}
private async doExtend() {
// this.yaml = { path: '' }
Expand Down
1 change: 1 addition & 0 deletions packages/parse-spec/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export interface IPluginAction {
level: `${IActionLevel}`;
projectName: string;
allow_failure?: boolean | IAllowFailure;
replace_output?: boolean;
}
export interface IComponentAction {
hookType: `${IHookType}`;
Expand Down
4 changes: 2 additions & 2 deletions packages/registry/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@serverless-devs/registry",
"version": "0.0.12-beta.5",
"version": "0.0.12-beta.7",
"description": "request for serverless-devs",
"main": "lib/index.js",
"scripts": {
Expand All @@ -23,7 +23,7 @@
"dependencies": {
"@serverless-devs/utils": "workspace:^",
"@serverless-devs/zip": "workspace:^",
"@serverless-devs/art-template": "4.13.16-beta.38",
"@serverless-devs/art-template": "4.13.16-beta.39",
"ajv": "^8.12.0",
"chalk": "^5.3.0",
"js-yaml": "^4.1.0",
Expand Down
18 changes: 0 additions & 18 deletions packages/registry/src/actions/constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,24 +34,6 @@ export const publishSchema = {
"type": "array",
"items": {
"type": "string",
"enum": [
"阿里云",
"腾讯云",
"华为云",
"百度智能云",
"AWS",
"Azure",
"Google Cloud Platform",
"专有云",
"其它",
"火山引擎",
"Alibaba Cloud",
"Tencent Cloud",
"Huawei Cloud",
"Baidu Cloud",
"Private Cloud",
"Others"
]
}
},
"Version": {
Expand Down
Loading
Loading