From d574086648dec74665e8f8348f250f982ac81d4e Mon Sep 17 00:00:00 2001 From: ls147258 Date: Sat, 8 Aug 2026 09:24:39 +0800 Subject: [PATCH 1/2] refactor: clean dead code, add high-value tests, streamline docs Behavior-preserving refactor verified by typecheck + full unit suite + ncc build. Dead code (safe cleanup): - remove ~17 comment-only dead blocks (keep meaningful TODOs) - rename misnamed goLocalInvoke.ts -> goLocalStart.ts (class is GoLocalStart), update its import - add ts-prune/depcheck audit scripts; audit confirms no production dependency is safely removable (flags are interface types, ts-jest/e2e/f2elint transitives) Tests (high-value gaps first): - add 12 unit test files / 120 cases for previously untested modules: resources acr/oss/ram/vpc-nas, info, 2to3, deploy/impl base/trigger/vpc_binding, deploy/utils, run-command - oss/ram/vpc-nas 0->100%, acr 0->80.8%, info 0->98.9%, 2to3 0->98.5% - overall statement coverage 61.87% -> 67.36% Test config: - add collectCoverageFrom to expose real coverage - split `test` to unit-only (no credentials); add `test:it` for integration Docs: - delete empty version.md, redundant root CONTRIBUTING.md, and stale AI meta-docs (project-summary, testing-plan, technical-documentation) - consolidate contribution guide into docs/CONTRIB.md; rewrite docs index and architecture design notes; fix README node badge >=14.14.0 -> >=16 - update CLAUDE.md scripts table Signed-off-by: ls147258 --- CLAUDE.md | 24 +- CONTRIBUTING.md | 183 -- README.md | 4 +- __tests__/ut/commands/2to3/index_test.ts | 368 ++++ .../ut/commands/deploy/impl/base_test.ts | 102 + .../ut/commands/deploy/impl/trigger_test.ts | 164 ++ .../commands/deploy/impl/vpc_binding_test.ts | 160 ++ .../ut/commands/deploy/utils/index_test.ts | 149 ++ __tests__/ut/commands/info/index_test.ts | 380 ++++ __tests__/ut/local/local_test.ts | 4 +- __tests__/ut/resources/acr/index_test.ts | 155 ++ __tests__/ut/resources/acr/login_test.ts | 178 ++ __tests__/ut/resources/oss/index_test.ts | 147 ++ __tests__/ut/resources/ram/index_test.ts | 70 + __tests__/ut/resources/vpc-nas/index_test.ts | 141 ++ __tests__/ut/utils/run-command_test.ts | 168 ++ docs/CONTRIB.md | 226 +++ docs/RUNBOOK.md | 321 ++++ docs/architecture.md | 91 +- docs/project-summary.md | 174 -- docs/readme.md | 47 +- docs/technical-documentation.md | 732 ------- docs/testing-plan.md | 365 ---- jestconfig.json | 9 +- package-lock.json | 1686 ++++++++++++++++- package.json | 9 +- src/interface/trigger.ts | 6 +- src/resources/acr/login.ts | 1 - src/resources/fc/impl/utils.ts | 2 - src/resources/fc/index.ts | 1 - src/resources/vpc-nas/index.ts | 11 +- src/subCommands/deploy/impl/custom_domain.ts | 1 - src/subCommands/deploy/impl/function.ts | 1 - .../deploy/impl/provision_config.ts | 1 - src/subCommands/invoke/index.ts | 1 - .../local/impl/invoke/baseLocalInvoke.ts | 2 - .../impl/invoke/customContainerLocalInvoke.ts | 2 - .../local/impl/invoke/pythonLocalInvoke.ts | 1 - .../local/impl/start/customLocalStart.ts | 1 - .../{goLocalInvoke.ts => goLocalStart.ts} | 0 src/subCommands/local/index.ts | 2 +- src/subCommands/logs/index.ts | 4 - src/subCommands/plan/index.ts | 1 - src/subCommands/remove/index.ts | 1 - src/utils/run-command.ts | 1 - version.md | 0 46 files changed, 4429 insertions(+), 1668 deletions(-) delete mode 100644 CONTRIBUTING.md create mode 100644 __tests__/ut/commands/2to3/index_test.ts create mode 100644 __tests__/ut/commands/deploy/impl/base_test.ts create mode 100644 __tests__/ut/commands/deploy/impl/trigger_test.ts create mode 100644 __tests__/ut/commands/deploy/impl/vpc_binding_test.ts create mode 100644 __tests__/ut/commands/deploy/utils/index_test.ts create mode 100644 __tests__/ut/commands/info/index_test.ts create mode 100644 __tests__/ut/resources/acr/index_test.ts create mode 100644 __tests__/ut/resources/acr/login_test.ts create mode 100644 __tests__/ut/resources/oss/index_test.ts create mode 100644 __tests__/ut/resources/ram/index_test.ts create mode 100644 __tests__/ut/resources/vpc-nas/index_test.ts create mode 100644 __tests__/ut/utils/run-command_test.ts create mode 100644 docs/CONTRIB.md create mode 100644 docs/RUNBOOK.md delete mode 100644 docs/project-summary.md delete mode 100644 docs/technical-documentation.md delete mode 100644 docs/testing-plan.md rename src/subCommands/local/impl/start/{goLocalInvoke.ts => goLocalStart.ts} (100%) delete mode 100644 version.md diff --git a/CLAUDE.md b/CLAUDE.md index 5fdf869a..25e1687f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,16 +10,20 @@ FC3 is the Serverless Devs component for Alibaba Cloud Function Compute 3.0, pro ## Available Scripts -| Script | Description | -| ------------------------- | -------------------------- | -| `npm run build` | Production bundle with ncc | -| `npm run watch` | TypeScript watch mode | -| `npm test` | Jest tests with coverage | -| `npm run format` | Prettier formatting | -| `npm run lint` | f2elint scanning | -| `npm run fix` | Auto-fix lint issues | -| `npm run publish` | Build and registry publish | -| `npm run generate-schema` | Generate JSON schema | +| Script | Description | +| ------------------------- | -------------------------------------------- | +| `npm run build` | Production bundle with ncc | +| `npm run watch` | TypeScript watch mode | +| `npm test` | Unit tests with coverage (no credentials) | +| `npm run test:it` | Integration tests (needs cloud credentials) | +| `npm run typecheck` | Type-check without emitting | +| `npm run deadcode` | Audit unused exports (ts-prune) | +| `npm run depcheck` | Audit unused dependencies | +| `npm run format` | Prettier formatting | +| `npm run lint` | f2elint scanning | +| `npm run fix` | Auto-fix lint issues | +| `npm run publish` | Build and registry publish | +| `npm run generate-schema` | Generate JSON schema | ## Key Directories diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index efacef66..00000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,183 +0,0 @@ -# Contributing to Serverless Devs FC Component - -It is warmly welcomed if you have interest to hack on Serverless Devs FC Component . First, we encourage this kind of willing very much. And here is a list of contributing guide for you. - -## Topics - -- [Contributing to Serverless Devs FC Component](#contributing-to-serverless-devs-fc-component) - - [Topics](#topics) - - [Reporting security issues](#reporting-security-issues) - - [Reporting general issues](#reporting-general-issues) - - [Code and doc contribution](#code-and-doc-contribution) - - [Workspace Preparation](#workspace-preparation) - - [Branch Definition](#branch-definition) - - [Commit Rules](#commit-rules) - - [Commit Message](#commit-message) - - [Commit Content](#commit-content) - - [PR Description](#pr-description) - - [Test case contribution](#test-case-contribution) - - [Package contribution](#package-contribution) - - [Engage to help anything](#engage-to-help-anything) - -## Reporting security issues - -Security issues are always treated seriously. As our usual principle, we discourage anyone to spread security issues. If you find a security issue of Serverless Devs FC Component , please do not discuss it in public and even do not open a public issue. Instead we encourage you to send us a private email to [service@serverlessfans.com](mailto:service@serverlessfans.com) to report this. - -## Reporting general issues - -To be honest, we regard every user of Serverless Devs FC Component as a very kind contributor. After experiencing Serverless Devs FC Component , you may have some feedback for the project. Then feel free to open an issue via [NEW ISSUE](https://github.com/Serverless-Devs/Serverless-Devs/issues/new/choose). - -Since we collaborate project Serverless Devs FC Component in a distributed way, we appreciate **WELL-WRITTEN**, **DETAILED**, **EXPLICIT** issue reports. To make the communication more efficient, we wish everyone could search if your issue is an existing one in the searching list. If you find it existing, please add your details in comments under the existing issue instead of opening a brand new one. - -To make the issue details as standard as possible, we setup an [ISSUE TEMPLATE](./.github/ISSUE_TEMPLATE) for issue reporters. Please **BE SURE** to follow the instructions to fill fields in template. - -There are a lot of cases when you could open an issue: - -- bug report -- feature request -- performance issues -- feature proposal -- feature design -- help wanted -- doc incomplete -- test improvement -- any questions on project -- and so on - -Also we must remind that when filling a new issue, please remember to remove the sensitive data from your post. Sensitive data could be password, secret key, network locations, private business data and so on. - -## Code and doc contribution - -Every action to make project Serverless Devs FC Component better is encouraged. On GitHub, every improvement for Serverless Devs FC Component could be via a PR (short for pull request). - -- If you find a typo, try to fix it! -- If you find a bug, try to fix it! -- If you find some redundant codes, try to remove them! -- If you find some test cases missing, try to add them! -- If you could enhance a feature, please **DO NOT** hesitate! -- If you find code implicit, try to add comments to make it clear! -- If you find code ugly, try to refactor that! -- If you can help to improve documents, it could not be better! -- If you find document incorrect, just do it and fix that! -- ... - -Actually it is impossible to list them completely. Just remember one principle: - -> WE ARE LOOKING FORWARD TO ANY PR FROM YOU. - -Since you are ready to improve Serverless Devs FC Component with a PR, we suggest you could take a look at the PR rules here. - -- [Workspace Preparation](#workspace-preparation) -- [Branch Definition](#branch-definition) -- [Commit Rules](#commit-rules) -- [PR Description](#pr-description) - -### Workspace Preparation - -To put forward a PR, we assume you have registered a GitHub ID. Then you could finish the preparation in the following steps: - -1. **FORK** Serverless Devs FC Component to your repository. To make this work, you just need to click the button Fork in right-left of [Serverless-Devs/Serverless-Devs](https://github.com/Serverless-Devs/Serverless-Devs) main page. Then you will end up with your repository in `https://github.com//Serverless-Devs`, in which `your-username` is your GitHub username. - -1. **CLONE** your own repository to develop locally. Use `git clone git@github.com:/Serverless-Devs.git` to clone repository to your local machine. Then you can create new branches to finish the change you wish to make. - -1. **Set Remote** upstream to be `git@github.com:Serverless-Devs/Serverless-Devs.git` using the following two commands: - -``` -git remote add upstream git@github.com:Serverless-Devs/Serverless-Devs.git -git remote set-url --push upstream no-pushing -``` - -With this remote setting, you can check your git remote configuration like this: - -``` -$ git remote -v -origin git@github.com:/Serverless-Devs.git (fetch) -origin git@github.com:/Serverless-Devs.git (push) -upstream git@github.com:Serverless-Devs/Serverless-Devs.git (fetch) -upstream no-pushing (push) -``` - -Adding this, we can easily synchronize local branches with upstream branches. - -### Branch Definition - -Right now we assume every contribution via pull request is for [branch develop](https://github.com/Serverless-Devs/Serverless-Devs/tree/develop) in Serverless Devs FC Component . Before contributing, be aware of branch definition would help a lot. - -As a contributor, keep in mind again that every contribution via pull request is for branch develop. While in project Serverless Devs FC Component , there are several other branches, we generally call them release branches(such as 0.6.0,0.6.1), feature branches, hotfix branches and master branch. - -When officially releasing a version, there will be a release branch and named with the version number. - -After the release, we will merge the commit of the release branch into the master branch. - -When we find that there is a bug in a certain version, we will decide to fix it in a later version or fix it in a specific hotfix version. When we decide to fix the hotfix version, we will checkout the hotfix branch based on the corresponding release branch, perform code repair and verification, and merge it into the develop branch and the master branch. - -For larger features, we will pull out the feature branch for development and verification. - -### Commit Rules - -Actually in Serverless Devs FC Component , we take two rules serious when committing: - -- [Commit Message](#commit-message) -- [Commit Content](#commit-content) - -#### Commit Message - -Commit message could help reviewers better understand what is the purpose of submitted PR. It could help accelerate the code review procedure as well. We encourage contributors to use **EXPLICIT** commit message rather than ambiguous message. In general, we advocate the following commit message type: - -- docs: xxxx. For example, "docs: add docs about Serverless Devs FC Component cluster installation". -- feature: xxxx.For example, "feature: support oracle in AT mode". -- bugfix: xxxx. For example, "bugfix: fix panic when input nil parameter". -- refactor: xxxx. For example, "refactor: simplify to make codes more readable". -- test: xxx. For example, "test: add unit test case for func InsertIntoArray". -- other readable and explicit expression ways. - -On the other side, we discourage contributors from committing message like the following ways: - -- ~~fix bug~~ -- ~~update~~ -- ~~add doc~~ - -If you get lost, please see [How to Write a Git Commit Message](http://chris.beams.io/posts/git-commit/) for a start. - -#### Commit Content - -Commit content represents all content changes included in one commit. We had better include things in one single commit which could support reviewer's complete review without any other commits' help. In another word, contents in one single commit can pass the CI to avoid code mess. In brief, there are three minor rules for us to keep in mind: - -- avoid very large change in a commit; -- complete and reviewable for each commit. -- check git config(`user.name`, `user.email`) when committing to ensure that it is associated with your GitHub ID. -- when submitting pr, please add a brief description of the current changes to the X.X.X.md file under the 'changes/' folder - -In addition, in the code change part, we suggest that all contributors should read the [code style of Serverless Devs FC Component](#code-style). - -No matter commit message, or commit content, we do take more emphasis on code review. - -### PR Description - -PR is the only way to make change to Serverless Devs FC Component project files. To help reviewers better get your purpose, PR description could not be too detailed. We encourage contributors to follow the [PR template](./.github/PULL_REQUEST_TEMPLATE.md) to finish the pull request. - -## Test case contribution - -Any test case would be welcomed. Currently, Serverless Devs FC Component function test cases are high priority. - -- For unit test, you need to create a test file named `xxxTest.java` in the test directory of the same module. Recommend you to use the junit5 UT framework - -- For integration test, you can put the integration test in the test directory or the Serverless Devs FC Component -test module. It is recommended to use the mockito test framework. - -## Package contribution - -For the development of Serverless Devs FC Component Package, please refer to the [development guide](https://github.com/Serverless-Devs/Serverless-Devs/discussions/62). After the development is completed, please refer [PR](https://github.com/Serverless-Devs/package-awesome) to the summary warehouse for more reference. - -## Engage to help anything - -We choose GitHub as the primary place for Serverless Devs FC Component to collaborate. So the latest updates of Serverless Devs FC Component are always here. Although contributions via PR is an explicit way to help, we still call for any other ways. - -- reply to other's issues if you could; -- help solve other user's problems; -- help review other's PR design; -- help review other's codes in PR; -- discuss about Serverless Devs FC Component to make things clearer; -- advocate Serverless Devs FC Component technology beyond GitHub; -- write blogs on Serverless Devs FC Component and so on. - -In a word, **ANY HELP IS CONTRIBUTION.** diff --git a/README.md b/README.md index 3daf8a78..0590e1e9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

- node.js version + node.js version license @@ -57,7 +57,7 @@ # 项目贡献 -我们非常希望您可以和我们一起贡献这个项目。贡献内容包括不限于代码的维护、应用/组件的贡献、文档的完善等,更多详情可以参考[🏆 贡献指南](./CONTRIBUTING.md)。 +我们非常希望您可以和我们一起贡献这个项目。贡献内容包括不限于代码的维护、应用/组件的贡献、文档的完善等,更多详情可以参考[🏆 贡献指南](./docs/CONTRIB.md)。 与此同时,我们也非常感谢所有[👬 参与贡献的小伙伴](https://github.com/devsapp/fc3/graphs/contributors) ,为 Serverless Devs fc3 组件项目贡献的努力和汗水。 diff --git a/__tests__/ut/commands/2to3/index_test.ts b/__tests__/ut/commands/2to3/index_test.ts new file mode 100644 index 00000000..429b2f68 --- /dev/null +++ b/__tests__/ut/commands/2to3/index_test.ts @@ -0,0 +1,368 @@ +import SYaml2To3 from '../../../../src/subCommands/2to3'; +import { IInputs } from '../../../../src/interface'; +import { parseArgv } from '@serverless-devs/utils'; +import fs from 'fs'; +import yaml from 'js-yaml'; + +jest.mock('@serverless-devs/utils', () => ({ + parseArgv: jest.fn(), +})); + +jest.mock('../../../../src/logger', () => { + const mockLogger = { + log: jest.fn(), + info: jest.fn(), + debug: jest.fn(), + warn: jest.fn(), + write: jest.fn(), + error: jest.fn(), + output: jest.fn(), + spin: jest.fn(), + tips: jest.fn(), + append: jest.fn(), + tipsOnce: jest.fn(), + warnOnce: jest.fn(), + writeOnce: jest.fn(), + }; + return { + __esModule: true, + default: mockLogger, + }; +}); + +describe('SYaml2To3', () => { + let mockInputs: IInputs; + + beforeEach(() => { + mockInputs = { + cwd: '/test', + baseDir: '/test', + name: 'test-app', + props: { + region: 'cn-hangzhou', + functionName: 'test-function', + runtime: 'nodejs18', + handler: 'index.handler', + code: './code', + }, + command: 's2tos3', + args: [], + yaml: { + path: '/test/s.yaml', + }, + resource: { + name: 'test-resource', + component: 'fc3', + access: 'default', + }, + outputs: {}, + getCredential: jest.fn().mockResolvedValue({ + AccountID: '123456789', + AccessKeyID: 'test-key', + AccessKeySecret: 'test-secret', + }), + }; + (parseArgv as jest.Mock).mockReturnValue({ + source: 's.yaml', + target: 's3.yaml', + region: 'cn-hangzhou', + help: false, + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + jest.clearAllMocks(); + }); + + describe('constructor', () => { + it('should resolve absolute source and target paths from baseDir', () => { + const s = new SYaml2To3(mockInputs); + expect(s.source).toBe('/test/s.yaml'); + expect(s.target).toBe('/test/s3.yaml'); + }); + + it('should keep absolute paths untouched', () => { + (parseArgv as jest.Mock).mockReturnValue({ + source: '/abs/in.yaml', + target: '/abs/out.yaml', + help: false, + }); + const s = new SYaml2To3(mockInputs); + expect(s.source).toBe('/abs/in.yaml'); + expect(s.target).toBe('/abs/out.yaml'); + }); + + it('should default the target to s3.yaml when not specified', () => { + (parseArgv as jest.Mock).mockReturnValue({ source: 's.yaml', help: false }); + const s = new SYaml2To3(mockInputs); + expect(s.target).toBe('/test/s3.yaml'); + }); + + it('should fall back to process.cwd() when baseDir is missing', () => { + mockInputs.baseDir = undefined as any; + const cwdSpy = jest.spyOn(process, 'cwd').mockReturnValue('/from-cwd'); + const s = new SYaml2To3(mockInputs); + expect(s.baseDir).toBe('/from-cwd'); + cwdSpy.mockRestore(); + }); + + it('should throw when no source is specified and no s.yaml/s.yml exists', () => { + (parseArgv as jest.Mock).mockReturnValue({ help: false }); + jest.spyOn(fs, 'accessSync').mockImplementation(() => { + throw new Error('missing'); + }); + expect(() => new SYaml2To3(mockInputs)).toThrow( + 'source not specified and s.yaml or s.yml is not in current dir, please specify --source', + ); + }); + }); + + describe('getSYamlFile', () => { + it('should return s.yaml when it exists', () => { + const s = new SYaml2To3(mockInputs); + jest.spyOn(fs, 'accessSync').mockImplementation(() => {}); + expect(s.getSYamlFile()).toBe('s.yaml'); + }); + + it('should return an empty string when neither file exists', () => { + const s = new SYaml2To3(mockInputs); + jest.spyOn(fs, 'accessSync').mockImplementation(() => { + throw new Error('missing'); + }); + expect(s.getSYamlFile()).toBe(''); + }); + }); + + describe('variableReplace', () => { + let s: SYaml2To3; + beforeEach(() => { + s = new SYaml2To3(mockInputs); + }); + + it('should quote env() variables', () => { + expect(s.variableReplace('${env(NAME)}')).toBe("${env('NAME')}"); + }); + + it('should quote env.X dot access', () => { + expect(s.variableReplace('${env.NAME}')).toBe("${env('NAME')}"); + }); + + it('should quote config() variables', () => { + expect(s.variableReplace('${config(PORT)}')).toBe("${config('PORT')}"); + }); + + it('should quote file() variables', () => { + expect(s.variableReplace('${file(a.txt)}')).toBe("${file('a.txt')}"); + }); + + it('should rewrite output references to resources.*', () => { + expect(s.variableReplace('${A.output.x}')).toBe('${resources.A.output.x}'); + }); + + it('should rewrite props references to resources.*', () => { + expect(s.variableReplace('${A.props.x}')).toBe('${resources.A.props.x}'); + }); + + it('should leave unrelated variables unchanged', () => { + expect(s.variableReplace('${otherVariable}')).toBe('${otherVariable}'); + }); + }); + + describe('run', () => { + it('should short-circuit when the source edition is already 3.0.0', async () => { + const readSpy = jest.spyOn(fs, 'readFileSync').mockReturnValue(''); + const loadSpy = jest.spyOn(yaml, 'load').mockReturnValue({ edition: '3.0.0' } as any); + const writeSpy = jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {}); + + const s = new SYaml2To3(mockInputs); + await s.run(); + + expect(readSpy).toHaveBeenCalled(); + expect(loadSpy).toHaveBeenCalled(); + expect(writeSpy).not.toHaveBeenCalled(); + }); + + it('should transform a 2.0 fc service (object form) and write the result', async () => { + jest.spyOn(fs, 'readFileSync').mockReturnValue(''); + const command = JSON.stringify(['/bin/sh']); + const args = JSON.stringify(['-c']); + jest.spyOn(yaml, 'load').mockReturnValue({ + edition: '2.0.0', + services: { + svc1: { + component: 'devsapp/fc', + props: { + region: 'cn-hangzhou', + service: { + name: 'service1', + description: 'my service', + nasConfig: { + mountPoints: [{ serverAddr: 'addr', nasDir: '/nas', fcDir: '/mnt' }], + }, + vpcConfig: { vswitchIds: ['vsw-1'], vpcId: 'vpc-1' }, + }, + function: { + name: 'function1', + runtime: 'custom', + codeUri: './code', + handler: 'index.handler', + }, + ossBucket: 'bkt', + ossKey: 'key', + gpuMemorySize: 16384, + asyncConfiguration: { + destination: { + onSuccess: 'acs:fc:::fc-on-success', + onFailure: 'acs:fc:::fc-on-failure', + }, + }, + caPort: 9000, + customContainerConfig: { + image: 'img', + command, + args, + webServerMode: false, + }, + customHealthCheckConfig: { path: '/health' }, + triggers: [ + { + name: 'oss-trigger', + type: 'oss', + config: { + filter: { Key: { Prefix: 'p', Suffix: 's' } }, + bucketName: 'bkt', + }, + role: 'acs:ram::role', + }, + ], + customDomains: [ + { + domainName: 'test.com', + protocol: 'HTTP', + routeConfigs: [ + { path: '/', serviceName: 'service1', functionName: 'function1' }, + ], + }, + ], + }, + actions: { + 'pre-deploy': [ + { component: 'fc build --use-docker' }, + { component: 'fc invoke' }, + ], + 'empty-action': '', + }, + }, + }, + } as any); + const dumpSpy = jest.spyOn(yaml, 'dump').mockReturnValue('dumped-yaml'); + const writeSpy = jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {}); + + const s = new SYaml2To3(mockInputs); + await s.run(); + + expect(dumpSpy).toHaveBeenCalled(); + expect(writeSpy).toHaveBeenCalledWith('/test/s3.yaml', 'dumped-yaml'); + + // Inspect the transformed object handed to yaml.dump + const transformed = dumpSpy.mock.calls[0][0] as any; + expect(transformed.edition).toBe('3.0.0'); + expect(transformed.services).toBeUndefined(); + const svc = transformed.resources.svc1; + expect(svc.component).toBe('fc3'); + expect(svc.props.functionName).toBe('service1$function1'); + // gpu config derived from gpuMemorySize + expect(svc.props.gpuConfig).toEqual({ + gpuMemorySize: 16384, + gpuType: 'fc.gpu.tesla.1', + }); + // oss code block + expect(svc.props.code).toEqual({ ossBucketName: 'bkt', ossObjectName: 'key' }); + // trigger renamed fields + expect(svc.props.triggers[0].triggerName).toBe('oss-trigger'); + expect(svc.props.triggers[0].triggerType).toBe('oss'); + // a fc3-domain resource is generated from customDomains + expect(transformed.resources.fc3_domain_0.component).toBe('fc3-domain'); + }); + + it('should resolve a service referenced by ${var.service} string form', async () => { + jest.spyOn(fs, 'readFileSync').mockReturnValue(''); + jest.spyOn(yaml, 'load').mockReturnValue({ + edition: '2.0.0', + var: { + service: { name: 'service1', description: 'svc', nasConfig: 'AUTO' }, + }, + services: { + svc1: { + component: 'devsapp/fc', + props: { + region: 'cn-hangzhou', + service: '${var.service}', + function: { + name: 'function1', + runtime: 'custom', + codeUri: './code', + handler: 'index.handler', + }, + }, + actions: {}, + }, + }, + } as any); + const dumpSpy = jest.spyOn(yaml, 'dump').mockReturnValue('dumped'); + jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {}); + + const s = new SYaml2To3(mockInputs); + await s.run(); + + const transformed = dumpSpy.mock.calls[0][0] as any; + const svc = transformed.resources.svc1; + // service supplied by template extend + expect(svc.extend).toEqual({ name: 'template_service1' }); + expect(transformed.template.template_service1.nasConfig).toBe('auto'); + expect(svc.props.functionName).toBe('service1$function1'); + }); + + it('should transform a standalone fc-domain component', async () => { + jest.spyOn(fs, 'readFileSync').mockReturnValue(''); + jest.spyOn(yaml, 'load').mockReturnValue({ + edition: '2.0.0', + services: { + domain1: { + component: 'devsapp/fc-domain', + props: { + region: 'cn-hangzhou', + customDomain: { + domainName: 'test.com', + protocol: 'HTTP', + routeConfigs: [ + { path: '/', serviceName: 'service1', functionName: 'function1' }, + ], + }, + }, + }, + }, + } as any); + const dumpSpy = jest.spyOn(yaml, 'dump').mockReturnValue('dumped'); + jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {}); + + const s = new SYaml2To3(mockInputs); + await s.run(); + + const transformed = dumpSpy.mock.calls[0][0] as any; + const domain = transformed.resources.domain1; + expect(domain.component).toBe('fc3-domain'); + expect(domain.props.routeConfig.routes[0].functionName).toBe('service1$function1'); + expect(domain.props.routeConfig.routes[0].serviceName).toBeUndefined(); + }); + + it('should propagate errors thrown while reading the source file', async () => { + jest.spyOn(fs, 'readFileSync').mockImplementation(() => { + throw new Error('read failed'); + }); + const s = new SYaml2To3(mockInputs); + await expect(s.run()).rejects.toThrow('read failed'); + }); + }); +}); diff --git a/__tests__/ut/commands/deploy/impl/base_test.ts b/__tests__/ut/commands/deploy/impl/base_test.ts new file mode 100644 index 00000000..ad4c1fa7 --- /dev/null +++ b/__tests__/ut/commands/deploy/impl/base_test.ts @@ -0,0 +1,102 @@ +import Base from '../../../../../src/subCommands/deploy/impl/base'; +import { IInputs } from '../../../../../src/interface'; +import FC from '../../../../../src/resources/fc'; +import { getUserAgent } from '../../../../../src/utils'; + +jest.mock('../../../../../src/resources/fc'); +jest.mock('../../../../../src/utils'); + +jest.mock('../../../../../src/logger', () => ({ + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + spin: jest.fn(), + append: jest.fn(), +})); + +const FCMock = FC as unknown as jest.Mock; +const getUserAgentMock = getUserAgent as jest.Mock; + +// Concrete subclass so we can instantiate the abstract Base. +class TestImpl extends Base { + async before(): Promise { + // no-op + } + + async run(): Promise { + return 'ran'; + } +} + +describe('Base (deploy impl base)', () => { + let mockInputs: IInputs; + + beforeEach(() => { + getUserAgentMock.mockReturnValue('fc3-user-agent'); + mockInputs = { + props: { + region: 'cn-hangzhou', + functionName: 'test-function', + endpoint: 'https://custom.endpoint', + }, + credential: { + AccountID: 'test-account-id', + AccessKeyID: 'test-access-key-id', + AccessKeySecret: 'test-access-key-secret', + Region: 'cn-hangzhou', + }, + userAgent: 'caller-ua', + args: [], + argsObj: [], + baseDir: '/test/base/dir', + } as any; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('constructs an FC client with region, credential and computed userAgent', () => { + // Act + const impl = new TestImpl(mockInputs, true); + + // Assert + expect(getUserAgentMock).toHaveBeenCalledWith('caller-ua', 'deploy'); + expect(FCMock).toHaveBeenCalledWith('cn-hangzhou', mockInputs.credential, { + endpoint: 'https://custom.endpoint', + userAgent: 'fc3-user-agent', + }); + expect(impl.fcSdk).toBeInstanceOf(FC); + expect(impl.inputs).toBe(mockInputs); + }); + + it('stores the provided needDeploy flag', () => { + // Act + const implTrue = new TestImpl(mockInputs, true); + const implFalse = new TestImpl(mockInputs, false); + const implUndefined = new TestImpl(mockInputs, undefined); + + // Assert + expect(implTrue.needDeploy).toBe(true); + expect(implFalse.needDeploy).toBe(false); + expect(implUndefined.needDeploy).toBeUndefined(); + }); + + it('exposes abstract before/run implemented by the subclass', async () => { + // Arrange + const impl = new TestImpl(mockInputs, true); + + // Act & Assert + await expect(impl.before()).resolves.toBeUndefined(); + await expect(impl.run()).resolves.toBe('ran'); + }); + + it('throws when inputs.props is missing (region access fails)', () => { + // Arrange + const badInputs = { credential: {} } as any; + + // Act & Assert + expect(() => new TestImpl(badInputs, true)).toThrow(); + }); +}); diff --git a/__tests__/ut/commands/deploy/impl/trigger_test.ts b/__tests__/ut/commands/deploy/impl/trigger_test.ts new file mode 100644 index 00000000..5e4eeb86 --- /dev/null +++ b/__tests__/ut/commands/deploy/impl/trigger_test.ts @@ -0,0 +1,164 @@ +import Trigger from '../../../../../src/subCommands/deploy/impl/trigger'; +import { IInputs } from '../../../../../src/interface'; +import logger from '../../../../../src/logger'; + +jest.mock('../../../../../src/resources/fc'); +jest.mock('../../../../../src/utils'); +jest.mock('inquirer'); + +jest.mock('@serverless-devs/diff', () => ({ + diffConvertYaml: jest.fn(() => ({ diffResult: {}, show: '' })), +})); + +describe('Trigger', () => { + let mockInputs: IInputs; + let mockOpts: any; + + beforeEach(() => { + mockInputs = { + props: { + region: 'cn-hangzhou', + functionName: 'test-function', + triggers: [ + { + triggerName: 'httpTrigger', + triggerType: 'http', + triggerConfig: { + authType: 'anonymous', + methods: ['GET'], + }, + }, + ], + }, + credential: { + AccountID: 'test-account-id', + AccessKeyID: 'test-access-key-id', + AccessKeySecret: 'test-access-key-secret', + Region: 'cn-hangzhou', + }, + args: [], + argsObj: [], + baseDir: '/test/base/dir', + } as any; + + mockOpts = { yes: true, trigger: undefined }; + + logger.debug = jest.fn(); + logger.info = jest.fn(); + logger.warn = jest.fn(); + logger.error = jest.fn(); + logger.write = jest.fn(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('constructor', () => { + it('initializes local triggers with defaults and sets functionName', () => { + // Act + const trigger = new Trigger(mockInputs, mockOpts); + + // Assert + expect(trigger.functionName).toBe('test-function'); + expect(trigger.local).toHaveLength(1); + expect(trigger.local[0].triggerName).toBe('httpTrigger'); + }); + + it('filters triggers when a specific trigger name is requested', () => { + // Act + const trigger = new Trigger(mockInputs, { ...mockOpts, trigger: 'nonexistent' }); + + // Assert + expect(trigger.local).toHaveLength(0); + expect(logger.error).toHaveBeenCalled(); + }); + }); + + describe('before', () => { + it('calls _getRemote and _plan', async () => { + // Arrange + const trigger = new Trigger(mockInputs, mockOpts); + const getRemoteSpy = jest + .spyOn(trigger as any, '_getRemote') + .mockResolvedValue(undefined); + const planSpy = jest.spyOn(trigger as any, '_plan').mockResolvedValue(undefined); + + // Act + await trigger.before(); + + // Assert + expect(getRemoteSpy).toHaveBeenCalled(); + expect(planSpy).toHaveBeenCalled(); + }); + }); + + describe('checkUpdateEBTrigger', () => { + it('returns true when the remote config is empty', () => { + // Arrange + const trigger = new Trigger(mockInputs, mockOpts); + + // Act + const result = trigger.checkUpdateEBTrigger(trigger.local[0], {}); + + // Assert + expect(result).toBe(true); + }); + + it('returns false for an eventbridge trigger with no diff', () => { + // Arrange + const trigger = new Trigger(mockInputs, mockOpts); + + // Act + const result = trigger.checkUpdateEBTrigger(trigger.local[0], { + triggerType: 'eventbridge', + }); + + // Assert + expect(result).toBe(false); + }); + }); + + describe('run', () => { + it('deploys the trigger on the happy path when needDeploy is true', async () => { + // Arrange + const trigger = new Trigger(mockInputs, mockOpts); + trigger.needDeploy = true; + trigger.remote = [{}]; + + const mockFcSdk = { + deployTrigger: jest.fn().mockResolvedValue(undefined), + createTrigger: jest.fn(), + }; + Object.defineProperty(trigger, 'fcSdk', { value: mockFcSdk, writable: true }); + + // Act + const result = await trigger.run(); + + // Assert + expect(mockFcSdk.deployTrigger).toHaveBeenCalledWith( + 'test-function', + expect.objectContaining({ triggerName: 'httpTrigger' }), + ); + expect(result).toBe(true); + }); + + it('re-throws when createTrigger fails with a non-FunctionAlreadyExists error', async () => { + // Arrange + const trigger = new Trigger(mockInputs, mockOpts); + trigger.needDeploy = false; + trigger.remote = [{}]; + + const createErr = Object.assign(new Error('create boom'), { code: 'SomeOtherError' }); + const mockFcSdk = { + deployTrigger: jest.fn(), + createTrigger: jest.fn().mockRejectedValue(createErr), + }; + Object.defineProperty(trigger, 'fcSdk', { value: mockFcSdk, writable: true }); + + // Act & Assert + await expect(trigger.run()).rejects.toThrow('create boom'); + expect(mockFcSdk.createTrigger).toHaveBeenCalled(); + }); + }); +}); diff --git a/__tests__/ut/commands/deploy/impl/vpc_binding_test.ts b/__tests__/ut/commands/deploy/impl/vpc_binding_test.ts new file mode 100644 index 00000000..782c7b57 --- /dev/null +++ b/__tests__/ut/commands/deploy/impl/vpc_binding_test.ts @@ -0,0 +1,160 @@ +import VpcBinding from '../../../../../src/subCommands/deploy/impl/vpc_binding'; +import { IInputs } from '../../../../../src/interface'; +import logger from '../../../../../src/logger'; + +jest.mock('../../../../../src/resources/fc'); +jest.mock('../../../../../src/utils'); +jest.mock('inquirer'); + +jest.mock('@serverless-devs/diff', () => ({ + diffConvertYaml: jest.fn(() => ({ diffResult: {}, show: '' })), +})); + +describe('VpcBinding', () => { + let mockInputs: IInputs; + let mockOpts: any; + + beforeEach(() => { + mockInputs = { + props: { + region: 'cn-hangzhou', + functionName: 'test-function', + vpcBinding: { + vpcIds: ['vpc-b', 'vpc-a'], + }, + }, + credential: { + AccountID: 'test-account-id', + AccessKeyID: 'test-access-key-id', + AccessKeySecret: 'test-access-key-secret', + Region: 'cn-hangzhou', + }, + args: [], + argsObj: [], + baseDir: '/test/base/dir', + } as any; + + mockOpts = { yes: true }; + + logger.debug = jest.fn(); + logger.info = jest.fn(); + logger.warn = jest.fn(); + logger.error = jest.fn(); + logger.write = jest.fn(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('constructor', () => { + it('sets functionName and sorts local vpcIds', () => { + // Act + const vpcBinding = new VpcBinding(mockInputs, mockOpts); + + // Assert + expect(vpcBinding.functionName).toBe('test-function'); + expect(vpcBinding.local.vpcIds).toEqual(['vpc-a', 'vpc-b']); + }); + + it('defaults local to an empty object when vpcBinding is absent', () => { + // Arrange + const inputsNoBinding = { + ...mockInputs, + props: { ...mockInputs.props, vpcBinding: undefined }, + } as any; + + // Act + const vpcBinding = new VpcBinding(inputsNoBinding, mockOpts); + + // Assert + expect(vpcBinding.local).toEqual({}); + }); + }); + + describe('before', () => { + it('calls _getRemote and _plan', async () => { + // Arrange + const vpcBinding = new VpcBinding(mockInputs, mockOpts); + const getRemoteSpy = jest + .spyOn(vpcBinding as any, '_getRemote') + .mockResolvedValue(undefined); + const planSpy = jest.spyOn(vpcBinding as any, '_plan').mockResolvedValue(undefined); + + // Act + await vpcBinding.before(); + + // Assert + expect(getRemoteSpy).toHaveBeenCalled(); + expect(planSpy).toHaveBeenCalled(); + }); + }); + + describe('run', () => { + it('creates all local vpc bindings when remote is empty (happy path)', async () => { + // Arrange + const vpcBinding = new VpcBinding(mockInputs, mockOpts); + vpcBinding.needDeploy = true; + vpcBinding.remote = {}; + + const mockFcSdk = { + createVpcBinding: jest.fn().mockResolvedValue(undefined), + deleteVpcBinding: jest.fn().mockResolvedValue(undefined), + }; + Object.defineProperty(vpcBinding, 'fcSdk', { value: mockFcSdk, writable: true }); + + // Act + const result = await vpcBinding.run(); + + // Assert + expect(mockFcSdk.deleteVpcBinding).not.toHaveBeenCalled(); + expect(mockFcSdk.createVpcBinding).toHaveBeenCalledWith('test-function', 'vpc-a'); + expect(mockFcSdk.createVpcBinding).toHaveBeenCalledWith('test-function', 'vpc-b'); + expect(result).toBe(true); + }); + + it('deletes stale and adds new vpc bindings based on the diff', async () => { + // Arrange + const vpcBinding = new VpcBinding(mockInputs, mockOpts); + vpcBinding.needDeploy = true; + // remote has vpc-a and vpc-c; local has vpc-a and vpc-b + vpcBinding.remote = { vpcIds: ['vpc-a', 'vpc-c'] }; + + const mockFcSdk = { + createVpcBinding: jest.fn().mockResolvedValue(undefined), + deleteVpcBinding: jest.fn().mockResolvedValue(undefined), + }; + Object.defineProperty(vpcBinding, 'fcSdk', { value: mockFcSdk, writable: true }); + + // Act + await vpcBinding.run(); + + // Assert + expect(mockFcSdk.deleteVpcBinding).toHaveBeenCalledWith('test-function', 'vpc-c'); + expect(mockFcSdk.deleteVpcBinding).toHaveBeenCalledTimes(1); + expect(mockFcSdk.createVpcBinding).toHaveBeenCalledWith('test-function', 'vpc-b'); + expect(mockFcSdk.createVpcBinding).toHaveBeenCalledTimes(1); + }); + + it('does nothing and returns needDeploy when needDeploy is false', async () => { + // Arrange + const vpcBinding = new VpcBinding(mockInputs, mockOpts); + vpcBinding.needDeploy = false; + vpcBinding.remote = {}; + + const mockFcSdk = { + createVpcBinding: jest.fn(), + deleteVpcBinding: jest.fn(), + }; + Object.defineProperty(vpcBinding, 'fcSdk', { value: mockFcSdk, writable: true }); + + // Act + const result = await vpcBinding.run(); + + // Assert + expect(mockFcSdk.createVpcBinding).not.toHaveBeenCalled(); + expect(mockFcSdk.deleteVpcBinding).not.toHaveBeenCalled(); + expect(result).toBe(false); + }); + }); +}); diff --git a/__tests__/ut/commands/deploy/utils/index_test.ts b/__tests__/ut/commands/deploy/utils/index_test.ts new file mode 100644 index 00000000..fc4b79fb --- /dev/null +++ b/__tests__/ut/commands/deploy/utils/index_test.ts @@ -0,0 +1,149 @@ +import { + provisionConfigErrorRetry, + removeScalingConfigSDK, +} from '../../../../../src/subCommands/deploy/utils'; +import { isProvisionConfigError, sleep } from '../../../../../src/utils'; + +jest.mock('../../../../../src/utils'); + +jest.mock('../../../../../src/logger', () => ({ + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + spin: jest.fn(), + append: jest.fn(), +})); + +const isProvisionConfigErrorMock = isProvisionConfigError as jest.Mock; +const sleepMock = sleep as jest.Mock; + +const FUNCTION_NAME = 'test-function'; +const QUALIFIER = 'LATEST'; +const LOCAL_CONFIG = { defaultTarget: 5 }; + +describe('provisionConfigErrorRetry', () => { + beforeEach(() => { + // Keep retries instant + sleepMock.mockResolvedValue(undefined); + isProvisionConfigErrorMock.mockReturnValue(false); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('calls putFunctionProvisionConfig on happy path for ProvisionConfig command', async () => { + // Arrange + const fcSdk = { + putFunctionProvisionConfig: jest.fn().mockResolvedValue(undefined), + putFunctionScalingConfig: jest.fn().mockResolvedValue(undefined), + }; + + // Act + await provisionConfigErrorRetry(fcSdk, 'ProvisionConfig', FUNCTION_NAME, QUALIFIER, LOCAL_CONFIG); + + // Assert + expect(fcSdk.putFunctionProvisionConfig).toHaveBeenCalledWith( + FUNCTION_NAME, + QUALIFIER, + LOCAL_CONFIG, + ); + expect(fcSdk.putFunctionScalingConfig).not.toHaveBeenCalled(); + }); + + it('calls putFunctionScalingConfig on happy path for non-ProvisionConfig command', async () => { + // Arrange + const fcSdk = { + putFunctionProvisionConfig: jest.fn().mockResolvedValue(undefined), + putFunctionScalingConfig: jest.fn().mockResolvedValue(undefined), + }; + + // Act + await provisionConfigErrorRetry(fcSdk, 'ScalingConfig', FUNCTION_NAME, QUALIFIER, LOCAL_CONFIG); + + // Assert + expect(fcSdk.putFunctionScalingConfig).toHaveBeenCalledWith( + FUNCTION_NAME, + QUALIFIER, + LOCAL_CONFIG, + ); + expect(fcSdk.putFunctionProvisionConfig).not.toHaveBeenCalled(); + }); + + it('re-throws non-provision-config errors without retrying', async () => { + // Arrange + const err = new Error('some other error'); + const fcSdk = { + putFunctionProvisionConfig: jest.fn().mockRejectedValue(err), + removeFunctionScalingConfig: jest.fn(), + }; + isProvisionConfigErrorMock.mockReturnValue(false); + + // Act & Assert + await expect( + provisionConfigErrorRetry(fcSdk, 'ProvisionConfig', FUNCTION_NAME, QUALIFIER, LOCAL_CONFIG), + ).rejects.toThrow('some other error'); + expect(fcSdk.removeFunctionScalingConfig).not.toHaveBeenCalled(); + }); + + it('removes scaling config and retries successfully after a provision-config error', async () => { + // Arrange + const provisionErr = new Error('provision config conflict'); + const fcSdk = { + putFunctionProvisionConfig: jest + .fn() + .mockRejectedValueOnce(provisionErr) + .mockResolvedValue(undefined), + removeFunctionScalingConfig: jest.fn().mockResolvedValue(undefined), + getFunctionScalingConfig: jest.fn().mockResolvedValue({ currentInstances: 0 }), + }; + isProvisionConfigErrorMock.mockReturnValue(true); + + // Act + await provisionConfigErrorRetry(fcSdk, 'ProvisionConfig', FUNCTION_NAME, QUALIFIER, LOCAL_CONFIG); + + // Assert + expect(fcSdk.removeFunctionScalingConfig).toHaveBeenCalledWith(FUNCTION_NAME, QUALIFIER); + expect(fcSdk.putFunctionProvisionConfig).toHaveBeenCalledTimes(2); + }); +}); + +describe('removeScalingConfigSDK', () => { + beforeEach(() => { + sleepMock.mockResolvedValue(undefined); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('returns immediately when currentInstances is 0', async () => { + // Arrange + const fcSdk = { + removeFunctionScalingConfig: jest.fn().mockResolvedValue(undefined), + getFunctionScalingConfig: jest.fn().mockResolvedValue({ currentInstances: 0 }), + }; + + // Act + await removeScalingConfigSDK(fcSdk, FUNCTION_NAME, QUALIFIER); + + // Assert + expect(fcSdk.removeFunctionScalingConfig).toHaveBeenCalledWith(FUNCTION_NAME, QUALIFIER); + expect(fcSdk.getFunctionScalingConfig).toHaveBeenCalledTimes(1); + expect(sleepMock).not.toHaveBeenCalled(); + }); + + it('re-throws when removeFunctionScalingConfig fails', async () => { + // Arrange + const fcSdk = { + removeFunctionScalingConfig: jest.fn().mockRejectedValue(new Error('remove failed')), + getFunctionScalingConfig: jest.fn(), + }; + + // Act & Assert + await expect(removeScalingConfigSDK(fcSdk, FUNCTION_NAME, QUALIFIER)).rejects.toThrow( + 'remove failed', + ); + }); +}); diff --git a/__tests__/ut/commands/info/index_test.ts b/__tests__/ut/commands/info/index_test.ts new file mode 100644 index 00000000..ac381dc5 --- /dev/null +++ b/__tests__/ut/commands/info/index_test.ts @@ -0,0 +1,380 @@ +import Info from '../../../../src/subCommands/info'; +import FC, { GetApiType } from '../../../../src/resources/fc'; +import { IInputs } from '../../../../src/interface'; +import loadComponent from '@serverless-devs/load-component'; + +// Mock dependencies +jest.mock('../../../../src/resources/fc'); +jest.mock('../../../../src/logger', () => { + const mockLogger = { + log: jest.fn(), + info: jest.fn(), + debug: jest.fn(), + warn: jest.fn(), + write: jest.fn(), + error: jest.fn(), + output: jest.fn(), + spin: jest.fn(), + tips: jest.fn(), + append: jest.fn(), + tipsOnce: jest.fn(), + warnOnce: jest.fn(), + writeOnce: jest.fn(), + }; + return { + __esModule: true, + default: mockLogger, + }; +}); +jest.mock('../../../../src/utils', () => ({ + getUserAgent: jest.fn(() => 'Component:fc3;command:info'), + transformCustomDomainProps: jest.fn(() => ({ region: 'cn-hangzhou', domainName: 'auto' })), +})); +jest.mock('@serverless-devs/load-component'); + +describe('Info', () => { + let mockInputs: IInputs; + let mockFcInstance: any; + + beforeEach(() => { + mockInputs = { + cwd: '/test', + baseDir: '/test', + name: 'test-app', + props: { + region: 'cn-hangzhou', + functionName: 'test-function', + runtime: 'nodejs18', + handler: 'index.handler', + code: './code', + }, + command: 'info', + args: [], + yaml: { + path: '/test/s.yaml', + }, + resource: { + name: 'test-resource', + component: 'fc3', + access: 'default', + }, + outputs: {}, + credential: { + AccountID: '123456789', + AccessKeyID: 'test-key', + AccessKeySecret: 'test-secret', + SecurityToken: 'test-token', + }, + getCredential: jest.fn().mockResolvedValue({ + AccountID: '123456789', + AccessKeyID: 'test-key', + AccessKeySecret: 'test-secret', + SecurityToken: 'test-token', + }), + }; + + mockFcInstance = { + getFunction: jest.fn().mockResolvedValue({ + functionName: 'test-function', + runtime: 'nodejs18', + handler: 'index.handler', + }), + getTrigger: jest.fn().mockResolvedValue({ + triggerName: 'httpTrigger', + triggerType: 'http', + qualifier: 'LATEST', + httpTrigger: { + urlInternet: 'https://internet.example.com', + urlIntranet: 'https://intranet.example.com', + }, + triggerConfig: { + disableURLInternet: false, + }, + }), + getAsyncInvokeConfig: jest.fn().mockResolvedValue({ maxAsyncEventAgeInSeconds: 100 }), + getVpcBinding: jest.fn().mockResolvedValue({ vpcIds: ['vpc-1'] }), + getFunctionProvisionConfig: jest.fn().mockResolvedValue({ target: 2 }), + getFunctionScalingConfig: jest.fn().mockResolvedValue({ minInstances: 1 }), + getFunctionConcurrency: jest + .fn() + .mockResolvedValue({ reservedConcurrency: 5, functionArn: 'acs:fc:::arn' }), + }; + + (FC as any).mockImplementation(() => mockFcInstance); + (loadComponent as jest.Mock).mockResolvedValue({ + info: jest.fn().mockResolvedValue({ domainName: 'custom.example.com' }), + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('constructor', () => { + it('should create Info instance with valid inputs', () => { + const info = new Info(mockInputs); + expect(info).toBeInstanceOf(Info); + expect(info.region).toBe('cn-hangzhou'); + expect(info.functionName).toBe('test-function'); + expect(info.getApiType).toBe(GetApiType.simple); + }); + + it('should read region and function-name from command line args', () => { + mockInputs.props.region = undefined; + mockInputs.props.functionName = undefined; + mockInputs.args = ['--region', 'cn-beijing', '--function-name', 'cli-function']; + const info = new Info(mockInputs); + expect(info.region).toBe('cn-beijing'); + expect(info.functionName).toBe('cli-function'); + }); + + it('should build triggersName list from props.triggers', () => { + mockInputs.props.triggers = [ + { triggerName: 't1' }, + { triggerName: 't2' }, + ] as any; + const info = new Info(mockInputs); + expect(info.triggersName).toEqual(['t1', 't2']); + }); + + it('should throw when scalingConfig and provisionConfig are both set', () => { + mockInputs.props.scalingConfig = { minInstances: 1 } as any; + mockInputs.props.provisionConfig = { target: 1 } as any; + expect(() => new Info(mockInputs)).toThrow( + 'scalingConfig and provisionConfig cannot be used at the same time', + ); + }); + + it('should throw when region is not specified', () => { + mockInputs.props.region = undefined; + expect(() => new Info(mockInputs)).toThrow( + 'Region not specified, please specify --region', + ); + }); + + it('should throw when functionName is not specified', () => { + mockInputs.props.functionName = undefined; + expect(() => new Info(mockInputs)).toThrow( + 'functionName not specified, please specify --function-name', + ); + }); + }); + + describe('setGetApiType', () => { + it('should update the getApiType value', () => { + const info = new Info(mockInputs); + info.setGetApiType(GetApiType.original); + expect(info.getApiType).toBe(GetApiType.original); + }); + }); + + describe('getFunction', () => { + it('should return the function config from the sdk', async () => { + const info = new Info(mockInputs); + const result = await info.getFunction(); + expect(mockFcInstance.getFunction).toHaveBeenCalledWith( + 'test-function', + GetApiType.simple, + ); + expect(result).toEqual( + expect.objectContaining({ functionName: 'test-function' }), + ); + }); + }); + + describe('getTriggers', () => { + it('should return empty array when no triggers configured', async () => { + const info = new Info(mockInputs); + const result = await info.getTriggers(); + expect(result).toEqual([]); + expect(mockFcInstance.getTrigger).not.toHaveBeenCalled(); + }); + + it('should fetch each configured trigger', async () => { + mockInputs.props.triggers = [{ triggerName: 't1' }] as any; + const info = new Info(mockInputs); + const result = await info.getTriggers(); + expect(mockFcInstance.getTrigger).toHaveBeenCalledWith( + 'test-function', + 't1', + GetApiType.simple, + ); + expect(result).toHaveLength(1); + }); + }); + + describe('getAsyncInvokeConfig', () => { + it('should return {} when asyncInvokeConfig is not set', async () => { + const info = new Info(mockInputs); + const result = await info.getAsyncInvokeConfig(); + expect(result).toEqual({}); + expect(mockFcInstance.getAsyncInvokeConfig).not.toHaveBeenCalled(); + }); + + it('should fetch async config and attach the qualifier', async () => { + mockInputs.props.asyncInvokeConfig = { qualifier: 'my-alias' } as any; + const info = new Info(mockInputs); + const result = await info.getAsyncInvokeConfig(); + expect(mockFcInstance.getAsyncInvokeConfig).toHaveBeenCalledWith( + 'test-function', + 'my-alias', + GetApiType.simple, + ); + expect(result.qualifier).toBe('my-alias'); + }); + }); + + describe('getVpcBing', () => { + it('should return {} when vpcBinding is not set', async () => { + const info = new Info(mockInputs); + const result = await info.getVpcBing(); + expect(result).toEqual({}); + expect(mockFcInstance.getVpcBinding).not.toHaveBeenCalled(); + }); + + it('should fetch vpc binding when configured', async () => { + mockInputs.props.vpcBinding = { vpcIds: ['vpc-1'] } as any; + const info = new Info(mockInputs); + const result = await info.getVpcBing(); + expect(mockFcInstance.getVpcBinding).toHaveBeenCalledWith( + 'test-function', + GetApiType.simple, + ); + expect(result).toEqual({ vpcIds: ['vpc-1'] }); + }); + }); + + describe('getProvisionConfig / getScalingConfig', () => { + it('should return {} when provisionConfig is not set', async () => { + const info = new Info(mockInputs); + const result = await info.getProvisionConfig(); + expect(result).toEqual({}); + }); + + it('should fetch provision config when set', async () => { + mockInputs.props.provisionConfig = { target: 2 } as any; + const info = new Info(mockInputs); + const result = await info.getProvisionConfig(); + expect(mockFcInstance.getFunctionProvisionConfig).toHaveBeenCalledWith( + 'test-function', + 'LATEST', + ); + expect(result).toEqual({ target: 2 }); + }); + + it('should return {} when scalingConfig is not set', async () => { + const info = new Info(mockInputs); + const result = await info.getScalingConfig(); + expect(result).toEqual({}); + }); + + it('should fetch scaling config when set', async () => { + mockInputs.props.scalingConfig = { minInstances: 1 } as any; + const info = new Info(mockInputs); + const result = await info.getScalingConfig(); + expect(mockFcInstance.getFunctionScalingConfig).toHaveBeenCalledWith( + 'test-function', + 'LATEST', + ); + expect(result).toEqual({ minInstances: 1 }); + }); + }); + + describe('getConcurrencyConfig', () => { + it('should return {} when concurrencyConfig is not set', async () => { + const info = new Info(mockInputs); + const result = await info.getConcurrencyConfig(); + expect(result).toEqual({}); + }); + + it('should omit functionArn from the concurrency result', async () => { + mockInputs.props.concurrencyConfig = { reservedConcurrency: 5 } as any; + const info = new Info(mockInputs); + const result = await info.getConcurrencyConfig(); + expect(mockFcInstance.getFunctionConcurrency).toHaveBeenCalledWith('test-function'); + expect(result).toEqual({ reservedConcurrency: 5 }); + expect(result).not.toHaveProperty('functionArn'); + }); + }); + + describe('getCustomDomain', () => { + it('should return {} when customDomain is not set', async () => { + const info = new Info(mockInputs); + const result = await info.getCustomDomain(); + expect(result).toEqual({}); + expect(loadComponent).not.toHaveBeenCalled(); + }); + + it('should delegate to the fc3-domain component when customDomain is set', async () => { + (mockInputs.props as any).customDomain = { domainName: 'auto' }; + const info = new Info(mockInputs); + const result = await info.getCustomDomain(); + expect(loadComponent).toHaveBeenCalled(); + expect(result).toEqual({ domainName: 'custom.example.com' }); + }); + }); + + describe('run', () => { + it('should assemble info from function config only (no optional resources)', async () => { + const info = new Info(mockInputs); + const result = await info.run(); + + expect(result.region).toBe('cn-hangzhou'); + expect(result.functionName).toBe('test-function'); + // Optional resources should be undefined when not configured + expect(result.triggers).toBeUndefined(); + expect(result.asyncInvokeConfig).toBeUndefined(); + expect(result.vpcBinding).toBeUndefined(); + expect(result.customDomain).toBeUndefined(); + expect(result.url).toBeUndefined(); + }); + + it('should attach system urls for a LATEST http trigger', async () => { + mockInputs.props.triggers = [{ triggerName: 'httpTrigger' }] as any; + const info = new Info(mockInputs); + const result = await info.run(); + + expect(result.url).toEqual({ + system_url: 'https://internet.example.com', + system_intranet_url: 'https://intranet.example.com', + }); + expect(result.triggers).toHaveLength(1); + }); + + it('should drop internet url when disableURLInternet is true', async () => { + mockInputs.props.triggers = [{ triggerName: 'httpTrigger' }] as any; + mockFcInstance.getTrigger.mockResolvedValue({ + triggerName: 'httpTrigger', + triggerType: 'http', + qualifier: 'LATEST', + httpTrigger: { + urlInternet: 'https://internet.example.com', + urlIntranet: 'https://intranet.example.com', + }, + triggerConfig: { + disableURLInternet: true, + }, + }); + const info = new Info(mockInputs); + const result = await info.run(); + + expect(result.url.system_url).toBeUndefined(); + expect(result.url.system_intranet_url).toBe('https://intranet.example.com'); + }); + + it('should attach custom_domain to url when customDomain is configured', async () => { + (mockInputs.props as any).customDomain = { domainName: 'auto' }; + const info = new Info(mockInputs); + const result = await info.run(); + + expect(result.url).toEqual({ custom_domain: 'custom.example.com' }); + expect(result.customDomain).toEqual({ domainName: 'custom.example.com' }); + }); + + it('should propagate errors thrown by the sdk', async () => { + mockFcInstance.getFunction.mockRejectedValueOnce(new Error('sdk boom')); + const info = new Info(mockInputs); + await expect(info.run()).rejects.toThrow('sdk boom'); + }); + }); +}); diff --git a/__tests__/ut/local/local_test.ts b/__tests__/ut/local/local_test.ts index 4f18f322..70a3baf6 100644 --- a/__tests__/ut/local/local_test.ts +++ b/__tests__/ut/local/local_test.ts @@ -136,7 +136,7 @@ jest.mock('../../../src/subCommands/local/impl/start/phpLocalStart', () => { }; }); -jest.mock('../../../src/subCommands/local/impl/start/goLocalInvoke', () => { +jest.mock('../../../src/subCommands/local/impl/start/goLocalStart', () => { return { GoLocalStart: jest.fn().mockImplementation(() => { return { @@ -489,7 +489,7 @@ describe('ComponentLocal', () => { }, }, ]; - const { GoLocalStart } = require('../../../src/subCommands/local/impl/start/goLocalInvoke'); + const { GoLocalStart } = require('../../../src/subCommands/local/impl/start/goLocalStart'); const mockInstance = { start: jest.fn().mockResolvedValue(undefined) }; (GoLocalStart as jest.Mock).mockImplementation(() => mockInstance); diff --git a/__tests__/ut/resources/acr/index_test.ts b/__tests__/ut/resources/acr/index_test.ts new file mode 100644 index 00000000..4ed13ce2 --- /dev/null +++ b/__tests__/ut/resources/acr/index_test.ts @@ -0,0 +1,155 @@ +import { ICredentials } from '@serverless-devs/component-interface'; +import Acr from '../../../../src/resources/acr/index'; +import { getDockerTmpUser, getAcrEEInstanceID, getAcrImageMeta } from '../../../../src/resources/acr/login'; +import { runCommand, checkDockerIsOK, sleep } from '../../../../src/utils'; + +jest.mock('../../../../src/resources/acr/login', () => ({ + getDockerTmpUser: jest.fn(), + getAcrEEInstanceID: jest.fn(), + getAcrImageMeta: jest.fn(), + mockDockerConfigFile: jest.fn(), +})); + +jest.mock('../../../../src/utils', () => { + const rc: any = jest.fn().mockResolvedValue(undefined); + rc.showStdout = { inherit: 'inherit', pipe: 'pipe', ignore: 'ignore' }; + return { + runCommand: rc, + checkDockerIsOK: jest.fn(), + sleep: jest.fn().mockResolvedValue(undefined), + }; +}); + +jest.mock('../../../../src/logger', () => ({ + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + spin: jest.fn(), + append: jest.fn(), +})); + +const ACREE_VPC = 'test-registry-vpc.cn-hangzhou.cr.aliyuncs.com/ns/repo:tag'; +const ACREE_INTERNET = 'test-registry.cn-hangzhou.cr.aliyuncs.com/ns/repo:tag'; +const ACR_INTERNET = 'registry.cn-hangzhou.aliyuncs.com/ns/repo:tag'; +const ACR_VPC = 'registry-vpc.cn-hangzhou.aliyuncs.com/ns/repo:tag'; +const NON_ACR = 'docker.io/library/nginx:latest'; + +const credentials: ICredentials = { + AccountID: 'test-account-id', + AccessKeyID: 'test-access-key-id', + AccessKeySecret: 'test-access-key-secret', + SecurityToken: 'test-security-token', +}; + +describe('Acr static helpers', () => { + describe('isAcreeRegistry', () => { + it('returns true for an ACR EE vpc registry url', () => { + expect(Acr.isAcreeRegistry(ACREE_VPC)).toBe(true); + }); + + it('returns true for an ACR EE internet registry url', () => { + expect(Acr.isAcreeRegistry(ACREE_INTERNET)).toBe(true); + }); + + it('returns false for a non-acr registry url', () => { + expect(Acr.isAcreeRegistry(NON_ACR)).toBe(false); + }); + }); + + describe('isAcrRegistry', () => { + it('returns true for a shared-instance acr registry', () => { + expect(Acr.isAcrRegistry(ACR_INTERNET)).toBe(true); + }); + + it('returns true for an acr ee registry', () => { + expect(Acr.isAcrRegistry(ACREE_VPC)).toBe(true); + }); + + it('returns false for docker hub images', () => { + expect(Acr.isAcrRegistry(NON_ACR)).toBe(false); + }); + }); + + describe('isVpcAcrRegistry', () => { + it('returns true for a vpc acr registry', () => { + expect(Acr.isVpcAcrRegistry(ACR_VPC)).toBe(true); + }); + + it('returns false for an internet acr registry', () => { + expect(Acr.isVpcAcrRegistry(ACR_INTERNET)).toBe(false); + }); + + it('returns false for a non-acr registry', () => { + expect(Acr.isVpcAcrRegistry(NON_ACR)).toBe(false); + }); + }); + + describe('vpcImage2InternetImage', () => { + it('rewrites a vpc acr registry to its internet counterpart', () => { + expect(Acr.vpcImage2InternetImage(ACR_VPC)).toBe(ACR_INTERNET); + }); + + it('rewrites a vpc acr ee registry to its internet counterpart', () => { + expect(Acr.vpcImage2InternetImage(ACREE_VPC)).toBe(ACREE_INTERNET); + }); + + it('leaves a non-vpc image unchanged', () => { + expect(Acr.vpcImage2InternetImage(ACR_INTERNET)).toBe(ACR_INTERNET); + }); + }); +}); + +describe('Acr instance methods', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('checkAcr', () => { + it('resolves the instance id and returns whether the image exists', async () => { + // Arrange + (getAcrEEInstanceID as jest.Mock).mockResolvedValue('inst-123'); + (getAcrImageMeta as jest.Mock).mockResolvedValue(true); + const acr = new Acr('cn-hangzhou' as any, credentials); + + // Act + const exists = await acr.checkAcr(ACREE_INTERNET); + + // Assert + expect(exists).toBe(true); + // instanceName parsed from "test-registry" -> "test" + expect(getAcrEEInstanceID).toHaveBeenCalledWith('cn-hangzhou', credentials, 'test'); + expect(getAcrImageMeta).toHaveBeenCalledWith( + 'cn-hangzhou', + credentials, + ACREE_INTERNET, + 'inst-123', + ); + }); + }); + + describe('pushAcr', () => { + it('tags, logs in and pushes the image on the happy path', async () => { + // Arrange + (getAcrEEInstanceID as jest.Mock).mockResolvedValue('inst-123'); + (getDockerTmpUser as jest.Mock).mockResolvedValue({ + dockerTmpUser: 'tmp-user', + dockerTmpToken: 'tmp-token', + }); + const acr = new Acr('cn-hangzhou' as any, credentials); + + // Act + await acr.pushAcr(ACREE_VPC); + + // Assert + expect(checkDockerIsOK).toHaveBeenCalled(); + expect(getDockerTmpUser).toHaveBeenCalledWith('cn-hangzhou', credentials, 'inst-123'); + // vpc -> internet requires a docker tag command plus login + push + const commands = (runCommand as unknown as jest.Mock).mock.calls.map((c) => c[0]); + expect(commands.some((c: string) => c.startsWith('docker tag'))).toBe(true); + expect(commands.some((c: string) => c.includes('docker login'))).toBe(true); + expect(commands.some((c: string) => c.startsWith('docker push'))).toBe(true); + expect(sleep).toHaveBeenCalledWith(3); + }); + }); +}); diff --git a/__tests__/ut/resources/acr/login_test.ts b/__tests__/ut/resources/acr/login_test.ts new file mode 100644 index 00000000..d05f8e4e --- /dev/null +++ b/__tests__/ut/resources/acr/login_test.ts @@ -0,0 +1,178 @@ +import { ICredentials } from '@serverless-devs/component-interface'; +import { + getDockerTmpUser, + getAcrEEInstanceID, + getAcrImageMeta, + mockDockerConfigFile, +} from '../../../../src/resources/acr/login'; +import fse from 'fs-extra'; + +jest.mock('@alicloud/pop-core', () => { + const popRequest = jest.fn(); + const roaRequest = jest.fn(); + const Pop = jest.fn().mockImplementation(() => ({ request: popRequest })); + const ROAClient = jest.fn().mockImplementation(() => ({ request: roaRequest })); + return { + __esModule: true, + default: Pop, + ROAClient, + // exposed for assertions/control + popRequest, + roaRequest, + }; +}); + +jest.mock('fs-extra', () => ({ + __esModule: true, + default: { + readJSON: jest.fn().mockResolvedValue({}), + outputFile: jest.fn().mockResolvedValue(undefined), + }, +})); + +jest.mock('string-random', () => ({ + __esModule: true, + default: jest.fn().mockReturnValue('rand'), +})); + +jest.mock('../../../../src/logger', () => ({ + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + spin: jest.fn(), + append: jest.fn(), +})); + +const { popRequest, roaRequest } = jest.requireMock('@alicloud/pop-core'); + +const credentials: ICredentials = { + AccountID: 'test-account-id', + AccessKeyID: 'test-access-key-id', + AccessKeySecret: 'test-access-key-secret', + SecurityToken: 'test-security-token', +}; + +describe('acr/login', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('getDockerTmpUser', () => { + it('uses the ACR EE token endpoint when an instanceID is provided', async () => { + // Arrange + popRequest.mockResolvedValue({ TempUsername: 'ee-user', AuthorizationToken: 'ee-token' }); + + // Act + const result = await getDockerTmpUser('cn-hangzhou' as any, credentials, 'inst-1'); + + // Assert + expect(result).toEqual({ dockerTmpUser: 'ee-user', dockerTmpToken: 'ee-token' }); + expect(popRequest).toHaveBeenCalledWith( + 'GetAuthorizationToken', + { InstanceId: 'inst-1' }, + expect.any(Object), + ); + }); + + it('uses the shared registry token endpoint when no instanceID is provided', async () => { + // Arrange + roaRequest.mockResolvedValue({ + data: { tempUserName: 'shared-user', authorizationToken: 'shared-token' }, + }); + + // Act + const result = await getDockerTmpUser('cn-hangzhou' as any, credentials, ''); + + // Assert + expect(result).toEqual({ dockerTmpUser: 'shared-user', dockerTmpToken: 'shared-token' }); + }); + + it('rethrows non-auth errors from the shared registry token endpoint', async () => { + // Arrange + roaRequest.mockRejectedValue({ statusCode: 500, message: 'boom' }); + + // Act / Assert + await expect(getDockerTmpUser('cn-hangzhou' as any, credentials, '')).rejects.toBeDefined(); + }); + }); + + describe('getAcrImageMeta', () => { + it('returns false immediately for ACR EE instances', async () => { + const exists = await getAcrImageMeta('cn-hangzhou' as any, credentials, 'x/ns/repo:tag', 'inst-1'); + expect(exists).toBe(false); + expect(roaRequest).not.toHaveBeenCalled(); + }); + + it('returns true when the tag lookup succeeds for a shared instance', async () => { + roaRequest.mockResolvedValue({ data: {} }); + const exists = await getAcrImageMeta( + 'cn-hangzhou' as any, + credentials, + 'registry.cn-hangzhou.aliyuncs.com/ns/repo:tag', + '', + ); + expect(exists).toBe(true); + }); + + it('returns false when the tag lookup 404s', async () => { + roaRequest.mockRejectedValue({ statusCode: 404 }); + const exists = await getAcrImageMeta( + 'cn-hangzhou' as any, + credentials, + 'registry.cn-hangzhou.aliyuncs.com/ns/repo:tag', + '', + ); + expect(exists).toBe(false); + }); + }); + + describe('getAcrEEInstanceID', () => { + it('returns undefined when no instance name is provided', async () => { + const id = await getAcrEEInstanceID('cn-hangzhou' as any, credentials, ''); + expect(id).toBeUndefined(); + }); + + it('returns the instance id for a running matching instance', async () => { + popRequest.mockResolvedValue({ + TotalCount: 1, + Instances: [{ InstanceName: 'my-inst', InstanceStatus: 'RUNNING', InstanceId: 'id-9' }], + }); + const id = await getAcrEEInstanceID('cn-hangzhou' as any, credentials, 'my-inst'); + expect(id).toBe('id-9'); + }); + + it('throws when the matching instance is not running', async () => { + popRequest.mockResolvedValue({ + TotalCount: 1, + Instances: [{ InstanceName: 'my-inst', InstanceStatus: 'STOPPED', InstanceId: 'id-9' }], + }); + await expect( + getAcrEEInstanceID('cn-hangzhou' as any, credentials, 'my-inst'), + ).rejects.toThrow(/STOPPED/); + }); + }); + + describe('mockDockerConfigFile', () => { + it('writes a base64 auth entry keyed by registry host', async () => { + // Arrange + popRequest.mockResolvedValue({ TempUsername: 'ee-user', AuthorizationToken: 'ee-token' }); + + // Act + await mockDockerConfigFile( + 'cn-hangzhou' as any, + 'test-registry.cn-hangzhou.cr.aliyuncs.com/ns/repo:tag', + credentials, + 'inst-1', + ); + + // Assert + expect(fse.outputFile).toHaveBeenCalled(); + const [, content] = (fse.outputFile as jest.Mock).mock.calls[0]; + const parsed = JSON.parse(content); + const host = 'test-registry.cn-hangzhou.cr.aliyuncs.com'; + const expectedAuth = Buffer.from('ee-user:ee-token').toString('base64'); + expect(parsed.auths[host].auth).toBe(expectedAuth); + }); + }); +}); diff --git a/__tests__/ut/resources/oss/index_test.ts b/__tests__/ut/resources/oss/index_test.ts new file mode 100644 index 00000000..0e37e49c --- /dev/null +++ b/__tests__/ut/resources/oss/index_test.ts @@ -0,0 +1,147 @@ +import { ICredentials } from '@serverless-devs/component-interface'; +import OSS from '../../../../src/resources/oss/index'; +import * as utils from '../../../../src/utils/index'; + +// Shared mock for the OSS SDK client instance method +const mockInitOss = jest.fn(); + +jest.mock('@serverless-cd/srm-aliyun-oss', () => ({ + __esModule: true, + default: jest.fn().mockImplementation(() => ({ + initOss: mockInitOss, + })), +})); + +jest.mock('../../../../src/logger', () => ({ + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + spin: jest.fn(), + append: jest.fn(), +})); + +jest.mock('../../../../src/utils/index'); + +import logger from '../../../../src/logger'; + +describe('OSS', () => { + let credentials: ICredentials; + + beforeEach(() => { + jest.clearAllMocks(); + credentials = { + AccountID: 'test-account-id', + AccessKeyID: 'test-access-key-id', + AccessKeySecret: 'test-access-key-secret', + SecurityToken: 'test-security-token', + }; + (utils.isAppCenter as jest.Mock).mockReturnValue(false); + }); + + describe('constructor', () => { + it('sets the client config from region, credentials and endpoint', () => { + // Act + const oss = new OSS('cn-hangzhou' as any, credentials, 'oss-cn-hangzhou.aliyuncs.com'); + + // Assert + const config = (oss as any).config; + expect(config.accountID).toBe('test-account-id'); + expect(config.accessKeyId).toBe('test-access-key-id'); + expect(config.accessKeySecret).toBe('test-access-key-secret'); + expect(config.securityToken).toBe('test-security-token'); + expect(config.endpoint).toBe('oss-cn-hangzhou.aliyuncs.com'); + expect(config.regionId).toBe('cn-hangzhou'); + }); + }); + + describe('deploy', () => { + it('returns the full result when initOss provides all fields', async () => { + // Arrange + mockInitOss.mockResolvedValue({ + ossBucket: 'my-bucket', + readOnly: true, + mountDir: '/custom/mount', + bucketPath: '/data', + }); + const oss = new OSS('cn-hangzhou' as any, credentials, 'endpoint'); + + // Act + const result = await oss.deploy(); + + // Assert + expect(result).toEqual({ + ossBucket: 'my-bucket', + readOnly: true, + mountDir: '/custom/mount', + bucketPath: '/data', + }); + expect(mockInitOss).toHaveBeenCalledWith((oss as any).config, 'auto'); + }); + + it('applies defaults when initOss returns only the bucket', async () => { + // Arrange + mockInitOss.mockResolvedValue({ ossBucket: 'bucket-x' }); + const oss = new OSS('cn-hangzhou' as any, credentials, 'endpoint'); + + // Act + const result = await oss.deploy(); + + // Assert + expect(result).toEqual({ + ossBucket: 'bucket-x', + readOnly: false, + mountDir: '/mnt/bucket-x', + bucketPath: '/', + }); + }); + + it('applies defaults when initOss returns nothing', async () => { + // Arrange + mockInitOss.mockResolvedValue(undefined); + const oss = new OSS('cn-hangzhou' as any, credentials, 'endpoint'); + + // Act + const result = await oss.deploy(); + + // Assert + expect(result).toEqual({ + ossBucket: '', + readOnly: false, + mountDir: '/mnt/', + bucketPath: '/', + }); + }); + + it('logs via logger.info in the AppCenter branch', async () => { + // Arrange + (utils.isAppCenter as jest.Mock).mockReturnValue(true); + mockInitOss.mockResolvedValue({ ossBucket: 'bucket-y' }); + const oss = new OSS('cn-shanghai' as any, credentials, 'endpoint'); + + // Act + await oss.deploy(); + + // Assert + expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('created oss region')); + expect(logger.spin).not.toHaveBeenCalled(); + }); + + it('logs via logger.spin when not in AppCenter', async () => { + // Arrange + (utils.isAppCenter as jest.Mock).mockReturnValue(false); + mockInitOss.mockResolvedValue({ ossBucket: 'bucket-z' }); + const oss = new OSS('cn-beijing' as any, credentials, 'endpoint'); + + // Act + await oss.deploy(); + + // Assert + expect(logger.spin).toHaveBeenCalledWith( + 'creating', + 'oss', + expect.stringContaining('bucket-z'), + ); + }); + }); +}); diff --git a/__tests__/ut/resources/ram/index_test.ts b/__tests__/ut/resources/ram/index_test.ts new file mode 100644 index 00000000..61a5d2df --- /dev/null +++ b/__tests__/ut/resources/ram/index_test.ts @@ -0,0 +1,70 @@ +import { ICredentials } from '@serverless-devs/component-interface'; +import Role, { RamClient } from '../../../../src/resources/ram/index'; + +// Mock external SDK dependencies +jest.mock('@serverless-cd/srm-aliyun-ram20150501'); +jest.mock('@alicloud/openapi-client'); +jest.mock('../../../../src/logger', () => ({ + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + spin: jest.fn(), + append: jest.fn(), +})); + +describe('Role', () => { + describe('isRoleArnFormat', () => { + it('returns true for a valid role arn', () => { + // Arrange / Act / Assert + expect(Role.isRoleArnFormat('acs:ram::123456789:role/my-role')).toBe(true); + }); + + it('returns false for a plain role name', () => { + expect(Role.isRoleArnFormat('my-role')).toBe(false); + }); + + it('returns false when the account id segment is missing digits', () => { + expect(Role.isRoleArnFormat('acs:ram:::role/my-role')).toBe(false); + }); + }); + + describe('completionArn', () => { + it('returns the value unchanged when input is not a string', () => { + // Arrange + const notAString: any = { foo: 'bar' }; + + // Act + const result = Role.completionArn(notAString, 'acct-1'); + + // Assert + expect(result).toBe(notAString); + }); + + it('returns the arn unchanged when it is already in arn format', () => { + const arn = 'acs:ram::123456789:role/existing-role'; + expect(Role.completionArn(arn, 'acct-1')).toBe(arn); + }); + + it('assembles an arn from a plain role name and account id', () => { + expect(Role.completionArn('my-role', '123456789')).toBe( + 'acs:ram::123456789:role/my-role', + ); + }); + }); +}); + +describe('RamClient', () => { + it('constructs without throwing given mock credentials', () => { + // Arrange + const credentials: ICredentials = { + AccountID: 'test-account-id', + AccessKeyID: 'test-access-key-id', + AccessKeySecret: 'test-access-key-secret', + SecurityToken: 'test-security-token', + }; + + // Act / Assert + expect(() => new RamClient(credentials)).not.toThrow(); + }); +}); diff --git a/__tests__/ut/resources/vpc-nas/index_test.ts b/__tests__/ut/resources/vpc-nas/index_test.ts new file mode 100644 index 00000000..baeed4fc --- /dev/null +++ b/__tests__/ut/resources/vpc-nas/index_test.ts @@ -0,0 +1,141 @@ +import { ICredentials } from '@serverless-devs/component-interface'; +import VpcNas from '../../../../src/resources/vpc-nas/index'; +import PopClient from '@serverless-cd/srm-aliyun-pop-core'; + +jest.mock('@serverless-cd/srm-aliyun-pop-core', () => { + const request = jest.fn(); + const getInitNasConfigAsFc = jest.fn(); + const getInitVpcConfigAsFc = jest.fn(); + const Client = jest + .fn() + .mockImplementation(() => ({ request, getInitNasConfigAsFc, getInitVpcConfigAsFc })); + (Client as any).__mocks = { request, getInitNasConfigAsFc, getInitVpcConfigAsFc }; + return { __esModule: true, default: Client }; +}); + +jest.mock('../../../../src/default/resources', () => ({ + VPC_AND_NAS_NAME: 'default-vpc-nas', +})); + +jest.mock('../../../../src/logger', () => ({ + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + spin: jest.fn(), + append: jest.fn(), +})); + +const mocks = (PopClient as any).__mocks as { + request: jest.Mock; + getInitNasConfigAsFc: jest.Mock; + getInitVpcConfigAsFc: jest.Mock; +}; + +const credentials: ICredentials = { + AccountID: 'test-account-id', + AccessKeyID: 'test-access-key-id', + AccessKeySecret: 'test-access-key-secret', + SecurityToken: 'test-security-token', +}; + +describe('VpcNas', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('constructor', () => { + it('creates a nas client and a vpc client', () => { + // Act + const vpcNas = new VpcNas('cn-hangzhou' as any, credentials); + + // Assert + expect(vpcNas).toBeDefined(); + expect((PopClient as unknown as jest.Mock)).toHaveBeenCalledTimes(2); + }); + }); + + describe('getVpcNasRule', () => { + it('returns the existing VpcName when the vpc already has a name', async () => { + // Arrange + mocks.request.mockResolvedValue({ VpcName: 'existing-vpc' }); + const vpcNas = new VpcNas('cn-hangzhou' as any, credentials); + + // Act + const rule = await vpcNas.getVpcNasRule({ vpcId: 'vpc-1' } as any); + + // Assert + expect(rule).toBe('existing-vpc'); + }); + + it('assigns a generated VpcName when the vpc name is empty', async () => { + // Arrange + mocks.request + .mockResolvedValueOnce({ VpcName: ' ' }) + .mockResolvedValueOnce({ ok: true }); + const vpcNas = new VpcNas('cn-hangzhou' as any, credentials); + + // Act + const rule = await vpcNas.getVpcNasRule({ vpcId: 'vpc-2' } as any); + + // Assert + expect(rule).toBe('VpcName-vpc-2'); + expect(mocks.request).toHaveBeenCalledTimes(2); + }); + + it('returns the default name when no vpcConfig is provided', async () => { + const vpcNas = new VpcNas('cn-hangzhou' as any, credentials); + const rule = await vpcNas.getVpcNasRule(undefined as any); + expect(rule).toBe('default-vpc-nas'); + expect(mocks.request).not.toHaveBeenCalled(); + }); + + it('falls back to the default name when the vpc request fails', async () => { + // Arrange + mocks.request.mockRejectedValue(new Error('network error')); + const vpcNas = new VpcNas('cn-hangzhou' as any, credentials); + + // Act + const rule = await vpcNas.getVpcNasRule({ vpcId: 'vpc-err' } as any); + + // Assert + expect(rule).toBe('default-vpc-nas'); + }); + }); + + describe('deploy', () => { + it('returns the auto nas config when nasAuto is true', async () => { + // Arrange + const nasResult = { mountTargetDomain: 'mt', fileSystemId: 'fs' }; + mocks.getInitNasConfigAsFc.mockResolvedValue(nasResult); + const vpcNas = new VpcNas('cn-hangzhou' as any, credentials); + + // Act + const result = await vpcNas.deploy({ nasAuto: true }); + + // Assert + expect(result).toBe(nasResult); + expect(mocks.getInitNasConfigAsFc).toHaveBeenCalled(); + expect(mocks.getInitVpcConfigAsFc).not.toHaveBeenCalled(); + }); + + it('returns a vpcConfig wrapper when nasAuto is false', async () => { + // Arrange + mocks.getInitVpcConfigAsFc.mockResolvedValue({ + vpcId: 'vpc-9', + vSwitchIds: ['vsw-1'], + securityGroupId: 'sg-1', + }); + const vpcNas = new VpcNas('cn-hangzhou' as any, credentials); + + // Act + const result = await vpcNas.deploy({ nasAuto: false }); + + // Assert + expect(result).toEqual({ + vpcConfig: { vpcId: 'vpc-9', vSwitchIds: ['vsw-1'], securityGroupId: 'sg-1' }, + }); + expect(mocks.getInitNasConfigAsFc).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/__tests__/ut/utils/run-command_test.ts b/__tests__/ut/utils/run-command_test.ts new file mode 100644 index 00000000..4adc5392 --- /dev/null +++ b/__tests__/ut/utils/run-command_test.ts @@ -0,0 +1,168 @@ +import { spawn } from 'child_process'; +import runCommand from '../../../src/utils/run-command'; +import logger from '../../../src/logger'; + +jest.mock('child_process'); + +jest.mock('../../../src/logger', () => ({ + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + spin: jest.fn(), + append: jest.fn(), +})); + +const spawnMock = spawn as unknown as jest.Mock; + +interface FakeProcess { + stdout: { on: jest.Mock }; + stderr: { on: jest.Mock }; + on: jest.Mock; + emitStdout: (data: string) => void; + emitStderr: (data: string) => void; + close: (code: number) => void; +} + +function createFakeProcess(): FakeProcess { + const stdoutHandlers: Record void> = {}; + const stderrHandlers: Record void> = {}; + const procHandlers: Record void> = {}; + + return { + stdout: { + on: jest.fn((event: string, cb: (data: string) => void) => { + stdoutHandlers[event] = cb; + }), + }, + stderr: { + on: jest.fn((event: string, cb: (data: string) => void) => { + stderrHandlers[event] = cb; + }), + }, + on: jest.fn((event: string, cb: (code: number) => void) => { + procHandlers[event] = cb; + }), + emitStdout: (data: string) => stdoutHandlers['data'] && stdoutHandlers['data'](data), + emitStderr: (data: string) => stderrHandlers['data'] && stderrHandlers['data'](data), + close: (code: number) => procHandlers['close'] && procHandlers['close'](code), + }; +} + +describe('runCommand', () => { + let consoleLogSpy: jest.SpyInstance; + + beforeEach(() => { + consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); + }); + + afterEach(() => { + jest.clearAllMocks(); + consoleLogSpy.mockRestore(); + }); + + it('exposes showStdout enum', () => { + // Assert + expect(runCommand.showStdout).toEqual({ + inherit: 'inherit', + pipe: 'pipe', + ignore: 'ignore', + }); + }); + + it('resolves when process closes with code 0', async () => { + // Arrange + const fake = createFakeProcess(); + spawnMock.mockReturnValue(fake); + + // Act + const promise = runCommand('echo hello', runCommand.showStdout.inherit); + fake.close(0); + + // Assert + await expect(promise).resolves.toBeUndefined(); + expect(spawnMock).toHaveBeenCalledTimes(1); + }); + + it('rejects with "command failed with code N" when close code is non-zero', async () => { + // Arrange + const fake = createFakeProcess(); + spawnMock.mockReturnValue(fake); + + // Act + const promise = runCommand('bad command', runCommand.showStdout.inherit); + fake.close(2); + + // Assert + await expect(promise).rejects.toThrow('command failed with code 2'); + }); + + it('forwards stdout and stderr data to logger.append in pipe mode', async () => { + // Arrange + const fake = createFakeProcess(); + spawnMock.mockReturnValue(fake); + + // Act + const promise = runCommand('run something', runCommand.showStdout.pipe); + fake.emitStdout('stdout-line'); + fake.emitStderr('stderr-line'); + fake.close(0); + await promise; + + // Assert + expect(logger.append).toHaveBeenCalledWith('stdout-line'); + expect(logger.append).toHaveBeenCalledWith('stderr-line'); + }); + + it('does not register stdout/stderr listeners when not in pipe mode', async () => { + // Arrange + const fake = createFakeProcess(); + spawnMock.mockReturnValue(fake); + + // Act + const promise = runCommand('run something', runCommand.showStdout.inherit); + fake.close(0); + await promise; + + // Assert + expect(fake.stdout.on).not.toHaveBeenCalled(); + expect(fake.stderr.on).not.toHaveBeenCalled(); + expect(logger.append).not.toHaveBeenCalled(); + }); + + it('passes shellScript as an extra argument and forwards cwd to spawn options', async () => { + // Arrange + const fake = createFakeProcess(); + spawnMock.mockReturnValue(fake); + + // Act + const promise = runCommand('bash', runCommand.showStdout.pipe, 'script.sh', '/work/dir'); + fake.close(0); + await promise; + + // Assert + expect(spawnMock).toHaveBeenCalledWith( + 'bash', + ['script.sh'], + expect.objectContaining({ shell: true, stdio: 'pipe', cwd: '/work/dir' }), + ); + }); + + it('merges env-style prefix into the command name', async () => { + // Arrange + const fake = createFakeProcess(); + spawnMock.mockReturnValue(fake); + + // Act + const promise = runCommand('KEY=val node app.js', runCommand.showStdout.inherit); + fake.close(0); + await promise; + + // Assert + expect(spawnMock).toHaveBeenCalledWith( + 'KEY=val node', + ['app.js'], + expect.objectContaining({ shell: true, stdio: 'inherit' }), + ); + }); +}); diff --git a/docs/CONTRIB.md b/docs/CONTRIB.md new file mode 100644 index 00000000..9814b42e --- /dev/null +++ b/docs/CONTRIB.md @@ -0,0 +1,226 @@ +# Contributing Guide + +## Reporting Issues + +- **Security issues**: do not open a public issue. Email [service@serverlessfans.com](mailto:service@serverlessfans.com) privately. +- **Bugs and feature requests**: open an issue at [fc3 issues](https://github.com/devsapp/fc3/issues). Search existing issues first, and remove any secrets (keys, tokens, private data) from your report. + +## Development Setup + +### Prerequisites + +- Node.js 16+ (Node 20 recommended) +- npm or yarn +- Docker (for container build operations) +- Alibaba Cloud account with Function Compute access +- Access credentials for the private Aliyun npm registry (see [Private Registry Authentication](#private-registry-authentication) below) + +### Private Registry Authentication + +Some dependencies are **not published to the public npm registry** and can only be +resolved from a private Aliyun package registry. Notably: + +- `@serverless-devs/docker-image-builder` (direct dependency) — returns 404 on both + `registry.npmjs.org` and `registry.npmmirror.com`. +- `@alicloud/sls20191023` (transitive, via `@serverless-cd/srm-aliyun-sls20201230`). + +Their `resolved` URLs are pinned in `package-lock.json` to +`https://packages.aliyun.com/670e108663cd360abfe4be65/npm/npm-registry/`, which +requires **HTTP Basic auth** (a direct fetch returns `401`). Without valid +credentials, `npm install` will fail for these packages. + +Add the auth token to your **global** `~/.npmrc` (never commit it): + +```ini +//packages.aliyun.com/670e108663cd360abfe4be65/npm/npm-registry/:_authToken= +``` + +> **CI note:** CI pipelines and new contributors must configure this token (e.g. via +> a masked secret) before `npm install`, or dependency resolution will break. This is +> a pre-existing project constraint, independent of the `overrides`/`resolutions` +> fields in `package.json`. + +### Installation + +```bash +# Clone the repository +git clone https://github.com/devsapp/fc3.git +cd fc3 + +# Install dependencies +npm install + +# Build the project +npm run build +``` + +## Available Scripts + +| Script | Command | Description | +| ----------------- | --------------------------------------------------------------------- | ----------------------------------------------- | +| `build` | `ncc build src/index.ts -m -o dist` | Build production bundle using Vercel ncc | +| `watch` | `npx tsc -w -p tsconfig.json` | Watch mode for development | +| `start` | `npm run watch` | Alias for watch mode | +| `test` | `jest --config jestconfig.json __tests__/ut --coverage` | Run unit tests with coverage (no credentials) | +| `test:it` | `jest --config jestconfig.json __tests__/it` | Run integration tests (needs cloud credentials) | +| `deadcode` | `ts-prune -p tsconfig.json` | Audit unused exports | +| `depcheck` | `depcheck` | Audit unused dependencies | +| `format` | `prettier --write src` | Format source code with Prettier | +| `lint` | `f2elint scan` | Run linter checks | +| `fix` | `f2elint fix` | Auto-fix linting issues | +| `publish` | `npm i && npm run build && s registry publish` | Build and publish to registry | +| `generate-schema` | `typescript-json-schema ./src/interface/index.ts IProps --required` | Generate JSON schema from TypeScript interfaces | +| `typecheck` | `tsc --noEmit -p tsconfig.json` | Type-check without emitting (CI gate) | +| `prebuild` | node one-liner: rm + mkdir `dist`, copy `src/schema.json` | Prepare dist directory before build (portable) | +| `prewatch` | node one-liner: mkdir `dist`, copy `src/schema.json` | Ensure dist and schema.json exist before watch | + +## Development Workflow + +### 1. Branch Strategy + +- `master` - Main branch for releases +- Feature branches: `feature/` +- Fix branches: `fix/` + +### 2. Code Style + +- TypeScript with strict typing +- Follow Prettier formatting rules +- Use f2elint for linting compliance +- Max line length: 120 characters + +### 3. Commit Convention + +``` +: + +Types: feat, fix, refactor, docs, test, chore, perf, ci +``` + +### 4. Testing + +#### Test Structure + +``` +__tests__/ +├── ut/ # Unit tests +│ ├── base_test.ts +│ ├── deploy_test.ts +│ └── ... +└── it/ # Integration tests + └── deploy_test.ts +``` + +#### Running Tests + +```bash +# Run unit tests with coverage (default, no credentials needed) +npm test + +# Run a specific test file +npx jest __tests__/ut/deploy_test.ts + +# Run integration tests (requires cloud credentials) +npm run test:it + +# Update snapshots +npx jest --updateSnapshot +``` + +#### Test Naming Convention + +- Unit test files: `{module}_test.ts` +- Test functions: `describe('{feature}', () => { test('{scenario}', ...) })` + +### 5. Building + +The project uses Vercel ncc for bundling, which produces a single JavaScript file suitable for distribution. + +```bash +# Production build +npm run build + +# Development with watch +npm run watch +``` + +### 6. Pull Request Process + +1. Create feature branch from `master` +2. Make changes with tests +3. Run linting: `npm run lint` +4. Fix issues: `npm run fix` if needed +5. Format code: `npm run format` +6. Run tests: `npm test` +7. Create PR with description and test plan + +## Project Structure + +``` +src/ +├── commands-help/ # Help documentation for CLI commands +├── default/ # Default configuration handlers +├── interface/ # TypeScript interfaces and types +├── resources/ # Cloud resource management (FC, RAM, SLS, OSS) +├── subCommands/ # CLI subcommand implementations +├── utils/ # Shared utility functions +├── base.ts # Base class with common functionality +├── constant.ts # Global constants +├── index.ts # Main entry point +└── logger.ts # Logging utilities +``` + +## Key Subcommands + +| Command | Description | +| --------- | ----------------------------- | +| `deploy` | Deploy functions and triggers | +| `build` | Build function code/packages | +| `local` | Local development and testing | +| `invoke` | Invoke functions remotely | +| `info` | Query function information | +| `logs` | Query function logs | +| `remove` | Remove deployed resources | +| `plan` | Show deployment plan | +| `layer` | Manage function layers | +| `version` | Version management | +| `alias` | Alias management | +| `sync` | Sync configurations | + +## Debugging + +### Local Debugging + +```bash +# Start local function +s local start + +# Invoke locally with debug mode +s local invoke --debug +``` + +### VS Code Debugging + +Create `.vscode/launch.json`: + +```json +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Debug Tests", + "program": "${workspaceFolder}/node_modules/.bin/jest", + "args": ["--runInBand", "--config", "jestconfig.json"], + "console": "integratedTerminal" + } + ] +} +``` + +## Resources + +- [Serverless Devs Documentation](https://github.com/Serverless-Devs/Serverless-Devs) +- [Alibaba Cloud FC3 Documentation](https://help.aliyun.com/product/fc.html) +- [Architecture Guide](./architecture.md) diff --git a/docs/RUNBOOK.md b/docs/RUNBOOK.md new file mode 100644 index 00000000..b40188cb --- /dev/null +++ b/docs/RUNBOOK.md @@ -0,0 +1,321 @@ +# Runbook + +## Deployment Procedures + +### Production Deployment + +#### Prerequisites + +- Alibaba Cloud credentials configured +- Serverless Devs CLI (`s`) installed +- Docker available (for container builds) + +#### Standard Deployment Flow + +```bash +# 1. Verify configuration +s plan + +# 2. Build function code +s build + +# 3. Deploy to Function Compute +s deploy + +# 4. Verify deployment +s info +``` + +#### Environment-Specific Deployment + +```bash +# Deploy to specific region +s deploy --region cn-hangzhou + +# Deploy with specific service name +s deploy --service-name my-service + +# Deploy only function (skip triggers) +s deploy --function-only +``` + +### Container Image Deployment + +```bash +# Build with Docker +s build --docker + +# Build with Kaniko (CI environments) +s build --kaniko + +# Build with BuildKit +s build --buildkit +``` + +### Layer Deployment + +```bash +# Publish a layer +s layer publish + +# Update function to use layer +s deploy --layers layerArn:version +``` + +## Monitoring and Alerts + +### SLS Log Integration + +The FC3 component integrates with Alibaba Cloud SLS (Log Service) for monitoring. + +```bash +# Query function logs (tail mode) +s logs --tail + +# Query logs for a specific instance +s logs --instance-id + +# Query logs with full-text search (SLS quoted syntax) +s logs --query "error AND timeout" + +# Query logs for specific time range +s logs --start-time "2026-01-01 00:00:00" --end-time "2026-01-01 23:59:59" +``` + +> **Note**: The `--query` parameter uses SLS full-text search syntax with quoted strings. +> Avoid field-specific query syntax (e.g., `Key:Value`) as it may not match all log entries. + +### Key Metrics to Monitor + +| Metric | Description | Alert Threshold | +| -------------------- | ------------------ | ---------------- | +| `FunctionInvocation` | Invocation count | Spike > 1000/min | +| `FunctionLatency` | Execution latency | > 5000ms | +| `FunctionErrors` | Error count | > 10 in 5min | +| `MemoryUsage` | Memory utilization | > 90% | +| `Throttles` | Throttled requests | > 0 | + +### SLS Project Structure + +- Project: `{serviceName}-project` +- Logstore: `{serviceName}-logstore` +- Metrics logstore: `{serviceName}-metrics` + +## Common Issues and Fixes + +### 1. Build Failures + +#### Docker Not Available + +**Error**: `Docker is not installed or daemon not running` + +**Fix**: + +```bash +# Check Docker status +docker info + +# Start Docker daemon (macOS) +open -a Docker + +# Install Docker if missing +# macOS: brew install --cask docker +``` + +#### Code Size Exceeded + +**Error**: `Code size exceeded maximum limit` + +**Fix**: + +```bash +# Use layer for dependencies +s layer publish --code ./dependencies + +# Deploy function with smaller code +s deploy --code ./src --layers +``` + +### 2. Deployment Failures + +#### Role Permission Issues + +**Error**: `The role ARN is invalid` or permission denied + +**Fix**: + +```bash +# Create required role with proper permissions +s deploy --role + +# Or use auto role creation +s deploy --auto-role +``` + +#### VPC Configuration Issues + +**Error**: `VPC configuration invalid` + +**Fix**: + +```yaml +# Verify VPC config in s.yaml +service: + vpcConfig: + vpcId: + securityGroupId: +``` + +#### Region Not Supported + +**Error**: `Region not supported` + +**Fix**: Use supported regions: + +- cn-hangzhou, cn-shanghai, cn-beijing +- cn-shenzhen, cn-zhangjiakou, cn-huhehaote +- cn-chengdu, cn-hongkong +- ap-northeast-1 (Tokyo), ap-southeast-1 (Singapore) + +### 3. Invocation Failures + +#### Timeout Errors + +**Error**: `Function execution timeout` + +**Fix**: + +```yaml +# Increase timeout in configuration +function: + timeout: 60 # Max 600 seconds +``` + +#### Memory Insufficient + +**Error**: `Out of memory` + +**Fix**: + +```yaml +# Increase memory configuration +function: + memorySize: 512 # Can be 128-3072 MB +``` + +### 4. HTTP URL Code Source + +**Issue**: Layer or function code supports HTTP URLs (v0.1.17+) + +**Configuration**: + +```yaml +# HTTP URL as code source +function: + code: https://example.com/function-code.zip + +layer: + code: https://example.com/layer-code.zip +``` + +### 5. SLS Query Syntax Issues + +**Issue**: Field-specific SLS query syntax (e.g., `Key:Value`) may not match all log entries + +**Fix**: Use quoted full-text search instead: + +```bash +# Wrong: field-specific syntax (may miss entries) +s logs --query "Level:Error" + +# Correct: full-text search with quoted syntax +s logs --query '"error"' +``` + +### 6. NAS Configuration Issues + +**Error**: `NAS mount point invalid` + +**Fix**: + +```yaml +# Configure NAS properly +function: + nasConfig: + userId: 10003 + groupId: 10003 + mountPoints: + - serverAddr: + mountDir: /mnt/dir +``` + +## Rollback Procedures + +### Version-Based Rollback + +```bash +# List versions +s version list + +# Rollback to previous version +s alias update --alias-name prod --version + +# Or create alias pointing to old version +s alias create --alias-name rollback --version +``` + +### Complete Redeployment + +```bash +# 1. Remove current deployment +s remove --all + +# 2. Deploy previous configuration +# Restore s.yaml from backup or git +git checkout HEAD~1 s.yaml + +# 3. Redeploy +s deploy +``` + +### Emergency Rollback Steps + +1. **Identify issue**: Check logs `s logs --tail` +2. **Stop traffic**: Update alias to old version +3. **Verify rollback**: `s info` and test invocation +4. **Investigate**: Review logs and metrics +5. **Fix forward**: Once identified, deploy fix to new version + +## Health Checks + +### Post-Deployment Verification + +```bash +# Check function status +s info + +# Test invocation +s invoke --event '{"test": true}' + +# Verify logs flowing +s logs --tail --limit 10 +``` + +### Routine Health Monitoring + +```bash +# Daily log review +s logs --start-time yesterday + +# Instance health check +s instance list + +# Provision status (if using provisioned concurrency) +s provision get +``` + +## Support Contacts + +- GitHub Issues: https://github.com/devsapp/fc3/issues +- Serverless Devs Community: https://github.com/Serverless-Devs/Serverless-Devs +- Alibaba Cloud Support: https://help.aliyun.com diff --git a/docs/architecture.md b/docs/architecture.md index 3868d2cc..90891d37 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -293,88 +293,21 @@ src/ - `IConcurrencyConfig` - 并发配置接口 - `IProvisionConfig` - 预留配置接口 -## 技术特点 +## 部署流程 -### 1. 全生命周期管理 +`deploy` 是最复杂的命令,串联了大多数资源模块,流程如下: -- 支持函数的创建、开发、调试、部署、运维全流程 -- 提供完整的 CI/CD 集成能力 +1. `base.ts` 的 `handlePreRun` 预处理输入:补全镜像、角色、NAS 配置,套用默认值。 +2. `deploy/impl/function.ts` 拉取线上函数配置,与本地做 diff(`_plan`),无差异则跳过,有差异则交互确认。 +3. 处理 `auto` 资源(`_deployAuto`):按需创建 SLS 日志、OSS 挂载、RAM 角色、VPC/NAS,并回填到函数配置。 +4. 处理代码:非容器运行时压缩上传代码包(校验 CRC64,未变更则跳过);容器运行时推送镜像到 ACR。 +5. 调用 FC SDK 完成函数、触发器、域名、并发、预留等配置的部署。 -### 2. 多环境支持 +## 配置验证 -- 支持多种构建环境(Docker、Kaniko、BuildKit) -- 支持多种运行环境(本地、云端) +组件用 `src/schema.json`(由 `npm run generate-schema` 从 `interface/` 生成)对 `IProps` 做 JSON Schema 校验,`utils/verify` 在运行前检查配置。 -### 3. 多语言支持 +## 已知设计说明 -- 支持 Python、Node.js、Java、Go、PHP、.NET 等多种语言 -- 支持自定义运行时和自定义容器 - -### 4. 安全发布 - -- 通过配置感知实现安全更新 -- 支持角色权限管理 - -### 5. 可观测性 - -- 集成 SLS 日志服务 -- 提供完善的日志查询功能 - -### 6. 多模调试 - -- 支持本地运行和在线运行 -- 提供多种调试模式 - -## 配置管理 - -### 默认配置 - -- `FUNCTION_DEFAULT_CONFIG` - 函数默认配置 -- `FUNCTION_CUSTOM_DEFAULT_CONFIG` - 自定义函数默认配置 -- `IMAGE_ACCELERATION_REGION` - 镜像加速地域配置 - -### 配置验证 - -- 使用 JSON Schema 进行配置验证 -- 支持运行时配置检查 - -## 错误处理 - -### 错误类型 - -- 配置错误 -- 权限错误 -- 网络错误 -- 资源错误 - -### 错误处理策略 - -- 提供具体的错误信息 -- 支持错误重试机制 -- 记录详细的错误日志 - -## 性能优化 - -### 并行处理 - -- 支持并行部署多个资源 -- 优化网络请求性能 - -### 缓存机制 - -- 配置缓存 -- 资源状态缓存 - -## 扩展性 - -### 插件机制 - -- 支持自定义构建器 -- 支持自定义触发器 -- 支持自定义运行时 - -### 配置扩展 - -- 支持自定义配置项 -- 支持环境变量配置 -- 支持配置文件继承 +- **model / fileManager 重复**:`subCommands/model/index.ts`(`ModelService`)与 `fileManager.ts`(`ArtModelService`)实现相近,按 `modelConfig.solution` 分支选择。两者逻辑存在差异,尚未合并——合并前需补齐覆盖两个分支的测试,属于风险改动。 +- **依赖重叠**:项目同时依赖 FC2 与 FC3 SDK、以及多套 OSS/归档库,均有实际引用(详见 `npm run depcheck` 报告),不可直接移除。 diff --git a/docs/project-summary.md b/docs/project-summary.md deleted file mode 100644 index dee1f54d..00000000 --- a/docs/project-summary.md +++ /dev/null @@ -1,174 +0,0 @@ -# FC3 组件项目总结 - -## 项目概述 - -FC3 是阿里云函数计算 3.0 的 Serverless Devs 组件,提供全生命周期的函数计算管理能力。本项目已完成代码库分析、架构设计、单元测试补充和技术文档创建。 - -## 完成的工作 - -### 1. 代码库分析 ✅ - -- **深度分析**: 全面分析了 114 个 TypeScript 文件,涵盖核心模块、子命令、资源管理等 -- **架构理解**: 理解了分层架构设计,包括用户接口层、命令处理层、业务逻辑层、资源管理层和基础设施层 -- **功能识别**: 识别了 18 个主要子命令和 5 个核心资源管理模块 - -### 2. 架构图和功能说明 ✅ - -- **架构图**: 使用 Mermaid 创建了详细的架构图,展示了模块间的关系和数据流 -- **功能说明**: 详细描述了每个模块的职责和功能 -- **技术特点**: 总结了全生命周期管理、多环境支持、多语言支持、安全发布、可观测性和多模调试等特点 - -### 3. 测试计划制定 ✅ - -- **测试覆盖分析**: 分析了现有测试覆盖情况,识别了 11 个已测试文件和大量未测试模块 -- **优先级排序**: 按高、中、低优先级对测试模块进行了分类 -- **实施计划**: 制定了 4 个阶段的测试实施计划,预计 4-5 周完成 - -### 4. 单元测试补充 ✅ - -创建了以下核心模块的完整单元测试: - -#### 核心模块测试 - -- **`index_test.ts`**: 主入口模块测试,覆盖所有 18 个子命令方法 -- **`base_test.ts`**: 基础模块测试,覆盖预处理逻辑、角色处理、配置应用等 - -#### 业务模块测试 - -- **`deploy_test.ts`**: 部署模块测试,覆盖函数和触发器部署逻辑 -- **`build_test.ts`**: 构建模块测试,覆盖多环境构建支持 -- **`fc_resource_test.ts`**: FC 资源管理测试,覆盖函数计算核心功能 - -### 5. 技术文档创建 ✅ - -- **架构文档**: `docs/architecture.md` - 详细的架构说明和模块介绍 -- **测试计划**: `docs/testing-plan.md` - 完整的测试策略和实施计划 -- **技术文档**: `docs/technical-documentation.md` - 全面的技术文档,包括 API 接口、配置说明、最佳实践等 - -## 项目架构 - -### 核心架构 - -``` -FC3 组件 -├── 主入口模块 (Fc) -│ ├── deploy() - 部署函数和触发器 -│ ├── build() - 构建函数代码 -│ ├── local() - 本地运行和调试 -│ ├── invoke() - 调用函数 -│ ├── info() - 查询资源信息 -│ └── 其他 13 个子命令 -├── 基础模块 (Base) -│ ├── handlePreRun() - 预处理逻辑 -│ ├── 角色权限处理 -│ ├── 默认配置应用 -│ └── 环境检测 -├── 子命令模块 (subCommands) -│ ├── deploy/ - 部署模块 -│ ├── build/ - 构建模块 -│ ├── local/ - 本地运行模块 -│ └── 其他 15 个子命令 -└── 资源管理模块 (resources) - ├── fc/ - 函数计算资源 - ├── ram/ - 权限管理 - ├── sls/ - 日志服务 - ├── vpc-nas/ - 网络存储 - └── acr/ - 容器镜像 -``` - -### 技术特点 - -- **全生命周期管理**: 支持创建、开发、调试、部署、运维全流程 -- **多环境支持**: Docker、Kaniko、BuildKit 等多种构建环境 -- **多语言支持**: Python、Node.js、Java、Go、PHP、.NET 等 -- **安全发布**: 配置感知的安全更新机制 -- **可观测性**: 集成 SLS 日志服务 -- **多模调试**: 本地运行和在线运行支持 - -## 测试覆盖情况 - -### 已完成的测试 - -- **核心模块**: 主入口和基础模块 - 100% 覆盖 -- **部署模块**: 部署逻辑和资源管理 - 95% 覆盖 -- **构建模块**: 多环境构建支持 - 90% 覆盖 -- **资源管理**: FC 核心功能 - 85% 覆盖 - -### 测试质量 - -- **测试用例数量**: 200+ 个测试用例 -- **覆盖场景**: 正常流程、异常处理、边界条件、错误恢复 -- **Mock 策略**: 完整的外部依赖 Mock -- **断言质量**: 详细的断言验证 - -## 文档质量 - -### 架构文档 - -- **完整性**: 覆盖所有核心模块和功能 -- **清晰性**: 使用图表和代码示例说明 -- **实用性**: 提供具体的配置示例和最佳实践 - -### 技术文档 - -- **API 接口**: 详细的接口说明和参数描述 -- **配置说明**: 完整的配置项说明和示例 -- **最佳实践**: 项目结构、环境管理、安全实践等 -- **故障排查**: 常见问题和解决方案 - -### 测试文档 - -- **测试策略**: 完整的测试计划和策略 -- **实施指南**: 详细的测试实施步骤 -- **质量保证**: 测试质量控制和维护指南 - -## 项目价值 - -### 1. 代码质量提升 - -- **测试覆盖**: 核心模块测试覆盖率达到 90% 以上 -- **代码规范**: 遵循 TypeScript 和 Jest 最佳实践 -- **错误处理**: 完善的错误处理和重试机制 - -### 2. 开发效率提升 - -- **文档完善**: 详细的技术文档和 API 说明 -- **最佳实践**: 提供完整的配置示例和最佳实践 -- **故障排查**: 常见问题的快速解决方案 - -### 3. 维护性提升 - -- **架构清晰**: 模块化设计,职责分离 -- **测试完备**: 自动化测试保证代码质量 -- **文档齐全**: 便于新开发者理解和维护 - -## 后续建议 - -### 1. 测试完善 - -- **继续补充**: 完成剩余子命令模块的测试 -- **集成测试**: 添加端到端的集成测试 -- **性能测试**: 添加性能基准测试 - -### 2. 文档维护 - -- **定期更新**: 随着功能更新及时更新文档 -- **用户反馈**: 收集用户反馈,持续改进文档质量 -- **示例丰富**: 添加更多实际使用场景的示例 - -### 3. 功能扩展 - -- **新特性**: 根据用户需求添加新功能 -- **性能优化**: 持续优化构建和部署性能 -- **安全增强**: 加强安全配置和权限管理 - -## 总结 - -本项目成功完成了 FC3 组件的全面分析和改进工作: - -1. **深度理解**: 全面分析了代码库结构和功能 -2. **架构设计**: 创建了清晰的架构图和功能说明 -3. **测试补充**: 为核心模块创建了完整的单元测试 -4. **文档完善**: 提供了详细的技术文档和最佳实践 - -这些工作显著提升了项目的代码质量、开发效率和维护性,为后续的功能扩展和优化奠定了坚实的基础。项目现在具备了良好的测试覆盖、清晰的架构设计和完善的技术文档,能够支持团队的高效开发和维护工作。 diff --git a/docs/readme.md b/docs/readme.md index 7b65e0b5..4aa93068 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -1,48 +1,19 @@ # FC3 组件文档 -## 简介 +FC3 是阿里云函数计算 3.0 的 Serverless Devs 组件,提供创建、开发、调试、部署、运维的全生命周期管理能力。 -FC3 是阿里云函数计算 3.0 的 Serverless Devs 组件,提供全生命周期的函数计算管理能力,包括创建、开发、调试、部署、运维等功能。 - -建议您直接阅读 [Serverless Devs 官方文档](https://manual.serverless-devs.com/user-guide/aliyun/#fc3) +用户使用请优先阅读 [Serverless Devs 官方文档](https://manual.serverless-devs.com/user-guide/aliyun/#fc3);本目录面向本仓库的开发与运维。 ## 文档目录 -- [架构文档](./architecture.md) - 详细的项目架构说明和模块介绍 -- [技术文档](./technical-documentation.md) - 全面的技术文档,包括 API 接口、配置说明、最佳实践等 -- [测试计划](./testing-plan.md) - 完整的测试策略和实施计划 -- [项目总结](./project-summary.md) - 项目完成情况和总结 - -## 快速开始 - -### 安装依赖 - -```bash -npm install -``` +- [架构说明](./architecture.md) — 模块划分、目录结构与核心流程 +- [贡献指南](./CONTRIB.md) — 开发环境、脚本、测试与提交流程 +- [运维手册](./RUNBOOK.md) — 部署、监控、常见故障处理与回滚 -### 构建项目 +## 本地开发 ```bash -npm run build +npm install # 安装依赖(需私有 Aliyun registry 鉴权,见贡献指南) +npm run build # 构建产物 +npm test # 运行单元测试(含覆盖率,无需云凭证) ``` - -### 运行测试 - -```bash -npm test -``` - -## 核心功能 - -1. **全生命周期管理**:组件拥有项目的创建、开发、调试、部署、运维全生命周期管理能力 -2. **安全发布**:通过其他形式对函数进行变更,组件可以感知并安全更新 -3. **快速集成**:借助于 Serverless Devs 的集成性和被集成性,可以与常见的 CI/CD 平台工具集成 -4. **可观测性**:拥有完善的可观测性,在客户端可以通过日志查询等命令进行执行日志观测 -5. **多模调试**:提出了多模调试方案,可以同时满足开发态、运维态的不同调试需求 - -## 贡献指南 - -我们非常希望您可以和我们一起贡献这个项目。贡献内容包括不限于代码的维护、应用/组件的贡献、文档的完善等。 - -请参考[贡献指南](../CONTRIBUTING.md)了解更多详情。 diff --git a/docs/technical-documentation.md b/docs/technical-documentation.md deleted file mode 100644 index 894082b4..00000000 --- a/docs/technical-documentation.md +++ /dev/null @@ -1,732 +0,0 @@ -# FC3 组件技术文档 - -## 概述 - -FC3 是阿里云函数计算 3.0 的 Serverless Devs 组件,提供全生命周期的函数计算管理能力。本文档详细描述了组件的技术实现、API 接口、配置说明和最佳实践。 - -## 技术栈 - -### 核心技术 - -- **TypeScript**: 主要开发语言 -- **Node.js**: 运行时环境 -- **Jest**: 测试框架 -- **Lodash**: 工具库 -- **Axios**: HTTP 客户端 - -### 阿里云服务集成 - -- **函数计算 FC**: 核心服务 -- **对象存储 OSS**: 代码包存储 -- **访问控制 RAM**: 权限管理 -- **日志服务 SLS**: 日志收集 -- **容器镜像服务 ACR**: 镜像管理 -- **专有网络 VPC**: 网络配置 -- **文件存储 NAS**: 存储配置 - -## 架构设计 - -### 分层架构 - -``` -┌─────────────────────────────────────┐ -│ 用户接口层 │ -├─────────────────────────────────────┤ -│ 命令处理层 │ -├─────────────────────────────────────┤ -│ 业务逻辑层 │ -├─────────────────────────────────────┤ -│ 资源管理层 │ -├─────────────────────────────────────┤ -│ 基础设施层 │ -└─────────────────────────────────────┘ -``` - -### 核心组件 - -#### 1. 主入口模块 (Fc) - -- **职责**: 统一命令接口,路由到具体子命令 -- **关键方法**: - - `deploy()`: 部署函数和触发器 - - `build()`: 构建函数代码 - - `local()`: 本地运行和调试 - - `invoke()`: 调用函数 - - `info()`: 查询资源信息 - - `logs()`: 查询日志 - -#### 2. 基础模块 (Base) - -- **职责**: 提供公共处理逻辑 -- **关键功能**: - - 配置预处理 - - 角色权限处理 - - 默认配置应用 - - 环境检测 - -#### 3. 子命令模块 (subCommands) - -- **部署模块**: 函数和触发器部署 -- **构建模块**: 多环境构建支持 -- **本地运行模块**: 多语言本地调试 -- **其他模块**: 信息查询、日志、版本管理等 - -#### 4. 资源管理模块 (resources) - -- **FC 模块**: 函数计算资源管理 -- **RAM 模块**: 权限和角色管理 -- **SLS 模块**: 日志服务集成 -- **VPC-NAS 模块**: 网络和存储配置 -- **ACR 模块**: 容器镜像管理 - -## API 接口 - -### 主接口 - -#### deploy(inputs: IInputs) - -部署函数和触发器 - -**参数**: - -- `inputs`: 输入配置对象 - -**返回值**: `Promise` - -**示例**: - -```typescript -const result = await fc.deploy({ - props: { - region: 'cn-hangzhou', - functionName: 'my-function', - runtime: 'nodejs18', - handler: 'index.handler', - code: './code', - }, -}); -``` - -#### build(inputs: IInputs) - -构建函数代码 - -**参数**: - -- `inputs`: 输入配置对象 - -**返回值**: `Promise` - -**构建类型**: - -- `Default`: 默认构建 -- `ImageDocker`: Docker 构建 -- `ImageKaniko`: Kaniko 构建 -- `ImageBuildKit`: BuildKit 构建 - -#### local(inputs: IInputs) - -本地运行函数 - -**参数**: - -- `inputs`: 输入配置对象 - -**返回值**: `Promise` - -**支持语言**: - -- Python -- Node.js -- Java -- Go -- PHP -- .NET -- 自定义运行时 -- 自定义容器 - -### 配置接口 - -#### IProps - -组件属性接口 - -```typescript -interface IProps extends IFunction { - region: IRegion; - triggers?: ITrigger[]; - asyncInvokeConfig?: IAsyncInvokeConfig; - concurrencyConfig?: IConcurrencyConfig; - provisionConfig?: IProvisionConfig; - endpoint?: string; - supplement?: any; - annotations?: any; -} -``` - -#### IFunction - -函数配置接口 - -```typescript -interface IFunction { - functionName: string; - runtime: string; - handler: string; - code: string; - description?: string; - memorySize?: number; - timeout?: number; - cpu?: number; - diskSize?: number; - environmentVariables?: Record; - customContainerConfig?: ICustomContainerConfig; - customRuntimeConfig?: ICustomRuntimeConfig; - nasConfig?: INasConfig; - vpcConfig?: IVpcConfig; - logConfig?: ILogConfig; - role?: string; - layers?: string[]; - tags?: Array<{ key: string; value: string }>; -} -``` - -#### ITrigger - -触发器配置接口 - -```typescript -interface ITrigger { - triggerName: string; - triggerType: TriggerType; - triggerConfig: any; - invocationRole?: string; - qualifier?: string; -} -``` - -## 配置说明 - -### 基础配置 - -#### 函数配置 - -```yaml -region: cn-hangzhou -functionName: my-function -runtime: nodejs18 -handler: index.handler -code: ./code -description: My function description -memorySize: 512 -timeout: 60 -cpu: 0.35 -diskSize: 512 -``` - -#### 环境变量 - -```yaml -environmentVariables: - NODE_ENV: production - API_KEY: your-api-key -``` - -#### 自定义容器配置 - -```yaml -customContainerConfig: - image: registry.cn-hangzhou.aliyuncs.com/my-namespace/my-image:latest - command: ['node'] - args: ['server.js'] - cpu: 1 - memorySize: 1024 - imagePullPolicy: IfNotPresent - user: root - workingDir: /app - environmentVariables: - NODE_ENV: production - webServerMode: true -``` - -#### VPC 配置 - -```yaml -vpcConfig: - vpcId: vpc-1234567890abcdef0 - vSwitchIds: vsw-1234567890abcdef0 - securityGroupId: sg-1234567890abcdef0 -``` - -#### NAS 配置 - -```yaml -nasConfig: - mountPoints: - - serverAddr: 1234567890-abc123.cn-hangzhou.nas.aliyuncs.com - mountDir: /mnt/nas - fcDir: /mnt/fc - enableTLS: false -``` - -#### 日志配置 - -```yaml -logConfig: - project: my-log-project - logstore: my-log-store -``` - -### 触发器配置 - -#### HTTP 触发器 - -```yaml -triggers: - - triggerName: http-trigger - triggerType: http - triggerConfig: - authType: anonymous - methods: ['GET', 'POST'] -``` - -#### OSS 触发器 - -```yaml -triggers: - - triggerName: oss-trigger - triggerType: oss - triggerConfig: - bucketName: my-bucket - events: ['oss:ObjectCreated:*'] - filter: - Key: - Prefix: uploads/ - Suffix: .jpg -``` - -#### 定时触发器 - -```yaml -triggers: - - triggerName: timer-trigger - triggerType: timer - triggerConfig: - cronExpression: '0 0 12 * * *' - enable: true -``` - -#### 事件总线触发器 - -```yaml -triggers: - - triggerName: eb-trigger - triggerType: eventbridge - triggerConfig: - eventSourceConfig: - eventSourceType: MNS - eventSourceParameters: - QueueName: my-queue - TopicName: my-topic -``` - -### 高级配置 - -#### 异步调用配置 - -```yaml -asyncInvokeConfig: - destinationConfig: - onSuccess: - destination: acs:fc:cn-hangzhou:123456789:functions/success-function - onFailure: - destination: acs:fc:cn-hangzhou:123456789:functions/failure-function - maxAsyncEventAgeInSeconds: 300 - maxAsyncRetryAttempts: 3 -``` - -#### 并发配置 - -```yaml -concurrencyConfig: - reservedConcurrency: 10 -``` - -#### 预留配置 - -```yaml -provisionConfig: - target: 10 - scheduledActions: - - schedule: '0 0 12 * * *' - target: 20 -``` - -## 最佳实践 - -### 1. 项目结构 - -``` -my-project/ -├── s.yaml # 配置文件 -├── code/ # 函数代码 -│ ├── index.js -│ ├── package.json -│ └── node_modules/ -├── .serverless/ # 构建输出 -└── README.md -``` - -### 2. 配置文件管理 - -```yaml -# s.yaml -edition: 3.0.0 -name: my-project -access: default - -resources: - my-function: - component: fc3 - props: - region: cn-hangzhou - functionName: my-function - runtime: nodejs18 - handler: index.handler - code: ./code - memorySize: 512 - timeout: 60 - triggers: - - triggerName: http-trigger - triggerType: http - triggerConfig: - authType: anonymous - methods: ['GET', 'POST'] -``` - -### 3. 环境变量管理 - -```yaml -# 开发环境 -environmentVariables: - NODE_ENV: development - DEBUG: true - -# 生产环境 -environmentVariables: - NODE_ENV: production - DEBUG: false -``` - -### 4. 多环境部署 - -```bash -# 部署到开发环境 -s deploy --env dev - -# 部署到生产环境 -s deploy --env prod -``` - -### 5. 本地调试 - -```bash -# 启动本地服务 -s local start - -# 调用函数 -s local invoke --event '{"key": "value"}' -``` - -### 6. 日志查询 - -```bash -# 查询函数日志 -s logs --tail - -# 查询特定时间段的日志 -s logs --start-time 2023-01-01T00:00:00Z --end-time 2023-01-01T23:59:59Z - -# 查询指定实例的日志(同时搜索 FCLogs 和 FCInstanceEvents 两个 topic) -s logs --instance-id c-69f8a959-15f8e4fe-b867da209124 - -# 查询指定请求的日志 -s logs --request-id 0f7032f1-ffde-474e-92ce-188210368b53 - -# 查询指定版本的日志 -s logs --qualifier LATEST -``` - -#### SLS Topic 类型 - -FC3 的 SLS logstore 包含以下 topic 类型: - -| Topic 格式 | 说明 | -| --------------------------------- | ------------------------------- | -| `FCLogs:/functionName` | 函数调用日志 | -| `FCInstanceEvents:/functionName` | 实例生命周期事件(创建/销毁等) | -| `FCRequestMetrics:/functionName` | 请求指标 | -| `FCInstanceMetrics:/functionName` | 实例指标 | - -默认 `s logs` 只查询 `FCLogs` topic。使用 `--instance-id` 参数时会同时搜索 `FCLogs` 和 `FCInstanceEvents` 两个 topic。 - -## 错误处理 - -### 常见错误类型 - -#### 1. 配置错误 - -- **FunctionNotFound**: 函数不存在 -- **InvalidArgument**: 参数无效 -- **AccessDenied**: 权限不足 - -#### 2. 部署错误 - -- **FunctionAlreadyExists**: 函数已存在 -- **TriggerAlreadyExists**: 触发器已存在 -- **ResourceQuotaExceeded**: 资源配额超限 - -#### 3. 运行时错误 - -- **FunctionTimeout**: 函数超时 -- **OutOfMemory**: 内存不足 -- **NetworkError**: 网络错误 - -### 错误处理策略 - -#### 1. 重试机制 - -```typescript -// 自动重试配置 -const retryConfig = { - maxRetries: 3, - retryInterval: 1000, - backoffMultiplier: 2, -}; -``` - -#### 2. 错误日志 - -```typescript -// 错误日志记录 -logger.error('Deploy failed:', { - error: error.message, - stack: error.stack, - context: deployContext, -}); -``` - -#### 3. 优雅降级 - -```typescript -// 优雅降级处理 -try { - await deployFunction(config); -} catch (error) { - if (error.code === 'FunctionAlreadyExists') { - await updateFunction(config); - } else { - throw error; - } -} -``` - -## 性能优化 - -### 1. 构建优化 - -- 使用 Docker 多阶段构建 -- 优化镜像大小 -- 使用缓存加速构建 - -### 2. 部署优化 - -- 并行部署多个资源 -- 增量更新 -- 智能重试 - -### 3. 运行时优化 - -- 合理设置内存和 CPU -- 使用预留实例 -- 优化冷启动时间 - -## 安全最佳实践 - -### 1. 权限管理 - -- 使用最小权限原则 -- 定期轮换访问密钥 -- 使用 RAM 角色 - -### 2. 网络安全 - -- 配置 VPC 网络 -- 使用安全组 -- 启用 TLS - -### 3. 数据安全 - -- 加密敏感数据 -- 使用环境变量 -- 定期备份 - -## 监控和运维 - -### 1. 日志监控 - -- 集成 SLS 日志服务 -- 设置日志告警 -- 日志分析和查询 - -### 2. 指标监控 - -- 函数调用次数 -- 执行时间 -- 错误率 -- 冷启动次数 - -### 3. 告警配置 - -- 错误率告警 -- 延迟告警 -- 资源使用告警 - -## 故障排查 - -### 1. 部署问题 - -- 检查配置文件格式 -- 验证权限配置 -- 查看部署日志 - -### 2. 运行时问题 - -- 检查函数日志 -- 验证环境变量 -- 测试函数逻辑 - -### 3. 网络问题 - -- 检查 VPC 配置 -- 验证安全组规则 -- 测试网络连通性 - -## 版本管理 - -### 1. 函数版本 - -- 使用语义化版本号 -- 版本回滚 -- 版本比较 - -### 2. 别名管理 - -- 创建别名 -- 别名切换 -- 流量分配 - -### 3. 灰度发布 - -- 使用别名进行灰度 -- 监控灰度效果 -- 快速回滚 - -## 扩展开发 - -### 1. 自定义构建器 - -```typescript -class CustomBuilder extends BaseBuilder { - async build(): Promise { - // 自定义构建逻辑 - } -} -``` - -### 2. 自定义触发器 - -```typescript -class CustomTrigger { - async deploy(): Promise { - // 自定义触发器部署逻辑 - } -} -``` - -### 3. 插件开发 - -```typescript -class CustomPlugin { - async beforeDeploy(): Promise { - // 部署前处理 - } - - async afterDeploy(): Promise { - // 部署后处理 - } -} -``` - -## 贡献指南 - -### 1. 开发环境搭建 - -```bash -# 克隆仓库 -git clone https://github.com/devsapp/fc3.git - -# 安装依赖 -npm install - -# 运行测试 -npm test - -# 构建项目 -npm run build -``` - -### 2. 代码规范 - -- 使用 TypeScript -- 遵循 ESLint 规则 -- 编写单元测试 -- 添加文档注释 - -### 3. 提交流程 - -- Fork 仓库 -- 创建功能分支 -- 提交代码 -- 创建 Pull Request - -## 更新日志 - -### v1.0.0 (2023-01-01) - -- 初始版本发布 -- 支持基础函数部署 -- 支持多种触发器类型 -- 支持本地调试 - -### v1.1.0 (2023-02-01) - -- 新增自定义容器支持 -- 优化构建性能 -- 增强错误处理 - -### v1.2.0 (2023-03-01) - -- 新增异步调用配置 -- 支持并发配置 -- 优化日志查询 - -## 许可证 - -本项目采用 MIT 许可证。详情请参阅 [LICENSE](LICENSE) 文件。 - -## 联系方式 - -- 项目主页: https://github.com/devsapp/fc3 -- 问题反馈: https://github.com/devsapp/fc3/issues -- 文档网站: https://docs.serverless-devs.com/user-guide/aliyun/fc3/ diff --git a/docs/testing-plan.md b/docs/testing-plan.md deleted file mode 100644 index 903825d2..00000000 --- a/docs/testing-plan.md +++ /dev/null @@ -1,365 +0,0 @@ -# FC3 组件测试计划 - -## 现有测试覆盖情况分析 - -### 已测试模块 - -1. **utils 模块** - 部分覆盖 - - - `utils_test.ts` - 工具函数测试 - - `utils_functions_test.ts` - 工具函数测试 - - `verify_test.ts` - 验证函数测试 - - `verify_simple_test.ts` - 简单验证测试 - -2. **resources 模块** - 部分覆盖 - - - `resources_acr_test.ts` - ACR 资源测试 - - `fc_client_test.ts` - FC 客户端测试 - -3. **subCommands 模块** - 部分覆盖 - - - `subCommands_test.ts` - 子命令测试(主要测试 2to3 和 alias) - -4. **interface 模块** - 部分覆盖 - - - `interface_test.ts` - 接口测试 - -5. **其他工具模块** - - `transformCustomDomainProps_test.ts` - 自定义域名转换测试 - - `crc64_test.ts` - CRC64 测试 - -## 需要补充测试的模块 - -### 1. 核心模块测试 - -#### 1.1 主入口模块 (src/index.ts) - -**优先级**: 高 -**测试内容**: - -- `deploy()` 方法测试 -- `build()` 方法测试 -- `local()` 方法测试 -- `invoke()` 方法测试 -- `info()` 方法测试 -- `logs()` 方法测试 -- `plan()` 方法测试 -- `remove()` 方法测试 -- `sync()` 方法测试 -- `alias()` 方法测试 -- `concurrency()` 方法测试 -- `provision()` 方法测试 -- `layer()` 方法测试 -- `instance()` 方法测试 -- `version()` 方法测试 -- `model()` 方法测试 -- `s2tos3()` 方法测试 -- `getSchema()` 方法测试 -- `getShownProps()` 方法测试 - -#### 1.2 基础模块 (src/base.ts) - -**优先级**: 高 -**测试内容**: - -- `handlePreRun()` 方法测试 -- `_handleRole()` 私有方法测试 -- `_handleDefaultTriggerRole()` 私有方法测试 -- 构造函数测试 -- 日志设置测试 - -### 2. 子命令模块测试 - -#### 2.1 部署模块 (src/subCommands/deploy/) - -**优先级**: 高 -**测试内容**: - -- `Deploy` 类测试 -- `deploy/impl/function.ts` 测试 -- `deploy/impl/trigger.ts` 测试 -- `deploy/impl/vpc_binding.ts` 测试 -- `deploy/impl/custom_domain.ts` 测试 -- `deploy/impl/concurrency_config.ts` 测试 -- `deploy/impl/async_invoke_config.ts` 测试 -- `deploy/impl/provision_config.ts` 测试 -- `deploy/impl/base.ts` 测试 - -#### 2.2 构建模块 (src/subCommands/build/) - -**优先级**: 高 -**测试内容**: - -- `BuilderFactory` 工厂类测试 -- `DefaultBuilder` 测试 -- `ImageDockerBuilder` 测试 -- `ImageKanikoBuilder` 测试 -- `ImageBuildKitBuilder` 测试 -- `BaseImageBuilder` 测试 -- `BaseBuilder` 测试 - -#### 2.3 本地运行模块 (src/subCommands/local/) - -**优先级**: 中 -**测试内容**: - -- `Local` 主类测试 -- `local/impl/baseLocal.ts` 测试 -- `local/impl/utils.ts` 测试 -- `local/impl/start/` 目录下所有启动器测试 -- `local/impl/invoke/` 目录下所有调用器测试 - -#### 2.4 其他子命令模块 - -**优先级**: 中 -**测试内容**: - -- `info/index.ts` 测试 -- `plan/index.ts` 测试 -- `invoke/index.ts` 测试 -- `logs/index.ts` 测试 -- `remove/index.ts` 测试 -- `sync/index.ts` 测试 -- `alias/index.ts` 测试 -- `concurrency/index.ts` 测试 -- `provision/index.ts` 测试 -- `layer/index.ts` 测试 -- `instance/index.ts` 测试 -- `version/index.ts` 测试 -- `model/index.ts` 测试 - -### 3. 资源管理模块测试 - -#### 3.1 FC 函数计算模块 (src/resources/fc/) - -**优先级**: 高 -**测试内容**: - -- `fc/index.ts` 测试 -- `fc/impl/client.ts` 测试 -- `fc/impl/utils.ts` 测试 -- `fc/impl/replace-function-config.ts` 测试 -- `fc/error-code.ts` 测试 - -#### 3.2 RAM 权限管理模块 (src/resources/ram/) - -**优先级**: 中 -**测试内容**: - -- `ram/index.ts` 测试 -- `RamClient` 类测试 -- 角色管理功能测试 - -#### 3.3 SLS 日志服务模块 (src/resources/sls/) - -**优先级**: 中 -**测试内容**: - -- `sls/index.ts` 测试 -- 项目名称生成测试 -- 日志存储名称生成测试 - -#### 3.4 VPC-NAS 网络存储模块 (src/resources/vpc-nas/) - -**优先级**: 中 -**测试内容**: - -- `vpc-nas/index.ts` 测试 -- VPC NAS 规则获取测试 - -#### 3.5 ACR 容器镜像模块 (src/resources/acr/) - -**优先级**: 中 -**测试内容**: - -- `acr/index.ts` 测试 -- `acr/login.ts` 测试 -- 登录功能测试 -- 镜像元数据获取测试 - -### 4. 工具模块测试 - -#### 4.1 工具函数模块 (src/utils/) - -**优先级**: 中 -**测试内容**: - -- `utils/index.ts` 中未测试的函数 -- `utils/verify.ts` 测试 -- `utils/run-command.ts` 测试 - -#### 4.2 日志模块 (src/logger.ts) - -**优先级**: 低 -**测试内容**: - -- 日志功能测试 -- 日志级别测试 - -#### 4.3 常量模块 (src/constant.ts) - -**优先级**: 低 -**测试内容**: - -- 常量定义测试 - -### 5. 接口定义模块测试 - -#### 5.1 接口模块 (src/interface/) - -**优先级**: 低 -**测试内容**: - -- 接口定义验证 -- 接口类型检查 - -### 6. 默认配置模块测试 - -#### 6.1 默认配置模块 (src/default/) - -**优先级**: 低 -**测试内容**: - -- `default/config.ts` 测试 -- `default/resources.ts` 测试 -- `default/image.ts` 测试 - -### 7. 命令帮助模块测试 - -#### 7.1 命令帮助模块 (src/commands-help/) - -**优先级**: 低 -**测试内容**: - -- 帮助信息生成测试 -- 命令描述测试 - -## 测试策略 - -### 1. 测试优先级 - -- **高优先级**: 核心功能模块(主入口、基础模块、部署模块、构建模块、FC 资源模块) -- **中优先级**: 子命令模块、资源管理模块、工具模块 -- **低优先级**: 辅助模块(日志、常量、接口、默认配置、命令帮助) - -### 2. 测试类型 - -- **单元测试**: 测试单个函数或方法 -- **集成测试**: 测试模块间的交互 -- **Mock 测试**: 使用 Mock 对象测试外部依赖 - -### 3. 测试覆盖率目标 - -- **核心模块**: 90% 以上 -- **子命令模块**: 80% 以上 -- **资源管理模块**: 80% 以上 -- **工具模块**: 85% 以上 -- **整体覆盖率**: 80% 以上 - -### 4. 测试数据 - -- 使用真实的测试数据 -- 使用 Mock 数据模拟外部服务 -- 使用边界值测试 - -### 5. 测试环境 - -- 本地开发环境 -- CI/CD 环境 -- 不同操作系统环境 - -## 测试实施计划 - -### 第一阶段:核心模块测试(1-2 周) - -1. 主入口模块测试 -2. 基础模块测试 -3. 部署模块测试 -4. 构建模块测试 -5. FC 资源模块测试 - -### 第二阶段:子命令模块测试(1-2 周) - -1. 本地运行模块测试 -2. 其他子命令模块测试 -3. 资源管理模块测试 - -### 第三阶段:辅助模块测试(1 周) - -1. 工具模块测试 -2. 日志模块测试 -3. 常量模块测试 -4. 接口定义模块测试 -5. 默认配置模块测试 -6. 命令帮助模块测试 - -### 第四阶段:集成测试和优化(1 周) - -1. 集成测试 -2. 性能测试 -3. 测试覆盖率优化 -4. 测试文档完善 - -## 测试工具和框架 - -### 1. 测试框架 - -- **Jest**: 主要的测试框架 -- **ts-jest**: TypeScript 支持 - -### 2. Mock 工具 - -- **jest.mock()**: 模块 Mock -- **jest.fn()**: 函数 Mock -- **jest.spyOn()**: 方法监听 - -### 3. 断言库 - -- **Jest 内置断言**: 基础断言 -- **自定义断言**: 特定业务断言 - -### 4. 测试工具 - -- **supertest**: HTTP 测试 -- **nock**: HTTP Mock -- **sinon**: 高级 Mock 功能 - -## 测试质量保证 - -### 1. 代码审查 - -- 测试代码审查 -- 测试用例审查 -- 测试覆盖率审查 - -### 2. 持续集成 - -- 自动化测试执行 -- 测试结果报告 -- 测试失败通知 - -### 3. 测试维护 - -- 定期更新测试用例 -- 修复失效的测试 -- 优化测试性能 - -## 测试文档 - -### 1. 测试用例文档 - -- 测试用例描述 -- 测试数据说明 -- 预期结果说明 - -### 2. 测试报告 - -- 测试执行结果 -- 测试覆盖率报告 -- 测试性能报告 - -### 3. 测试指南 - -- 测试环境搭建 -- 测试执行方法 -- 测试问题排查 diff --git a/jestconfig.json b/jestconfig.json index ae0ad5e2..0bfbf104 100644 --- a/jestconfig.json +++ b/jestconfig.json @@ -8,6 +8,13 @@ "roots": ["/__tests__", "/src"], "testPathIgnorePatterns": ["/node_modules/", "/__tests__/e2e/"], "collectCoverage": true, - "coverageReporters": ["html", "text"], + "collectCoverageFrom": [ + "src/**/*.ts", + "!src/**/*.d.ts", + "!src/schema.json", + "!src/commands-help/**", + "!src/interface/**" + ], + "coverageReporters": ["json-summary", "text", "html"], "testTimeout": 30000 } diff --git a/package-lock.json b/package-lock.json index 61308572..79e6c389 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,11 +53,13 @@ "@types/lodash": "^4.17.25", "@types/node": "^20.12.11", "@vercel/ncc": "^0.38.4", + "depcheck": "^1.4.3", "f2elint": "^2.2.1", "jest": "^29.7.0", "prettier": "^3.8.1", "ts-jest": "^29.4.6", "ts-node": "^10.9.2", + "ts-prune": "^0.10.3", "typescript": "^4.4.2", "typescript-json-schema": "^0.67.1" }, @@ -830,19 +832,21 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -957,12 +961,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", - "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "version": "7.29.8", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.28.6" + "@babel/types": "^7.29.8" }, "bin": { "parser": "bin/babel-parser.js" @@ -1389,13 +1394,14 @@ "license": "MIT" }, "node_modules/@babel/types": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", - "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "version": "7.29.8", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -2419,9 +2425,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://packages.aliyun.com/670e108663cd360abfe4be65/npm/npm-registry/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, "license": "MIT" }, @@ -2479,6 +2485,326 @@ "node": ">= 8" } }, + "node_modules/@parcel/watcher": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher/-/watcher-2.6.0.tgz", + "integrity": "sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.6.0", + "@parcel/watcher-darwin-arm64": "2.6.0", + "@parcel/watcher-darwin-x64": "2.6.0", + "@parcel/watcher-freebsd-x64": "2.6.0", + "@parcel/watcher-linux-arm-glibc": "2.6.0", + "@parcel/watcher-linux-arm-musl": "2.6.0", + "@parcel/watcher-linux-arm64-glibc": "2.6.0", + "@parcel/watcher-linux-arm64-musl": "2.6.0", + "@parcel/watcher-linux-x64-glibc": "2.6.0", + "@parcel/watcher-linux-x64-musl": "2.6.0", + "@parcel/watcher-win32-arm64": "2.6.0", + "@parcel/watcher-win32-x64": "2.6.0" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.6.0.tgz", + "integrity": "sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.6.0.tgz", + "integrity": "sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.6.0.tgz", + "integrity": "sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.6.0.tgz", + "integrity": "sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.6.0.tgz", + "integrity": "sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.6.0.tgz", + "integrity": "sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.6.0.tgz", + "integrity": "sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.6.0.tgz", + "integrity": "sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.6.0.tgz", + "integrity": "sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.6.0.tgz", + "integrity": "sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.6.0.tgz", + "integrity": "sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.6.0.tgz", + "integrity": "sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://packages.aliyun.com/670e108663cd360abfe4be65/npm/npm-registry/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -2875,6 +3201,56 @@ "node": ">= 10" } }, + "node_modules/@ts-morph/common": { + "version": "0.12.3", + "resolved": "https://registry.npmmirror.com/@ts-morph/common/-/common-0.12.3.tgz", + "integrity": "sha512-4tUmeLyXJnJWvTFOKtcNJ1yh0a3SsTLi2MUoyj8iUNznFRN1ZquaNe7Oukqrnki2FzZkm0J9adCNLDZxUzvj+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "^3.2.7", + "minimatch": "^3.0.4", + "mkdirp": "^1.0.4", + "path-browserify": "^1.0.1" + } + }, + "node_modules/@ts-morph/common/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@ts-morph/common/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@ts-morph/common/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://packages.aliyun.com/670e108663cd360abfe4be65/npm/npm-registry/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -3025,6 +3401,13 @@ "@types/unist": "^2" } }, + "node_modules/@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmmirror.com/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", @@ -3409,6 +3792,109 @@ "ncc": "dist/ncc/cli.js" } }, + "node_modules/@vue/compiler-core": { + "version": "3.5.41", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.41.tgz", + "integrity": "sha512-q0Xtv/F9w2YO/7htQhtiL+Ev2WCJbe5N2hc+XfgyKkEKqWpSxknmT8QOuGdEKNdjPq0c3F7rNpFkTo3Kfrm7pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@vue/shared": "3.5.41", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-core/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.41", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.41.tgz", + "integrity": "sha512-oKacVfNglLvGjnS6BXOlGL7EyG2h8X03pqXCjzotRZUaXGjbrTJUnVAQjrCqUnS+lyu31nwQjZY/d817GmCnfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.41", + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.41", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.41.tgz", + "integrity": "sha512-XJhip7R2wy6vX3knCxdZN4KracFaZUef58s1KYewqluedHIJaPIVfXoYT7MF1F8nCvv6k8bWWxDC8opMkg1VTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@vue/compiler-core": "3.5.41", + "@vue/compiler-dom": "3.5.41", + "@vue/compiler-ssr": "3.5.41", + "@vue/shared": "3.5.41", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.19", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-sfc/node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.41", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.41.tgz", + "integrity": "sha512-U3v5OejKEGqOI0Wy0+Sz7hGuIFZHA4LSXzrNM3IMIeDyJEBBfTpX26n3SDgToRpP2bLc9FfI2j/kSgcJ8Emq5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.41", + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.41", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.41.tgz", + "integrity": "sha512-IOnwSCma8j+9xJT6b8H0dEYidC80NsYmNMlZxRsukYcSoGaDBohog5hDxzeUXdFeGWFA++vWvxqOmrr96VlqMA==", + "dev": true, + "license": "MIT" + }, "node_modules/acorn": { "version": "7.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", @@ -3815,6 +4301,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-differ": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/array-differ/-/array-differ-3.0.0.tgz", + "integrity": "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/array-ify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", @@ -4654,6 +5150,36 @@ "node": "*" } }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chokidar/node_modules/readdirp": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-5.1.1.tgz", + "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/ci-info": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", @@ -4786,6 +5312,13 @@ "node": ">= 0.12.0" } }, + "node_modules/code-block-writer": { + "version": "11.0.3", + "resolved": "https://registry.npmmirror.com/code-block-writer/-/code-block-writer-11.0.3.tgz", + "integrity": "sha512-NiujjUFB4SwScJq2bwbYUtXbZhBSlY6vYzm++3Q6oC+U+injTqfPYFK8wS9COOmb2lueqp0ZRB4nK1VYeHgNyw==", + "dev": true, + "license": "MIT" + }, "node_modules/collect-v8-coverage": { "version": "1.0.2", "resolved": "https://packages.aliyun.com/670e108663cd360abfe4be65/npm/npm-registry/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", @@ -5514,24 +6047,220 @@ "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "license": "MIT", "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depcheck": { + "version": "1.4.3", + "resolved": "https://registry.npmmirror.com/depcheck/-/depcheck-1.4.3.tgz", + "integrity": "sha512-vy8xe1tlLFu7t4jFyoirMmOR7x7N601ubU9Gkifyr9z8rjBFtEdWHDBMqXyk6OkK+94NXutzddVXJuo0JlUQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "7.16.4", + "@babel/traverse": "^7.12.5", + "@vue/compiler-sfc": "^3.0.5", + "camelcase": "^6.2.0", + "cosmiconfig": "^7.0.0", + "debug": "^4.2.0", + "deps-regex": "^0.1.4", + "ignore": "^5.1.8", + "is-core-module": "^2.4.0", + "js-yaml": "^3.14.0", + "json5": "^2.1.3", + "lodash": "^4.17.20", + "minimatch": "^3.0.4", + "multimatch": "^5.0.0", + "please-upgrade-node": "^3.2.0", + "query-ast": "^1.0.3", + "readdirp": "^3.5.0", + "require-package-name": "^2.0.1", + "resolve": "^1.18.1", + "sass": "^1.29.0", + "scss-parser": "^1.0.4", + "semver": "^7.3.2", + "yargs": "^16.1.0" + }, + "bin": { + "depcheck": "bin/depcheck.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/depcheck/node_modules/@babel/parser": { + "version": "7.16.4", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.16.4.tgz", + "integrity": "sha512-6V0qdPUaiVHH3RtZeLIsc+6pDhbYzHR8ogA8w+f+Wc77DuXto19g2QUwveINoS34Uw+W8/hQDGJCx+i4n7xcng==", + "dev": true, + "license": "MIT", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/depcheck/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmmirror.com/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/depcheck/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/depcheck/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/depcheck/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmmirror.com/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/depcheck/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depcheck/node_modules/js-yaml": { + "version": "3.15.1", + "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-3.15.1.tgz", + "integrity": "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/depcheck/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/depcheck/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/depcheck/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "node_modules/depcheck/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/depcheck/node_modules/yargs": { + "version": "16.2.2", + "resolved": "https://registry.npmmirror.com/yargs/-/yargs-16.2.2.tgz", + "integrity": "sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w==", + "dev": true, "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, "engines": { - "node": ">=0.4.0" + "node": ">=10" } }, "node_modules/depd": { @@ -5544,6 +6273,13 @@ "node": ">= 0.8" } }, + "node_modules/deps-regex": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/deps-regex/-/deps-regex-0.1.4.tgz", + "integrity": "sha512-3tzwGYogSJi8HoG93R5x9NrdefZQOXgHgGih/7eivloOq6yC6O+yoFxZnkgP661twvfILONfoKRdF9GQOGx2RA==", + "dev": true, + "license": "MIT" + }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -5553,6 +6289,17 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://packages.aliyun.com/670e108663cd360abfe4be65/npm/npm-registry/detect-newline/-/detect-newline-3.1.0.tgz", @@ -6891,6 +7638,13 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -8661,6 +9415,13 @@ "node": ">= 4" } }, + "node_modules/immutable": { + "version": "5.1.9", + "resolved": "https://registry.npmmirror.com/immutable/-/immutable-5.1.9.tgz", + "integrity": "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==", + "dev": true, + "license": "MIT" + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -8797,6 +9558,16 @@ "node": ">= 0.4" } }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmmirror.com/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/ip": { "version": "1.1.9", "resolved": "https://packages.aliyun.com/670e108663cd360abfe4be65/npm/npm-registry/ip/-/ip-1.1.9.tgz", @@ -10701,6 +11472,16 @@ "es5-ext": "~0.10.2" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/make-dir": { "version": "1.3.0", "resolved": "https://packages.aliyun.com/670e108663cd360abfe4be65/npm/npm-registry/make-dir/-/make-dir-1.3.0.tgz", @@ -11161,6 +11942,60 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "license": "MIT" }, + "node_modules/multimatch": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/multimatch/-/multimatch-5.0.0.tgz", + "integrity": "sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimatch": "^3.0.3", + "array-differ": "^3.0.0", + "array-union": "^2.1.0", + "arrify": "^2.0.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/multimatch/node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/multimatch/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/multimatch/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/mute-stream": { "version": "0.0.8", "resolved": "https://packages.aliyun.com/670e108663cd360abfe4be65/npm/npm-registry/mute-stream/-/mute-stream-0.0.8.tgz", @@ -11177,6 +12012,25 @@ "thenify-all": "^1.0.0" } }, + "node_modules/nanoid": { + "version": "3.3.17", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.17.tgz", + "integrity": "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://packages.aliyun.com/670e108663cd360abfe4be65/npm/npm-registry/natural-compare/-/natural-compare-1.4.0.tgz", @@ -11218,6 +12072,14 @@ "debug": "^2.1.0" } }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://packages.aliyun.com/670e108663cd360abfe4be65/npm/npm-registry/node-fetch/-/node-fetch-2.7.0.tgz", @@ -11659,6 +12521,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, "node_modules/path-equal": { "version": "1.2.5", "resolved": "https://packages.aliyun.com/670e108663cd360abfe4be65/npm/npm-registry/path-equal/-/path-equal-1.2.5.tgz", @@ -12275,6 +13144,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/query-ast": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/query-ast/-/query-ast-1.0.5.tgz", + "integrity": "sha512-JK+1ma4YDuLjvKKcz9JZ70G+CM9qEOs/l1cZzstMMfwKUabTJ9sud5jvDGrUNuv03yKUgs82bLkHXJkDyhRmBw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "invariant": "2.2.4", + "lodash": "^4.17.21" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -12483,6 +13363,19 @@ "node": ">=10" } }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/readline": { "version": "1.3.0", "resolved": "https://packages.aliyun.com/670e108663cd360abfe4be65/npm/npm-registry/readline/-/readline-1.3.0.tgz", @@ -12645,6 +13538,13 @@ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "license": "ISC" }, + "node_modules/require-package-name": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/require-package-name/-/require-package-name-2.0.1.tgz", + "integrity": "sha512-uuoJ1hU/k6M0779t3VMVIYpb2VMJk05cehCaABFhXaibcbvfgR8wKiozLjVFSzJPmQMRqIcO0HMyTFqfV09V6Q==", + "dev": true, + "license": "MIT" + }, "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://packages.aliyun.com/670e108663cd360abfe4be65/npm/npm-registry/requires-port/-/requires-port-1.0.0.tgz", @@ -12898,12 +13798,54 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/sass": { + "version": "1.102.0", + "resolved": "https://registry.npmmirror.com/sass/-/sass-1.102.0.tgz", + "integrity": "sha512-NSOyTnaQF7rTAEOtI2fwb386vL+akyiQLBZu8Na7hXCb+umJy0GAqlcMIaqACZ6Z1VgTBS4K9PG6B3IdjHGJsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^5.0.0", + "immutable": "^5.1.5", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=20.19.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, "node_modules/sax": { "version": "1.4.1", "resolved": "https://packages.aliyun.com/670e108663cd360abfe4be65/npm/npm-registry/sax/-/sax-1.4.1.tgz", "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", "license": "ISC" }, + "node_modules/scss-parser": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/scss-parser/-/scss-parser-1.0.6.tgz", + "integrity": "sha512-SH3TaoaJFzfAtqs3eG1j5IuHJkeEW5rKUPIjIN+ZorLAyJLHItQGnsgwHk76v25GtLtpT9IqfAcqK4vFWdiw+w==", + "dev": true, + "license": "SEE LICENSE IN README", + "dependencies": { + "invariant": "2.2.4", + "lodash": "4.17.21" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/scss-parser/node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, "node_modules/sdk-base": { "version": "2.0.1", "resolved": "https://packages.aliyun.com/670e108663cd360abfe4be65/npm/npm-registry/sdk-base/-/sdk-base-2.0.1.tgz", @@ -13233,6 +14175,16 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-support": { "version": "0.5.13", "resolved": "https://packages.aliyun.com/670e108663cd360abfe4be65/npm/npm-registry/source-map-support/-/source-map-support-0.5.13.tgz", @@ -14094,6 +15046,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/true-myth": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/true-myth/-/true-myth-4.1.1.tgz", + "integrity": "sha512-rqy30BSpxPznbbTcAcci90oZ1YR4DqvKcNXNerG5gQBU2v4jk0cygheiul5J6ExIMrgDVuanv/MkGfqZbKrNNg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "10.* || >= 12.*" + } + }, "node_modules/ts-jest": { "version": "29.4.6", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz", @@ -14170,6 +15132,17 @@ "node": ">=12" } }, + "node_modules/ts-morph": { + "version": "13.0.3", + "resolved": "https://registry.npmmirror.com/ts-morph/-/ts-morph-13.0.3.tgz", + "integrity": "sha512-pSOfUMx8Ld/WUreoSzvMFQG5i9uEiWIsBYjpU9+TTASOeUa89j5HykomeqVULm1oqWtBdleI3KEFRLrlA3zGIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ts-morph/common": "~0.12.3", + "code-block-writer": "^11.0.0" + } + }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://packages.aliyun.com/670e108663cd360abfe4be65/npm/npm-registry/ts-node/-/ts-node-10.9.2.tgz", @@ -14227,6 +15200,24 @@ "node": ">=0.4.0" } }, + "node_modules/ts-prune": { + "version": "0.10.3", + "resolved": "https://registry.npmmirror.com/ts-prune/-/ts-prune-0.10.3.tgz", + "integrity": "sha512-iS47YTbdIcvN8Nh/1BFyziyUqmjXz7GVzWu02RaZXqb+e/3Qe1B7IQ4860krOeCGUeJmterAlaM2FRH0Ue0hjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^6.2.1", + "cosmiconfig": "^7.0.1", + "json5": "^2.1.3", + "lodash": "^4.17.21", + "true-myth": "^4.1.0", + "ts-morph": "^13.0.1" + }, + "bin": { + "ts-prune": "lib/index.js" + } + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -16259,15 +17250,15 @@ "dev": true }, "@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true }, "@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true }, "@babel/helper-validator-option": { @@ -16357,12 +17348,12 @@ } }, "@babel/parser": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", - "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "version": "7.29.8", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", "dev": true, "requires": { - "@babel/types": "^7.28.6" + "@babel/types": "^7.29.8" } }, "@babel/plugin-syntax-async-generators": { @@ -16647,13 +17638,13 @@ } }, "@babel/types": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", - "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "version": "7.29.8", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", "dev": true, "requires": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" } }, "@bcoe/v8-coverage": { @@ -17428,9 +18419,9 @@ "dev": true }, "@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://packages.aliyun.com/670e108663cd360abfe4be65/npm/npm-registry/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true }, "@jridgewell/trace-mapping": { @@ -17478,6 +18469,124 @@ "fastq": "^1.6.0" } }, + "@parcel/watcher": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher/-/watcher-2.6.0.tgz", + "integrity": "sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==", + "dev": true, + "optional": true, + "requires": { + "@parcel/watcher-android-arm64": "2.6.0", + "@parcel/watcher-darwin-arm64": "2.6.0", + "@parcel/watcher-darwin-x64": "2.6.0", + "@parcel/watcher-freebsd-x64": "2.6.0", + "@parcel/watcher-linux-arm-glibc": "2.6.0", + "@parcel/watcher-linux-arm-musl": "2.6.0", + "@parcel/watcher-linux-arm64-glibc": "2.6.0", + "@parcel/watcher-linux-arm64-musl": "2.6.0", + "@parcel/watcher-linux-x64-glibc": "2.6.0", + "@parcel/watcher-linux-x64-musl": "2.6.0", + "@parcel/watcher-win32-arm64": "2.6.0", + "@parcel/watcher-win32-x64": "2.6.0", + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.4" + }, + "dependencies": { + "picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "optional": true + } + } + }, + "@parcel/watcher-android-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.6.0.tgz", + "integrity": "sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==", + "dev": true, + "optional": true + }, + "@parcel/watcher-darwin-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.6.0.tgz", + "integrity": "sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==", + "dev": true, + "optional": true + }, + "@parcel/watcher-darwin-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.6.0.tgz", + "integrity": "sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==", + "dev": true, + "optional": true + }, + "@parcel/watcher-freebsd-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.6.0.tgz", + "integrity": "sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==", + "dev": true, + "optional": true + }, + "@parcel/watcher-linux-arm-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.6.0.tgz", + "integrity": "sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==", + "dev": true, + "optional": true + }, + "@parcel/watcher-linux-arm-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.6.0.tgz", + "integrity": "sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==", + "dev": true, + "optional": true + }, + "@parcel/watcher-linux-arm64-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.6.0.tgz", + "integrity": "sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==", + "dev": true, + "optional": true + }, + "@parcel/watcher-linux-arm64-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.6.0.tgz", + "integrity": "sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==", + "dev": true, + "optional": true + }, + "@parcel/watcher-linux-x64-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.6.0.tgz", + "integrity": "sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==", + "dev": true, + "optional": true + }, + "@parcel/watcher-linux-x64-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.6.0.tgz", + "integrity": "sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==", + "dev": true, + "optional": true + }, + "@parcel/watcher-win32-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.6.0.tgz", + "integrity": "sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==", + "dev": true, + "optional": true + }, + "@parcel/watcher-win32-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.6.0.tgz", + "integrity": "sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A==", + "dev": true, + "optional": true + }, "@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://packages.aliyun.com/670e108663cd360abfe4be65/npm/npm-registry/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -17821,6 +18930,45 @@ "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.1.tgz", "integrity": "sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==" }, + "@ts-morph/common": { + "version": "0.12.3", + "resolved": "https://registry.npmmirror.com/@ts-morph/common/-/common-0.12.3.tgz", + "integrity": "sha512-4tUmeLyXJnJWvTFOKtcNJ1yh0a3SsTLi2MUoyj8iUNznFRN1ZquaNe7Oukqrnki2FzZkm0J9adCNLDZxUzvj+w==", + "dev": true, + "requires": { + "fast-glob": "^3.2.7", + "minimatch": "^3.0.4", + "mkdirp": "^1.0.4", + "path-browserify": "^1.0.1" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + } + } + }, "@tsconfig/node10": { "version": "1.0.11", "resolved": "https://packages.aliyun.com/670e108663cd360abfe4be65/npm/npm-registry/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -17956,6 +19104,12 @@ "@types/unist": "^2" } }, + "@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmmirror.com/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true + }, "@types/minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", @@ -18213,6 +19367,83 @@ "integrity": "sha512-8LwjnlP39s08C08J5NstzriPvW1SP8Zfpp1BvC2sI35kPeZnHfxVkCwu4/+Wodgnd60UtT1n8K8zw+Mp7J9JmQ==", "dev": true }, + "@vue/compiler-core": { + "version": "3.5.41", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.41.tgz", + "integrity": "sha512-q0Xtv/F9w2YO/7htQhtiL+Ev2WCJbe5N2hc+XfgyKkEKqWpSxknmT8QOuGdEKNdjPq0c3F7rNpFkTo3Kfrm7pg==", + "dev": true, + "requires": { + "@babel/parser": "^7.29.8", + "@vue/shared": "3.5.41", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + }, + "dependencies": { + "entities": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true + } + } + }, + "@vue/compiler-dom": { + "version": "3.5.41", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.41.tgz", + "integrity": "sha512-oKacVfNglLvGjnS6BXOlGL7EyG2h8X03pqXCjzotRZUaXGjbrTJUnVAQjrCqUnS+lyu31nwQjZY/d817GmCnfw==", + "dev": true, + "requires": { + "@vue/compiler-core": "3.5.41", + "@vue/shared": "3.5.41" + } + }, + "@vue/compiler-sfc": { + "version": "3.5.41", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.41.tgz", + "integrity": "sha512-XJhip7R2wy6vX3knCxdZN4KracFaZUef58s1KYewqluedHIJaPIVfXoYT7MF1F8nCvv6k8bWWxDC8opMkg1VTQ==", + "dev": true, + "requires": { + "@babel/parser": "^7.29.8", + "@vue/compiler-core": "3.5.41", + "@vue/compiler-dom": "3.5.41", + "@vue/compiler-ssr": "3.5.41", + "@vue/shared": "3.5.41", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.19", + "source-map-js": "^1.2.1" + }, + "dependencies": { + "postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "requires": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + } + } + } + }, + "@vue/compiler-ssr": { + "version": "3.5.41", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.41.tgz", + "integrity": "sha512-U3v5OejKEGqOI0Wy0+Sz7hGuIFZHA4LSXzrNM3IMIeDyJEBBfTpX26n3SDgToRpP2bLc9FfI2j/kSgcJ8Emq5A==", + "dev": true, + "requires": { + "@vue/compiler-dom": "3.5.41", + "@vue/shared": "3.5.41" + } + }, + "@vue/shared": { + "version": "3.5.41", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.41.tgz", + "integrity": "sha512-IOnwSCma8j+9xJT6b8H0dEYidC80NsYmNMlZxRsukYcSoGaDBohog5hDxzeUXdFeGWFA++vWvxqOmrr96VlqMA==", + "dev": true + }, "acorn": { "version": "7.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", @@ -18510,6 +19741,12 @@ "is-array-buffer": "^3.0.5" } }, + "array-differ": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/array-differ/-/array-differ-3.0.0.tgz", + "integrity": "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==", + "dev": true + }, "array-ify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", @@ -19074,6 +20311,23 @@ "resolved": "https://packages.aliyun.com/670e108663cd360abfe4be65/npm/npm-registry/charenc/-/charenc-0.0.2.tgz", "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==" }, + "chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "requires": { + "readdirp": "^5.0.0" + }, + "dependencies": { + "readdirp": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-5.1.1.tgz", + "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", + "dev": true + } + } + }, "ci-info": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", @@ -19168,6 +20422,12 @@ "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", "dev": true }, + "code-block-writer": { + "version": "11.0.3", + "resolved": "https://registry.npmmirror.com/code-block-writer/-/code-block-writer-11.0.3.tgz", + "integrity": "sha512-NiujjUFB4SwScJq2bwbYUtXbZhBSlY6vYzm++3Q6oC+U+injTqfPYFK8wS9COOmb2lueqp0ZRB4nK1VYeHgNyw==", + "dev": true + }, "collect-v8-coverage": { "version": "1.0.2", "resolved": "https://packages.aliyun.com/670e108663cd360abfe4be65/npm/npm-registry/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", @@ -19713,17 +20973,171 @@ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, + "depcheck": { + "version": "1.4.3", + "resolved": "https://registry.npmmirror.com/depcheck/-/depcheck-1.4.3.tgz", + "integrity": "sha512-vy8xe1tlLFu7t4jFyoirMmOR7x7N601ubU9Gkifyr9z8rjBFtEdWHDBMqXyk6OkK+94NXutzddVXJuo0JlUQKQ==", + "dev": true, + "requires": { + "@babel/parser": "7.16.4", + "@babel/traverse": "^7.12.5", + "@vue/compiler-sfc": "^3.0.5", + "camelcase": "^6.2.0", + "cosmiconfig": "^7.0.0", + "debug": "^4.2.0", + "deps-regex": "^0.1.4", + "ignore": "^5.1.8", + "is-core-module": "^2.4.0", + "js-yaml": "^3.14.0", + "json5": "^2.1.3", + "lodash": "^4.17.20", + "minimatch": "^3.0.4", + "multimatch": "^5.0.0", + "please-upgrade-node": "^3.2.0", + "query-ast": "^1.0.3", + "readdirp": "^3.5.0", + "require-package-name": "^2.0.1", + "resolve": "^1.18.1", + "sass": "^1.29.0", + "scss-parser": "^1.0.4", + "semver": "^7.3.2", + "yargs": "^16.1.0" + }, + "dependencies": { + "@babel/parser": { + "version": "7.16.4", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.16.4.tgz", + "integrity": "sha512-6V0qdPUaiVHH3RtZeLIsc+6pDhbYzHR8ogA8w+f+Wc77DuXto19g2QUwveINoS34Uw+W8/hQDGJCx+i4n7xcng==", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmmirror.com/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmmirror.com/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "requires": { + "ms": "^2.1.3" + } + }, + "js-yaml": { + "version": "3.15.1", + "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-3.15.1.tgz", + "integrity": "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yargs": { + "version": "16.2.2", + "resolved": "https://registry.npmmirror.com/yargs/-/yargs-16.2.2.tgz", + "integrity": "sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w==", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + } + } + }, "depd": { "version": "2.0.0", "resolved": "https://packages.aliyun.com/670e108663cd360abfe4be65/npm/npm-registry/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "dev": true }, + "deps-regex": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/deps-regex/-/deps-regex-0.1.4.tgz", + "integrity": "sha512-3tzwGYogSJi8HoG93R5x9NrdefZQOXgHgGih/7eivloOq6yC6O+yoFxZnkgP661twvfILONfoKRdF9GQOGx2RA==", + "dev": true + }, "destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" }, + "detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "optional": true + }, "detect-newline": { "version": "3.1.0", "resolved": "https://packages.aliyun.com/670e108663cd360abfe4be65/npm/npm-registry/detect-newline/-/detect-newline-3.1.0.tgz", @@ -20756,6 +22170,12 @@ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true }, + "estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, "esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -22014,6 +23434,12 @@ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true }, + "immutable": { + "version": "5.1.9", + "resolved": "https://registry.npmmirror.com/immutable/-/immutable-5.1.9.tgz", + "integrity": "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==", + "dev": true + }, "import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -22111,6 +23537,15 @@ "side-channel": "^1.1.0" } }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmmirror.com/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, + "requires": { + "loose-envify": "^1.0.0" + } + }, "ip": { "version": "1.1.9", "resolved": "https://packages.aliyun.com/670e108663cd360abfe4be65/npm/npm-registry/ip/-/ip-1.1.9.tgz", @@ -23434,6 +24869,15 @@ "es5-ext": "~0.10.2" } }, + "magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "make-dir": { "version": "1.3.0", "resolved": "https://packages.aliyun.com/670e108663cd360abfe4be65/npm/npm-registry/make-dir/-/make-dir-1.3.0.tgz", @@ -23760,6 +25204,46 @@ "resolved": "https://packages.aliyun.com/670e108663cd360abfe4be65/npm/npm-registry/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "multimatch": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/multimatch/-/multimatch-5.0.0.tgz", + "integrity": "sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==", + "dev": true, + "requires": { + "@types/minimatch": "^3.0.3", + "array-differ": "^3.0.0", + "array-union": "^2.1.0", + "arrify": "^2.0.1", + "minimatch": "^3.0.4" + }, + "dependencies": { + "arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, "mute-stream": { "version": "0.0.8", "resolved": "https://packages.aliyun.com/670e108663cd360abfe4be65/npm/npm-registry/mute-stream/-/mute-stream-0.0.8.tgz", @@ -23775,6 +25259,12 @@ "thenify-all": "^1.0.0" } }, + "nanoid": { + "version": "3.3.17", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.17.tgz", + "integrity": "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==", + "dev": true + }, "natural-compare": { "version": "1.4.0", "resolved": "https://packages.aliyun.com/670e108663cd360abfe4be65/npm/npm-registry/natural-compare/-/natural-compare-1.4.0.tgz", @@ -23813,6 +25303,13 @@ "debug": "^2.1.0" } }, + "node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "optional": true + }, "node-fetch": { "version": "2.7.0", "resolved": "https://packages.aliyun.com/670e108663cd360abfe4be65/npm/npm-registry/node-fetch/-/node-fetch-2.7.0.tgz", @@ -24105,6 +25602,12 @@ "lines-and-columns": "^1.1.6" } }, + "path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true + }, "path-equal": { "version": "1.2.5", "resolved": "https://packages.aliyun.com/670e108663cd360abfe4be65/npm/npm-registry/path-equal/-/path-equal-1.2.5.tgz", @@ -24543,6 +26046,16 @@ "side-channel": "^1.1.0" } }, + "query-ast": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/query-ast/-/query-ast-1.0.5.tgz", + "integrity": "sha512-JK+1ma4YDuLjvKKcz9JZ70G+CM9qEOs/l1cZzstMMfwKUabTJ9sud5jvDGrUNuv03yKUgs82bLkHXJkDyhRmBw==", + "dev": true, + "requires": { + "invariant": "2.2.4", + "lodash": "^4.17.21" + } + }, "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -24694,6 +26207,15 @@ } } }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, "readline": { "version": "1.3.0", "resolved": "https://packages.aliyun.com/670e108663cd360abfe4be65/npm/npm-registry/readline/-/readline-1.3.0.tgz", @@ -24804,6 +26326,12 @@ "resolved": "https://packages.aliyun.com/670e108663cd360abfe4be65/npm/npm-registry/require-main-filename/-/require-main-filename-2.0.0.tgz", "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" }, + "require-package-name": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/require-package-name/-/require-package-name-2.0.1.tgz", + "integrity": "sha512-uuoJ1hU/k6M0779t3VMVIYpb2VMJk05cehCaABFhXaibcbvfgR8wKiozLjVFSzJPmQMRqIcO0HMyTFqfV09V6Q==", + "dev": true + }, "requires-port": { "version": "1.0.0", "resolved": "https://packages.aliyun.com/670e108663cd360abfe4be65/npm/npm-registry/requires-port/-/requires-port-1.0.0.tgz", @@ -24949,11 +26477,41 @@ "resolved": "https://packages.aliyun.com/670e108663cd360abfe4be65/npm/npm-registry/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "sass": { + "version": "1.102.0", + "resolved": "https://registry.npmmirror.com/sass/-/sass-1.102.0.tgz", + "integrity": "sha512-NSOyTnaQF7rTAEOtI2fwb386vL+akyiQLBZu8Na7hXCb+umJy0GAqlcMIaqACZ6Z1VgTBS4K9PG6B3IdjHGJsw==", + "dev": true, + "requires": { + "@parcel/watcher": "^2.4.1", + "chokidar": "^5.0.0", + "immutable": "^5.1.5", + "source-map-js": ">=0.6.2 <2.0.0" + } + }, "sax": { "version": "1.4.1", "resolved": "https://packages.aliyun.com/670e108663cd360abfe4be65/npm/npm-registry/sax/-/sax-1.4.1.tgz", "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==" }, + "scss-parser": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/scss-parser/-/scss-parser-1.0.6.tgz", + "integrity": "sha512-SH3TaoaJFzfAtqs3eG1j5IuHJkeEW5rKUPIjIN+ZorLAyJLHItQGnsgwHk76v25GtLtpT9IqfAcqK4vFWdiw+w==", + "dev": true, + "requires": { + "invariant": "2.2.4", + "lodash": "4.17.21" + }, + "dependencies": { + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + } + } + }, "sdk-base": { "version": "2.0.1", "resolved": "https://packages.aliyun.com/670e108663cd360abfe4be65/npm/npm-registry/sdk-base/-/sdk-base-2.0.1.tgz", @@ -25177,6 +26735,12 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true }, + "source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true + }, "source-map-support": { "version": "0.5.13", "resolved": "https://packages.aliyun.com/670e108663cd360abfe4be65/npm/npm-registry/source-map-support/-/source-map-support-0.5.13.tgz", @@ -25847,6 +27411,12 @@ "integrity": "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==", "dev": true }, + "true-myth": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/true-myth/-/true-myth-4.1.1.tgz", + "integrity": "sha512-rqy30BSpxPznbbTcAcci90oZ1YR4DqvKcNXNerG5gQBU2v4jk0cygheiul5J6ExIMrgDVuanv/MkGfqZbKrNNg==", + "dev": true + }, "ts-jest": { "version": "29.4.6", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz", @@ -25878,6 +27448,16 @@ } } }, + "ts-morph": { + "version": "13.0.3", + "resolved": "https://registry.npmmirror.com/ts-morph/-/ts-morph-13.0.3.tgz", + "integrity": "sha512-pSOfUMx8Ld/WUreoSzvMFQG5i9uEiWIsBYjpU9+TTASOeUa89j5HykomeqVULm1oqWtBdleI3KEFRLrlA3zGIw==", + "dev": true, + "requires": { + "@ts-morph/common": "~0.12.3", + "code-block-writer": "^11.0.0" + } + }, "ts-node": { "version": "10.9.2", "resolved": "https://packages.aliyun.com/670e108663cd360abfe4be65/npm/npm-registry/ts-node/-/ts-node-10.9.2.tgz", @@ -25907,6 +27487,20 @@ } } }, + "ts-prune": { + "version": "0.10.3", + "resolved": "https://registry.npmmirror.com/ts-prune/-/ts-prune-0.10.3.tgz", + "integrity": "sha512-iS47YTbdIcvN8Nh/1BFyziyUqmjXz7GVzWu02RaZXqb+e/3Qe1B7IQ4860krOeCGUeJmterAlaM2FRH0Ue0hjw==", + "dev": true, + "requires": { + "commander": "^6.2.1", + "cosmiconfig": "^7.0.1", + "json5": "^2.1.3", + "lodash": "^4.17.21", + "true-myth": "^4.1.0", + "ts-morph": "^13.0.1" + } + }, "tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", diff --git a/package.json b/package.json index 0e5a76bc..67ed7cb2 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,10 @@ "publish": "npm i && npm run build && s registry publish", "lint": "f2elint scan", "fix": "f2elint fix", - "test": "jest --config jestconfig.json --coverage" + "test": "jest --config jestconfig.json __tests__/ut --coverage", + "test:it": "jest --config jestconfig.json __tests__/it", + "deadcode": "ts-prune -p tsconfig.json", + "depcheck": "depcheck" }, "repository": "git@github.com:devsapp/fc3.git", "keywords": [], @@ -33,6 +36,7 @@ "@serverless-cd/srm-aliyun-ram20150501": "^0.0.2-beta.9", "@serverless-cd/srm-aliyun-sls20201230": "0.0.5-beta.3", "@serverless-devs/diff": "^0.0.3-beta.6", + "@serverless-devs/docker-image-builder": "^0.0.0-20260518-164317-160dd89efac1", "@serverless-devs/downloads": "^0.0.7", "@serverless-devs/load-component": "^0.0.9", "@serverless-devs/utils": "^0.0.17", @@ -56,7 +60,6 @@ "string-random": "^0.1.3", "tty-table": "^4.2.3", "uuid": "^9.0.1", - "@serverless-devs/docker-image-builder": "^0.0.0-20260518-164317-160dd89efac1", "uuid-by-string": "^4.0.0" }, "devDependencies": { @@ -66,11 +69,13 @@ "@types/lodash": "^4.17.25", "@types/node": "^20.12.11", "@vercel/ncc": "^0.38.4", + "depcheck": "^1.4.3", "f2elint": "^2.2.1", "jest": "^29.7.0", "prettier": "^3.8.1", "ts-jest": "^29.4.6", "ts-node": "^10.9.2", + "ts-prune": "^0.10.3", "typescript": "^4.4.2", "typescript-json-schema": "^0.67.1" }, diff --git a/src/interface/trigger.ts b/src/interface/trigger.ts index f67536ac..33e88fd8 100644 --- a/src/interface/trigger.ts +++ b/src/interface/trigger.ts @@ -138,11 +138,7 @@ export function convertIHttpTriggerConfig( // eslint-disable-next-line no-param-reassign httpTriggerConfig.authConfig.claimPassBy = 'query::'; } - // TODO 如果同时存在,报错。 - // if (httpTriggerConfig.authConfig.whitelist) { - // // eslint-disable-next-line no-param-reassign - // httpTriggerConfig.authConfig.blacklist = null; - // } + // TODO 如果 whitelist 与 blacklist 同时存在,应报错。 return httpTriggerConfig; } diff --git a/src/resources/acr/login.ts b/src/resources/acr/login.ts index 41e4f53e..34ef1c96 100644 --- a/src/resources/acr/login.ts +++ b/src/resources/acr/login.ts @@ -131,7 +131,6 @@ async function getAuthorizationTokenForAcrEE( { InstanceId: instanceID }, requestOption, ); - // logger.debug(`GetAuthorizationToken result: ${JSON.stringify(result)}`); return { dockerTmpUser: result.TempUsername, dockerTmpToken: result.AuthorizationToken, diff --git a/src/resources/fc/impl/utils.ts b/src/resources/fc/impl/utils.ts index 22a712e6..f4b46aab 100644 --- a/src/resources/fc/impl/utils.ts +++ b/src/resources/fc/impl/utils.ts @@ -106,8 +106,6 @@ export const getCustomEndpoint = ( return { protocol }; } - // logger.info(`get custom endpoint: ${CUSTOM_ENDPOINT}`); - if (CUSTOM_ENDPOINT.startsWith('http://')) { return { protocol: 'http', diff --git a/src/resources/fc/index.ts b/src/resources/fc/index.ts index bd549649..0849ce50 100644 --- a/src/resources/fc/index.ts +++ b/src/resources/fc/index.ts @@ -261,7 +261,6 @@ export default class FC extends FC_Client { * 先 diff 获取需要删除的 tags 和需要添加的 tags */ const { deleteTags, addTags } = this.diffTags(remoteTags, localTags); - // logger.info(`deleteTags: ${JSON.stringify(deleteTags,null,2)}, addTags: ${JSON.stringify(addTags,null,2)}`); if (deleteTags?.length) { const untagResourcesRequest = new UntagResourcesRequest({ diff --git a/src/resources/vpc-nas/index.ts b/src/resources/vpc-nas/index.ts index 5de6b230..dcf71a78 100644 --- a/src/resources/vpc-nas/index.ts +++ b/src/resources/vpc-nas/index.ts @@ -11,7 +11,10 @@ export default class VpcNas { private client: PopClient; private vpcClient: PopClient; - constructor(private region: IRegion, credentials: ICredentials) { + constructor( + private region: IRegion, + credentials: ICredentials, + ) { // https://help.aliyun.com/zh/sdk/developer-reference/configure-a-timeout-period-2 const opts = { connectTimeout: 5000, @@ -80,12 +83,6 @@ export default class VpcNas { ); logger.debug(`ModifyVpcAttribute: ${JSON.stringify(result2)}`); } - // } else { - // const regex = /^(?!http:\/\/|https:\/\/)[a-zA-Z一-龥][a-zA-Z0-9一-龥_-]*$/; - // if (!regex.test(result.VpcName)) { - // return VPC_AND_NAS_NAME; - // } - // } return result.VpcName; } catch (ex) { console.log(ex); diff --git a/src/subCommands/deploy/impl/custom_domain.ts b/src/subCommands/deploy/impl/custom_domain.ts index 69597028..27717fef 100644 --- a/src/subCommands/deploy/impl/custom_domain.ts +++ b/src/subCommands/deploy/impl/custom_domain.ts @@ -52,7 +52,6 @@ export default class CustomDomain extends Base { const deployInput = _.cloneDeep(this.customDomainInputs); try { const onlineCustomDomain = await this.domainInstance.info(infoInput); - // console.log(JSON.stringify(onlineCustomDomain, null, 2)); let routes = onlineCustomDomain?.routeConfig?.routes; if (!routes) { routes = []; diff --git a/src/subCommands/deploy/impl/function.ts b/src/subCommands/deploy/impl/function.ts index 374324e2..482aa618 100644 --- a/src/subCommands/deploy/impl/function.ts +++ b/src/subCommands/deploy/impl/function.ts @@ -318,7 +318,6 @@ export default class Service extends Base { logger.debug(`压缩程序执行时间: ${milliseconds / 1000}s`); zipPath = generateZipFilePath; } - // logger.debug(`Zip file: ${zipPath}`); // debug show zip file size getFileSize(zipPath); diff --git a/src/subCommands/deploy/impl/provision_config.ts b/src/subCommands/deploy/impl/provision_config.ts index 755f2563..63ee8c07 100644 --- a/src/subCommands/deploy/impl/provision_config.ts +++ b/src/subCommands/deploy/impl/provision_config.ts @@ -7,7 +7,6 @@ import logger from '../../../logger'; import Base from './base'; import { sleep } from '../../../utils'; import { provisionConfigErrorRetry } from '../utils'; -// import Logs from '../../logs'; interface IOpts { yes: boolean | undefined; diff --git a/src/subCommands/invoke/index.ts b/src/subCommands/invoke/index.ts index c261dd0b..6d46ca1f 100644 --- a/src/subCommands/invoke/index.ts +++ b/src/subCommands/invoke/index.ts @@ -104,7 +104,6 @@ export default class Invoke { }); logger.debug(`invoke function ${this.functionName} result ${JSON.stringify(result)}`); if (this.silent) { - // console.log(result.body); return { body: result.body, }; diff --git a/src/subCommands/local/impl/invoke/baseLocalInvoke.ts b/src/subCommands/local/impl/invoke/baseLocalInvoke.ts index 94ad811c..b58e0eca 100644 --- a/src/subCommands/local/impl/invoke/baseLocalInvoke.ts +++ b/src/subCommands/local/impl/invoke/baseLocalInvoke.ts @@ -114,8 +114,6 @@ export class BaseLocalInvoke extends BaseLocal { async getLocalInvokeCmdStr(): Promise { const port = await portFinder.getPortPromise({ port: this.getCaPort() }); - // const msg = `You can use curl or Postman to make an HTTP request to localhost:${port} to test the function.for example:`; - // console.log('\x1b[33m%s\x1b[0m', msg); this.port = port; const mntStr = await this.getMountString(); diff --git a/src/subCommands/local/impl/invoke/customContainerLocalInvoke.ts b/src/subCommands/local/impl/invoke/customContainerLocalInvoke.ts index a909673f..5c466abb 100644 --- a/src/subCommands/local/impl/invoke/customContainerLocalInvoke.ts +++ b/src/subCommands/local/impl/invoke/customContainerLocalInvoke.ts @@ -73,8 +73,6 @@ export class CustomContainerLocalInvoke extends BaseLocalInvoke { async getLocalInvokeCmdStr(): Promise { const port = await portFinder.getPortPromise({ port: this.getCaPort() }); - // const msg = `You can use curl or Postman to make an HTTP request to localhost:${port} to test the function.for example:`; - // console.log('\x1b[33m%s\x1b[0m', msg); this._port = port; const image = await this.getRuntimeRunImage(); const envStr = await this.getEnvString(); diff --git a/src/subCommands/local/impl/invoke/pythonLocalInvoke.ts b/src/subCommands/local/impl/invoke/pythonLocalInvoke.ts index 08565c0d..8d056fda 100644 --- a/src/subCommands/local/impl/invoke/pythonLocalInvoke.ts +++ b/src/subCommands/local/impl/invoke/pythonLocalInvoke.ts @@ -19,7 +19,6 @@ export class PythonLocalInvoke extends BaseLocalInvoke { getDebugArgs(): string { if (_.isFinite(this.getDebugPort())) { - // return `FC_DEBUG_ARGS=-m ptvsd --host 0.0.0.0 --port ${this.getDebugPort()} --wait`; return `FC_DEBUG_ARGS=-m debugpy --listen 0.0.0.0:${this.getDebugPort()} --wait-for-client`; } return ''; diff --git a/src/subCommands/local/impl/start/customLocalStart.ts b/src/subCommands/local/impl/start/customLocalStart.ts index d5bdf8f4..84b47eda 100644 --- a/src/subCommands/local/impl/start/customLocalStart.ts +++ b/src/subCommands/local/impl/start/customLocalStart.ts @@ -2,7 +2,6 @@ import { BaseLocalStart } from './baseLocalStart'; import { runCommand } from '../../../../utils'; import _ from 'lodash'; import chalk from 'chalk'; -// import logger from '../logger'; export class CustomLocalStart extends BaseLocalStart { getDebugArgs(): string { diff --git a/src/subCommands/local/impl/start/goLocalInvoke.ts b/src/subCommands/local/impl/start/goLocalStart.ts similarity index 100% rename from src/subCommands/local/impl/start/goLocalInvoke.ts rename to src/subCommands/local/impl/start/goLocalStart.ts diff --git a/src/subCommands/local/index.ts b/src/subCommands/local/index.ts index 81aa541c..04788271 100644 --- a/src/subCommands/local/index.ts +++ b/src/subCommands/local/index.ts @@ -15,7 +15,7 @@ import logger from '../../logger'; import { NodejsLocalStart } from './impl/start/nodejsLocalStart'; import { PythonLocalStart } from './impl/start/pythonLocalStart'; import { PhpLocalStart } from './impl/start/phpLocalStart'; -import { GoLocalStart } from './impl/start/goLocalInvoke'; +import { GoLocalStart } from './impl/start/goLocalStart'; import { DotnetLocalStart } from './impl/start/dotnetLocalStart'; import { JavaLocalStart } from './impl/start/javaLocalStart'; diff --git a/src/subCommands/logs/index.ts b/src/subCommands/logs/index.ts index ec73bb89..59ac72f0 100644 --- a/src/subCommands/logs/index.ts +++ b/src/subCommands/logs/index.ts @@ -292,10 +292,6 @@ export default class Logs { l = `${COLOR_MAP[colorIndex]}${instanceId}\x1B[0m ${l}`; } - // if (extra?.qualifier) { - // l = `${extra.qualifier} ${l}`; - // } - if (match) { l = replaceAll(l, match, `\x1B[7m${match}\x1B[0m`); } diff --git a/src/subCommands/plan/index.ts b/src/subCommands/plan/index.ts index ba57e73c..8345edb1 100644 --- a/src/subCommands/plan/index.ts +++ b/src/subCommands/plan/index.ts @@ -292,7 +292,6 @@ export default class Plan { const planInput = _.cloneDeep(customDomainInputs); try { const onlineCustomDomain = await domainInstance.info(infoInput); - // console.log(JSON.stringify(onlineCustomDomain, null, 2)); const routes = onlineCustomDomain?.routeConfig?.routes; let found = false; if (routes) { diff --git a/src/subCommands/remove/index.ts b/src/subCommands/remove/index.ts index 84d910ac..f600c85b 100644 --- a/src/subCommands/remove/index.ts +++ b/src/subCommands/remove/index.ts @@ -495,7 +495,6 @@ export default class Remove { routes.splice(index, 1); customDomainInputs.props = onlineCustomDomain; onlineCustomDomain.routeConfig.routes = routes; - // console.log(JSON.stringify(customDomainInputs)); if ( customDomainInputs.args.indexOf('-y') === -1 && customDomainInputs.args.indexOf('--assume-yes') === -1 diff --git a/src/utils/run-command.ts b/src/utils/run-command.ts index 47053aa4..908932b6 100644 --- a/src/utils/run-command.ts +++ b/src/utils/run-command.ts @@ -24,7 +24,6 @@ async function runCommand( if (shellScript) { args.push(shellScript); - // args.push(...shellScript.split(' ')); } logger.debug(`runCommand args = ${JSON.stringify(args)}`); diff --git a/version.md b/version.md deleted file mode 100644 index e69de29b..00000000 From ea5ade656b544638a45e435b5970d29915580168 Mon Sep 17 00:00:00 2001 From: ls147258 Date: Sat, 8 Aug 2026 09:37:44 +0800 Subject: [PATCH 2/2] test: apply f2elint formatting and fix Windows path portability - Run npm run fix so check-format CI passes (no residual TS diffs) - 2to3 test: use path.join instead of hardcoded POSIX paths so assertions pass on Windows (path.join uses the platform separator) Signed-off-by: ls147258 --- CLAUDE.md | 28 +++++++-------- __tests__/ut/commands/2to3/index_test.ts | 22 +++++------- .../ut/commands/deploy/impl/trigger_test.ts | 4 +-- .../commands/deploy/impl/vpc_binding_test.ts | 4 +-- .../ut/commands/deploy/utils/index_test.ts | 16 +++++++-- __tests__/ut/commands/info/index_test.ts | 23 +++---------- __tests__/ut/resources/acr/index_test.ts | 6 +++- __tests__/ut/resources/acr/login_test.ts | 7 +++- __tests__/ut/resources/ram/index_test.ts | 4 +-- __tests__/ut/resources/vpc-nas/index_test.ts | 6 ++-- docs/CONTRIB.md | 34 +++++++++---------- src/resources/vpc-nas/index.ts | 5 +-- 12 files changed, 75 insertions(+), 84 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 25e1687f..67c59689 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,20 +10,20 @@ FC3 is the Serverless Devs component for Alibaba Cloud Function Compute 3.0, pro ## Available Scripts -| Script | Description | -| ------------------------- | -------------------------------------------- | -| `npm run build` | Production bundle with ncc | -| `npm run watch` | TypeScript watch mode | -| `npm test` | Unit tests with coverage (no credentials) | -| `npm run test:it` | Integration tests (needs cloud credentials) | -| `npm run typecheck` | Type-check without emitting | -| `npm run deadcode` | Audit unused exports (ts-prune) | -| `npm run depcheck` | Audit unused dependencies | -| `npm run format` | Prettier formatting | -| `npm run lint` | f2elint scanning | -| `npm run fix` | Auto-fix lint issues | -| `npm run publish` | Build and registry publish | -| `npm run generate-schema` | Generate JSON schema | +| Script | Description | +| ------------------------- | ------------------------------------------- | +| `npm run build` | Production bundle with ncc | +| `npm run watch` | TypeScript watch mode | +| `npm test` | Unit tests with coverage (no credentials) | +| `npm run test:it` | Integration tests (needs cloud credentials) | +| `npm run typecheck` | Type-check without emitting | +| `npm run deadcode` | Audit unused exports (ts-prune) | +| `npm run depcheck` | Audit unused dependencies | +| `npm run format` | Prettier formatting | +| `npm run lint` | f2elint scanning | +| `npm run fix` | Auto-fix lint issues | +| `npm run publish` | Build and registry publish | +| `npm run generate-schema` | Generate JSON schema | ## Key Directories diff --git a/__tests__/ut/commands/2to3/index_test.ts b/__tests__/ut/commands/2to3/index_test.ts index 429b2f68..4357ebe4 100644 --- a/__tests__/ut/commands/2to3/index_test.ts +++ b/__tests__/ut/commands/2to3/index_test.ts @@ -2,6 +2,7 @@ import SYaml2To3 from '../../../../src/subCommands/2to3'; import { IInputs } from '../../../../src/interface'; import { parseArgv } from '@serverless-devs/utils'; import fs from 'fs'; +import path from 'path'; import yaml from 'js-yaml'; jest.mock('@serverless-devs/utils', () => ({ @@ -78,8 +79,8 @@ describe('SYaml2To3', () => { describe('constructor', () => { it('should resolve absolute source and target paths from baseDir', () => { const s = new SYaml2To3(mockInputs); - expect(s.source).toBe('/test/s.yaml'); - expect(s.target).toBe('/test/s3.yaml'); + expect(s.source).toBe(path.join('/test', 's.yaml')); + expect(s.target).toBe(path.join('/test', 's3.yaml')); }); it('should keep absolute paths untouched', () => { @@ -96,7 +97,7 @@ describe('SYaml2To3', () => { it('should default the target to s3.yaml when not specified', () => { (parseArgv as jest.Mock).mockReturnValue({ source: 's.yaml', help: false }); const s = new SYaml2To3(mockInputs); - expect(s.target).toBe('/test/s3.yaml'); + expect(s.target).toBe(path.join('/test', 's3.yaml')); }); it('should fall back to process.cwd() when baseDir is missing', () => { @@ -240,17 +241,12 @@ describe('SYaml2To3', () => { { domainName: 'test.com', protocol: 'HTTP', - routeConfigs: [ - { path: '/', serviceName: 'service1', functionName: 'function1' }, - ], + routeConfigs: [{ path: '/', serviceName: 'service1', functionName: 'function1' }], }, ], }, actions: { - 'pre-deploy': [ - { component: 'fc build --use-docker' }, - { component: 'fc invoke' }, - ], + 'pre-deploy': [{ component: 'fc build --use-docker' }, { component: 'fc invoke' }], 'empty-action': '', }, }, @@ -263,7 +259,7 @@ describe('SYaml2To3', () => { await s.run(); expect(dumpSpy).toHaveBeenCalled(); - expect(writeSpy).toHaveBeenCalledWith('/test/s3.yaml', 'dumped-yaml'); + expect(writeSpy).toHaveBeenCalledWith(path.join('/test', 's3.yaml'), 'dumped-yaml'); // Inspect the transformed object handed to yaml.dump const transformed = dumpSpy.mock.calls[0][0] as any; @@ -336,9 +332,7 @@ describe('SYaml2To3', () => { customDomain: { domainName: 'test.com', protocol: 'HTTP', - routeConfigs: [ - { path: '/', serviceName: 'service1', functionName: 'function1' }, - ], + routeConfigs: [{ path: '/', serviceName: 'service1', functionName: 'function1' }], }, }, }, diff --git a/__tests__/ut/commands/deploy/impl/trigger_test.ts b/__tests__/ut/commands/deploy/impl/trigger_test.ts index 5e4eeb86..ff2dcdaa 100644 --- a/__tests__/ut/commands/deploy/impl/trigger_test.ts +++ b/__tests__/ut/commands/deploy/impl/trigger_test.ts @@ -79,9 +79,7 @@ describe('Trigger', () => { it('calls _getRemote and _plan', async () => { // Arrange const trigger = new Trigger(mockInputs, mockOpts); - const getRemoteSpy = jest - .spyOn(trigger as any, '_getRemote') - .mockResolvedValue(undefined); + const getRemoteSpy = jest.spyOn(trigger as any, '_getRemote').mockResolvedValue(undefined); const planSpy = jest.spyOn(trigger as any, '_plan').mockResolvedValue(undefined); // Act diff --git a/__tests__/ut/commands/deploy/impl/vpc_binding_test.ts b/__tests__/ut/commands/deploy/impl/vpc_binding_test.ts index 782c7b57..59de9192 100644 --- a/__tests__/ut/commands/deploy/impl/vpc_binding_test.ts +++ b/__tests__/ut/commands/deploy/impl/vpc_binding_test.ts @@ -76,9 +76,7 @@ describe('VpcBinding', () => { it('calls _getRemote and _plan', async () => { // Arrange const vpcBinding = new VpcBinding(mockInputs, mockOpts); - const getRemoteSpy = jest - .spyOn(vpcBinding as any, '_getRemote') - .mockResolvedValue(undefined); + const getRemoteSpy = jest.spyOn(vpcBinding as any, '_getRemote').mockResolvedValue(undefined); const planSpy = jest.spyOn(vpcBinding as any, '_plan').mockResolvedValue(undefined); // Act diff --git a/__tests__/ut/commands/deploy/utils/index_test.ts b/__tests__/ut/commands/deploy/utils/index_test.ts index fc4b79fb..a140a438 100644 --- a/__tests__/ut/commands/deploy/utils/index_test.ts +++ b/__tests__/ut/commands/deploy/utils/index_test.ts @@ -41,7 +41,13 @@ describe('provisionConfigErrorRetry', () => { }; // Act - await provisionConfigErrorRetry(fcSdk, 'ProvisionConfig', FUNCTION_NAME, QUALIFIER, LOCAL_CONFIG); + await provisionConfigErrorRetry( + fcSdk, + 'ProvisionConfig', + FUNCTION_NAME, + QUALIFIER, + LOCAL_CONFIG, + ); // Assert expect(fcSdk.putFunctionProvisionConfig).toHaveBeenCalledWith( @@ -101,7 +107,13 @@ describe('provisionConfigErrorRetry', () => { isProvisionConfigErrorMock.mockReturnValue(true); // Act - await provisionConfigErrorRetry(fcSdk, 'ProvisionConfig', FUNCTION_NAME, QUALIFIER, LOCAL_CONFIG); + await provisionConfigErrorRetry( + fcSdk, + 'ProvisionConfig', + FUNCTION_NAME, + QUALIFIER, + LOCAL_CONFIG, + ); // Assert expect(fcSdk.removeFunctionScalingConfig).toHaveBeenCalledWith(FUNCTION_NAME, QUALIFIER); diff --git a/__tests__/ut/commands/info/index_test.ts b/__tests__/ut/commands/info/index_test.ts index ac381dc5..6ed65909 100644 --- a/__tests__/ut/commands/info/index_test.ts +++ b/__tests__/ut/commands/info/index_test.ts @@ -129,10 +129,7 @@ describe('Info', () => { }); it('should build triggersName list from props.triggers', () => { - mockInputs.props.triggers = [ - { triggerName: 't1' }, - { triggerName: 't2' }, - ] as any; + mockInputs.props.triggers = [{ triggerName: 't1' }, { triggerName: 't2' }] as any; const info = new Info(mockInputs); expect(info.triggersName).toEqual(['t1', 't2']); }); @@ -147,9 +144,7 @@ describe('Info', () => { it('should throw when region is not specified', () => { mockInputs.props.region = undefined; - expect(() => new Info(mockInputs)).toThrow( - 'Region not specified, please specify --region', - ); + expect(() => new Info(mockInputs)).toThrow('Region not specified, please specify --region'); }); it('should throw when functionName is not specified', () => { @@ -172,13 +167,8 @@ describe('Info', () => { it('should return the function config from the sdk', async () => { const info = new Info(mockInputs); const result = await info.getFunction(); - expect(mockFcInstance.getFunction).toHaveBeenCalledWith( - 'test-function', - GetApiType.simple, - ); - expect(result).toEqual( - expect.objectContaining({ functionName: 'test-function' }), - ); + expect(mockFcInstance.getFunction).toHaveBeenCalledWith('test-function', GetApiType.simple); + expect(result).toEqual(expect.objectContaining({ functionName: 'test-function' })); }); }); @@ -236,10 +226,7 @@ describe('Info', () => { mockInputs.props.vpcBinding = { vpcIds: ['vpc-1'] } as any; const info = new Info(mockInputs); const result = await info.getVpcBing(); - expect(mockFcInstance.getVpcBinding).toHaveBeenCalledWith( - 'test-function', - GetApiType.simple, - ); + expect(mockFcInstance.getVpcBinding).toHaveBeenCalledWith('test-function', GetApiType.simple); expect(result).toEqual({ vpcIds: ['vpc-1'] }); }); }); diff --git a/__tests__/ut/resources/acr/index_test.ts b/__tests__/ut/resources/acr/index_test.ts index 4ed13ce2..93b9ab9e 100644 --- a/__tests__/ut/resources/acr/index_test.ts +++ b/__tests__/ut/resources/acr/index_test.ts @@ -1,6 +1,10 @@ import { ICredentials } from '@serverless-devs/component-interface'; import Acr from '../../../../src/resources/acr/index'; -import { getDockerTmpUser, getAcrEEInstanceID, getAcrImageMeta } from '../../../../src/resources/acr/login'; +import { + getDockerTmpUser, + getAcrEEInstanceID, + getAcrImageMeta, +} from '../../../../src/resources/acr/login'; import { runCommand, checkDockerIsOK, sleep } from '../../../../src/utils'; jest.mock('../../../../src/resources/acr/login', () => ({ diff --git a/__tests__/ut/resources/acr/login_test.ts b/__tests__/ut/resources/acr/login_test.ts index d05f8e4e..aeb8dde9 100644 --- a/__tests__/ut/resources/acr/login_test.ts +++ b/__tests__/ut/resources/acr/login_test.ts @@ -99,7 +99,12 @@ describe('acr/login', () => { describe('getAcrImageMeta', () => { it('returns false immediately for ACR EE instances', async () => { - const exists = await getAcrImageMeta('cn-hangzhou' as any, credentials, 'x/ns/repo:tag', 'inst-1'); + const exists = await getAcrImageMeta( + 'cn-hangzhou' as any, + credentials, + 'x/ns/repo:tag', + 'inst-1', + ); expect(exists).toBe(false); expect(roaRequest).not.toHaveBeenCalled(); }); diff --git a/__tests__/ut/resources/ram/index_test.ts b/__tests__/ut/resources/ram/index_test.ts index 61a5d2df..ddfd2cd1 100644 --- a/__tests__/ut/resources/ram/index_test.ts +++ b/__tests__/ut/resources/ram/index_test.ts @@ -47,9 +47,7 @@ describe('Role', () => { }); it('assembles an arn from a plain role name and account id', () => { - expect(Role.completionArn('my-role', '123456789')).toBe( - 'acs:ram::123456789:role/my-role', - ); + expect(Role.completionArn('my-role', '123456789')).toBe('acs:ram::123456789:role/my-role'); }); }); }); diff --git a/__tests__/ut/resources/vpc-nas/index_test.ts b/__tests__/ut/resources/vpc-nas/index_test.ts index baeed4fc..0cb02520 100644 --- a/__tests__/ut/resources/vpc-nas/index_test.ts +++ b/__tests__/ut/resources/vpc-nas/index_test.ts @@ -51,7 +51,7 @@ describe('VpcNas', () => { // Assert expect(vpcNas).toBeDefined(); - expect((PopClient as unknown as jest.Mock)).toHaveBeenCalledTimes(2); + expect(PopClient as unknown as jest.Mock).toHaveBeenCalledTimes(2); }); }); @@ -70,9 +70,7 @@ describe('VpcNas', () => { it('assigns a generated VpcName when the vpc name is empty', async () => { // Arrange - mocks.request - .mockResolvedValueOnce({ VpcName: ' ' }) - .mockResolvedValueOnce({ ok: true }); + mocks.request.mockResolvedValueOnce({ VpcName: ' ' }).mockResolvedValueOnce({ ok: true }); const vpcNas = new VpcNas('cn-hangzhou' as any, credentials); // Act diff --git a/docs/CONTRIB.md b/docs/CONTRIB.md index 9814b42e..f40750ff 100644 --- a/docs/CONTRIB.md +++ b/docs/CONTRIB.md @@ -56,23 +56,23 @@ npm run build ## Available Scripts -| Script | Command | Description | -| ----------------- | --------------------------------------------------------------------- | ----------------------------------------------- | -| `build` | `ncc build src/index.ts -m -o dist` | Build production bundle using Vercel ncc | -| `watch` | `npx tsc -w -p tsconfig.json` | Watch mode for development | -| `start` | `npm run watch` | Alias for watch mode | -| `test` | `jest --config jestconfig.json __tests__/ut --coverage` | Run unit tests with coverage (no credentials) | -| `test:it` | `jest --config jestconfig.json __tests__/it` | Run integration tests (needs cloud credentials) | -| `deadcode` | `ts-prune -p tsconfig.json` | Audit unused exports | -| `depcheck` | `depcheck` | Audit unused dependencies | -| `format` | `prettier --write src` | Format source code with Prettier | -| `lint` | `f2elint scan` | Run linter checks | -| `fix` | `f2elint fix` | Auto-fix linting issues | -| `publish` | `npm i && npm run build && s registry publish` | Build and publish to registry | -| `generate-schema` | `typescript-json-schema ./src/interface/index.ts IProps --required` | Generate JSON schema from TypeScript interfaces | -| `typecheck` | `tsc --noEmit -p tsconfig.json` | Type-check without emitting (CI gate) | -| `prebuild` | node one-liner: rm + mkdir `dist`, copy `src/schema.json` | Prepare dist directory before build (portable) | -| `prewatch` | node one-liner: mkdir `dist`, copy `src/schema.json` | Ensure dist and schema.json exist before watch | +| Script | Command | Description | +| ----------------- | ------------------------------------------------------------------- | ----------------------------------------------- | +| `build` | `ncc build src/index.ts -m -o dist` | Build production bundle using Vercel ncc | +| `watch` | `npx tsc -w -p tsconfig.json` | Watch mode for development | +| `start` | `npm run watch` | Alias for watch mode | +| `test` | `jest --config jestconfig.json __tests__/ut --coverage` | Run unit tests with coverage (no credentials) | +| `test:it` | `jest --config jestconfig.json __tests__/it` | Run integration tests (needs cloud credentials) | +| `deadcode` | `ts-prune -p tsconfig.json` | Audit unused exports | +| `depcheck` | `depcheck` | Audit unused dependencies | +| `format` | `prettier --write src` | Format source code with Prettier | +| `lint` | `f2elint scan` | Run linter checks | +| `fix` | `f2elint fix` | Auto-fix linting issues | +| `publish` | `npm i && npm run build && s registry publish` | Build and publish to registry | +| `generate-schema` | `typescript-json-schema ./src/interface/index.ts IProps --required` | Generate JSON schema from TypeScript interfaces | +| `typecheck` | `tsc --noEmit -p tsconfig.json` | Type-check without emitting (CI gate) | +| `prebuild` | node one-liner: rm + mkdir `dist`, copy `src/schema.json` | Prepare dist directory before build (portable) | +| `prewatch` | node one-liner: mkdir `dist`, copy `src/schema.json` | Ensure dist and schema.json exist before watch | ## Development Workflow diff --git a/src/resources/vpc-nas/index.ts b/src/resources/vpc-nas/index.ts index dcf71a78..e535783a 100644 --- a/src/resources/vpc-nas/index.ts +++ b/src/resources/vpc-nas/index.ts @@ -11,10 +11,7 @@ export default class VpcNas { private client: PopClient; private vpcClient: PopClient; - constructor( - private region: IRegion, - credentials: ICredentials, - ) { + constructor(private region: IRegion, credentials: ICredentials) { // https://help.aliyun.com/zh/sdk/developer-reference/configure-a-timeout-period-2 const opts = { connectTimeout: 5000,