├── .circleci └── config.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github └── FUNDING.yml ├── .gitignore ├── .prettierrc ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── README.md ├── jest.config.js ├── lerna.json ├── package-lock.json ├── package.json ├── packages ├── cloudformation-schema │ ├── LICENSE.txt │ ├── NOTICE.txt │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── schema.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── config │ ├── .npmignore │ ├── LICENSE │ ├── NOTICE.txt │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── model │ │ │ ├── custom-tags.ts │ │ │ ├── document.ts │ │ │ ├── globals.ts │ │ │ ├── index.ts │ │ │ ├── jsonSchema.ts │ │ │ ├── referenceables.ts │ │ │ ├── references.ts │ │ │ ├── resources.ts │ │ │ ├── sortedHash.ts │ │ │ └── sub-stack.ts │ │ ├── parser │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── custom-tags.test.ts.snap │ │ │ │ ├── custom-tags.test.ts │ │ │ │ ├── globals.test.ts │ │ │ │ └── sub-stacks.test.ts │ │ │ ├── index.ts │ │ │ ├── json │ │ │ │ ├── array-ast-node.ts │ │ │ │ ├── ast-node.ts │ │ │ │ ├── boolean-ast-node.ts │ │ │ │ ├── document.ts │ │ │ │ ├── external-import-ast-node.ts │ │ │ │ ├── globals.ts │ │ │ │ ├── index.ts │ │ │ │ ├── json-document.ts │ │ │ │ ├── localize.ts │ │ │ │ ├── null-ast-node.ts │ │ │ │ ├── number-ast-node.ts │ │ │ │ ├── object-ast-node.ts │ │ │ │ ├── property-ast-node.ts │ │ │ │ ├── schema-collector.ts │ │ │ │ ├── string-ast-node.ts │ │ │ │ └── validation-result.ts │ │ │ ├── referenceables │ │ │ │ ├── __tests__ │ │ │ │ │ └── referenceables.test.ts │ │ │ │ └── index.ts │ │ │ ├── references │ │ │ │ ├── __tests__ │ │ │ │ │ └── references.test.ts │ │ │ │ ├── index.ts │ │ │ │ └── utils.ts │ │ │ ├── scalar-type.ts │ │ │ └── util.ts │ │ └── utils │ │ │ ├── __tests__ │ │ │ ├── documents.test.ts │ │ │ └── objects.test.ts │ │ │ ├── document.ts │ │ │ ├── index.ts │ │ │ ├── objects.ts │ │ │ ├── resources.ts │ │ │ ├── url.ts │ │ │ └── yaml.ts │ └── tsconfig.json ├── language-server │ ├── .npmignore │ ├── LICENSE │ ├── NOTICE.txt │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── language-service │ │ │ ├── jsonContributions.ts │ │ │ ├── languageService.ts │ │ │ ├── model │ │ │ │ └── settings.ts │ │ │ ├── services │ │ │ │ ├── analytics │ │ │ │ │ └── index.ts │ │ │ │ ├── completion │ │ │ │ │ ├── __tests__ │ │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ │ ├── completion-cfn.test.ts.snap │ │ │ │ │ │ │ └── completion-sls.test.ts.snap │ │ │ │ │ │ ├── completion-cfn.test.ts │ │ │ │ │ │ └── completion-sls.test.ts │ │ │ │ │ ├── completions.ts │ │ │ │ │ ├── constants.ts │ │ │ │ │ ├── custom-tags.ts │ │ │ │ │ ├── defaultPropertyCompletions.ts │ │ │ │ │ ├── helpers.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── pattern-properties.ts │ │ │ │ │ └── text.ts │ │ │ │ ├── definition │ │ │ │ │ └── index.ts │ │ │ │ ├── document │ │ │ │ │ └── index.ts │ │ │ │ ├── documentSymbols │ │ │ │ │ ├── __tests__ │ │ │ │ │ │ └── documentSymbols.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── documentation │ │ │ │ │ ├── __tests__ │ │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ │ └── documentation.test.ts.snap │ │ │ │ │ │ └── documentation.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── model.ts │ │ │ │ │ └── samDocumentation.ts │ │ │ │ ├── hover │ │ │ │ │ └── index.ts │ │ │ │ ├── jsonSchema │ │ │ │ │ ├── __tests__ │ │ │ │ │ │ └── index.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── mutation │ │ │ │ │ │ ├── __tests__ │ │ │ │ │ │ └── mutation.test.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── sam.ts │ │ │ │ │ │ └── serverless.ts │ │ │ │ ├── links │ │ │ │ │ └── index.ts │ │ │ │ ├── reference │ │ │ │ │ └── index.ts │ │ │ │ ├── request │ │ │ │ │ └── index.ts │ │ │ │ └── validation │ │ │ │ │ ├── __tests__ │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ └── validation.test.ts.snap │ │ │ │ │ └── validation.test.ts │ │ │ │ │ ├── errors.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── references.ts │ │ │ └── utils │ │ │ │ ├── __tests__ │ │ │ │ ├── arrayUtils.test.ts │ │ │ │ ├── documentPositionCalculator.test.ts │ │ │ │ └── strings.test.ts │ │ │ │ ├── arrayUtils.ts │ │ │ │ ├── completion-helper.ts │ │ │ │ ├── documentPositionCalculator.ts │ │ │ │ ├── errorHandler.ts │ │ │ │ └── strings.ts │ │ └── server.ts │ └── tsconfig.json ├── sam-schema │ ├── LICENSE.txt │ ├── NOTICE.txt │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── schema.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── serverless-framework-schema │ ├── LICENSE.txt │ ├── README.md │ ├── json │ │ ├── aws │ │ │ ├── common │ │ │ │ ├── arn.json │ │ │ │ ├── authorizer.json │ │ │ │ ├── policy.json │ │ │ │ ├── role-statement.json │ │ │ │ ├── runtime.json │ │ │ │ └── vpc.json │ │ │ ├── functions │ │ │ │ ├── events │ │ │ │ │ ├── alb.json │ │ │ │ │ ├── alexaSkill.json │ │ │ │ │ ├── alexaSmartHome.json │ │ │ │ │ ├── cloudFront.json │ │ │ │ │ ├── cloudwatchEvent.json │ │ │ │ │ ├── cloudwatchLog.json │ │ │ │ │ ├── cognitoUserPool.json │ │ │ │ │ ├── eventBridge.json │ │ │ │ │ ├── http.json │ │ │ │ │ ├── httpApi.json │ │ │ │ │ ├── iot.json │ │ │ │ │ ├── s3.json │ │ │ │ │ ├── schedule.json │ │ │ │ │ ├── sns.json │ │ │ │ │ ├── sqs.json │ │ │ │ │ ├── stream.json │ │ │ │ │ └── websocket.json │ │ │ │ ├── function.json │ │ │ │ └── functions.json │ │ │ ├── layers.json │ │ │ ├── provider │ │ │ │ └── provider.json │ │ │ └── service.json │ │ └── common │ │ │ ├── package-config.json │ │ │ └── tags.json │ ├── package-lock.json │ ├── package.json │ ├── schema.json │ ├── src │ │ └── index.ts │ └── tsconfig.json └── vscode │ ├── .npmignore │ ├── .vscodeignore │ ├── CHANGELOG.md │ ├── LICENSE │ ├── NOTICE.txt │ ├── README.md │ ├── demo │ ├── autocomplete.gif │ ├── documentation.gif │ ├── serverless_framework.gif │ └── validation.gif │ ├── icon │ └── icon.png │ ├── language-configuration.json │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── analytics │ │ ├── amplitude.ts │ │ └── index.ts │ ├── commands │ │ └── index.ts │ ├── extension.ts │ ├── settings │ │ └── index.ts │ ├── validation-error-dialog │ │ └── index.ts │ └── workplace │ │ └── find.ts │ ├── syntaxes │ └── yaml.tmLanguage.json │ └── tsconfig.json └── tsconfig.base.json /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | working_directory: ~/serverless-ide 5 | docker: 6 | - image: circleci/node:8.16.0-jessie 7 | steps: 8 | - checkout 9 | # Download and cache dependencies 10 | - restore_cache: 11 | keys: 12 | - v3-dependencies-{{ checksum "package-lock.json" }} 13 | # fallback to using the latest cache if no exact match is found 14 | - v3-dependencies- 15 | 16 | - run: sudo npm install lerna -g 17 | - run: lerna bootstrap --no-ci 18 | - run: npm install 19 | 20 | - save_cache: 21 | paths: 22 | - node_modules 23 | key: v3-dependencies-{{ checksum "package-lock.json" }} 24 | 25 | - run: npm run build 26 | - run: npm run lint:types 27 | - run: npm run lint:ts 28 | - run: npm test -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Tab indentation 2 | [*] 3 | indent_style = tab 4 | indent_size = 4 5 | trim_trailing_whitespace = true 6 | 7 | [{.travis.yml,npm-shrinkwrap.json,package.json}] 8 | indent_style = space 9 | indent_size = 2 -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules/** 2 | **/dist/** -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "@typescript-eslint/parser", 3 | plugins: ["@typescript-eslint", "simple-import-sort"], 4 | extends: [ 5 | "plugin:@typescript-eslint/recommended", 6 | "prettier/@typescript-eslint", 7 | "plugin:prettier/recommended" 8 | ], 9 | parserOptions: { 10 | ecmaVersion: 2018, 11 | sourceType: "module" 12 | }, 13 | rules: { 14 | "no-console": 2, 15 | "prettier/prettier": "error", 16 | "simple-import-sort/sort": "error", 17 | "@typescript-eslint/explicit-member-accessibility": [ 18 | 2, 19 | { 20 | accessibility: "no-public" 21 | } 22 | ], 23 | "@typescript-eslint/no-unused-vars": 2, 24 | "@typescript-eslint/indent": 0, 25 | "@typescript-eslint/interface-name-prefix": 2, 26 | "@typescript-eslint/no-explicit-any": 1, 27 | "@typescript-eslint/interface-name-prefix": 0, 28 | "@typescript-eslint/explicit-function-return-type": "off", 29 | "@typescript-eslint/no-non-null-assertion": "off", 30 | "@typescript-eslint/no-use-before-define": "off", 31 | "sort-imports": "off", 32 | "import/order": "off" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [pavelvlasov] 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | node_modules 3 | **/dist 4 | *.log 5 | *.vsix 6 | .cache -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | printWidth: 80 2 | parser: typescript 3 | tabWidth: 4 4 | semi: false 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | { 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "Launch Extension", 7 | "type": "extensionHost", 8 | "request": "launch", 9 | "runtimeExecutable": "${execPath}", 10 | "args": [ 11 | "--extensionDevelopmentPath=${workspaceRoot}/packages/vscode" 12 | ], 13 | "stopOnEntry": false, 14 | "sourceMaps": true, 15 | "outFiles": [ 16 | "${workspaceRoot}/packages/vscode/dist/**/*.js" 17 | ], 18 | "preLaunchTask": "build" 19 | }, 20 | { 21 | "name": "Attach Server", 22 | "type": "node", 23 | "request": "attach", 24 | "port": 6009, 25 | "sourceMaps": true, 26 | "outFiles": [ 27 | "${workspaceRoot}/packages/language-server/dist/**/*.js" 28 | ], 29 | "protocol": "inspector", 30 | "trace": true 31 | }, 32 | { 33 | "type": "node", 34 | "request": "launch", 35 | "name": "Jest Current File", 36 | "program": "${workspaceFolder}/node_modules/.bin/jest", 37 | "args": [ 38 | "${relativeFile}", 39 | "--config", 40 | "jest.config.js" 41 | ], 42 | "console": "integratedTerminal", 43 | "internalConsoleOptions": "neverOpen", 44 | "disableOptimisticBPs": true, 45 | "windows": { 46 | "program": "${workspaceFolder}/node_modules/jest/bin/jest", 47 | } 48 | } 49 | ] 50 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | "editor.quickSuggestions": { 10 | "other": true, 11 | "comments": true, 12 | "strings": true 13 | }, 14 | "typescript.tsdk": "node_modules/typescript/lib" 15 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "type": "shell", 7 | "command": "lerna run build", 8 | "group": "build" 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: [ 3 | "/packages/language-server", 4 | "/packages/vscode", 5 | "/packages/sam-schema", 6 | "/packages/config" 7 | ], 8 | testMatch: ["**/__tests__/**/?(*.)+(spec|test).+(ts|js)"], 9 | modulePathIgnorePatterns: ["node_modules"], 10 | moduleNameMapper: { 11 | "@serverless-ide/(.*)$": "/packages/$1" 12 | }, 13 | preset: "ts-jest", 14 | testEnvironment: "node", 15 | verbose: false 16 | } 17 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "version": "0.6.5", 6 | "npmClient": "npm", 7 | "ignoreChanges": [ 8 | "*.md", 9 | "*.txt", 10 | "lerna.json" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-ide", 3 | "private": true, 4 | "engines": { 5 | "vscode": "^1.26.0" 6 | }, 7 | "devDependencies": { 8 | "@types/jest": "^23.3.12", 9 | "@typescript-eslint/eslint-plugin": "^6.9.0", 10 | "@typescript-eslint/parser": "^6.9.0", 11 | "eslint": "^7.0.0", 12 | "eslint-config-prettier": "^6.0.0", 13 | "eslint-plugin-prettier": "^3.1.0", 14 | "eslint-plugin-simple-import-sort": "^4.0.0", 15 | "husky": "^1.3.1", 16 | "jest": "^29.4.1", 17 | "lerna": "^6.4.1", 18 | "lint-staged": "^9.0.1", 19 | "prettier": "^1.18.2", 20 | "prettier-check": "^2.0.0", 21 | "ts-jest": "^29.0.5", 22 | "ts-node": "^5.0.1", 23 | "typescript": "^4.9.4" 24 | }, 25 | "scripts": { 26 | "test": "jest", 27 | "test:changed": "jest --bail --findRelatedTests", 28 | "pre-commit": "./node_modules/.bin/lint-staged", 29 | "build": "lerna run build", 30 | "lint:types": "lerna run lint:types", 31 | "lint:ts": "eslint **/*.ts", 32 | "lint:ts:fix": "eslint **/*.ts --fix" 33 | }, 34 | "husky": { 35 | "hooks": { 36 | "pre-commit": "npm run pre-commit" 37 | } 38 | }, 39 | "lint-staged": { 40 | "**/*.ts": [ 41 | "eslint --fix", 42 | "npm run lint:types", 43 | "npm run test:changed", 44 | "git add" 45 | ] 46 | } 47 | } -------------------------------------------------------------------------------- /packages/cloudformation-schema/NOTICE.txt: -------------------------------------------------------------------------------- 1 | ========================================================================= 2 | NOTICE file for use with, and corresponding to Section 4 of, 3 | the Apache License, Version 2.0, 4 | in this case for the @serverless-ide/sam-schema project. 5 | ========================================================================= 6 | 7 | This product includes software developed by 8 | Amazon.com, Inc. or its affiliates. 9 | Copyright (c) 2019 Amazon.com, Inc. All rights reserved. 10 | -------------------------------------------------------------------------------- /packages/cloudformation-schema/README.md: -------------------------------------------------------------------------------- 1 | # @serverless-ide/cloudformation-schema 2 | 3 | ## Json schema for Cloudformation template configuration 4 | 5 | Based on [awslabs/goformation](https://raw.githubusercontent.com/awslabs/goformation/master/schema/sam.schema.json) 6 | and adds support of globals configuration 7 | 8 | ## How to 9 | 10 | 1. Install dependencies 11 | 12 | ```sh 13 | npm install 14 | ``` 15 | 2. Generate schema 16 | 17 | ```sh 18 | npm run generate 19 | ``` 20 | 21 | ## License 22 | 23 | ### License 24 | 25 | Apache License 2.0 26 | -------------------------------------------------------------------------------- /packages/cloudformation-schema/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@serverless-ide/cloudformation-schema", 3 | "version": "0.6.5", 4 | "description": "Json schema for AWS SAM template configuration", 5 | "main": "dist/index.js", 6 | "repository": "git@github.com:threadheap/aws-sam-json-schema.git", 7 | "author": "Pavel Vlasov ", 8 | "keywords": [ 9 | "aws", 10 | "sam", 11 | "cloudformation", 12 | "json", 13 | "schema", 14 | "validation" 15 | ], 16 | "license": "MIT", 17 | "private": false, 18 | "devDependencies": { 19 | "@types/lodash": "^4.14.119", 20 | "@types/node": "^10.12.18", 21 | "@types/request": "^2.48.1", 22 | "@types/request-promise": "^4.1.42", 23 | "lodash": "^4.17.11", 24 | "request": "^2.88.0", 25 | "request-promise": "^4.2.2", 26 | "typescript": "^4.9.4" 27 | }, 28 | "scripts": { 29 | "build": "npm run clean && npm run compile && node dist/index.js", 30 | "clean": "rm -rf ./dist", 31 | "compile": "tsc", 32 | "lint:types": "tsc --noEmit" 33 | }, 34 | "publishConfig": { 35 | "access": "public" 36 | }, 37 | "gitHead": "390fa05ac004e80dd92b96e08eded82c162ffcd9" 38 | } 39 | -------------------------------------------------------------------------------- /packages/cloudformation-schema/src/index.ts: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | import fs = require("fs") 4 | import map = require("lodash/map") 5 | import cloneDeep = require("lodash/cloneDeep") 6 | import path = require("path") 7 | import request = require("request-promise") 8 | 9 | const downloadSchema = async (): Promise => { 10 | const response = await request.get( 11 | "https://raw.githubusercontent.com/awslabs/goformation/master/schema/cloudformation.schema.json", 12 | { json: true } 13 | ) 14 | 15 | return response 16 | } 17 | 18 | export const enrichResources = (schema: any) => { 19 | schema.definitions["CustomResource"] = { 20 | additionalProperties: true, 21 | properties: { 22 | Type: { 23 | type: "string", 24 | pattern: "^Custom::[a-zA-Z0-9]+$" 25 | }, 26 | Properties: { 27 | type: "object" 28 | } 29 | } 30 | } 31 | 32 | schema.properties.Resources.patternProperties["^[a-zA-Z0-9]+$"].anyOf.push({ 33 | $ref: "#/definitions/CustomResource" 34 | }) 35 | 36 | map(schema.definitions, definition => { 37 | if ( 38 | definition.properties && 39 | definition.properties.DeletionPolicy && 40 | !definition.properties.UpdateReplacePolicy 41 | ) { 42 | definition.properties.UpdateReplacePolicy = cloneDeep( 43 | definition.properties.DeletionPolicy 44 | ) 45 | } 46 | 47 | if ( 48 | definition.properties && 49 | definition.properties.DependsOn && 50 | !definition.properties.Condition 51 | ) { 52 | definition.properties.Condition = { 53 | anyOf: [ 54 | { 55 | pattern: "^[a-zA-Z0-9]+$", 56 | type: "string" 57 | }, 58 | { 59 | items: { 60 | pattern: "^[a-zA-Z0-9]+$", 61 | type: "string" 62 | }, 63 | type: "array" 64 | } 65 | ] 66 | } 67 | } 68 | }) 69 | } 70 | 71 | const generateSchema = (schema: any): any => { 72 | enrichResources(schema) 73 | 74 | return schema 75 | } 76 | 77 | const main = async () => { 78 | const schema = await downloadSchema() 79 | 80 | fs.writeFileSync( 81 | path.join(process.cwd(), "schema.json"), 82 | JSON.stringify(generateSchema(schema), null, 4), 83 | "utf-8" 84 | ) 85 | } 86 | 87 | main() 88 | -------------------------------------------------------------------------------- /packages/cloudformation-schema/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "include": [ 7 | "src" 8 | ] 9 | } -------------------------------------------------------------------------------- /packages/config/.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | test/ 3 | tsconfig.json 4 | jest.config.js 5 | -------------------------------------------------------------------------------- /packages/config/NOTICE.txt: -------------------------------------------------------------------------------- 1 | @serverless-ide/language-server 2 | 3 | THIRD-PARTY SOFTWARE NOTICES AND INFORMATION 4 | Do Not Translate or Localize 5 | 6 | This project incorporates components from the projects listed below. The original copyright notices and the licenses under which Red Hat received such components are set forth below. Red Hat reserves all rights not expressly granted herein, whether by implication, estoppel or otherwise. 7 | 8 | 1. textmate/yaml.tmbundle (https://github.com/textmate/yaml.tmbundle) 9 | 2. microsoft/vscode (https://github.com/Microsoft/vscode) 10 | 11 | %% textmate/yaml.tmbundle NOTICES AND INFORMATION BEGIN HERE 12 | ========================================= 13 | Copyright (c) 2019 FichteFoll 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy 16 | of this software and associated documentation files (the "Software"), to deal 17 | in the Software without restriction, including without limitation the rights 18 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | copies of the Software, and to permit persons to whom the Software is 20 | furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in all 23 | copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 31 | ========================================= 32 | END OF textmate/yaml.tmbundle NOTICES AND INFORMATION 33 | 34 | %% vscode NOTICES AND INFORMATION BEGIN HERE 35 | ========================================= 36 | MIT License 37 | 38 | Copyright (c) 2019 - present Microsoft Corporation 39 | 40 | All rights reserved. 41 | 42 | Permission is hereby granted, free of charge, to any person obtaining a copy 43 | of this software and associated documentation files (the "Software"), to deal 44 | in the Software without restriction, including without limitation the rights 45 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 46 | copies of the Software, and to permit persons to whom the Software is 47 | furnished to do so, subject to the following conditions: 48 | 49 | The above copyright notice and this permission notice shall be included in all 50 | copies or substantial portions of the Software. 51 | 52 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 53 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 54 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 55 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 56 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 57 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 58 | SOFTWARE. 59 | ========================================= 60 | END OF vscode NOTICES AND INFORMATION 61 | 62 | ========================================================================= 63 | NOTICE file for use with, and corresponding to Section 4 of, 64 | the Apache License, Version 2.0, 65 | in this case for the @serverless-ide/language-server project. 66 | ========================================================================= 67 | 68 | This product includes software developed by 69 | Amazon.com, Inc. or its affiliates. 70 | Copyright (c) 2019 Amazon.com, Inc. All rights reserved. 71 | -------------------------------------------------------------------------------- /packages/config/README.md: -------------------------------------------------------------------------------- 1 | # Serverless IDE Config 2 | 3 | Set of utilities to work with documents in VSCode extension. 4 | 5 | ## License 6 | 7 | Apache License 2.0 8 | -------------------------------------------------------------------------------- /packages/config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@serverless-ide/config", 3 | "description": "Serverless IDE config model and parser", 4 | "version": "0.6.0", 5 | "author": "Pavel Vlasov ", 6 | "license": "MIT", 7 | "engines": { 8 | "node": "*" 9 | }, 10 | "main": "dist/index.js", 11 | "keywords": [ 12 | "aws", 13 | "sam", 14 | "cloudformation", 15 | "cfn", 16 | "serverless", 17 | "yaml", 18 | "autocompletion", 19 | "validation", 20 | "LSP" 21 | ], 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/threadheap/serverless-ide-vscode.git" 25 | }, 26 | "scripts": { 27 | "build": "npm run clean && npm run compile && node dist/index.js", 28 | "clean": "rm -rf ./dist", 29 | "compile": "tsc", 30 | "prepublishOnly": "npm run build", 31 | "lint:types": "tsc --noEmit", 32 | "lint:ts": "eslint ./src/**/*.ts", 33 | "lint:ts:fix": "eslint ./src/**/*.ts --fix", 34 | "test": "jest" 35 | }, 36 | "dependencies": { 37 | "js-yaml": "^3.13.1", 38 | "jsonc-parser": "^1.0.3", 39 | "lodash": "^4.17.11", 40 | "vscode-json-languageservice": "3.0.12", 41 | "vscode-languageserver": "^5.2.1", 42 | "vscode-languageserver-types": "^3.14.0", 43 | "vscode-nls": "^3.2.2", 44 | "yaml-ast-parser-custom-tags": "0.0.43" 45 | }, 46 | "devDependencies": { 47 | "@types/js-yaml": "^3.12.1", 48 | "@types/lodash": "^4.14.144", 49 | "@types/lru-cache": "^4.1.1", 50 | "@types/node": "^9.4.7", 51 | "@types/vscode": "^1.37.0", 52 | "typescript": "^4.9.4" 53 | }, 54 | "publishConfig": { 55 | "access": "public" 56 | }, 57 | "gitHead": "390fa05ac004e80dd92b96e08eded82c162ffcd9" 58 | } 59 | -------------------------------------------------------------------------------- /packages/config/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./parser" 2 | export * from "./model" 3 | export * from "./utils" 4 | -------------------------------------------------------------------------------- /packages/config/src/model/document.ts: -------------------------------------------------------------------------------- 1 | export enum DocumentType { 2 | CLOUD_FORMATION = "CLOUD_FORMATION", 3 | SAM = "SAM", 4 | SERVERLESS_FRAMEWORK = "SERVERLESS_FRAMEWORK", 5 | UNKNOWN = "UNKNOWN" 6 | } 7 | -------------------------------------------------------------------------------- /packages/config/src/model/globals.ts: -------------------------------------------------------------------------------- 1 | import { Segment } from "vscode-json-languageservice" 2 | 3 | export const API: "Api" = "Api" 4 | export const FUNCTION: "Function" = "Function" 5 | export const SIMPLE_TABLE: "SimpleTable" = "SimpleTable" 6 | 7 | export type GlobalKeyType = typeof API | typeof FUNCTION | typeof SIMPLE_TABLE 8 | 9 | export const GlobalKeys = { 10 | [API]: { 11 | resourceType: "AWS::Serverless::Api" 12 | }, 13 | [FUNCTION]: { 14 | resourceType: "AWS::Serverless::Function" 15 | }, 16 | [SIMPLE_TABLE]: { 17 | resourceType: "AWS::Serverless::SimpleTable" 18 | } 19 | } 20 | 21 | export interface GlobalConfigItem { 22 | resourceType: string 23 | properties: Segment[] 24 | } 25 | 26 | export interface GlobalsConfig { 27 | [API]: GlobalConfigItem 28 | [FUNCTION]: GlobalConfigItem 29 | [SIMPLE_TABLE]: GlobalConfigItem 30 | } 31 | 32 | const KEYS = [API, FUNCTION, SIMPLE_TABLE] 33 | 34 | export const isEmpty = (globalsConfig: GlobalsConfig): boolean => { 35 | return KEYS.every(key => globalsConfig[key].properties.length === 0) 36 | } 37 | -------------------------------------------------------------------------------- /packages/config/src/model/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./custom-tags" 2 | export * from "./document" 3 | export * from "./globals" 4 | export * from "./referenceables" 5 | export * from "./references" 6 | export * from "./resources" 7 | export * from "./sortedHash" 8 | export * from "./sub-stack" 9 | export * from "./jsonSchema" 10 | -------------------------------------------------------------------------------- /packages/config/src/model/jsonSchema.ts: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | export interface JSONSchema { 4 | id?: string 5 | $schema?: string 6 | type?: string | string[] 7 | title?: string 8 | default?: any 9 | definitions?: JSONSchemaMap 10 | description?: string 11 | doNotSuggest?: boolean 12 | properties?: JSONSchemaMap 13 | patternProperties?: JSONSchemaMap 14 | additionalProperties?: any 15 | minProperties?: number 16 | maxProperties?: number 17 | dependencies?: JSONSchemaMap | string[] 18 | items?: any 19 | minItems?: number 20 | maxItems?: number 21 | uniqueItems?: boolean 22 | additionalItems?: boolean 23 | pattern?: string 24 | minLength?: number 25 | maxLength?: number 26 | minimum?: number 27 | maximum?: number 28 | exclusiveMinimum?: boolean 29 | exclusiveMaximum?: boolean 30 | multipleOf?: number 31 | required?: string[] 32 | $ref?: string 33 | anyOf?: JSONSchema[] 34 | allOf?: JSONSchema[] 35 | oneOf?: JSONSchema[] 36 | not?: JSONSchema 37 | enum?: any[] 38 | format?: string 39 | errorMessage?: string // VSCode extension 40 | patternErrorMessage?: string // VSCode extension 41 | deprecationMessage?: string // VSCode extension 42 | enumDescriptions?: string[] // VSCode extension 43 | } 44 | 45 | export interface JSONSchemaMap { 46 | [name: string]: JSONSchema 47 | } 48 | -------------------------------------------------------------------------------- /packages/config/src/model/referenceables.ts: -------------------------------------------------------------------------------- 1 | import { PropertyASTNode } from "../parser/json" 2 | import { ReferenceEntityType } from "./references" 3 | import { SortedHash } from "./sortedHash" 4 | 5 | export interface Referenceable { 6 | id: string 7 | node: PropertyASTNode 8 | entityType: ReferenceEntityType 9 | resourceType?: string 10 | } 11 | 12 | export type ReferenceableLookup = WeakMap 13 | 14 | export interface Referenceables { 15 | hash: { 16 | [key in ReferenceEntityType]: SortedHash 17 | } 18 | lookup: ReferenceableLookup 19 | } 20 | -------------------------------------------------------------------------------- /packages/config/src/model/references.ts: -------------------------------------------------------------------------------- 1 | import { ASTNode } from "./../parser/json" 2 | 3 | export enum ReferenceType { 4 | // getters 5 | FIND_IN_MAP = "FIND_IN_MAP", 6 | GET_ATT = "GET_ATT", 7 | // transformers 8 | SUB = "SUB", 9 | // ref/sub 10 | REF = "REF", 11 | // depends on 12 | DEPENDS_ON = "DEPENDS_ON", 13 | // conditions 14 | CONDITION = "CONDITION" 15 | } 16 | 17 | export const enum ReferenceEntityType { 18 | RESOURCE = "RESOURCE", 19 | PARAMETER = "PARAMETER", 20 | OUTPUT = "OUTPUT", 21 | MAPPING = "MAPPING", 22 | CONDITION = "CONDITION" 23 | } 24 | 25 | export interface Reference { 26 | type: ReferenceType 27 | key: string 28 | node: ASTNode 29 | } 30 | 31 | export type ReferencesLookup = WeakMap 32 | 33 | export interface References { 34 | hash: { [key: string]: Reference[] } 35 | lookup: ReferencesLookup 36 | } 37 | -------------------------------------------------------------------------------- /packages/config/src/model/resources.ts: -------------------------------------------------------------------------------- 1 | import { SortedHash } from "./sortedHash" 2 | 3 | export interface ResourceDefinition { 4 | id: string 5 | resourceType: string | void 6 | } 7 | 8 | export type ResourcesDefinitions = SortedHash 9 | -------------------------------------------------------------------------------- /packages/config/src/model/sortedHash.ts: -------------------------------------------------------------------------------- 1 | export interface SerializedSortedHash { 2 | sequence: string[] 3 | hash: { 4 | [key: string]: TItem 5 | } 6 | } 7 | 8 | export class SortedHash { 9 | private sequence: string[] 10 | private hash: { [key: string]: TItem } 11 | 12 | constructor( 13 | defaultValue: SerializedSortedHash = { sequence: [], hash: {} } 14 | ) { 15 | this.sequence = defaultValue.sequence 16 | this.hash = defaultValue.hash 17 | } 18 | 19 | add(key: string, item: TItem) { 20 | if (this.hash[key]) { 21 | throw new Error(`Object with \`${key}\` already exists.`) 22 | } 23 | 24 | this.sequence.push(key) 25 | this.hash[key] = item 26 | } 27 | 28 | get(key: string): TItem | void { 29 | return this.hash[key] 30 | } 31 | 32 | keys(): string[] { 33 | return this.sequence 34 | } 35 | 36 | contains(key: string): boolean { 37 | return key in this.hash 38 | } 39 | 40 | getList(): TItem[] { 41 | return this.map(item => item) 42 | } 43 | 44 | map(callback: (item: TItem, index: number) => TValue): TValue[] { 45 | return this.sequence.map((key, index) => { 46 | return callback(this.hash[key], index) 47 | }) 48 | } 49 | 50 | forEach(callback: (item: TItem, index: number) => void) { 51 | return this.sequence.forEach((key, index) => { 52 | return callback(this.hash[key], index) 53 | }) 54 | } 55 | 56 | insertAtIndex(key: string, item: TItem, index: number) { 57 | this.sequence = [ 58 | ...this.sequence.slice(0, index), 59 | key, 60 | ...this.sequence.slice(index) 61 | ] 62 | this.hash[key] = item 63 | } 64 | 65 | remove(key: string) { 66 | const index = this.sequence.indexOf(key) 67 | 68 | if (index !== -1) { 69 | this.sequence.splice(index, 1) 70 | delete this.hash[key] 71 | } 72 | } 73 | 74 | serialize() { 75 | return { 76 | sequence: [...this.sequence], 77 | hash: { 78 | ...this.hash 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /packages/config/src/model/sub-stack.ts: -------------------------------------------------------------------------------- 1 | import { PropertyASTNode } from "../parser/json" 2 | 3 | export interface SubStack { 4 | path: string 5 | node: PropertyASTNode 6 | } 7 | -------------------------------------------------------------------------------- /packages/config/src/parser/__tests__/custom-tags.test.ts: -------------------------------------------------------------------------------- 1 | import { TextDocument } from "vscode-languageserver" 2 | 3 | import { parse as parseYaml } from ".." 4 | import { CUSTOM_TAGS, TagKind } from "./../../model/custom-tags" 5 | 6 | const optionsMapping: { [key in TagKind]: string } = { 7 | scalar: "logicVariable", 8 | sequence: ["", " - logicalVariable1", " - logicalVariable2"].join("\n"), 9 | mapping: "{ val1: logicalVariable1, val2: logicalVariable2 }" 10 | } 11 | 12 | describe("custom tags parse", () => { 13 | describe("smoke tests", () => { 14 | const generateNode = (text: string) => { 15 | const document = TextDocument.create("", "", 1, text) 16 | return parseYaml(document).root 17 | } 18 | 19 | CUSTOM_TAGS.forEach(tag => { 20 | describe(tag.type, () => { 21 | if (tag.type) { 22 | tag.kind.forEach(kind => { 23 | test(`${tag.type} ${kind}`, () => { 24 | const text = [ 25 | `property: ${tag.tag} `, 26 | optionsMapping[kind] 27 | ].join("") 28 | const node = generateNode(text) 29 | 30 | expect(node).toMatchSnapshot() 31 | }) 32 | }) 33 | } 34 | }) 35 | }) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /packages/config/src/parser/__tests__/sub-stacks.test.ts: -------------------------------------------------------------------------------- 1 | import { TextDocument } from "vscode-languageserver" 2 | 3 | import { parse as parseYaml } from ".." 4 | 5 | const templateWithSubStack = ` 6 | Transform: AWS::Serverless-2016-10-31 7 | Resources: 8 | One: 9 | Type: AWS::CloudFormation::Stack 10 | Properties: 11 | TemplateURL: ./index.yaml 12 | ` 13 | 14 | const templateWithMultipleSubStacks = ` 15 | Transform: AWS::Serverless-2016-10-31 16 | Resources: 17 | One: 18 | Type: AWS::CloudFormation::Stack 19 | Properties: 20 | TemplateURL: ./index.yaml 21 | Two: 22 | Type: AWS::Serverless::Application 23 | Properties: 24 | Location: ../my-other-app/template.yaml 25 | ` 26 | const templateWithRemoteSubStack = ` 27 | Transform: AWS::Serverless-2016-10-31 28 | Resources: 29 | One: 30 | Type: AWS::CloudFormation::Stack 31 | Properties: 32 | TemplateURL: https://s3.amazonaws.com/cloudformation-templates-us-east-2/EC2ChooseAMI.template 33 | Two: 34 | Type: AWS::Serverless::Application 35 | Properties: 36 | Location: 37 | ApplicationId: arn:aws:serverlessrepo:us-east-1:123456789012:applications/application-alias-name 38 | SemanticVersion: 1.0.0 39 | ` 40 | 41 | test("should collect sub-stacks paths", () => { 42 | const document = TextDocument.create("", "", 1, templateWithSubStack) 43 | const doc = parseYaml(document) 44 | 45 | expect(doc.collectSubStacks()).toEqual([ 46 | false, 47 | [ 48 | { 49 | path: "./index.yaml", 50 | node: expect.any(Object) 51 | } 52 | ] 53 | ]) 54 | }) 55 | 56 | test("should collect multiple sub stacks", () => { 57 | const document = TextDocument.create( 58 | "", 59 | "", 60 | 1, 61 | templateWithMultipleSubStacks 62 | ) 63 | const doc = parseYaml(document) 64 | 65 | expect(doc.collectSubStacks()).toEqual([ 66 | false, 67 | [ 68 | { path: "./index.yaml", node: expect.any(Object) }, 69 | { path: "../my-other-app/template.yaml", node: expect.any(Object) } 70 | ] 71 | ]) 72 | }) 73 | 74 | test("should not collect remote sub stacks", () => { 75 | const document = TextDocument.create("", "", 1, templateWithRemoteSubStack) 76 | const doc = parseYaml(document) 77 | 78 | expect(doc.collectSubStacks()).toEqual([true, []]) 79 | }) 80 | -------------------------------------------------------------------------------- /packages/config/src/parser/index.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_TAGS } from "../model" 2 | import { 3 | ErrorCode, 4 | ExternalImportsCallbacks, 5 | Problem, 6 | YAMLDocument 7 | } from "./json" 8 | 9 | import noop = require("lodash/noop") 10 | 11 | import { Schema, Type } from "js-yaml" 12 | import { TextDocument } from "vscode-languageserver-types" 13 | import * as Yaml from "yaml-ast-parser-custom-tags" 14 | 15 | import { DocumentType } from "../model" 16 | import { getDocumentType } from "../utils" 17 | import { ParentParams } from "./json/document" 18 | 19 | export { YAMLDocument, Problem, ExternalImportsCallbacks } 20 | export * from "./json" 21 | export { collectReferencesFromStringNode } from "./references" 22 | 23 | function convertError(e: Yaml.YAMLException): Problem { 24 | return { 25 | message: `${e.reason}`, 26 | location: { 27 | start: e.mark.position, 28 | end: e.mark.position + e.mark.column 29 | }, 30 | code: ErrorCode.Undefined 31 | } 32 | } 33 | 34 | function createJSONDocument( 35 | document: TextDocument, 36 | yamlDoc: Yaml.YAMLNode | void, 37 | callbacks: ExternalImportsCallbacks, 38 | parentParams?: ParentParams 39 | ) { 40 | const doc = new YAMLDocument( 41 | document.uri, 42 | parentParams 43 | ? DocumentType.UNKNOWN 44 | : getDocumentType(document.getText()), 45 | yamlDoc, 46 | callbacks, 47 | parentParams 48 | ) 49 | 50 | if (!yamlDoc || !doc.root) { 51 | // TODO: When this is true, consider not pushing the other errors. 52 | doc.errors.push({ 53 | message: "Expected a YAML object, array or literal", 54 | code: ErrorCode.Undefined, 55 | location: yamlDoc 56 | ? { 57 | start: yamlDoc.startPosition, 58 | end: yamlDoc.endPosition 59 | } 60 | : { start: 0, end: 0 } 61 | }) 62 | 63 | return doc 64 | } 65 | 66 | const duplicateKeyReason = "duplicate key" 67 | 68 | // Patch ontop of yaml-ast-parser to disable duplicate key message on merge key 69 | const isDuplicateAndNotMergeKey = ( 70 | error: Yaml.YAMLException, 71 | yamlText: string 72 | ) => { 73 | const errorConverted = convertError(error) 74 | const errorStart = errorConverted.location.start 75 | const errorEnd = errorConverted.location.end 76 | if ( 77 | error.reason === duplicateKeyReason && 78 | yamlText.substring(errorStart, errorEnd).startsWith("<<") 79 | ) { 80 | return false 81 | } 82 | return true 83 | } 84 | doc.errors = yamlDoc.errors 85 | .filter(e => e.reason !== duplicateKeyReason && !e.isWarning) 86 | .map(e => convertError(e)) 87 | doc.warnings = yamlDoc.errors 88 | .filter( 89 | e => 90 | (e.reason === duplicateKeyReason && 91 | isDuplicateAndNotMergeKey(e, document.getText())) || 92 | e.isWarning 93 | ) 94 | .map(e => convertError(e)) 95 | 96 | return doc 97 | } 98 | 99 | export const parse = ( 100 | document: TextDocument, 101 | callbacks: ExternalImportsCallbacks = { 102 | onRegisterExternalImport: noop, 103 | onValidateExternalImport: noop 104 | }, 105 | parentParams?: ParentParams 106 | ): YAMLDocument => { 107 | // We need compiledTypeMap to be available from schemaWithAdditionalTags before we add the new custom propertie 108 | const compiledTypeMap: { [key: string]: Type } = {} 109 | 110 | CUSTOM_TAGS.forEach(customTag => { 111 | if (customTag.tag) { 112 | const [kind, ...additionalKinds] = customTag.kind 113 | 114 | compiledTypeMap[customTag.tag] = new Type(customTag.tag, { 115 | kind, 116 | construct: data => { 117 | if (data) { 118 | data.customTag = customTag 119 | 120 | return data 121 | } 122 | 123 | return null 124 | } 125 | }) 126 | // @ts-ignore 127 | compiledTypeMap[customTag.tag].additionalKinds = additionalKinds 128 | } 129 | }) 130 | 131 | const schemaWithAdditionalTags = Schema.create( 132 | Object.values(compiledTypeMap) 133 | ) 134 | ;(schemaWithAdditionalTags as any).compiledTypeMap = compiledTypeMap 135 | 136 | const additionalOptions: Yaml.LoadOptions = { 137 | schema: schemaWithAdditionalTags 138 | } 139 | const text = document.getText() 140 | 141 | return createJSONDocument( 142 | document, 143 | Yaml.load(text, additionalOptions), 144 | callbacks, 145 | parentParams 146 | ) 147 | } 148 | -------------------------------------------------------------------------------- /packages/config/src/parser/json/boolean-ast-node.ts: -------------------------------------------------------------------------------- 1 | import * as Json from "jsonc-parser" 2 | 3 | import { YAMLDocument } from "../index" 4 | import { ASTNode } from "./ast-node" 5 | 6 | export class BooleanASTNode extends ASTNode { 7 | constructor( 8 | document: YAMLDocument, 9 | parent: ASTNode, 10 | name: Json.Segment, 11 | value: boolean | string, 12 | start: number, 13 | end?: number 14 | ) { 15 | super(document, parent, "boolean", name, start, end) 16 | this.value = value 17 | } 18 | 19 | getValue(): boolean | string { 20 | return this.value 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/config/src/parser/json/external-import-ast-node.ts: -------------------------------------------------------------------------------- 1 | import * as Json from "jsonc-parser" 2 | import * as Path from "path" 3 | 4 | import { JSONSchema } from "../../model" 5 | import { YAMLDocument } from "../index" 6 | import { ASTNode } from "./ast-node" 7 | 8 | const IMPORT_REGEXP = /^\${file\((.+)\.(yml|yaml)\)(:\w+)?}$/ 9 | const FILE_URI_PREFIX = "file://" 10 | 11 | export type OnRegisterExternalImport = (uri: string, parentUri: string) => void 12 | 13 | export type OnValidateExternalImport = ( 14 | uri: string, 15 | parentUri: string, 16 | schema: JSONSchema, 17 | property: string | void 18 | ) => void 19 | 20 | export interface ExternalImportsCallbacks { 21 | onRegisterExternalImport: OnRegisterExternalImport 22 | onValidateExternalImport: OnValidateExternalImport 23 | } 24 | 25 | export class ExternalImportASTNode extends ASTNode { 26 | static isImportPath(path: string) { 27 | return IMPORT_REGEXP.test(path) 28 | } 29 | 30 | private uri: string | void 31 | private parameter: string | void = undefined 32 | private callbacks: ExternalImportsCallbacks 33 | 34 | constructor( 35 | document: YAMLDocument, 36 | parent: ASTNode, 37 | name: Json.Segment, 38 | value: string, 39 | start: number, 40 | end: number, 41 | callbacks: ExternalImportsCallbacks 42 | ) { 43 | super(document, parent, "string", name, start, end) 44 | this.callbacks = callbacks 45 | this.value = value 46 | } 47 | 48 | set value(newValue: string) { 49 | const [, path, extension, parameter] = IMPORT_REGEXP.exec(newValue) 50 | 51 | this.uri = this.resolvePath(`${path}.${extension}`) 52 | // remove leading `:` 53 | this.parameter = parameter && parameter.substr(1) 54 | 55 | if (this.uri) { 56 | this.callbacks.onRegisterExternalImport(this.uri, this.document.uri) 57 | } 58 | } 59 | 60 | validate(schema: JSONSchema): void { 61 | if (this.uri) { 62 | this.callbacks.onValidateExternalImport( 63 | this.uri, 64 | this.document.uri, 65 | schema, 66 | this.parameter 67 | ) 68 | } 69 | } 70 | 71 | getUri(): string | void { 72 | return this.uri 73 | } 74 | 75 | private resolveOpts(path: string): string | void { 76 | const newPath = path.replace("${opt:stage}", "dev") 77 | 78 | if (newPath.includes("${")) { 79 | return undefined 80 | } 81 | 82 | return newPath 83 | } 84 | 85 | private resolvePath(path: string): string | void { 86 | if (Path.isAbsolute(path)) { 87 | return this.resolveOpts(path) 88 | } else { 89 | if (this.document.uri.startsWith(FILE_URI_PREFIX)) { 90 | return this.resolveOpts( 91 | FILE_URI_PREFIX + 92 | Path.join( 93 | Path.dirname( 94 | this.document.uri.replace(FILE_URI_PREFIX, "") 95 | ), 96 | path 97 | ) 98 | ) 99 | } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /packages/config/src/parser/json/globals.ts: -------------------------------------------------------------------------------- 1 | import { Segment } from "vscode-json-languageservice" 2 | 3 | import { 4 | API, 5 | DocumentType, 6 | FUNCTION, 7 | GlobalConfigItem, 8 | GlobalKeys, 9 | GlobalsConfig, 10 | SIMPLE_TABLE 11 | } from "../../model" 12 | import { ObjectASTNode, PropertyASTNode } from "../json" 13 | import { getPropertyNodeValue } from "../util" 14 | import { YAMLDocument } from "./document" 15 | 16 | export const GLOBALS_PATH: Segment[] = ["Globals"] 17 | 18 | export const getDefaultGlobalsConfig = (): GlobalsConfig => ({ 19 | [API]: { 20 | ...GlobalKeys[API], 21 | properties: [] 22 | }, 23 | [FUNCTION]: { 24 | ...GlobalKeys[FUNCTION], 25 | properties: [] 26 | }, 27 | [SIMPLE_TABLE]: { 28 | ...GlobalKeys[SIMPLE_TABLE], 29 | properties: [] 30 | } 31 | }) 32 | 33 | export const collectGlobalPropertiesFromNode = ( 34 | globalsNode: ObjectASTNode 35 | ): GlobalsConfig => { 36 | const globalsConfig: GlobalsConfig = getDefaultGlobalsConfig() 37 | 38 | globalsNode.getChildNodes().forEach(node => { 39 | if (node instanceof PropertyASTNode) { 40 | const propertyNode = node 41 | const location = propertyNode.getLocation() 42 | 43 | if (location in globalsConfig) { 44 | const configItem: GlobalConfigItem = globalsConfig[location] 45 | const propertyObjectNode = getPropertyNodeValue( 46 | propertyNode, 47 | String(location) 48 | ) 49 | 50 | if ( 51 | propertyObjectNode && 52 | propertyObjectNode instanceof ObjectASTNode 53 | ) { 54 | propertyObjectNode.getChildNodes().forEach(childNode => { 55 | if (childNode.type === "property") { 56 | const propertyChildNode = childNode as PropertyASTNode 57 | 58 | configItem.properties.push( 59 | propertyChildNode.key.location 60 | ) 61 | } 62 | }) 63 | } 64 | } 65 | } 66 | }) 67 | 68 | return globalsConfig 69 | } 70 | 71 | export const collectGlobals = (document: YAMLDocument): GlobalsConfig => { 72 | if ( 73 | document.documentType === DocumentType.CLOUD_FORMATION || 74 | document.documentType === DocumentType.SAM 75 | ) { 76 | const globalsNode = document.root.get(GLOBALS_PATH) 77 | 78 | if (globalsNode && globalsNode instanceof ObjectASTNode) { 79 | return collectGlobalPropertiesFromNode(globalsNode) 80 | } 81 | } 82 | 83 | return getDefaultGlobalsConfig() 84 | } 85 | -------------------------------------------------------------------------------- /packages/config/src/parser/json/index.ts: -------------------------------------------------------------------------------- 1 | export { YAMLDocument, Problem } from "./document" 2 | export { ASTNode } from "./ast-node" 3 | export { PropertyASTNode } from "./property-ast-node" 4 | export { StringASTNode } from "./string-ast-node" 5 | export { 6 | ExternalImportASTNode, 7 | ExternalImportsCallbacks 8 | } from "./external-import-ast-node" 9 | export { NumberASTNode } from "./number-ast-node" 10 | export { BooleanASTNode } from "./boolean-ast-node" 11 | export { ObjectASTNode } from "./object-ast-node" 12 | export { ArrayASTNode } from "./array-ast-node" 13 | export { NullASTNode } from "./null-ast-node" 14 | export { ErrorCode } from "./validation-result" 15 | export { getDefaultGlobalsConfig } from "./globals" 16 | -------------------------------------------------------------------------------- /packages/config/src/parser/json/json-document.ts: -------------------------------------------------------------------------------- 1 | import { JSONSchema } from "../../model/jsonSchema" 2 | import { ASTNode, IApplicableSchema } from "./ast-node" 3 | import { NoOpSchemaCollector, SchemaCollector } from "./schema-collector" 4 | import { IProblem, ValidationResult } from "./validation-result" 5 | 6 | export class JSONDocument { 7 | readonly uri: string 8 | readonly root: ASTNode 9 | readonly syntaxErrors: IProblem[] 10 | 11 | constructor(uri: string, root: ASTNode, syntaxErrors: IProblem[]) { 12 | this.uri = uri 13 | this.root = root 14 | this.syntaxErrors = syntaxErrors 15 | } 16 | 17 | getNodeFromOffset(offset: number): ASTNode { 18 | return this.root && this.root.getNodeFromOffset(offset) 19 | } 20 | 21 | getNodeFromOffsetEndInclusive(offset: number): ASTNode { 22 | return this.root && this.root.getNodeFromOffsetEndInclusive(offset) 23 | } 24 | 25 | visit(visitor: (node: ASTNode) => boolean): void { 26 | if (this.root) { 27 | this.root.visit(visitor) 28 | } 29 | } 30 | 31 | validate(schema: JSONSchema): IProblem[] { 32 | if (this.root && schema) { 33 | const validationResult = new ValidationResult() 34 | this.root.validate( 35 | schema, 36 | validationResult, 37 | new NoOpSchemaCollector() 38 | ) 39 | return validationResult.problems 40 | } 41 | return null 42 | } 43 | 44 | getMatchingSchemas( 45 | schema: JSONSchema, 46 | focusOffset: number = -1, 47 | exclude: ASTNode = null 48 | ): IApplicableSchema[] { 49 | const matchingSchemas = new SchemaCollector(focusOffset, exclude) 50 | const validationResult = new ValidationResult() 51 | if (this.root && schema) { 52 | this.root.validate(schema, validationResult, matchingSchemas) 53 | } 54 | return matchingSchemas.schemas 55 | } 56 | 57 | getValidationProblems( 58 | schema: JSONSchema, 59 | focusOffset: number = -1, 60 | exclude: ASTNode = null 61 | ) { 62 | const matchingSchemas = new SchemaCollector(focusOffset, exclude) 63 | const validationResult = new ValidationResult() 64 | if (this.root && schema) { 65 | this.root.validate(schema, validationResult, matchingSchemas) 66 | } 67 | return validationResult.problems 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/config/src/parser/json/localize.ts: -------------------------------------------------------------------------------- 1 | import * as nls from "vscode-nls" 2 | 3 | export default nls.loadMessageBundle() 4 | -------------------------------------------------------------------------------- /packages/config/src/parser/json/null-ast-node.ts: -------------------------------------------------------------------------------- 1 | import * as Json from "jsonc-parser" 2 | 3 | import { YAMLDocument } from "../index" 4 | import { ASTNode } from "./ast-node" 5 | 6 | export class NullASTNode extends ASTNode { 7 | constructor( 8 | document: YAMLDocument, 9 | parent: ASTNode, 10 | name: Json.Segment, 11 | start: number, 12 | end?: number 13 | ) { 14 | super(document, parent, "null", name, start, end) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/config/src/parser/json/number-ast-node.ts: -------------------------------------------------------------------------------- 1 | import * as Json from "jsonc-parser" 2 | 3 | import { JSONSchema } from "../../model" 4 | import { YAMLDocument } from "../index" 5 | import { ASTNode, ISchemaCollector } from "./ast-node" 6 | import localize from "./localize" 7 | import { ProblemSeverity, ValidationResult } from "./validation-result" 8 | 9 | export class NumberASTNode extends ASTNode { 10 | isInteger: boolean 11 | 12 | constructor( 13 | document: YAMLDocument, 14 | parent: ASTNode, 15 | name: Json.Segment, 16 | start: number, 17 | end?: number 18 | ) { 19 | super(document, parent, "number", name, start, end) 20 | this.isInteger = true 21 | this.value = Number.NaN 22 | } 23 | 24 | validate( 25 | schema: JSONSchema, 26 | validationResult: ValidationResult, 27 | matchingSchemas: ISchemaCollector 28 | ): void { 29 | if (!matchingSchemas.include(this)) { 30 | return 31 | } 32 | 33 | // work around type validation in the base class 34 | let typeIsInteger = false 35 | if ( 36 | schema.type === "integer" || 37 | (Array.isArray(schema.type) && 38 | (schema.type as string[]).indexOf("integer") !== -1) 39 | ) { 40 | typeIsInteger = true 41 | } 42 | if (typeIsInteger && this.isInteger === true) { 43 | this.type = "integer" 44 | } 45 | super.validate(schema, validationResult, matchingSchemas) 46 | this.type = "number" 47 | 48 | const val = this.value 49 | 50 | if (typeof schema.multipleOf === "number") { 51 | if (val % schema.multipleOf !== 0) { 52 | validationResult.problems.push({ 53 | location: { start: this.start, end: this.end }, 54 | severity: ProblemSeverity.Warning, 55 | message: localize( 56 | "multipleOfWarning", 57 | "Value is not divisible by {0}.", 58 | schema.multipleOf 59 | ) 60 | }) 61 | } 62 | } 63 | 64 | if (typeof schema.minimum === "number") { 65 | if (schema.exclusiveMinimum && val <= schema.minimum) { 66 | validationResult.problems.push({ 67 | location: { start: this.start, end: this.end }, 68 | severity: ProblemSeverity.Warning, 69 | message: localize( 70 | "exclusiveMinimumWarning", 71 | "Value is below the exclusive minimum of {0}.", 72 | schema.minimum 73 | ) 74 | }) 75 | } 76 | if (!schema.exclusiveMinimum && val < schema.minimum) { 77 | validationResult.problems.push({ 78 | location: { start: this.start, end: this.end }, 79 | severity: ProblemSeverity.Warning, 80 | message: localize( 81 | "minimumWarning", 82 | "Value is below the minimum of {0}.", 83 | schema.minimum 84 | ) 85 | }) 86 | } 87 | } 88 | 89 | if (typeof schema.maximum === "number") { 90 | if (schema.exclusiveMaximum && val >= schema.maximum) { 91 | validationResult.problems.push({ 92 | location: { start: this.start, end: this.end }, 93 | severity: ProblemSeverity.Warning, 94 | message: localize( 95 | "exclusiveMaximumWarning", 96 | "Value is above the exclusive maximum of {0}.", 97 | schema.maximum 98 | ) 99 | }) 100 | } 101 | if (!schema.exclusiveMaximum && val > schema.maximum) { 102 | validationResult.problems.push({ 103 | location: { start: this.start, end: this.end }, 104 | severity: ProblemSeverity.Warning, 105 | message: localize( 106 | "maximumWarning", 107 | "Value is above the maximum of {0}.", 108 | schema.maximum 109 | ) 110 | }) 111 | } 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /packages/config/src/parser/json/property-ast-node.ts: -------------------------------------------------------------------------------- 1 | import { Segment } from "vscode-json-languageservice" 2 | 3 | import { CustomTag } from "../../model" 4 | import { JSONSchema } from "../../model/jsonSchema" 5 | import { YAMLDocument } from "../index" 6 | import { ASTNode, ISchemaCollector } from "./ast-node" 7 | import { StringASTNode } from "./string-ast-node" 8 | import { ValidationResult } from "./validation-result" 9 | 10 | export class PropertyASTNode extends ASTNode { 11 | key: StringASTNode 12 | colonOffset: number 13 | 14 | constructor( 15 | document: YAMLDocument, 16 | parent: ASTNode, 17 | key: StringASTNode, 18 | customTag: CustomTag 19 | ) { 20 | super(document, parent, "property", null, key.start, key.end, customTag) 21 | this.key = key 22 | key.parent = this 23 | key.location = key.value 24 | this.colonOffset = -1 25 | } 26 | 27 | getChildNodes(): ASTNode[] { 28 | return this.value ? [this.key, this.value] : [this.key] 29 | } 30 | 31 | getLastChild(): ASTNode { 32 | return this.value 33 | } 34 | 35 | getLocation(): Segment | null { 36 | return this.key.location 37 | } 38 | 39 | visit(visitor: (node: ASTNode) => boolean): boolean { 40 | return ( 41 | visitor(this) && 42 | this.key.visit(visitor) && 43 | this.value && 44 | this.value.visit(visitor) 45 | ) 46 | } 47 | 48 | validate( 49 | schema: JSONSchema, 50 | validationResult: ValidationResult, 51 | matchingSchemas: ISchemaCollector 52 | ): void { 53 | if (!matchingSchemas.include(this)) { 54 | return 55 | } 56 | if (this.value) { 57 | this.value.validate(schema, validationResult, matchingSchemas) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/config/src/parser/json/schema-collector.ts: -------------------------------------------------------------------------------- 1 | import { ASTNode, IApplicableSchema, ISchemaCollector } from "./ast-node" 2 | 3 | // tslint:disable-next-line: max-classes-per-file 4 | export class SchemaCollector implements ISchemaCollector { 5 | schemas: IApplicableSchema[] = [] 6 | private focusOffset: number 7 | private exclude: ASTNode 8 | constructor(focusOffset: number = -1, exclude: ASTNode = null) { 9 | this.focusOffset = focusOffset 10 | this.exclude = exclude 11 | } 12 | add(schema: IApplicableSchema) { 13 | this.schemas.push(schema) 14 | } 15 | merge(other: ISchemaCollector) { 16 | this.schemas.push(...other.schemas) 17 | } 18 | include(node: ASTNode) { 19 | return ( 20 | (this.focusOffset === -1 || node.contains(this.focusOffset)) && 21 | node !== this.exclude 22 | ) 23 | } 24 | newSub(): ISchemaCollector { 25 | return new SchemaCollector(-1, this.exclude) 26 | } 27 | } 28 | 29 | // tslint:disable-next-line: max-classes-per-file 30 | export class NoOpSchemaCollector implements ISchemaCollector { 31 | get schemas() { 32 | return [] 33 | } 34 | add() { 35 | return 36 | } 37 | merge() { 38 | return 39 | } 40 | include() { 41 | return true 42 | } 43 | newSub(): ISchemaCollector { 44 | return this 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/config/src/parser/json/string-ast-node.ts: -------------------------------------------------------------------------------- 1 | import * as Json from "jsonc-parser" 2 | 3 | import { CustomTag, JSONSchema } from "../../model" 4 | import { YAMLDocument } from "../index" 5 | import { ASTNode, ISchemaCollector } from "./ast-node" 6 | import localize from "./localize" 7 | import { ProblemSeverity, ValidationResult } from "./validation-result" 8 | 9 | export class StringASTNode extends ASTNode { 10 | isKey: boolean 11 | 12 | constructor( 13 | document: YAMLDocument, 14 | parent: ASTNode, 15 | name: Json.Segment, 16 | isKey: boolean, 17 | start: number, 18 | end?: number, 19 | customTag?: CustomTag 20 | ) { 21 | super(document, parent, "string", name, start, end, customTag) 22 | this.isKey = isKey 23 | this.value = "" 24 | } 25 | 26 | validate( 27 | schema: JSONSchema, 28 | validationResult: ValidationResult, 29 | matchingSchemas: ISchemaCollector 30 | ): void { 31 | if (!matchingSchemas.include(this)) { 32 | return 33 | } 34 | 35 | super.validate(schema, validationResult, matchingSchemas) 36 | 37 | if (schema.minLength && this.value.length < schema.minLength) { 38 | validationResult.problems.push({ 39 | location: { start: this.start, end: this.end }, 40 | severity: ProblemSeverity.Warning, 41 | message: localize( 42 | "minLengthWarning", 43 | "String is shorter than the minimum length of {0}.", 44 | schema.minLength 45 | ) 46 | }) 47 | } 48 | 49 | if (schema.maxLength && this.value.length > schema.maxLength) { 50 | validationResult.problems.push({ 51 | location: { start: this.start, end: this.end }, 52 | severity: ProblemSeverity.Warning, 53 | message: localize( 54 | "maxLengthWarning", 55 | "String is longer than the maximum length of {0}.", 56 | schema.maxLength 57 | ) 58 | }) 59 | } 60 | 61 | if (schema.pattern) { 62 | const regex = new RegExp(schema.pattern) 63 | if (!regex.test(this.value)) { 64 | validationResult.problems.push({ 65 | location: { start: this.start, end: this.end }, 66 | severity: ProblemSeverity.Warning, 67 | message: 68 | schema.patternErrorMessage || 69 | schema.errorMessage || 70 | localize( 71 | "patternWarning", 72 | 'String does not match the pattern of "{0}".', 73 | schema.pattern 74 | ) 75 | }) 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /packages/config/src/parser/json/validation-result.ts: -------------------------------------------------------------------------------- 1 | import localize from "./localize" 2 | 3 | export interface IProblem { 4 | location: IRange 5 | severity: ProblemSeverity 6 | code?: ErrorCode 7 | message: string 8 | } 9 | 10 | export interface IRange { 11 | start: number 12 | end: number 13 | } 14 | 15 | export enum ProblemSeverity { 16 | Error, 17 | Warning 18 | } 19 | 20 | export enum ErrorCode { 21 | Undefined = 0, 22 | EnumValueMismatch = 1, 23 | CommentsNotAllowed = 2 24 | } 25 | 26 | export class ValidationResult { 27 | problems: IProblem[] 28 | 29 | propertiesMatches: number 30 | propertiesValueMatches: number 31 | primaryValueMatches: number 32 | enumValueMatch: boolean 33 | enumValues: any[] 34 | warnings 35 | errors 36 | 37 | constructor() { 38 | this.problems = [] 39 | this.propertiesMatches = 0 40 | this.propertiesValueMatches = 0 41 | this.primaryValueMatches = 0 42 | this.enumValueMatch = false 43 | this.enumValues = null 44 | this.warnings = [] 45 | this.errors = [] 46 | } 47 | 48 | hasProblems(): boolean { 49 | return !!this.problems.length 50 | } 51 | 52 | mergeAll(validationResults: ValidationResult[]): void { 53 | validationResults.forEach(validationResult => { 54 | this.merge(validationResult) 55 | }) 56 | } 57 | 58 | merge(validationResult: ValidationResult): void { 59 | this.problems = this.problems.concat(validationResult.problems) 60 | } 61 | 62 | mergeEnumValues(validationResult: ValidationResult): void { 63 | if ( 64 | !this.enumValueMatch && 65 | !validationResult.enumValueMatch && 66 | this.enumValues && 67 | validationResult.enumValues 68 | ) { 69 | this.enumValues = this.enumValues.concat( 70 | validationResult.enumValues 71 | ) 72 | for (const error of this.problems) { 73 | if (error.code === ErrorCode.EnumValueMismatch) { 74 | error.message = localize( 75 | "enumWarning", 76 | "Value is not accepted. Valid values: {0}.", 77 | this.enumValues.map(v => JSON.stringify(v)).join(", ") 78 | ) 79 | } 80 | } 81 | } 82 | } 83 | 84 | mergePropertyMatch(propertyValidationResult: ValidationResult): void { 85 | this.merge(propertyValidationResult) 86 | this.propertiesMatches++ 87 | if ( 88 | propertyValidationResult.enumValueMatch || 89 | (!this.hasProblems() && propertyValidationResult.propertiesMatches) 90 | ) { 91 | this.propertiesValueMatches++ 92 | } 93 | if ( 94 | propertyValidationResult.enumValueMatch && 95 | propertyValidationResult.enumValues && 96 | propertyValidationResult.enumValues.length === 1 97 | ) { 98 | this.primaryValueMatches++ 99 | } 100 | } 101 | 102 | compareGeneric(other: ValidationResult): number { 103 | const hasProblems = this.hasProblems() 104 | if (hasProblems !== other.hasProblems()) { 105 | return hasProblems ? -1 : 1 106 | } 107 | if (this.enumValueMatch !== other.enumValueMatch) { 108 | return other.enumValueMatch ? -1 : 1 109 | } 110 | if (this.propertiesValueMatches !== other.propertiesValueMatches) { 111 | return this.propertiesValueMatches - other.propertiesValueMatches 112 | } 113 | if (this.primaryValueMatches !== other.primaryValueMatches) { 114 | return this.primaryValueMatches - other.primaryValueMatches 115 | } 116 | return this.propertiesMatches - other.propertiesMatches 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /packages/config/src/parser/references/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CUSTOM_TAGS, 3 | CustomTag, 4 | Reference, 5 | References, 6 | ReferenceType 7 | } from "../../model" 8 | import { ArrayASTNode } from "../json/array-ast-node" 9 | import { ASTNode } from "../json/ast-node" 10 | import { ObjectASTNode } from "../json/object-ast-node" 11 | import { StringASTNode } from "../json/string-ast-node" 12 | import * as utils from "./utils" 13 | import keyBy = require("lodash/keyBy") 14 | import { PropertyASTNode } from "../json" 15 | 16 | const CUSTOM_TAGS_BY_PROPERTY_NAME = keyBy(CUSTOM_TAGS, "propertyName") 17 | 18 | export const generateEmptyReferences = (): References => ({ 19 | hash: {}, 20 | lookup: new WeakMap() 21 | }) 22 | 23 | export const collectReferencesFromStringNode = ( 24 | node: StringASTNode, 25 | customTag: CustomTag 26 | ): Reference[] => { 27 | switch (customTag.type) { 28 | case ReferenceType.GET_ATT: 29 | return utils.getGetAtt(node) 30 | case ReferenceType.REF: 31 | return utils.getRef(node) 32 | case ReferenceType.SUB: 33 | return utils.getSub(node) 34 | case ReferenceType.DEPENDS_ON: 35 | return utils.getDependsOn(node) 36 | case ReferenceType.CONDITION: 37 | return utils.getCondition(node) 38 | default: 39 | return [] 40 | } 41 | } 42 | 43 | export const collectReferences = (node: ASTNode): References => { 44 | const references = generateEmptyReferences() 45 | 46 | const addToReferences = (item: Reference) => { 47 | if (!references.hash[item.key]) { 48 | references.hash[item.key] = [item] 49 | } else { 50 | references.hash[item.key].push(item) 51 | } 52 | references.lookup.set(item.node, item) 53 | } 54 | 55 | const traverse = (node: ASTNode, customTag?: CustomTag) => { 56 | let currentCustomTag = customTag 57 | 58 | if (node.customTag && node.customTag.type !== undefined) { 59 | currentCustomTag = node.customTag 60 | } 61 | 62 | if (node instanceof StringASTNode) { 63 | if (currentCustomTag) { 64 | collectReferencesFromStringNode(node, currentCustomTag).forEach( 65 | addToReferences 66 | ) 67 | } 68 | } else if (node instanceof ArrayASTNode) { 69 | if ( 70 | currentCustomTag && 71 | currentCustomTag.type === ReferenceType.SUB 72 | ) { 73 | if (node.items.length === 1) { 74 | const firstItem = node.items[0] 75 | 76 | if (firstItem instanceof StringASTNode) { 77 | utils.getSub(firstItem).forEach(addToReferences) 78 | } 79 | } else { 80 | node.items.slice(1).forEach(item => { 81 | traverse(item, currentCustomTag) 82 | }) 83 | } 84 | } else if ( 85 | currentCustomTag && 86 | currentCustomTag.type === ReferenceType.GET_ATT 87 | ) { 88 | const firstItem = node.items[0] 89 | 90 | if (firstItem instanceof StringASTNode) { 91 | utils.getGetAtt(firstItem).forEach(addToReferences) 92 | } 93 | } else { 94 | node.items.forEach(item => { 95 | traverse(item, currentCustomTag) 96 | }) 97 | } 98 | } else if (node instanceof ObjectASTNode) { 99 | node.properties.forEach(item => { 100 | traverse(item, currentCustomTag) 101 | }) 102 | } else if (node instanceof PropertyASTNode) { 103 | if (node.key.value in CUSTOM_TAGS_BY_PROPERTY_NAME) { 104 | currentCustomTag = CUSTOM_TAGS_BY_PROPERTY_NAME[node.key.value] 105 | } 106 | 107 | traverse(node.value, currentCustomTag) 108 | } 109 | } 110 | 111 | traverse(node) 112 | 113 | return references 114 | } 115 | -------------------------------------------------------------------------------- /packages/config/src/parser/references/utils.ts: -------------------------------------------------------------------------------- 1 | import { Reference, ReferenceType } from "../../model/references" 2 | import { StringASTNode } from "../json/string-ast-node" 3 | 4 | const parameterRegExp = new RegExp("\\${[^}]*}", "g") 5 | 6 | const REST_API_RESOURCE = "ServerlessRestApi" 7 | 8 | const isValidKey = (key: string) => 9 | !key.startsWith("AWS::") && key !== REST_API_RESOURCE 10 | 11 | export const getSub = (node: StringASTNode): Reference[] => { 12 | // rest regexp 13 | parameterRegExp.lastIndex = 0 14 | 15 | let match: RegExpExecArray 16 | const references: Reference[] = [] 17 | while ( 18 | (match = parameterRegExp.exec(node.value) as RegExpExecArray) != null 19 | ) { 20 | // Trim the ${} off of the match 21 | const referencedKey = match[0].substring(2, match[0].length - 1) 22 | // skip AWS variables references 23 | if (isValidKey(referencedKey)) { 24 | const reference = { 25 | key: referencedKey, 26 | type: ReferenceType.SUB, 27 | node 28 | } 29 | references.push(reference) 30 | } 31 | } 32 | return references 33 | } 34 | 35 | export const getRef = (node: StringASTNode): Reference[] => { 36 | const referencedKey = node.value 37 | 38 | if (isValidKey(referencedKey)) { 39 | const reference = { 40 | key: referencedKey, 41 | type: ReferenceType.REF, 42 | node 43 | } 44 | return [reference] 45 | } 46 | return [] 47 | } 48 | 49 | export const getGetAtt = (node: StringASTNode): Reference[] => { 50 | // TODO: handle reference properties 51 | const [referencedKey] = node.value.split(".") 52 | 53 | if (isValidKey(referencedKey)) { 54 | return [ 55 | { 56 | key: referencedKey, 57 | type: ReferenceType.GET_ATT, 58 | node 59 | } 60 | ] 61 | } 62 | 63 | return [] 64 | } 65 | 66 | export const getDependsOn = (node: StringASTNode): Reference[] => { 67 | const referencedKey = node.value 68 | if (isValidKey(referencedKey)) { 69 | return [ 70 | { 71 | key: referencedKey, 72 | type: ReferenceType.DEPENDS_ON, 73 | node 74 | } 75 | ] 76 | } 77 | return [] 78 | } 79 | 80 | export const getCondition = (node: StringASTNode): Reference[] => { 81 | const referencedCondition = node.value 82 | 83 | return [ 84 | { 85 | key: referencedCondition, 86 | type: ReferenceType.CONDITION, 87 | node 88 | } 89 | ] 90 | } 91 | -------------------------------------------------------------------------------- /packages/config/src/parser/scalar-type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Parse a boolean according to the specification 3 | * 4 | * Return: 5 | * true if its a true value 6 | * false if its a false value 7 | */ 8 | export function parseYamlBoolean(input: string): boolean { 9 | if ( 10 | [ 11 | "true", 12 | "True", 13 | "TRUE", 14 | "y", 15 | "Y", 16 | "yes", 17 | "Yes", 18 | "YES", 19 | "on", 20 | "On", 21 | "ON" 22 | ].lastIndexOf(input) >= 0 23 | ) { 24 | return true 25 | } else if ( 26 | [ 27 | "false", 28 | "False", 29 | "FALSE", 30 | "n", 31 | "N", 32 | "no", 33 | "No", 34 | "NO", 35 | "off", 36 | "Off", 37 | "OFF" 38 | ].lastIndexOf(input) >= 0 39 | ) { 40 | return false 41 | } 42 | throw new Error(`Invalid boolean "${input}"`) 43 | } 44 | -------------------------------------------------------------------------------- /packages/config/src/parser/util.ts: -------------------------------------------------------------------------------- 1 | import { ObjectASTNode, PropertyASTNode } from "./json" 2 | 3 | export const findProperty = ( 4 | objectNode: ObjectASTNode | void, 5 | predicate: (node: PropertyASTNode) => boolean 6 | ): PropertyASTNode | void => { 7 | if (!objectNode) { 8 | return 9 | } 10 | return objectNode.getChildNodes().find(node => { 11 | return node.type === "property" && predicate(node as PropertyASTNode) 12 | }) as PropertyASTNode | void 13 | } 14 | 15 | export const getPropertyNodeValue = ( 16 | propertyNode: PropertyASTNode | void, 17 | location: string 18 | ): ObjectASTNode | void => { 19 | if (!propertyNode) { 20 | return 21 | } 22 | 23 | return propertyNode.getChildNodes().find(node => { 24 | return node instanceof ObjectASTNode && node.location === location 25 | }) as ObjectASTNode | void 26 | } 27 | -------------------------------------------------------------------------------- /packages/config/src/utils/__tests__/documents.test.ts: -------------------------------------------------------------------------------- 1 | import { DocumentType } from "../../model/document" 2 | import { 3 | getDocumentType, 4 | isCloudFormationTemplate, 5 | isSAMTemplate, 6 | isServerlessFrameworkTemplate, 7 | isSupportedDocument 8 | } from "./../document" 9 | 10 | const slsTemplate = ` 11 | service: 12 | name: my-service 13 | provider: 14 | name: aws 15 | Resources: 16 | Table: 17 | Type: AWS::DynamoDB::Table 18 | ` 19 | 20 | const cfnTemplate = ` 21 | Resources: 22 | DomainName: 23 | Type: AWS::ApiGateway::DomainName 24 | Properties: 25 | DomainName: blah.example.com 26 | ` 27 | 28 | const cfnTemplateWithFormatVersion = ` 29 | AWSTemplateFormatVersion: 12345 30 | ` 31 | 32 | const samTemplate = ` 33 | AWSTemplateFormatVersion: 2010-09-09 34 | Transform: AWS::Serverless-2016-10-31 35 | Globals: 36 | Function: 37 | Runtime: nodejs8.10 38 | Resources: 39 | Function1: 40 | Type: AWS::Serverless::Function 41 | Properties: 42 | CodeUri: . 43 | Handler: index.handler 44 | ` 45 | 46 | test("should check for serverless framework templates", () => { 47 | expect(isServerlessFrameworkTemplate(slsTemplate)).toBe(true) 48 | expect(isServerlessFrameworkTemplate(cfnTemplate)).toBe(false) 49 | expect(isServerlessFrameworkTemplate(cfnTemplateWithFormatVersion)).toBe( 50 | false 51 | ) 52 | expect(isServerlessFrameworkTemplate(samTemplate)).toBe(false) 53 | }) 54 | 55 | test("should check for cfn templates", () => { 56 | expect(isCloudFormationTemplate(slsTemplate)).toBe(false) 57 | expect(isCloudFormationTemplate(cfnTemplate)).toBe(true) 58 | expect(isCloudFormationTemplate(cfnTemplateWithFormatVersion)).toBe(true) 59 | expect(isCloudFormationTemplate(samTemplate)).toBe(false) 60 | }) 61 | 62 | test("should check for sam template", () => { 63 | expect(isSAMTemplate(slsTemplate)).toBe(false) 64 | expect(isSAMTemplate(cfnTemplate)).toBe(false) 65 | expect(isSAMTemplate(cfnTemplateWithFormatVersion)).toBe(false) 66 | expect(isSAMTemplate(samTemplate)).toBe(true) 67 | }) 68 | 69 | test("should detect document type", () => { 70 | expect(getDocumentType(slsTemplate)).toBe(DocumentType.SERVERLESS_FRAMEWORK) 71 | expect(getDocumentType(cfnTemplate)).toBe(DocumentType.CLOUD_FORMATION) 72 | expect(getDocumentType(cfnTemplateWithFormatVersion)).toBe( 73 | DocumentType.CLOUD_FORMATION 74 | ) 75 | expect(getDocumentType(samTemplate)).toBe(DocumentType.SAM) 76 | }) 77 | 78 | test("should detect supported documents", () => { 79 | expect(isSupportedDocument(slsTemplate)).toBe(true) 80 | expect(isSupportedDocument(cfnTemplate)).toBe(true) 81 | expect(isSupportedDocument(cfnTemplateWithFormatVersion)).toBe(true) 82 | expect(isSupportedDocument(samTemplate)).toBe(true) 83 | }) 84 | -------------------------------------------------------------------------------- /packages/config/src/utils/__tests__/objects.test.ts: -------------------------------------------------------------------------------- 1 | import { equals } from "../objects" 2 | 3 | describe("Object Equals Tests", () => { 4 | it("Both are null", () => { 5 | const one = null 6 | const other = null 7 | 8 | const result = equals(one, other) 9 | expect(result).toBe(true) 10 | }) 11 | 12 | it("One is null the other is true", () => { 13 | const one = null 14 | const other = true 15 | 16 | const result = equals(one, other) 17 | expect(result).toBe(false) 18 | }) 19 | 20 | it("One is string the other is boolean", () => { 21 | const one = "test" 22 | const other = false 23 | 24 | const result = equals(one, other) 25 | expect(result).toBe(false) 26 | }) 27 | 28 | it("One is not object", () => { 29 | const one = "test" 30 | const other = false 31 | 32 | const result = equals(one, other) 33 | expect(result).toBe(false) 34 | }) 35 | 36 | it("One is array the other is not", () => { 37 | const one = new Proxy([], {}) 38 | const other = Object.keys({ 39 | 1: "2", 40 | 2: "3" 41 | }) 42 | const result = equals(one, other) 43 | expect(result).toBe(false) 44 | }) 45 | 46 | it("Both are arrays of different length", () => { 47 | const one = [1, 2, 3] 48 | const other = [1, 2, 3, 4] 49 | 50 | const result = equals(one, other) 51 | expect(result).toBe(false) 52 | }) 53 | 54 | it("Both are arrays of same elements but in different order", () => { 55 | const one = [1, 2, 3] 56 | const other = [3, 2, 1] 57 | 58 | const result = equals(one, other) 59 | expect(result).toBe(false) 60 | }) 61 | 62 | it("Arrays that are equal", () => { 63 | const one = [1, 2, 3] 64 | const other = [1, 2, 3] 65 | 66 | const result = equals(one, other) 67 | expect(result).toBe(true) 68 | }) 69 | 70 | it("Objects that are equal", () => { 71 | const one = { 72 | test: 1 73 | } 74 | const other = { 75 | test: 1 76 | } 77 | 78 | const result = equals(one, other) 79 | expect(result).toBe(true) 80 | }) 81 | 82 | it("Objects that have same keys but different values", () => { 83 | const one = { 84 | test: 1 85 | } 86 | const other = { 87 | test: 5 88 | } 89 | 90 | const result = equals(one, other) 91 | expect(result).toBe(false) 92 | }) 93 | 94 | it("Objects that have different keys", () => { 95 | const one = { 96 | testOne: 1 97 | } 98 | const other = { 99 | testOther: 1 100 | } 101 | 102 | const result = equals(one, other) 103 | expect(result).toBe(false) 104 | }) 105 | }) 106 | -------------------------------------------------------------------------------- /packages/config/src/utils/document.ts: -------------------------------------------------------------------------------- 1 | import { DocumentType } from "../model" 2 | 3 | const transformRegExp = /"?'?Transform"?'?:\s*"?'?AWS::Serverless-2016-10-31"?'?/ 4 | const slsServiceRegExp = /service:/ 5 | const slsProviderRegExp = /provider:/ 6 | const slsProviderNameRegExp = /name: aws/ 7 | const cfnResourcesRegExp = /"?Resources"?:/ 8 | const cfnResourceTypeRegExp = /(AWS|Custom)::/ 9 | const cfnFormatVersionRegExp = /AWSTemplateFormatVersion/ 10 | 11 | export const isServerlessFrameworkTemplate = (document: string): boolean => { 12 | return ( 13 | slsServiceRegExp.test(document) && 14 | slsProviderRegExp.test(document) && 15 | slsProviderNameRegExp.test(document) 16 | ) 17 | } 18 | 19 | const isBaseCloudFormationTemplate = (document: string): boolean => { 20 | return ( 21 | cfnFormatVersionRegExp.test(document) || 22 | (cfnResourcesRegExp.test(document) && 23 | cfnResourceTypeRegExp.test(document) && 24 | !isServerlessFrameworkTemplate(document)) 25 | ) 26 | } 27 | 28 | const isBaseSAMTemplate = (document: string): boolean => { 29 | return transformRegExp.test(document) 30 | } 31 | 32 | export const isCloudFormationTemplate = (document: string): boolean => { 33 | return ( 34 | isBaseCloudFormationTemplate(document) && !isBaseSAMTemplate(document) 35 | ) 36 | } 37 | 38 | export const isSAMTemplate = (document: string): boolean => { 39 | return isBaseCloudFormationTemplate(document) && isBaseSAMTemplate(document) 40 | } 41 | 42 | export const getDocumentType = (text: string): DocumentType => { 43 | if (!text) { 44 | return DocumentType.UNKNOWN 45 | } 46 | 47 | if (text) { 48 | if (isSAMTemplate(text)) { 49 | return DocumentType.SAM 50 | } else if (isCloudFormationTemplate(text)) { 51 | return DocumentType.CLOUD_FORMATION 52 | } else if (isServerlessFrameworkTemplate(text)) { 53 | return DocumentType.SERVERLESS_FRAMEWORK 54 | } 55 | 56 | return DocumentType.UNKNOWN 57 | } 58 | } 59 | 60 | export const isSupportedDocument = (text: string): boolean => { 61 | const documentType = getDocumentType(text) 62 | 63 | const isSupported = 64 | documentType === DocumentType.SAM || 65 | documentType === DocumentType.CLOUD_FORMATION || 66 | documentType === DocumentType.SERVERLESS_FRAMEWORK 67 | 68 | return isSupported 69 | } 70 | -------------------------------------------------------------------------------- /packages/config/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./document" 2 | export * from "./objects" 3 | export * from "./resources" 4 | export * from "./url" 5 | export * from "./yaml" 6 | -------------------------------------------------------------------------------- /packages/config/src/utils/objects.ts: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | import map = require("lodash/map") 4 | 5 | export function equals(one: any, other: any): boolean { 6 | if (one === other) { 7 | return true 8 | } 9 | if ( 10 | one === null || 11 | one === undefined || 12 | other === null || 13 | other === undefined 14 | ) { 15 | return false 16 | } 17 | if (typeof one !== typeof other) { 18 | return false 19 | } 20 | if (typeof one !== "object") { 21 | return false 22 | } 23 | if (Array.isArray(one) !== Array.isArray(other)) { 24 | return false 25 | } 26 | 27 | if (Array.isArray(one)) { 28 | if (one.length !== other.length) { 29 | return false 30 | } 31 | for (let i = 0; i < one.length; i++) { 32 | if (!equals(one[i], other[i])) { 33 | return false 34 | } 35 | } 36 | } else { 37 | const oneKeys: string[] = map(one, (item, oneKey) => oneKey) 38 | 39 | oneKeys.sort() 40 | const otherKeys: string[] = map(other, (item, otherKey) => otherKey) 41 | otherKeys.sort() 42 | if (!equals(oneKeys, otherKeys)) { 43 | return false 44 | } 45 | // tslint:disable-next-line: prefer-for-of 46 | for (let i = 0; i < oneKeys.length; i++) { 47 | if (!equals(one[oneKeys[i]], other[oneKeys[i]])) { 48 | return false 49 | } 50 | } 51 | } 52 | return true 53 | } 54 | 55 | export const logObject = (obj: any) => { 56 | let cache = [] 57 | const res = JSON.stringify( 58 | obj, 59 | (key, value) => { 60 | if (typeof value === "object" && value !== null) { 61 | if (cache.indexOf(value) !== -1) { 62 | // Duplicate reference found 63 | try { 64 | // If this value does not reference a parent it can be deduped 65 | return JSON.parse(JSON.stringify(value)) 66 | } catch (error) { 67 | // discard key if value cannot be deduped 68 | return 69 | } 70 | } 71 | // Store value in our collection 72 | cache.push(value) 73 | } 74 | return value 75 | }, 76 | 2 77 | ) 78 | cache = null 79 | 80 | // eslint-disable-next-line no-console 81 | console.log(res) 82 | } 83 | -------------------------------------------------------------------------------- /packages/config/src/utils/resources.ts: -------------------------------------------------------------------------------- 1 | import { Segment } from "vscode-json-languageservice" 2 | 3 | import { ASTNode, PropertyASTNode } from "../parser" 4 | 5 | export const getRelativeNodePath = (node: ASTNode): Segment[] => { 6 | let path = node.getPath() 7 | 8 | // remove leading `resources` part to support serverless framework 9 | if (path[0] === "resources") { 10 | return path.slice(1) 11 | } 12 | 13 | return path 14 | } 15 | 16 | export const getResourceName = (node: ASTNode): void | string => { 17 | const path = getRelativeNodePath(node) 18 | 19 | const getResourceName = (targetNode: ASTNode): string | void => { 20 | if (targetNode.type !== "object") { 21 | return 22 | } 23 | 24 | const children = targetNode.getChildNodes() 25 | let resourceType: string | void 26 | 27 | children.find(child => { 28 | if (child && child.type === "property") { 29 | const property = child as PropertyASTNode 30 | 31 | if (property.key.location === "Type") { 32 | const resourceTypeValue = 33 | property.value && property.value.value 34 | 35 | if ( 36 | typeof resourceTypeValue === "string" && 37 | resourceTypeValue.indexOf("AWS::") === 0 38 | ) { 39 | resourceType = resourceTypeValue 40 | return true 41 | } 42 | } 43 | } 44 | 45 | return false 46 | }) 47 | 48 | return resourceType 49 | } 50 | 51 | if (path[0] === "Resources" && path.length > 1) { 52 | let currentNode = node.parent 53 | let resourceName = getResourceName(node) 54 | let depth = 0 55 | const maxDepth = 7 56 | 57 | while (resourceName === undefined && currentNode && depth < maxDepth) { 58 | resourceName = getResourceName(currentNode) 59 | currentNode = currentNode.parent 60 | depth += 1 61 | } 62 | 63 | return resourceName 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/config/src/utils/url.ts: -------------------------------------------------------------------------------- 1 | import * as url from "url" 2 | 3 | export const isRemoteUrl = (str: string): boolean => { 4 | return Boolean(url.parse(str).hostname) 5 | } 6 | -------------------------------------------------------------------------------- /packages/config/src/utils/yaml.ts: -------------------------------------------------------------------------------- 1 | import { ASTNode, ObjectASTNode, PropertyASTNode } from "../parser" 2 | 3 | export const getNodeItemByKey = ( 4 | node: ObjectASTNode | ASTNode, 5 | key: string 6 | ): PropertyASTNode | void => { 7 | if (node instanceof ObjectASTNode) { 8 | return node.properties.find(property => { 9 | return property.key.value === key 10 | }) as PropertyASTNode 11 | } 12 | 13 | return 14 | } 15 | -------------------------------------------------------------------------------- /packages/config/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "include": [ 7 | "src" 8 | ] 9 | } -------------------------------------------------------------------------------- /packages/language-server/.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | test/ 3 | tsconfig.json 4 | jest.config.js 5 | -------------------------------------------------------------------------------- /packages/language-server/NOTICE.txt: -------------------------------------------------------------------------------- 1 | @serverless-ide/language-server 2 | 3 | THIRD-PARTY SOFTWARE NOTICES AND INFORMATION 4 | Do Not Translate or Localize 5 | 6 | This project incorporates components from the projects listed below. The original copyright notices and the licenses under which Red Hat received such components are set forth below. Red Hat reserves all rights not expressly granted herein, whether by implication, estoppel or otherwise. 7 | 8 | 1. textmate/yaml.tmbundle (https://github.com/textmate/yaml.tmbundle) 9 | 2. microsoft/vscode (https://github.com/Microsoft/vscode) 10 | 11 | %% textmate/yaml.tmbundle NOTICES AND INFORMATION BEGIN HERE 12 | ========================================= 13 | Copyright (c) 2019 FichteFoll 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy 16 | of this software and associated documentation files (the "Software"), to deal 17 | in the Software without restriction, including without limitation the rights 18 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | copies of the Software, and to permit persons to whom the Software is 20 | furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in all 23 | copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 31 | ========================================= 32 | END OF textmate/yaml.tmbundle NOTICES AND INFORMATION 33 | 34 | %% vscode NOTICES AND INFORMATION BEGIN HERE 35 | ========================================= 36 | MIT License 37 | 38 | Copyright (c) 2019 - present Microsoft Corporation 39 | 40 | All rights reserved. 41 | 42 | Permission is hereby granted, free of charge, to any person obtaining a copy 43 | of this software and associated documentation files (the "Software"), to deal 44 | in the Software without restriction, including without limitation the rights 45 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 46 | copies of the Software, and to permit persons to whom the Software is 47 | furnished to do so, subject to the following conditions: 48 | 49 | The above copyright notice and this permission notice shall be included in all 50 | copies or substantial portions of the Software. 51 | 52 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 53 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 54 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 55 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 56 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 57 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 58 | SOFTWARE. 59 | ========================================= 60 | END OF vscode NOTICES AND INFORMATION 61 | 62 | ========================================================================= 63 | NOTICE file for use with, and corresponding to Section 4 of, 64 | the Apache License, Version 2.0, 65 | in this case for the @serverless-ide/language-server project. 66 | ========================================================================= 67 | 68 | This product includes software developed by 69 | Amazon.com, Inc. or its affiliates. 70 | Copyright (c) 2019 Amazon.com, Inc. All rights reserved. 71 | -------------------------------------------------------------------------------- /packages/language-server/README.md: -------------------------------------------------------------------------------- 1 | # Serverless IDE Language Server 2 | 3 | ## License 4 | 5 | Apache License 2.0 6 | -------------------------------------------------------------------------------- /packages/language-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@serverless-ide/language-server", 3 | "description": "Serverless IDE language server", 4 | "version": "0.6.5", 5 | "author": "Pavel Vlasov ", 6 | "license": "MIT", 7 | "engines": { 8 | "node": "*" 9 | }, 10 | "keywords": [ 11 | "aws", 12 | "sam", 13 | "cloudformation", 14 | "cfn", 15 | "serverless", 16 | "yaml", 17 | "autocompletion", 18 | "validation", 19 | "LSP" 20 | ], 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/threadheap/serverless-ide-language-server.git" 24 | }, 25 | "dependencies": { 26 | "@serverless-ide/cloudformation-schema": "^0.6.5", 27 | "@serverless-ide/config": "^0.6.0", 28 | "@serverless-ide/sam-schema": "^0.6.5", 29 | "@serverless-ide/serverless-framework-schema": "^0.6.5", 30 | "es6-promise-pool": "^2.5.0", 31 | "jsonc-parser": "^1.0.3", 32 | "lodash": "^4.17.11", 33 | "lru-cache": "^5.1.1", 34 | "request-light": "^0.2.3", 35 | "ts-get": "^1.0.3", 36 | "vscode-json-languageservice": "3.0.12", 37 | "vscode-languageserver": "^5.2.1", 38 | "vscode-languageserver-types": "^3.14.0", 39 | "vscode-nls": "^3.2.2", 40 | "vscode-uri": "^1.0.6" 41 | }, 42 | "devDependencies": { 43 | "@types/js-yaml": "^3.12.1", 44 | "@types/lodash": "^4.14.144", 45 | "@types/lru-cache": "^4.1.1", 46 | "@types/node": "^9.4.7", 47 | "@types/vscode": "^1.37.0", 48 | "source-map-support": "^0.5.4", 49 | "typescript": "^4.9.4" 50 | }, 51 | "scripts": { 52 | "build": "npm run clean && npm run compile", 53 | "clean": "rm -rf ./dist", 54 | "compile": "tsc", 55 | "watch": "npm run build --watch", 56 | "prepublishOnly": "npm run build", 57 | "lint:types": "tsc --noEmit", 58 | "lint:ts": "eslint ./src/**/*.ts", 59 | "lint:ts:fix": "eslint ./src/**/*.ts --fix", 60 | "test": "jest" 61 | }, 62 | "publishConfig": { 63 | "access": "public" 64 | }, 65 | "gitHead": "390fa05ac004e80dd92b96e08eded82c162ffcd9" 66 | } 67 | -------------------------------------------------------------------------------- /packages/language-server/src/language-service/jsonContributions.ts: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | import { CompletionItem, MarkedString } from "vscode-json-languageservice" 4 | 5 | export interface JSONWorkerContribution { 6 | getInfoContribution( 7 | uri: string, 8 | location: JSONPath 9 | ): Promise 10 | collectPropertyCompletions( 11 | uri: string, 12 | location: JSONPath, 13 | currentWord: string, 14 | addValue: boolean, 15 | isLast: boolean, 16 | result: CompletionsCollector 17 | ): Promise 18 | collectValueCompletions( 19 | uri: string, 20 | location: JSONPath, 21 | propertyKey: string, 22 | result: CompletionsCollector 23 | ): Promise 24 | collectDefaultCompletions( 25 | uri: string, 26 | result: CompletionsCollector 27 | ): Promise 28 | resolveCompletion?(item: CompletionItem): Promise 29 | } 30 | export type Segment = string | number 31 | export type JSONPath = Segment[] 32 | 33 | export interface CompletionsCollector { 34 | add(suggestion: CompletionItem): void 35 | error(message: string): void 36 | log(message: string): void 37 | setAsIncomplete(): void 38 | getNumberOfProposals(): number 39 | } 40 | -------------------------------------------------------------------------------- /packages/language-server/src/language-service/model/settings.ts: -------------------------------------------------------------------------------- 1 | export enum ValidationProvider { 2 | default = "default", 3 | "cfn-lint" = "cfn-lint" 4 | } 5 | 6 | export interface CFNLintExtensionSettings { 7 | path?: string 8 | appendRules?: string[] 9 | ignoreRules?: string[] 10 | overrideSpecPath?: string 11 | } 12 | 13 | export interface ExtensionSettings { 14 | serverlessIDE: { 15 | validationProvider?: ValidationProvider 16 | cfnLint?: CFNLintExtensionSettings 17 | validate?: boolean 18 | hover?: boolean 19 | completion?: boolean 20 | } 21 | http: { 22 | proxy: string 23 | proxyStrictSSL: boolean 24 | } 25 | } 26 | 27 | export interface CFNLintSettings { 28 | path: string 29 | appendRules: string[] 30 | ignoreRules: string[] 31 | overrideSpecPath?: string 32 | } 33 | 34 | export interface LanguageSettings { 35 | validate: boolean 36 | hover: boolean 37 | completion: boolean 38 | validationProvider: ValidationProvider 39 | cfnLint: CFNLintSettings 40 | workspaceRoot: string 41 | } 42 | 43 | export const getDefaultLanguageSettings = (): LanguageSettings => ({ 44 | validate: true, 45 | hover: true, 46 | completion: true, 47 | validationProvider: ValidationProvider["cfn-lint"], 48 | cfnLint: { 49 | path: "cfn-lint", 50 | appendRules: [], 51 | ignoreRules: [] 52 | }, 53 | workspaceRoot: "" 54 | }) 55 | -------------------------------------------------------------------------------- /packages/language-server/src/language-service/services/analytics/index.ts: -------------------------------------------------------------------------------- 1 | import { IConnection } from "vscode-languageserver" 2 | 3 | export interface AnalyticsPayload { 4 | action: string 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 6 | attributes: { [key: string]: any } 7 | } 8 | 9 | export interface ExceptionPayload { 10 | message: string 11 | stack: string 12 | } 13 | 14 | let connection: IConnection = void 0 15 | 16 | export const initializeAnalytics = (serverConnection: IConnection) => { 17 | connection = serverConnection 18 | } 19 | 20 | export const sendAnalytics = (payload: AnalyticsPayload) => { 21 | if (connection) { 22 | connection.sendNotification("custom/analytics", payload) 23 | } 24 | } 25 | 26 | export const sendException = (error: Error, message: string = "") => { 27 | if (connection) { 28 | connection.sendNotification("custom/analytics", { 29 | message: `${message}: ${error.message}`, 30 | stack: error.stack 31 | }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/language-server/src/language-service/services/completion/__tests__/completion-cfn.test.ts: -------------------------------------------------------------------------------- 1 | import { parse as parseYAML } from "@serverless-ide/config" 2 | import { TextDocument } from "vscode-languageserver" 3 | 4 | import { completionHelper } from "../../../utils/completion-helper" 5 | import { JSONSchemaService } from "./../../jsonSchema/index" 6 | import { YAMLCompletion } from "./../index" 7 | 8 | const schemaService = new JSONSchemaService() 9 | const completionProvider = new YAMLCompletion(schemaService) 10 | 11 | describe("Auto Completion Tests", () => { 12 | describe("doComplete", () => { 13 | const setup = (content: string) => { 14 | return TextDocument.create( 15 | "file://~/Desktop/cfn.yaml", 16 | "yaml", 17 | 0, 18 | content 19 | ) 20 | } 21 | 22 | const parseSetup = (content: string, offset: number) => { 23 | const testTextDocument = setup(content) 24 | const position = testTextDocument.positionAt(offset) 25 | const output = completionHelper(testTextDocument, position) 26 | 27 | return completionProvider.doComplete( 28 | testTextDocument, 29 | output.newPosition, 30 | parseYAML(output.newDocument) 31 | ) 32 | } 33 | 34 | test("should autocomplete on the root node", async () => { 35 | const content = [ 36 | "AWSTemplateFormatVersion: 2010-09-09", 37 | "Resources:" 38 | ].join("\n") 39 | const result = await parseSetup(content, content.length - 1) 40 | expect(result.items).not.toHaveLength(0) 41 | expect(result).toMatchSnapshot() 42 | }) 43 | 44 | test("should autocomplete resources", async () => { 45 | const content = [ 46 | "AWSTemplateFormatVersion: 2010-09-09", 47 | "Resources:", 48 | "\t" 49 | ].join("\n\r") 50 | const result = await parseSetup(content, content.length - 1) 51 | expect(result.items).not.toHaveLength(0) 52 | expect(result).toMatchSnapshot() 53 | }) 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /packages/language-server/src/language-service/services/completion/__tests__/completion-sls.test.ts: -------------------------------------------------------------------------------- 1 | import { parse as parseYAML } from "@serverless-ide/config" 2 | import { TextDocument } from "vscode-languageserver" 3 | 4 | import { JSONSchemaService } from "../../jsonSchema" 5 | import { YAMLCompletion } from "./../index" 6 | 7 | const schemaService = new JSONSchemaService() 8 | const completionProvider = new YAMLCompletion(schemaService) 9 | 10 | const RESOURCES_TEMPLATE = ` 11 | service: 12 | name: myService 13 | provider: 14 | name: aws 15 | resources: 16 | Resources: 17 | 18 | ` 19 | 20 | describe("Serverless Framework autocompletion", () => { 21 | describe("doComplete", () => { 22 | const setup = (content: string) => { 23 | return TextDocument.create( 24 | "file://~/Desktop/serverless.yml", 25 | "yaml", 26 | 0, 27 | content 28 | ) 29 | } 30 | 31 | const parseSetup = async (content: string, offset: number) => { 32 | const testTextDocument = setup(content) 33 | const position = testTextDocument.positionAt(offset) 34 | 35 | return await completionProvider.doComplete( 36 | testTextDocument, 37 | position, 38 | parseYAML(testTextDocument) 39 | ) 40 | } 41 | 42 | test("should autocomplete resources", async () => { 43 | const content = RESOURCES_TEMPLATE 44 | const result = await parseSetup(content, content.length - 1) 45 | 46 | expect(result.items).not.toHaveLength(0) 47 | expect(result).toMatchSnapshot() 48 | }) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /packages/language-server/src/language-service/services/completion/constants.ts: -------------------------------------------------------------------------------- 1 | export const RUNTIMES = [ 2 | "nodejs20.x", 3 | "nodejs18.x", 4 | "nodejs16.x", 5 | "nodejs14.x", 6 | "nodejs12.x", 7 | "nodejs10.x", 8 | "nodejs8.10", 9 | "python3.11", 10 | "python3.10", 11 | "python3.9", 12 | "python3.8", 13 | "python3.6", 14 | "python3.7", 15 | "python2.7", 16 | "ruby2.7", 17 | "java11", 18 | "java8", 19 | "java8.al2", 20 | "go1.x", 21 | "dotnetcore3.1", 22 | "dotnet6", 23 | "provided", 24 | "provided.al2" 25 | ] 26 | -------------------------------------------------------------------------------- /packages/language-server/src/language-service/services/completion/custom-tags.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CUSTOM_TAGS, 3 | CustomTag, 4 | Referenceables, 5 | ReferenceType 6 | } from "@serverless-ide/config" 7 | import { 8 | CompletionItemKind, 9 | InsertTextFormat 10 | } from "vscode-languageserver-types" 11 | 12 | import { CompletionsCollector } from "./../../jsonContributions" 13 | 14 | export const getCustomTagValueCompletions = ( 15 | collector: CompletionsCollector, 16 | referenceables: Referenceables 17 | ) => { 18 | CUSTOM_TAGS.forEach(customTag => { 19 | addCustomTagValueCompletion(collector, customTag, referenceables) 20 | }) 21 | } 22 | 23 | const addReferenceablesOptions = ( 24 | customTag: CustomTag, 25 | referenceables: Referenceables 26 | ): string => { 27 | const options = customTag.referenceEntityTypes.reduce( 28 | (memo, entityType) => { 29 | return memo.concat(referenceables.hash[entityType].keys()) 30 | }, 31 | [] 32 | ) 33 | 34 | return "|" + options.sort().join(",") + "|" 35 | } 36 | 37 | export const addCustomTagValueCompletion = ( 38 | collector: CompletionsCollector, 39 | customTag: CustomTag, 40 | referenceables: Referenceables 41 | ): void => { 42 | if (customTag.type) { 43 | switch (customTag.type) { 44 | case ReferenceType.FIND_IN_MAP: { 45 | const options = addReferenceablesOptions( 46 | customTag, 47 | referenceables 48 | ) 49 | 50 | const value = `[ \${1${options}\}, $2, $3 ]` 51 | 52 | collector.add({ 53 | kind: CompletionItemKind.Value, 54 | label: customTag.tag, 55 | insertText: `${customTag.tag} ${value}`, 56 | insertTextFormat: InsertTextFormat.Snippet 57 | }) 58 | 59 | collector.add({ 60 | kind: CompletionItemKind.Property, 61 | label: customTag.propertyName, 62 | insertText: `${customTag.propertyName}: ${value}`, 63 | insertTextFormat: InsertTextFormat.Snippet 64 | }) 65 | } 66 | case ReferenceType.SUB: { 67 | collector.add({ 68 | kind: CompletionItemKind.Value, 69 | label: customTag.tag, 70 | insertText: `${customTag.tag} $1`, 71 | insertTextFormat: InsertTextFormat.Snippet 72 | }) 73 | 74 | collector.add({ 75 | kind: CompletionItemKind.Property, 76 | label: customTag.propertyName, 77 | insertText: `${customTag.propertyName}: $1`, 78 | insertTextFormat: InsertTextFormat.Snippet 79 | }) 80 | } 81 | // TODO: add support for !GetAtt attributes 82 | case ReferenceType.GET_ATT: 83 | case ReferenceType.REF: 84 | case ReferenceType.CONDITION: 85 | case ReferenceType.DEPENDS_ON: { 86 | const options = addReferenceablesOptions( 87 | customTag, 88 | referenceables 89 | ) 90 | const value = `\${1${options}\}` 91 | 92 | collector.add({ 93 | kind: CompletionItemKind.Value, 94 | label: customTag.tag, 95 | insertText: `${customTag.tag} ${value}`, 96 | insertTextFormat: InsertTextFormat.Snippet 97 | }) 98 | 99 | collector.add({ 100 | kind: CompletionItemKind.Property, 101 | label: customTag.propertyName, 102 | insertText: `${customTag.propertyName}: ${value}`, 103 | insertTextFormat: InsertTextFormat.Snippet 104 | }) 105 | } 106 | } 107 | } else { 108 | collector.add({ 109 | kind: CompletionItemKind.Value, 110 | label: customTag.tag, 111 | insertText: customTag.tag + " ", 112 | insertTextFormat: InsertTextFormat.Snippet, 113 | documentation: customTag.description 114 | }) 115 | 116 | collector.add({ 117 | kind: CompletionItemKind.Property, 118 | label: customTag.propertyName, 119 | insertText: "", 120 | insertTextFormat: InsertTextFormat.Snippet, 121 | documentation: customTag.description 122 | }) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /packages/language-server/src/language-service/services/completion/defaultPropertyCompletions.ts: -------------------------------------------------------------------------------- 1 | import { ASTNode } from "@serverless-ide/config" 2 | import { 3 | CompletionItemKind, 4 | InsertTextFormat 5 | } from "vscode-languageserver-types" 6 | 7 | import { CompletionsCollector } from "./../../jsonContributions" 8 | import { RUNTIMES } from "./constants" 9 | 10 | export const getDefaultPropertyCompletions = ( 11 | node: ASTNode, 12 | collector: CompletionsCollector 13 | ) => { 14 | if (node) { 15 | switch (node.location) { 16 | case "Runtime": { 17 | RUNTIMES.forEach(runtime => { 18 | collector.add({ 19 | kind: CompletionItemKind.EnumMember, 20 | label: runtime, 21 | insertText: runtime, 22 | insertTextFormat: InsertTextFormat.PlainText, 23 | documentation: "" 24 | }) 25 | }) 26 | } 27 | default: 28 | return 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/language-server/src/language-service/services/completion/helpers.ts: -------------------------------------------------------------------------------- 1 | import { ASTNode } from "@serverless-ide/config" 2 | import * as JSONParser from "jsonc-parser" 3 | import { CompletionItemKind, TextDocument } from "vscode-languageserver-types" 4 | 5 | export const getLabelForValue = (value: any): string => { 6 | const label = typeof value === "string" ? value : JSON.stringify(value) 7 | if (label.length > 57) { 8 | return label.substr(0, 57).trim() + "..." 9 | } 10 | return label 11 | } 12 | 13 | export const getSuggestionKind = (type: any): CompletionItemKind => { 14 | if (Array.isArray(type)) { 15 | const array = type as any[] 16 | type = array.length > 0 ? array[0] : null 17 | } 18 | if (!type) { 19 | return CompletionItemKind.Value 20 | } 21 | switch (type) { 22 | case "string": 23 | return CompletionItemKind.Value 24 | case "object": 25 | return CompletionItemKind.Module 26 | case "property": 27 | return CompletionItemKind.Property 28 | default: 29 | return CompletionItemKind.Value 30 | } 31 | } 32 | 33 | export const getCurrentWord = (document: TextDocument, offset: number) => { 34 | let i = offset - 1 35 | const text = document.getText() 36 | while (i >= 0 && ' \t\n\r\v":{[,]}'.indexOf(text.charAt(i)) === -1) { 37 | i-- 38 | } 39 | return text.substring(i + 1, offset) 40 | } 41 | 42 | export const findItemAtOffset = ( 43 | node: ASTNode, 44 | document: TextDocument, 45 | offset: number 46 | ) => { 47 | const scanner = JSONParser.createScanner(document.getText(), true) 48 | const children = node.getChildNodes() 49 | for (let i = children.length - 1; i >= 0; i--) { 50 | const child = children[i] 51 | if (offset > child.end) { 52 | scanner.setPosition(child.end) 53 | const token = scanner.scan() 54 | if ( 55 | token === JSONParser.SyntaxKind.CommaToken && 56 | offset >= scanner.getTokenOffset() + scanner.getTokenLength() 57 | ) { 58 | return i + 1 59 | } 60 | return i 61 | } else if (offset >= child.start) { 62 | return i 63 | } 64 | } 65 | return 0 66 | } 67 | 68 | export const isInComment = ( 69 | document: TextDocument, 70 | start: number, 71 | offset: number 72 | ) => { 73 | const scanner = JSONParser.createScanner(document.getText(), false) 74 | scanner.setPosition(start) 75 | let token = scanner.scan() 76 | while ( 77 | token !== JSONParser.SyntaxKind.EOF && 78 | scanner.getTokenOffset() + scanner.getTokenLength() < offset 79 | ) { 80 | token = scanner.scan() 81 | } 82 | return ( 83 | (token === JSONParser.SyntaxKind.LineCommentTrivia || 84 | token === JSONParser.SyntaxKind.BlockCommentTrivia) && 85 | scanner.getTokenOffset() <= offset 86 | ) 87 | } 88 | 89 | export const evaluateSeparatorAfter = ( 90 | document: TextDocument, 91 | offset: number 92 | ) => { 93 | const scanner = JSONParser.createScanner(document.getText(), true) 94 | scanner.setPosition(offset) 95 | const token = scanner.scan() 96 | switch (token) { 97 | case JSONParser.SyntaxKind.CommaToken: 98 | case JSONParser.SyntaxKind.CloseBraceToken: 99 | case JSONParser.SyntaxKind.CloseBracketToken: 100 | case JSONParser.SyntaxKind.EOF: 101 | return "" 102 | default: 103 | return "" 104 | } 105 | } 106 | 107 | export const isInArray = (document: TextDocument, node: ASTNode): boolean => { 108 | if (node.parent && node.parent.type === "array") { 109 | const nodePosition = document.positionAt(node.start) 110 | const arrayPosition = document.positionAt(node.start) 111 | 112 | return nodePosition.line === arrayPosition.line 113 | } 114 | 115 | return false 116 | } 117 | -------------------------------------------------------------------------------- /packages/language-server/src/language-service/services/completion/pattern-properties.ts: -------------------------------------------------------------------------------- 1 | import { JSONSchema } from "@serverless-ide/config" 2 | import { last, map } from "lodash" 3 | import { 4 | CompletionItemKind, 5 | InsertTextFormat, 6 | MarkupKind 7 | } from "vscode-languageserver-types" 8 | 9 | import { CompletionsCollector } from "../../jsonContributions" 10 | import { sendException } from "../analytics" 11 | import documentationService from "../documentation" 12 | import { getInsertTextForObject } from "./text" 13 | 14 | const getResourceType = (schema: JSONSchema): string | null => { 15 | if (schema.properties && schema.properties.Type) { 16 | const typeEnum = schema.properties.Type 17 | 18 | if (typeEnum.enum && typeEnum.enum.length === 1) { 19 | return typeEnum.enum[0] 20 | } 21 | } 22 | 23 | return null 24 | } 25 | 26 | const getResourceKey = (resourceType: string): string => { 27 | const parts = resourceType.split("::") 28 | 29 | return last(parts) 30 | } 31 | 32 | export const addPatternPropertiesCompletions = async ( 33 | schema: JSONSchema, 34 | collector: CompletionsCollector, 35 | separatorAfter: string, 36 | indent = "\t" 37 | ) => { 38 | await Promise.all( 39 | map(schema, async (propertiesSchema: JSONSchema) => { 40 | if (propertiesSchema.anyOf) { 41 | await Promise.all( 42 | propertiesSchema.anyOf.map(async propertySchema => { 43 | const resourceType = getResourceType(propertySchema) 44 | 45 | if (resourceType) { 46 | const insertText = getInsertTextForObject( 47 | propertySchema, 48 | separatorAfter, 49 | indent, 50 | 2 51 | ) 52 | 53 | const key = getResourceKey(resourceType) 54 | const text = `\${1:${key}}:\n${indent}${insertText.insertText.trimLeft()}` 55 | let docs: string | void = "" 56 | try { 57 | docs = await documentationService.getResourceDocumentation( 58 | resourceType 59 | ) 60 | } catch (err) { 61 | sendException(err) 62 | } 63 | 64 | collector.add({ 65 | kind: CompletionItemKind.Snippet, 66 | label: resourceType, 67 | insertText: text, 68 | insertTextFormat: InsertTextFormat.Snippet, 69 | documentation: { 70 | kind: MarkupKind.Markdown, 71 | value: docs || "" 72 | } 73 | }) 74 | } 75 | }) 76 | ) 77 | } 78 | }) 79 | ) 80 | } 81 | -------------------------------------------------------------------------------- /packages/language-server/src/language-service/services/definition/index.ts: -------------------------------------------------------------------------------- 1 | import { CustomTag } from "@serverless-ide/config" 2 | import { ASTNode } from "@serverless-ide/config" 3 | import { 4 | collectReferencesFromStringNode, 5 | PropertyASTNode, 6 | StringASTNode, 7 | YAMLDocument 8 | } from "@serverless-ide/config" 9 | import { 10 | Definition, 11 | Location, 12 | Range, 13 | TextDocument, 14 | TextDocumentPositionParams 15 | } from "vscode-languageserver" 16 | 17 | export const getDefinition = ( 18 | documentPosition: TextDocumentPositionParams, 19 | document: TextDocument, 20 | yamlDocument: YAMLDocument 21 | ): Definition => { 22 | const node = yamlDocument.getNodeFromOffset( 23 | document.offsetAt(documentPosition.position) 24 | ) 25 | let definitionNode: ASTNode | void = undefined 26 | let customTag: CustomTag | void = undefined 27 | 28 | if (node instanceof StringASTNode) { 29 | if (node.customTag && node.customTag.type) { 30 | customTag = node.customTag 31 | } else if ( 32 | node.parent instanceof PropertyASTNode && 33 | node.parent.customTag 34 | ) { 35 | customTag = node.parent.customTag 36 | } 37 | 38 | if (customTag) { 39 | const references = collectReferencesFromStringNode(node, customTag) 40 | 41 | for (let reference of references) { 42 | for (let entityType of customTag.referenceEntityTypes) { 43 | const referenceable = yamlDocument.referenceables.hash[ 44 | entityType 45 | ].get(reference.key) 46 | 47 | if (referenceable) { 48 | definitionNode = referenceable.node 49 | break 50 | } 51 | } 52 | 53 | if (definitionNode) { 54 | break 55 | } 56 | } 57 | } 58 | } 59 | 60 | if (definitionNode) { 61 | return Location.create( 62 | documentPosition.textDocument.uri, 63 | Range.create( 64 | document.positionAt(definitionNode.start), 65 | document.positionAt(definitionNode.end) 66 | ) 67 | ) 68 | } 69 | 70 | return [] 71 | } 72 | -------------------------------------------------------------------------------- /packages/language-server/src/language-service/services/documentSymbols/__tests__/documentSymbols.test.ts: -------------------------------------------------------------------------------- 1 | import { parse as parseYAML } from "@serverless-ide/config" 2 | import { TextDocument } from "vscode-languageserver" 3 | 4 | import { findDocumentSymbols } from "../" 5 | 6 | describe("Document Symbols Tests", () => { 7 | function setup(content: string) { 8 | return TextDocument.create( 9 | "file://~/Desktop/vscode-k8s/test.yaml", 10 | "yaml", 11 | 0, 12 | content 13 | ) 14 | } 15 | 16 | function parseSetup(content: string) { 17 | const testTextDocument = setup(content) 18 | const jsonDocument = parseYAML(testTextDocument) 19 | return findDocumentSymbols(testTextDocument, jsonDocument) 20 | } 21 | 22 | it("Document is empty", done => { 23 | const content = "" 24 | const symbols = parseSetup(content) 25 | expect(symbols).toHaveLength(0) 26 | done() 27 | }) 28 | 29 | it("Simple document symbols", done => { 30 | const content = "cwd: test" 31 | const symbols = parseSetup(content) 32 | expect(symbols).toHaveLength(1) 33 | done() 34 | }) 35 | 36 | it("Document Symbols with number", done => { 37 | const content = "node1: 10000" 38 | const symbols = parseSetup(content) 39 | expect(symbols).toHaveLength(1) 40 | done() 41 | }) 42 | 43 | it("Document Symbols with boolean", done => { 44 | const content = "node1: False" 45 | const symbols = parseSetup(content) 46 | expect(symbols).toHaveLength(1) 47 | done() 48 | }) 49 | 50 | it("Document Symbols with object", done => { 51 | const content = "scripts:\n node1: test\n node2: test" 52 | const symbols = parseSetup(content) 53 | expect(symbols).toHaveLength(1) 54 | done() 55 | }) 56 | 57 | it("Document Symbols with null", done => { 58 | const content = "apiVersion: null" 59 | const symbols = parseSetup(content) 60 | expect(symbols).toHaveLength(1) 61 | done() 62 | }) 63 | 64 | it("Document Symbols with array of strings", done => { 65 | const content = "items:\n - test\n - test" 66 | const symbols = parseSetup(content) 67 | expect(symbols).toHaveLength(1) 68 | done() 69 | }) 70 | 71 | it("Document Symbols with array", done => { 72 | const content = "authors:\n - name: Josh\n - email: jp" 73 | const symbols = parseSetup(content) 74 | expect(symbols).toHaveLength(1) 75 | done() 76 | }) 77 | 78 | it("Document Symbols with object and array", done => { 79 | const content = 80 | "scripts:\n node1: test\n node2: test\nauthors:\n - name: Josh\n - email: jp" 81 | const symbols = parseSetup(content) 82 | expect(symbols).toHaveLength(2) 83 | done() 84 | }) 85 | }) 86 | -------------------------------------------------------------------------------- /packages/language-server/src/language-service/services/documentSymbols/index.ts: -------------------------------------------------------------------------------- 1 | import * as Parser from "@serverless-ide/config" 2 | import { 3 | DocumentSymbol, 4 | Range, 5 | SymbolKind, 6 | TextDocument 7 | } from "vscode-languageserver-types" 8 | 9 | const filterChildren = ( 10 | children: (DocumentSymbol | void)[] 11 | ): DocumentSymbol[] => { 12 | return children.filter(Boolean) as DocumentSymbol[] 13 | } 14 | 15 | export const findDocumentSymbols = ( 16 | document: TextDocument, 17 | yamlDocument: Parser.YAMLDocument 18 | ): DocumentSymbol[] => { 19 | const { root } = yamlDocument 20 | 21 | if (root instanceof Parser.ObjectASTNode) { 22 | return filterChildren( 23 | root.getChildNodes().map((childNode: Parser.PropertyASTNode) => { 24 | return collectOutlineEntries(document, childNode) 25 | }) 26 | ) 27 | } 28 | 29 | return [] 30 | } 31 | 32 | const collectOutlineEntries = ( 33 | document: TextDocument, 34 | node: Parser.ASTNode 35 | ): DocumentSymbol | void => { 36 | const range = Range.create( 37 | document.positionAt(node.start), 38 | document.positionAt(node.end) 39 | ) 40 | 41 | if (node instanceof Parser.ArrayASTNode) { 42 | return DocumentSymbol.create( 43 | node.location.toString(), 44 | undefined, 45 | SymbolKind.Array, 46 | range, 47 | range, 48 | filterChildren( 49 | node.getChildNodes().map(itemNode => { 50 | return collectOutlineEntries(document, itemNode) 51 | }) 52 | ) 53 | ) 54 | } else if (node instanceof Parser.ObjectASTNode) { 55 | return DocumentSymbol.create( 56 | node.location.toString(), 57 | undefined, 58 | SymbolKind.Module, 59 | range, 60 | range, 61 | filterChildren( 62 | node.getChildNodes().map(childNode => { 63 | return collectOutlineEntries(document, childNode) 64 | }) 65 | ) 66 | ) 67 | } else if (node instanceof Parser.PropertyASTNode) { 68 | return collectOutlineEntries(document, node.value) 69 | } else if (node instanceof Parser.BooleanASTNode) { 70 | return DocumentSymbol.create( 71 | node.location.toString(), 72 | undefined, 73 | SymbolKind.Boolean, 74 | range, 75 | range 76 | ) 77 | } else if (node instanceof Parser.NumberASTNode) { 78 | return DocumentSymbol.create( 79 | node.location.toString(), 80 | undefined, 81 | SymbolKind.Number, 82 | range, 83 | range 84 | ) 85 | } else if (node instanceof Parser.NullASTNode) { 86 | return DocumentSymbol.create( 87 | node.location.toString(), 88 | undefined, 89 | SymbolKind.Null, 90 | range, 91 | range 92 | ) 93 | } else if (node instanceof Parser.StringASTNode) { 94 | return DocumentSymbol.create( 95 | node.location.toString(), 96 | undefined, 97 | SymbolKind.String, 98 | range, 99 | range 100 | ) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /packages/language-server/src/language-service/services/documentation/__tests__/documentation.test.ts: -------------------------------------------------------------------------------- 1 | import map = require("lodash/map") 2 | import { DocumentationService } from "./../index" 3 | import { Specification } from "./../model" 4 | 5 | const docUrl = 6 | "https://d1uauaxba7bl26.cloudfront.net/latest/gzip/CloudFormationResourceSpecification.json" 7 | 8 | describe("DocumentationService", () => { 9 | describe("e2e", () => { 10 | let service: DocumentationService = void 0 11 | 12 | beforeEach(() => { 13 | service = new DocumentationService(docUrl) 14 | }) 15 | 16 | test("should return undefined if documentation source is not found", async () => { 17 | const brokenService = new DocumentationService("unknown") 18 | 19 | const resourceDoc = await brokenService.getResourceDocumentation( 20 | "AWS::DynamoDB::Table" 21 | ) 22 | const propertyDoc = await brokenService.getPropertyDocumentation( 23 | "AWS::DynamoDB::Table", 24 | "KeySchema" 25 | ) 26 | 27 | expect(resourceDoc).toBeUndefined() 28 | expect(propertyDoc).toBeUndefined() 29 | }) 30 | 31 | test("return dynamodb resource documentation", async () => { 32 | const docs = await service.getResourceDocumentation( 33 | "AWS::DynamoDB::Table" 34 | ) 35 | 36 | expect(docs).toMatchSnapshot() 37 | }) 38 | 39 | test("should return undefined for non-existing resource", async () => { 40 | const doc = await service.getResourceDocumentation("unknown") 41 | 42 | expect(doc).toBeUndefined() 43 | }) 44 | 45 | test("should return dynamodb resource documentation", async () => { 46 | const docs = await service.getPropertyDocumentation( 47 | "AWS::DynamoDB::Table", 48 | "KeySchema" 49 | ) 50 | 51 | expect(docs).toMatchSnapshot() 52 | }) 53 | 54 | test("should return undefined if property documentation is not found", async () => { 55 | const docs1 = await service.getPropertyDocumentation( 56 | "unknown", 57 | "KeySchema" 58 | ) 59 | 60 | const docs2 = await service.getPropertyDocumentation( 61 | "AWS::DynamoDB::Table", 62 | "unknown" 63 | ) 64 | 65 | expect(docs1).toBeUndefined() 66 | expect(docs2).toBeUndefined() 67 | }) 68 | 69 | test("should return docs for sam resources", async () => { 70 | const docs = await service.getResourceDocumentation( 71 | "AWS::Serverless::Function" 72 | ) 73 | 74 | expect(docs).toMatchSnapshot() 75 | }) 76 | }) 77 | 78 | describe("smoke tests", () => { 79 | let service: DocumentationService = void 0 80 | let specifications: Specification = void 0 81 | 82 | beforeEach(async () => { 83 | service = new DocumentationService(docUrl) 84 | specifications = await service.sourcePromise 85 | }) 86 | 87 | test("should contain resources and properties", () => { 88 | expect( 89 | Object.keys(specifications.ResourceTypes).length 90 | ).toBeGreaterThan(0) 91 | expect( 92 | Object.keys(specifications.PropertyTypes).length 93 | ).toBeGreaterThan(0) 94 | }) 95 | 96 | test("should return documentation for resources", async () => { 97 | await Promise.all( 98 | map( 99 | specifications.ResourceTypes, 100 | async (resource, resourceType) => { 101 | const doc = await service.getResourceDocumentation( 102 | resourceType 103 | ) 104 | 105 | expect(doc).toBeDefined() 106 | 107 | await Promise.all( 108 | map( 109 | resource.Properties, 110 | async (property, propertyName) => { 111 | const propertyDoc = await service.getPropertyDocumentation( 112 | resourceType, 113 | propertyName 114 | ) 115 | 116 | expect(propertyDoc).toBeDefined() 117 | } 118 | ) 119 | ) 120 | } 121 | ) 122 | ) 123 | }) 124 | }) 125 | }) 126 | -------------------------------------------------------------------------------- /packages/language-server/src/language-service/services/documentation/model.ts: -------------------------------------------------------------------------------- 1 | export const LIST_TYPE: "List" = "List" 2 | export const MAP_TYPE: "Map" = "Map" 3 | export const STRING_TYPE = "String" 4 | export const INTEGER_TYPE = "Integer" 5 | export const BOOLEAN_TYPE = "Boolean" 6 | 7 | export type PrimitiveType = 8 | | typeof STRING_TYPE 9 | | typeof INTEGER_TYPE 10 | | typeof BOOLEAN_TYPE 11 | 12 | export interface ItemType { 13 | Type?: typeof LIST_TYPE | typeof MAP_TYPE | string 14 | ItemType?: string 15 | PrimitiveType?: PrimitiveType 16 | PrimitiveItemType?: typeof STRING_TYPE | typeof INTEGER_TYPE 17 | } 18 | 19 | export type Property = { 20 | Documentation: string 21 | Required: boolean 22 | UpdateType?: "Mutable" | "Immutable" | "Conditional" 23 | DuplicatesAllowed?: boolean 24 | } & ItemType 25 | 26 | export type Attribute = ItemType 27 | 28 | export interface PropertySpecification { 29 | Documentation: string 30 | Properties: { 31 | [key: string]: Property 32 | } 33 | } 34 | 35 | export interface ResourceSpecification { 36 | Documentation: string 37 | Attributes: { 38 | [key: string]: Attribute 39 | } 40 | Properties: { 41 | [key: string]: Property 42 | } 43 | } 44 | 45 | export interface Specification { 46 | PropertyTypes: { 47 | [key: string]: PropertySpecification 48 | } 49 | ResourceTypes: { 50 | [key: string]: ResourceSpecification 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/language-server/src/language-service/services/hover/index.ts: -------------------------------------------------------------------------------- 1 | import * as Parser from "@serverless-ide/config" 2 | import { 3 | Hover, 4 | MarkedString, 5 | Position, 6 | Range, 7 | TextDocument 8 | } from "vscode-languageserver-types" 9 | 10 | import documentationService, { DocumentationService } from "../documentation" 11 | import * as SchemaService from "../jsonSchema" 12 | import { LanguageSettings } from "./../../model/settings" 13 | 14 | const createHover = (contents: MarkedString[], hoverRange: Range): Hover => { 15 | const result: Hover = { 16 | contents, 17 | range: hoverRange 18 | } 19 | return result 20 | } 21 | 22 | export class YAMLHover { 23 | private schemaService: SchemaService.JSONSchemaService 24 | private shouldHover: boolean 25 | private documentationService: DocumentationService = documentationService 26 | 27 | constructor(schemaService: SchemaService.JSONSchemaService) { 28 | this.schemaService = schemaService 29 | this.shouldHover = true 30 | } 31 | 32 | configure(languageSettings: LanguageSettings) { 33 | if (languageSettings) { 34 | this.shouldHover = languageSettings.hover 35 | } 36 | } 37 | 38 | async doHover( 39 | document: TextDocument, 40 | position: Position, 41 | doc: Parser.YAMLDocument 42 | ): Promise { 43 | const contents = [] 44 | if (!this.shouldHover || !document) { 45 | return Promise.resolve(void 0) 46 | } 47 | 48 | const offset = document.offsetAt(position) 49 | const schema = await this.schemaService.getSchemaForDocument( 50 | document, 51 | doc 52 | ) 53 | 54 | if (!schema) { 55 | return 56 | } 57 | const node = doc.getNodeFromOffset(offset) 58 | 59 | if ( 60 | !node || 61 | ((node.type === "object" || node.type === "array") && 62 | offset > node.start + 1 && 63 | offset < node.end - 1) 64 | ) { 65 | return Promise.resolve(void 0) 66 | } 67 | const hoverRangeNode = node 68 | const hoverRange = Range.create( 69 | document.positionAt(hoverRangeNode.start), 70 | document.positionAt(hoverRangeNode.end) 71 | ) 72 | const path = node.getPath() 73 | 74 | if (path.length > 1) { 75 | const description = schema.getLastDescription( 76 | node.getPath().map(String) 77 | ) 78 | 79 | if (description) { 80 | contents.push(description) 81 | } 82 | } 83 | 84 | if (node.getPath().length >= 2) { 85 | const resourceType = Parser.getResourceName(node) 86 | const propertyName = this.getPropertyName(node) 87 | 88 | if (resourceType) { 89 | let markdown: string | void 90 | 91 | if (propertyName) { 92 | markdown = await this.documentationService.getPropertyDocumentation( 93 | resourceType, 94 | String(propertyName) 95 | ) 96 | } else { 97 | markdown = await this.documentationService.getResourceDocumentation( 98 | resourceType 99 | ) 100 | } 101 | 102 | if (markdown) { 103 | contents.push(markdown) 104 | } 105 | } 106 | } 107 | 108 | if (contents.length > 0) { 109 | return createHover(contents, hoverRange) 110 | } 111 | 112 | return void 0 113 | } 114 | 115 | private getPropertyName(node: Parser.ASTNode): string | void { 116 | const path = Parser.getRelativeNodePath(node) 117 | 118 | if ( 119 | path.length >= 4 && 120 | path[0] === "Resources" && 121 | path[2] === "Properties" 122 | ) { 123 | return path[3] as string 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /packages/language-server/src/language-service/services/jsonSchema/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { getDocumentType, YAMLDocument } from "@serverless-ide/config" 2 | import { TextDocument } from "vscode-languageserver" 3 | 4 | import { JSONSchemaService } from "./../index" 5 | 6 | const slsTemplate = ` 7 | service: 8 | name: my-service 9 | provider: 10 | name: aws 11 | Resources: 12 | Table: 13 | Type: AWS::DynamoDB::Table 14 | ` 15 | 16 | const cfnTemplate = ` 17 | Resources: 18 | DomainName: 19 | Type: AWS::ApiGateway::DomainName 20 | Properties: 21 | DomainName: blah.example.com 22 | ` 23 | 24 | const samTemplate = ` 25 | AWSTemplateFormatVersion: 2010-09-09 26 | Transform: AWS::Serverless-2016-10-31 27 | Globals: 28 | Function: 29 | Runtime: nodejs8.10 30 | Resources: 31 | Function1: 32 | Type: AWS::Serverless::Function 33 | Properties: 34 | CodeUri: . 35 | Handler: index.handler 36 | ` 37 | 38 | test("should resolve CloudFormation schema", async () => { 39 | const service = new JSONSchemaService() 40 | const document = TextDocument.create("cfn", "yaml", 1, cfnTemplate) 41 | 42 | const schema = await service.getSchemaForDocument( 43 | document, 44 | new YAMLDocument("", getDocumentType(document.getText())) 45 | ) 46 | 47 | expect(schema).toBeDefined() 48 | 49 | if (schema) { 50 | expect(schema.schema).toBeDefined() 51 | expect(schema.errors).toHaveLength(0) 52 | } 53 | }) 54 | 55 | test("should resolve SAM schema", async () => { 56 | const service = new JSONSchemaService() 57 | const document = TextDocument.create("sam", "yaml", 1, samTemplate) 58 | 59 | const schema = await service.getSchemaForDocument( 60 | document, 61 | new YAMLDocument("", getDocumentType(document.getText())) 62 | ) 63 | 64 | expect(schema).toBeDefined() 65 | 66 | if (schema) { 67 | expect(schema.schema).toBeDefined() 68 | expect(schema.errors).toHaveLength(0) 69 | } 70 | }) 71 | 72 | test("should return Serverless schema for sls document", async () => { 73 | const service = new JSONSchemaService() 74 | const document = TextDocument.create("sls", "yaml", 1, slsTemplate) 75 | 76 | const schema = await service.getSchemaForDocument( 77 | document, 78 | new YAMLDocument("", getDocumentType(document.getText())) 79 | ) 80 | 81 | if (schema) { 82 | expect(schema).toBeDefined() 83 | expect(schema.errors).toHaveLength(0) 84 | } 85 | }) 86 | -------------------------------------------------------------------------------- /packages/language-server/src/language-service/services/jsonSchema/mutation/index.ts: -------------------------------------------------------------------------------- 1 | import { DocumentType, isEmpty } from "@serverless-ide/config" 2 | import { YAMLDocument } from "@serverless-ide/config" 3 | 4 | import { ResolvedSchema } from ".." 5 | import { applyGlobalsConfigMutations } from "./sam" 6 | import { applyProviderMutations } from "./serverless" 7 | 8 | export const applyDocumentMutations = ( 9 | schema: ResolvedSchema | void, 10 | yamlDocument: YAMLDocument 11 | ): ResolvedSchema | void => { 12 | // early exit, if schema is not defined 13 | if (!schema) { 14 | return schema 15 | } 16 | 17 | if (yamlDocument.documentType === DocumentType.SAM) { 18 | const { globalsConfig } = yamlDocument 19 | 20 | if (isEmpty(globalsConfig)) { 21 | return schema 22 | } 23 | 24 | return applyGlobalsConfigMutations(schema, globalsConfig) 25 | } 26 | 27 | if (yamlDocument.documentType === DocumentType.SERVERLESS_FRAMEWORK) { 28 | return applyProviderMutations(schema, yamlDocument) 29 | } 30 | 31 | return schema 32 | } 33 | -------------------------------------------------------------------------------- /packages/language-server/src/language-service/services/jsonSchema/mutation/sam.ts: -------------------------------------------------------------------------------- 1 | import cloneDeep = require("lodash/cloneDeep") 2 | import toArray = require("lodash/toArray") 3 | import without = require("lodash/without") 4 | import { GlobalsConfig } from "@serverless-ide/config" 5 | 6 | import { ResolvedSchema } from ".." 7 | import { sendException } from "../../analytics" 8 | 9 | export const applyGlobalsConfigMutations = ( 10 | schema: ResolvedSchema, 11 | globalsConfig: GlobalsConfig 12 | ): ResolvedSchema => { 13 | const jsonSchema = schema.schema 14 | const globalsItems = toArray(globalsConfig) 15 | 16 | if ( 17 | jsonSchema.definitions && 18 | globalsItems.some(item => { 19 | return Boolean( 20 | jsonSchema.definitions[item.resourceType] && 21 | item.properties.length > 0 22 | ) 23 | }) 24 | ) { 25 | const clonedSchema = cloneDeep(jsonSchema) 26 | 27 | try { 28 | globalsItems.forEach(item => { 29 | const resourceDefinition = 30 | clonedSchema.definitions[item.resourceType] 31 | 32 | if (!resourceDefinition) { 33 | return 34 | } 35 | 36 | const originalRequired = 37 | resourceDefinition.properties.Properties.required 38 | 39 | resourceDefinition.properties.Properties.required = without( 40 | originalRequired, 41 | ...(item.properties as string[]) 42 | ) 43 | }) 44 | } catch (err) { 45 | sendException(err) 46 | return schema 47 | } 48 | 49 | return new ResolvedSchema(clonedSchema, schema.errors) 50 | } 51 | 52 | return schema 53 | } 54 | -------------------------------------------------------------------------------- /packages/language-server/src/language-service/services/jsonSchema/mutation/serverless.ts: -------------------------------------------------------------------------------- 1 | import cloneDeep = require("lodash/cloneDeep") 2 | import without = require("lodash/without") 3 | import { DocumentType, JSONSchema, YAMLDocument } from "@serverless-ide/config" 4 | 5 | import { ResolvedSchema } from ".." 6 | import { sendException } from "../../analytics" 7 | 8 | const updateFunctionProperties = ( 9 | functionProperties: JSONSchema 10 | ): JSONSchema => { 11 | functionProperties.required = without( 12 | functionProperties.required, 13 | "runtime" 14 | ) 15 | 16 | return functionProperties 17 | } 18 | 19 | export const applyProviderMutations = ( 20 | schema: ResolvedSchema, 21 | yamlDocument: YAMLDocument 22 | ): ResolvedSchema => { 23 | const jsonSchema = schema.schema 24 | 25 | if ( 26 | yamlDocument.documentType === DocumentType.SERVERLESS_FRAMEWORK && 27 | yamlDocument.root 28 | ) { 29 | const clonedSchema = cloneDeep(jsonSchema) 30 | const runtimeNode = yamlDocument.root.get(["provider", "runtime"]) 31 | 32 | if (runtimeNode) { 33 | try { 34 | updateFunctionProperties( 35 | clonedSchema.properties.functions.oneOf[0] 36 | .patternProperties["^[a-zA-Z0-9]+$"] 37 | ) 38 | updateFunctionProperties( 39 | clonedSchema.properties.functions.oneOf[1].items 40 | .patternProperties["^[a-zA-Z0-9]+$"] 41 | ) 42 | } catch (err) { 43 | sendException(err) 44 | return schema 45 | } 46 | } 47 | 48 | return new ResolvedSchema(clonedSchema, schema.errors) 49 | } 50 | 51 | return schema 52 | } 53 | -------------------------------------------------------------------------------- /packages/language-server/src/language-service/services/links/index.ts: -------------------------------------------------------------------------------- 1 | import * as Parser from "@serverless-ide/config" 2 | import { DocumentLink, Range, TextDocument } from "vscode-languageserver-types" 3 | 4 | export const findDocumentLinks = ( 5 | document: TextDocument, 6 | yamlDocument: Parser.YAMLDocument 7 | ): DocumentLink[] => { 8 | const { root } = yamlDocument 9 | const links: DocumentLink[] = [] 10 | 11 | const collectLinks = (node: Parser.ASTNode) => { 12 | if (node instanceof Parser.ExternalImportASTNode) { 13 | const uri = node.getUri() 14 | 15 | if (uri) { 16 | links.push( 17 | DocumentLink.create( 18 | Range.create( 19 | document.positionAt(node.start), 20 | document.positionAt(node.end) 21 | ), 22 | uri 23 | ) 24 | ) 25 | } 26 | } else { 27 | node.getChildNodes().forEach(collectLinks) 28 | } 29 | } 30 | 31 | if (root) { 32 | collectLinks(root) 33 | } 34 | 35 | return links 36 | } 37 | -------------------------------------------------------------------------------- /packages/language-server/src/language-service/services/reference/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CUSTOM_TAGS_BY_TYPE, 3 | PropertyASTNode, 4 | YAMLDocument 5 | } from "@serverless-ide/config" 6 | import { 7 | Location, 8 | Range, 9 | TextDocument, 10 | TextDocumentPositionParams 11 | } from "vscode-languageserver" 12 | 13 | export const getReferences = ( 14 | documentPosition: TextDocumentPositionParams, 15 | document: TextDocument, 16 | yamlDocument: YAMLDocument 17 | ): Location[] => { 18 | const locations: Location[] = [] 19 | 20 | const node = yamlDocument.getNodeFromOffset( 21 | document.offsetAt(documentPosition.position) 22 | ) 23 | 24 | if (node && node.parent instanceof PropertyASTNode) { 25 | const referenceable = yamlDocument.referenceables.lookup.get( 26 | node.parent 27 | ) 28 | 29 | if (referenceable) { 30 | const references = yamlDocument.references.hash[referenceable.id] 31 | 32 | references.forEach(reference => { 33 | const customTag = CUSTOM_TAGS_BY_TYPE[reference.type] 34 | 35 | if ( 36 | customTag && 37 | customTag.referenceEntityTypes.includes( 38 | referenceable.entityType 39 | ) 40 | ) { 41 | locations.push( 42 | Location.create( 43 | documentPosition.textDocument.uri, 44 | Range.create( 45 | document.positionAt(reference.node.start), 46 | document.positionAt(reference.node.end) 47 | ) 48 | ) 49 | ) 50 | } 51 | }) 52 | } 53 | } 54 | 55 | return locations 56 | } 57 | -------------------------------------------------------------------------------- /packages/language-server/src/language-service/services/request/index.ts: -------------------------------------------------------------------------------- 1 | import { getErrorStatusDescription, xhr, XHROptions } from "request-light" 2 | 3 | import { sendException } from "../analytics" 4 | 5 | export default async ( 6 | uri: string, 7 | options: XHROptions = {} 8 | ): Promise => { 9 | try { 10 | const response = await xhr( 11 | Object.assign( 12 | { 13 | url: uri, 14 | followRedirects: 5, 15 | headers: { "Accept-Encoding": "gzip, deflate" } 16 | }, 17 | options 18 | ) 19 | ) 20 | return response.responseText 21 | } catch (error) { 22 | sendException(error) 23 | return ( 24 | error.responseText || 25 | getErrorStatusDescription(error.status) || 26 | error.toString() 27 | ) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/language-server/src/language-service/services/validation/__tests__/__snapshots__/validation.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Validation default does basic validation for cloud formation template 1`] = ` 4 | Array [ 5 | Object { 6 | "message": "[Serverless IDE] Missing property \\"Properties\\".", 7 | "range": Object { 8 | "end": Object { 9 | "character": 7, 10 | "line": 1, 11 | }, 12 | "start": Object { 13 | "character": 2, 14 | "line": 1, 15 | }, 16 | }, 17 | "severity": 1, 18 | }, 19 | ] 20 | `; 21 | 22 | exports[`Validation default does basic validation for sam template 1`] = ` 23 | Array [ 24 | Object { 25 | "message": "[Serverless IDE] Missing property \\"Properties\\".", 26 | "range": Object { 27 | "end": Object { 28 | "character": 7, 29 | "line": 2, 30 | }, 31 | "start": Object { 32 | "character": 2, 33 | "line": 2, 34 | }, 35 | }, 36 | "severity": 1, 37 | }, 38 | ] 39 | `; 40 | -------------------------------------------------------------------------------- /packages/language-server/src/language-service/services/validation/__tests__/validation.test.ts: -------------------------------------------------------------------------------- 1 | import { parse as parseYAML } from "@serverless-ide/config" 2 | import { Diagnostic, IConnection, TextDocument } from "vscode-languageserver" 3 | 4 | import { YAMLValidation } from ".." 5 | import { 6 | getDefaultLanguageSettings, 7 | ValidationProvider 8 | } from "../../../model/settings" 9 | import { LanguageSettings } from "../../../model/settings" 10 | import { JSONSchemaService } from "../../jsonSchema" 11 | 12 | const schemaService = new JSONSchemaService() 13 | 14 | // Tests for validator 15 | describe("Validation", () => { 16 | const mockSendDiagnostics = jest.fn() 17 | const mockConnection = { 18 | sendDiagnostics: mockSendDiagnostics 19 | } 20 | let languageSettings: LanguageSettings 21 | const validationProvider = new YAMLValidation( 22 | schemaService, 23 | "", 24 | (mockConnection as unknown) as IConnection 25 | ) 26 | 27 | const setup = (content: string) => { 28 | return TextDocument.create( 29 | "file://~/Desktop/sam.yaml", 30 | "yaml", 31 | 0, 32 | content 33 | ) 34 | } 35 | 36 | const parseSetup = async (content: string) => { 37 | mockConnection.sendDiagnostics.mockClear() 38 | 39 | const testTextDocument = setup(content) 40 | const yDoc = parseYAML(testTextDocument) 41 | await validationProvider.doValidation(testTextDocument, yDoc) 42 | return mockConnection.sendDiagnostics.mock.calls[0][0] 43 | .diagnostics as Diagnostic[] 44 | } 45 | describe("default", () => { 46 | beforeEach(() => { 47 | languageSettings = getDefaultLanguageSettings() 48 | 49 | languageSettings.validationProvider = ValidationProvider.default 50 | validationProvider.configure(languageSettings) 51 | }) 52 | 53 | test("does basic validation for empty file", async () => { 54 | const content = "" 55 | const result = await parseSetup(content) 56 | expect(result).toHaveLength(1) 57 | }) 58 | 59 | test("does basic validation for cloud formation template", async () => { 60 | const content = [ 61 | "Resources:", 62 | " Table:", 63 | " Type: AWS::DynamoDB::Table" 64 | ].join("\n") 65 | 66 | const result = await parseSetup(content) 67 | expect(result).not.toHaveLength(0) 68 | expect(result).toMatchSnapshot() 69 | }) 70 | 71 | test("does basic validation for sam template", async () => { 72 | const content = [ 73 | "Transform: AWS::Serverless-2016-10-31", 74 | "Resources:", 75 | " Table:", 76 | " Type: AWS::DynamoDB::Table" 77 | ].join("\n") 78 | 79 | const result = await parseSetup(content) 80 | expect(result).not.toHaveLength(0) 81 | expect(result).toMatchSnapshot() 82 | }) 83 | 84 | test("should considers globals for sam template", async () => { 85 | const content = [ 86 | "Transform: AWS::Serverless-2016-10-31", 87 | "Globals:", 88 | " Function:", 89 | " Runtime: nodejs8.10", 90 | "Resources:", 91 | " Function:", 92 | " Type: AWS::Serverless::Function", 93 | " Properties:", 94 | " Handler: index.default", 95 | " CodeUri: ." 96 | ].join("\n") 97 | 98 | const result = await parseSetup(content) 99 | expect(result).toHaveLength(0) 100 | }) 101 | 102 | test("should validation references", async () => { 103 | const content = [ 104 | "Transform: AWS::Serverless-2016-10-31", 105 | "Globals:", 106 | " Function:", 107 | " Runtime: nodejs8.10", 108 | "Resources:", 109 | " Function:", 110 | " Type: AWS::Serverless::Function", 111 | " Properties:", 112 | " Handler: index.default", 113 | " CodeUri: !Ref MyTable", 114 | " MyTable:", 115 | " Type: AWS::DynamoDB::Table", 116 | " Properties:", 117 | " KeySchema: !Sub Function", 118 | " AttributeDefinitions:", 119 | " - AttributeName: id", 120 | " AttributeType: S" 121 | ].join("\n") 122 | 123 | const result = await parseSetup(content) 124 | expect(result).toHaveLength(1) 125 | }) 126 | }) 127 | }) 128 | -------------------------------------------------------------------------------- /packages/language-server/src/language-service/services/validation/errors.ts: -------------------------------------------------------------------------------- 1 | export class CfnLintFailedToExecuteError extends Error { 2 | constructor(message: string) { 3 | super(message) 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/language-server/src/language-service/services/validation/references.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CUSTOM_TAGS_BY_TYPE, 3 | ReferenceEntityType, 4 | YAMLDocument 5 | } from "@serverless-ide/config" 6 | import { 7 | Diagnostic, 8 | DiagnosticSeverity, 9 | TextDocument 10 | } from "vscode-languageserver" 11 | 12 | export const validateReferences = async ( 13 | document: TextDocument, 14 | yamlDocument: YAMLDocument 15 | ): Promise => { 16 | const { referenceables, references } = yamlDocument 17 | const diagnostics: Diagnostic[] = [] 18 | 19 | Object.keys(references.hash).forEach(referenceKey => { 20 | references.hash[referenceKey].forEach(reference => { 21 | const customTag = CUSTOM_TAGS_BY_TYPE[reference.type] 22 | 23 | if ( 24 | !customTag.referenceEntityTypes.some( 25 | (entityType: ReferenceEntityType) => { 26 | return Boolean( 27 | referenceables.hash[entityType].contains( 28 | reference.key 29 | ) 30 | ) 31 | } 32 | ) 33 | ) { 34 | diagnostics.push({ 35 | severity: DiagnosticSeverity.Error, 36 | range: { 37 | start: document.positionAt(reference.node.start), 38 | end: document.positionAt(reference.node.end) 39 | }, 40 | message: `[Serverless IDE]: Cannot find ${customTag.referenceEntityTypes 41 | .map((entityType: ReferenceEntityType) => 42 | entityType.toLowerCase() 43 | ) 44 | .join(" or ")} with key "${reference.key}"` 45 | }) 46 | } 47 | }) 48 | }) 49 | 50 | return diagnostics 51 | } 52 | -------------------------------------------------------------------------------- /packages/language-server/src/language-service/utils/__tests__/arrayUtils.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getLineOffsets, 3 | removeDuplicates, 4 | removeDuplicatesObj 5 | } from "../arrayUtils" 6 | 7 | describe("Array Utils", () => { 8 | describe("removeDuplicates", () => { 9 | it("Remove one duplicate with property", () => { 10 | const obj1 = { 11 | testKey: "test_value" 12 | } 13 | 14 | const obj2 = { 15 | testKey: "test_value" 16 | } 17 | 18 | const arr = [obj1, obj2] 19 | const prop = "test_key" 20 | 21 | const result = removeDuplicates(arr, prop) 22 | expect(result).toHaveLength(1) 23 | }) 24 | 25 | it("Remove multiple duplicates with property", () => { 26 | const obj1 = { 27 | testKey: "test_value" 28 | } 29 | 30 | const obj2 = { 31 | testKey: "test_value" 32 | } 33 | 34 | const obj3 = { 35 | testKey: "test_value" 36 | } 37 | 38 | const obj4 = { 39 | anotherKeyToo: "test_value" 40 | } 41 | 42 | const arr = [obj1, obj2, obj3, obj4] 43 | const prop = "testKey" 44 | 45 | const result = removeDuplicates(arr, prop) 46 | expect(result).toHaveLength(2) 47 | }) 48 | 49 | it("Do NOT remove items without duplication", () => { 50 | const obj1 = { 51 | firstKey: "test_value" 52 | } 53 | 54 | const obj2 = { 55 | secondKey: "test_value" 56 | } 57 | 58 | const arr = [obj1, obj2] 59 | const prop = "firstKey" 60 | 61 | const result = removeDuplicates(arr, prop) 62 | expect(result).toHaveLength(2) 63 | }) 64 | }) 65 | 66 | describe("getLineOffsets", () => { 67 | it("No offset", () => { 68 | const offsets = getLineOffsets("") 69 | expect(offsets).toHaveLength(0) 70 | }) 71 | 72 | it("One offset", () => { 73 | const offsets = getLineOffsets("test_offset") 74 | expect(offsets).toHaveLength(1) 75 | expect(offsets[0]).toBe(0) 76 | }) 77 | 78 | it("One offset with \\r\\n", () => { 79 | const offsets = getLineOffsets("first_offset\r\n") 80 | expect(offsets).toHaveLength(2) 81 | expect(offsets[0]).toBe(0) 82 | }) 83 | 84 | it("Multiple offsets", () => { 85 | const offsets = getLineOffsets( 86 | "first_offset\n second_offset\n third_offset" 87 | ) 88 | expect(offsets).toHaveLength(3) 89 | expect(offsets[0]).toBe(0) 90 | expect(offsets[1]).toBe(13) 91 | expect(offsets[2]).toBe(29) 92 | }) 93 | }) 94 | 95 | describe("removeDuplicatesObj", () => { 96 | it("Remove one duplicate with property", () => { 97 | const obj1 = { 98 | testKey: "test_value" 99 | } 100 | 101 | const obj2 = { 102 | testKey: "test_value" 103 | } 104 | 105 | const arr = [obj1, obj2] 106 | const result = removeDuplicatesObj(arr) 107 | expect(result).toHaveLength(1) 108 | }) 109 | 110 | it("Does not remove anything unneccessary", () => { 111 | const obj1 = { 112 | testKey: "test_value" 113 | } 114 | 115 | const obj2 = { 116 | otherKey: "test_value" 117 | } 118 | 119 | const arr = [obj1, obj2] 120 | 121 | const result = removeDuplicatesObj(arr) 122 | expect(result).toHaveLength(2) 123 | }) 124 | }) 125 | }) 126 | -------------------------------------------------------------------------------- /packages/language-server/src/language-service/utils/__tests__/documentPositionCalculator.test.ts: -------------------------------------------------------------------------------- 1 | import { binarySearch } from "../documentPositionCalculator" 2 | 3 | describe("binarySearch", () => { 4 | it("Binary Search where we are looking for element to the left of center", () => { 5 | const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 6 | const find = 2 7 | 8 | const result = binarySearch(arr, find) 9 | expect(result).toBe(1) 10 | }) 11 | 12 | it("Binary Search where we are looking for element to the right of center", () => { 13 | const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 14 | const find = 8 15 | 16 | const result = binarySearch(arr, find) 17 | expect(result).toBe(7) 18 | }) 19 | 20 | it("Binary Search found at first check", () => { 21 | const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 22 | const find = 5 23 | 24 | const result = binarySearch(arr, find) 25 | expect(result).toBe(4) 26 | }) 27 | 28 | it("Binary Search item not found", () => { 29 | const arr = [1] 30 | const find = 5 31 | 32 | const result = binarySearch(arr, find) 33 | expect(result).toBe(-2) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /packages/language-server/src/language-service/utils/__tests__/strings.test.ts: -------------------------------------------------------------------------------- 1 | import { convertSimple2RegExp, endsWith, startsWith } from "../strings" 2 | 3 | describe("startsWith", () => { 4 | it("String with different lengths", () => { 5 | const one = "hello" 6 | const other = "goodbye" 7 | 8 | const result = startsWith(one, other) 9 | expect(result).toBe(false) 10 | }) 11 | 12 | it("String with same length different first letter", () => { 13 | const one = "hello" 14 | const other = "jello" 15 | 16 | const result = startsWith(one, other) 17 | expect(result).toBe(false) 18 | }) 19 | 20 | it("Same string", () => { 21 | const one = "hello" 22 | const other = "hello" 23 | 24 | const result = startsWith(one, other) 25 | expect(result).toBe(true) 26 | }) 27 | }) 28 | 29 | describe("endsWith", () => { 30 | it("String with different lengths", () => { 31 | const one = "hello" 32 | const other = "goodbye" 33 | 34 | const result = endsWith(one, other) 35 | expect(result).toBe(false) 36 | }) 37 | 38 | it("Strings that are the same", () => { 39 | const one = "hello" 40 | const other = "hello" 41 | 42 | const result = endsWith(one, other) 43 | expect(result).toBe(true) 44 | }) 45 | 46 | it("Other is smaller then one", () => { 47 | const one = "hello" 48 | const other = "hi" 49 | 50 | const result = endsWith(one, other) 51 | expect(result).toBe(false) 52 | }) 53 | }) 54 | 55 | describe("convertSimple2RegExp", () => { 56 | it("Test of convertRegexString2RegExp", () => { 57 | const result = convertSimple2RegExp("/toc\\.yml/i").test("TOC.yml") 58 | expect(result).toBe(true) 59 | }) 60 | 61 | it("Test of convertGlobalPattern2RegExp", () => { 62 | let result = convertSimple2RegExp("toc.yml").test("toc.yml") 63 | expect(result).toBe(true) 64 | 65 | result = convertSimple2RegExp("toc.yml").test("TOC.yml") 66 | expect(result).toBe(false) 67 | }) 68 | }) 69 | -------------------------------------------------------------------------------- /packages/language-server/src/language-service/utils/arrayUtils.ts: -------------------------------------------------------------------------------- 1 | import toArray = require("lodash/toArray") 2 | 3 | export function removeDuplicates(arr, prop) { 4 | const lookup = {} 5 | 6 | arr.forEach(item => { 7 | lookup[item[prop]] = item 8 | }) 9 | 10 | return toArray(lookup) 11 | } 12 | 13 | export function getLineOffsets(textDocString: string): number[] { 14 | const lineOffsets: number[] = [] 15 | const text = textDocString 16 | let isLineStart = true 17 | for (let i = 0; i < text.length; i++) { 18 | if (isLineStart) { 19 | lineOffsets.push(i) 20 | isLineStart = false 21 | } 22 | const ch = text.charAt(i) 23 | isLineStart = ch === "\r" || ch === "\n" 24 | if (ch === "\r" && i + 1 < text.length && text.charAt(i + 1) === "\n") { 25 | i++ 26 | } 27 | } 28 | if (isLineStart && text.length > 0) { 29 | lineOffsets.push(text.length) 30 | } 31 | 32 | return lineOffsets 33 | } 34 | 35 | export function removeDuplicatesObj(objArray) { 36 | const nonDuplicateSet = new Set() 37 | const nonDuplicateArr = [] 38 | 39 | objArray.forEach(currObj => { 40 | const stringifiedObj = JSON.stringify(currObj) 41 | if (!nonDuplicateSet.has(stringifiedObj)) { 42 | nonDuplicateArr.push(currObj) 43 | nonDuplicateSet.add(stringifiedObj) 44 | } 45 | }) 46 | 47 | return nonDuplicateArr 48 | } 49 | -------------------------------------------------------------------------------- /packages/language-server/src/language-service/utils/completion-helper.ts: -------------------------------------------------------------------------------- 1 | import { Position, TextDocument } from "vscode-languageserver" 2 | 3 | import { getLineOffsets } from "./arrayUtils" 4 | 5 | const isEOL = (c: number) => { 6 | return c === 0x0a /* LF */ || c === 0x0d /* CR */ 7 | } 8 | 9 | interface Output { 10 | newDocument: TextDocument 11 | newPosition: Position 12 | } 13 | 14 | export const completionHelper = ( 15 | document: TextDocument, 16 | textDocumentPosition: Position 17 | ): Output => { 18 | // Get the string we are looking at via a substring 19 | const linePos = textDocumentPosition.line 20 | const position = textDocumentPosition 21 | const lineOffset = getLineOffsets(document.getText()) 22 | const start = lineOffset[linePos] // Start of where the autocompletion is happening 23 | let end = 0 // End of where the autocompletion is happening 24 | if (lineOffset[linePos + 1]) { 25 | end = lineOffset[linePos + 1] 26 | } else { 27 | end = document.getText().length 28 | } 29 | 30 | while (end - 1 >= 0 && isEOL(document.getText().charCodeAt(end - 1))) { 31 | end-- 32 | } 33 | 34 | const textLine = document.getText().substring(start, end) 35 | 36 | // Check if the string we are looking at is a node 37 | if (textLine.indexOf(":") === -1) { 38 | // We need to add the ":" to load the nodes 39 | 40 | let newText = "" 41 | 42 | // This is for the empty line case 43 | const trimmedText = textLine.trim() 44 | if ( 45 | trimmedText.length === 0 || 46 | (trimmedText.length === 1 && trimmedText[0] === "-") 47 | ) { 48 | // Add a temp node that is in the document but we don't use at all. 49 | newText = 50 | document.getText().substring(0, start + textLine.length) + 51 | (trimmedText[0] === "-" && !textLine.endsWith(" ") ? " " : "") + 52 | "holder:\r\n" + 53 | document 54 | .getText() 55 | .substr( 56 | lineOffset[linePos + 1] || document.getText().length 57 | ) 58 | 59 | // For when missing semi colon case 60 | } else { 61 | // Add a semicolon to the end of the current line so we can validate the node 62 | newText = 63 | document.getText().substring(0, start + textLine.length) + 64 | ":\r\n" + 65 | document 66 | .getText() 67 | .substr( 68 | lineOffset[linePos + 1] || document.getText().length 69 | ) 70 | } 71 | 72 | return { 73 | newDocument: TextDocument.create( 74 | document.uri, 75 | document.languageId, 76 | document.version, 77 | newText 78 | ), 79 | newPosition: textDocumentPosition 80 | } 81 | } else { 82 | // All the nodes are loaded 83 | position.character = position.character - 1 84 | return { 85 | newDocument: document, 86 | newPosition: position 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /packages/language-server/src/language-service/utils/documentPositionCalculator.ts: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | export function insertionPointReturnValue(pt: number) { 4 | return -pt - 1 5 | } 6 | 7 | export function binarySearch(array: number[], sought: number) { 8 | let lower = 0 9 | let upper = array.length - 1 10 | 11 | while (lower <= upper) { 12 | const idx = Math.floor((lower + upper) / 2) 13 | const value = array[idx] 14 | 15 | if (value === sought) { 16 | return idx 17 | } 18 | 19 | if (lower === upper) { 20 | const insertionPoint = value < sought ? idx + 1 : idx 21 | return insertionPointReturnValue(insertionPoint) 22 | } 23 | 24 | if (sought > value) { 25 | lower = idx + 1 26 | } else if (sought < value) { 27 | upper = idx - 1 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/language-server/src/language-service/utils/errorHandler.ts: -------------------------------------------------------------------------------- 1 | import { sendException } from "../services/analytics" 2 | 3 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 4 | type PromiseProducer = (...args: any[]) => Promise 5 | 6 | type PromiseProducerParams> = TF extends ( 7 | ...args: infer P 8 | ) => Promise 9 | ? P 10 | : never 11 | 12 | export const promiseRejectionHandler = < 13 | TR, 14 | TProducer extends PromiseProducer 15 | >( 16 | promiseProducer: TProducer 17 | ): PromiseProducer => { 18 | return (...args: PromiseProducerParams) => 19 | promiseProducer(...args) 20 | .catch(err => { 21 | sendException(err) 22 | }) 23 | .then() 24 | } 25 | -------------------------------------------------------------------------------- /packages/language-server/src/language-service/utils/strings.ts: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | export function startsWith(haystack: string, needle: string): boolean { 4 | if (haystack.length < needle.length) { 5 | return false 6 | } 7 | 8 | for (let i = 0; i < needle.length; i++) { 9 | if (haystack[i] !== needle[i]) { 10 | return false 11 | } 12 | } 13 | 14 | return true 15 | } 16 | 17 | /** 18 | * Determines if haystack ends with needle. 19 | */ 20 | export function endsWith(haystack: string, needle: string): boolean { 21 | const diff = haystack.length - needle.length 22 | if (diff > 0) { 23 | return haystack.lastIndexOf(needle) === diff 24 | } else if (diff === 0) { 25 | return haystack === needle 26 | } else { 27 | return false 28 | } 29 | } 30 | 31 | export function convertSimple2RegExp(pattern: string): RegExp { 32 | const match = pattern.match(new RegExp("^/(.*?)/([gimy]*)$")) 33 | return match 34 | ? convertRegexString2RegExp(match[1], match[2]) 35 | : convertGlobalPattern2RegExp(pattern) 36 | } 37 | 38 | function convertGlobalPattern2RegExp(pattern: string): RegExp { 39 | return new RegExp( 40 | pattern 41 | .replace(/[\-\\\{\}\+\?\|\^\$\.\,\[\]\(\)\#\s]/g, "\\$&") 42 | .replace(/[\*]/g, ".*") + "$" 43 | ) 44 | } 45 | 46 | function convertRegexString2RegExp(pattern: string, flag: string): RegExp { 47 | return new RegExp(pattern, flag) 48 | } 49 | -------------------------------------------------------------------------------- /packages/language-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "include": [ 7 | "src" 8 | ] 9 | } -------------------------------------------------------------------------------- /packages/sam-schema/NOTICE.txt: -------------------------------------------------------------------------------- 1 | ========================================================================= 2 | NOTICE file for use with, and corresponding to Section 4 of, 3 | the Apache License, Version 2.0, 4 | in this case for the @serverless-ide/sam-schema project. 5 | ========================================================================= 6 | 7 | This product includes software developed by 8 | Amazon.com, Inc. or its affiliates. 9 | Copyright (c) 2019 Amazon.com, Inc. All rights reserved. 10 | -------------------------------------------------------------------------------- /packages/sam-schema/README.md: -------------------------------------------------------------------------------- 1 | # @serverless-ide/sam-schema 2 | 3 | ## Json schema for AWS SAM template configuration 4 | 5 | Based on [awslabs/goformation](https://raw.githubusercontent.com/awslabs/goformation/master/schema/sam.schema.json) 6 | and adds support of globals configuration 7 | 8 | ## How to 9 | 10 | 1. Install dependencies 11 | 12 | ```sh 13 | npm install 14 | ``` 15 | 2. Generate schema 16 | 17 | ```sh 18 | npm run generate 19 | ``` 20 | 21 | ## License 22 | 23 | ### License 24 | 25 | Apache License 2.0 26 | -------------------------------------------------------------------------------- /packages/sam-schema/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@serverless-ide/sam-schema", 3 | "version": "0.6.5", 4 | "description": "Json schema for AWS SAM template configuration", 5 | "main": "index.js", 6 | "repository": "git@github.com:threadheap/aws-sam-json-schema.git", 7 | "author": "Pavel Vlasov ", 8 | "keywords": [ 9 | "aws", 10 | "sam", 11 | "cloudformation", 12 | "json", 13 | "schema", 14 | "validation" 15 | ], 16 | "license": "MIT", 17 | "private": false, 18 | "devDependencies": { 19 | "@serverless-ide/cloudformation-schema": "^0.6.5", 20 | "@types/json-stable-stringify": "^1.0.32", 21 | "@types/lodash": "^4.14.119", 22 | "@types/node": "^10.12.18", 23 | "@types/request": "^2.48.1", 24 | "@types/request-promise": "^4.1.42", 25 | "json-stable-stringify": "^1.0.1", 26 | "lodash": "^4.17.11", 27 | "request": "^2.88.0", 28 | "request-promise": "^4.2.2", 29 | "typescript": "^4.9.4" 30 | }, 31 | "scripts": { 32 | "build": "npm run clean && npm run compile && node dist/index.js", 33 | "clean": "rm -rf ./dist", 34 | "compile": "tsc", 35 | "lint:types": "tsc --noEmit" 36 | }, 37 | "publishConfig": { 38 | "access": "public" 39 | }, 40 | "gitHead": "390fa05ac004e80dd92b96e08eded82c162ffcd9" 41 | } 42 | -------------------------------------------------------------------------------- /packages/sam-schema/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "include": [ 7 | "src" 8 | ] 9 | } -------------------------------------------------------------------------------- /packages/serverless-framework-schema/README.md: -------------------------------------------------------------------------------- 1 | # aws-sam-json-schema 2 | 3 | ## Json schema for AWS SAM template configuration 4 | 5 | Based on [awslabs/goformation](https://raw.githubusercontent.com/awslabs/goformation/master/schema/sam.schema.json) 6 | and adds support of globals configuration 7 | 8 | ## How to 9 | 10 | 1. Install dependencies 11 | 12 | ```sh 13 | npm install 14 | ``` 15 | 16 | 2. Generate schema 17 | 18 | ```sh 19 | npm run generate 20 | ``` 21 | 22 | ## License 23 | 24 | ### License 25 | 26 | Apache License 2.0 27 | -------------------------------------------------------------------------------- /packages/serverless-framework-schema/json/aws/common/arn.json: -------------------------------------------------------------------------------- 1 | { 2 | "anyOf": [ 3 | { 4 | "type": "string" 5 | }, 6 | { 7 | "type": "object" 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /packages/serverless-framework-schema/json/aws/common/authorizer.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "An AWS API Gateway custom authorizer function", 3 | "oneOf": [ 4 | { 5 | "type": "object", 6 | "additionalProperties": true, 7 | "properties": { 8 | "name": { 9 | "type": "string", 10 | "description": "The name of the authorizer function (must be in this service)", 11 | "default": "authorizerFunc" 12 | }, 13 | "arn": { 14 | "$ref": "#/definitions/aws:common:arn", 15 | "description": "Can be used instead of name to reference a function outside of service", 16 | "default": "xxx:xxx:Lambda-Name" 17 | }, 18 | "authorizerId": { 19 | "type": "string", 20 | "description": "Can be used instead of name or arn to reference an authorizer defined elsewhere", 21 | "default": "AuthorizerID" 22 | }, 23 | "resultTtlInSeconds": { 24 | "type": "number", 25 | "default": 0 26 | }, 27 | "identitySource": { 28 | "oneOf": [ 29 | { 30 | "type": "string", 31 | "default": "method.request.header.Authorization" 32 | }, 33 | { 34 | "type": "array", 35 | "items": [ 36 | { 37 | "type": "string" 38 | } 39 | ] 40 | } 41 | ] 42 | }, 43 | "identityValidationExpression": { 44 | "type": "string", 45 | "default": "someRegex" 46 | }, 47 | "type": { 48 | "anyOf": [ 49 | { 50 | "type": "string", 51 | "enum": [ 52 | "token", 53 | "request", 54 | "cognito_user_pools", 55 | "TOKEN", 56 | "REQUEST", 57 | "COGNITO_USER_POOLS", 58 | "JWT", 59 | "NONE", 60 | "AWS_IAM", 61 | "AWS_CROSS_ACCOUNT_IAM" 62 | ] 63 | }, 64 | { 65 | "type": "string" 66 | } 67 | ], 68 | "default": "token", 69 | "description": "Determines input to the authorier function. Defaults to token." 70 | }, 71 | "scopes": { 72 | "type": "array", 73 | "items": { 74 | "type": "string" 75 | } 76 | } 77 | }, 78 | "oneOf": [ 79 | { 80 | "required": [ 81 | "name" 82 | ] 83 | }, 84 | { 85 | "required": [ 86 | "arn" 87 | ] 88 | }, 89 | { 90 | "required": [ 91 | "authorizerId" 92 | ] 93 | } 94 | ] 95 | }, 96 | { 97 | "type": "string" 98 | } 99 | ] 100 | } -------------------------------------------------------------------------------- /packages/serverless-framework-schema/json/aws/common/policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "additionalProperties": false, 3 | "properties": { 4 | "Effect": { 5 | "type": "string", 6 | "enum": [ 7 | "Allow", 8 | "Deny" 9 | ], 10 | "default": "Allow" 11 | }, 12 | "Principal": { 13 | "oneOf": [ 14 | { 15 | "type": "string", 16 | "default": "*" 17 | }, 18 | { 19 | "type": "array", 20 | "items": { 21 | "type": "string" 22 | } 23 | } 24 | ] 25 | }, 26 | "Action": { 27 | "oneOf": [ 28 | { 29 | "type": "string" 30 | }, 31 | { 32 | "type": "array", 33 | "items": { 34 | "type": "string" 35 | } 36 | } 37 | ] 38 | }, 39 | "Resource": {}, 40 | "Condition": {} 41 | }, 42 | "required": [ 43 | "Effect", 44 | "Principal", 45 | "Action", 46 | "Resource" 47 | ] 48 | } -------------------------------------------------------------------------------- /packages/serverless-framework-schema/json/aws/common/role-statement.json: -------------------------------------------------------------------------------- 1 | { 2 | "additionalProperties": false, 3 | "properties": { 4 | "Sid": { 5 | "type": "string" 6 | }, 7 | "Effect": { 8 | "type": "string", 9 | "enum": [ 10 | "Allow", 11 | "Deny" 12 | ], 13 | "default": "Allow" 14 | }, 15 | "Action": { 16 | "oneOf": [ 17 | { 18 | "type": "string" 19 | }, 20 | { 21 | "type": "array", 22 | "items": { 23 | "type": "string" 24 | } 25 | } 26 | ] 27 | }, 28 | "Resource": {} 29 | }, 30 | "required": [ 31 | "Effect", 32 | "Action" 33 | ] 34 | } -------------------------------------------------------------------------------- /packages/serverless-framework-schema/json/aws/common/runtime.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "string", 3 | "enum": [ 4 | "nodejs20.x", 5 | "nodejs18.x", 6 | "nodejs16.x", 7 | "nodejs14.x", 8 | "nodejs12.x", 9 | "nodejs10.x", 10 | "nodejs8.10", 11 | "python3.11", 12 | "python3.10", 13 | "python3.9", 14 | "python3.8", 15 | "python3.6", 16 | "python3.7", 17 | "python2.7", 18 | "ruby2.7", 19 | "java11", 20 | "java8", 21 | "java8.al2", 22 | "go1.x", 23 | "dotnetcore3.1", 24 | "dotnet6", 25 | "provided", 26 | "provided.al2" 27 | ], 28 | "default": "nodejs18.x" 29 | } 30 | -------------------------------------------------------------------------------- /packages/serverless-framework-schema/json/aws/common/vpc.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Optional VPC. If you use VPC then both subproperties (securityGroupIds and subnetIds) are required. Can be set to ~ to specify no VPC.", 3 | "oneOf": [ 4 | { 5 | "type": "object", 6 | "additionalProperties": false, 7 | "properties": { 8 | "securityGroupIds": { 9 | "type": "array", 10 | "items": { 11 | "type": "string" 12 | } 13 | }, 14 | "subnetIds": { 15 | "type": "array", 16 | "items": { 17 | "type": "string" 18 | } 19 | } 20 | } 21 | }, 22 | { 23 | "type": "null" 24 | }, 25 | { 26 | "type": "boolean", 27 | "enum": [false] 28 | }, 29 | { 30 | "type": "string", 31 | "enum": [""] 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /packages/serverless-framework-schema/json/aws/functions/events/alb.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "additionalProperties": false, 4 | "properties": { 5 | "alb": { 6 | "type": "object", 7 | "additionalProperties": false, 8 | "properties": { 9 | "listenerArn": { 10 | "type": "string", 11 | "default": "arn:aws:elasticloadbalancing:us-east-1:12345:listener/app/my-load-balancer/50dc6c495c0c9188/" 12 | }, 13 | "priority": { 14 | "type": "number", 15 | "default": 1 16 | }, 17 | "conditions": { 18 | "type": "object" 19 | } 20 | }, 21 | "require": [ 22 | "listenerArn" 23 | ] 24 | } 25 | }, 26 | "required": [ 27 | "alb" 28 | ] 29 | } -------------------------------------------------------------------------------- /packages/serverless-framework-schema/json/aws/functions/events/alexaSkill.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "additionalProperties": false, 4 | "properties": { 5 | "alexaSkill": { 6 | "type": "object", 7 | "additionalProperties": false, 8 | "properties": { 9 | "appId": { 10 | "type": "string", 11 | "default": "amzn1.ask.skill.xx-xx-xx-xx" 12 | }, 13 | "enabled": { 14 | "type": "boolean", 15 | "default": false 16 | } 17 | }, 18 | "required": [ 19 | "appId" 20 | ] 21 | } 22 | }, 23 | "required": [ 24 | "alexaSkill" 25 | ] 26 | } -------------------------------------------------------------------------------- /packages/serverless-framework-schema/json/aws/functions/events/alexaSmartHome.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "additionalProperties": false, 4 | "properties": { 5 | "alexaSmartHome": { 6 | "type": "object", 7 | "additionalProperties": false, 8 | "properties": { 9 | "appId": { 10 | "type": "string", 11 | "default": "amzn1.ask.skill.xx-xx-xx-xx" 12 | }, 13 | "enabled": { 14 | "type": "boolean", 15 | "default": false 16 | } 17 | }, 18 | "required": [ 19 | "appId" 20 | ] 21 | } 22 | }, 23 | "required": [ 24 | "alexaSmartHome" 25 | ] 26 | } -------------------------------------------------------------------------------- /packages/serverless-framework-schema/json/aws/functions/events/cloudFront.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "additionalProperties": false, 4 | "properties": { 5 | "cloudFront": { 6 | "type": "object", 7 | "description": "Amazon CloudFront is a content delivery network (CDN) service that allows Lambda functions to be executed at edge locations.", 8 | "additionalProperties": true, 9 | "properties": { 10 | "eventType": { 11 | "type": "string", 12 | "enum": [ 13 | "viewer-request", 14 | "origin-request", 15 | "origin-response", 16 | "viewer-response" 17 | ] 18 | }, 19 | "origin": { 20 | "description": "Origin is the endpoint definition of the service that is delivered, e.g. S3 bucket or a website.", 21 | "oneOf": [ 22 | { 23 | "type": "string" 24 | }, 25 | { 26 | "type": "object", 27 | "additionalProperties": false, 28 | "properties": { 29 | "DomainName": { 30 | "type": "string" 31 | }, 32 | "OriginAccessControlId": { 33 | "oneOf": [ 34 | { 35 | "type": "string" 36 | }, 37 | { 38 | "type": "object" 39 | } 40 | ] 41 | }, 42 | "OriginPath": { 43 | "type": "string" 44 | }, 45 | "CustomOriginConfig": { 46 | "type": "object" 47 | }, 48 | "S3OriginConfig": { 49 | "type": "object" 50 | } 51 | } 52 | } 53 | ] 54 | }, 55 | "includeBody": { 56 | "type": "boolean" 57 | }, 58 | "pathPattern": { 59 | "type": "string" 60 | } 61 | }, 62 | "required": [ 63 | "eventType", 64 | "origin" 65 | ] 66 | } 67 | }, 68 | "required": [ 69 | "cloudFront" 70 | ] 71 | } -------------------------------------------------------------------------------- /packages/serverless-framework-schema/json/aws/functions/events/cloudwatchEvent.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "additionalProperties": false, 4 | "properties": { 5 | "cloudwatchEvent": { 6 | "type": "object", 7 | "additionalProperties": false, 8 | "properties": { 9 | "event": { 10 | "type": "object", 11 | "additionalProperties": false, 12 | "properties": { 13 | "source": { 14 | "type": "array", 15 | "items": { 16 | "type": "string" 17 | } 18 | }, 19 | "detail-type": { 20 | "type": "array", 21 | "items": { 22 | "type": "string" 23 | } 24 | }, 25 | "detail": { 26 | "type": "object" 27 | } 28 | } 29 | }, 30 | "input": { 31 | "type": "object" 32 | }, 33 | "inputPath": { 34 | "type": "string", 35 | "default": "$.stageVariables" 36 | }, 37 | "inputTransformer": { 38 | "type": "object" 39 | } 40 | }, 41 | "required": [ 42 | "event" 43 | ] 44 | } 45 | }, 46 | "required": [ 47 | "cloudwatchEvent" 48 | ] 49 | } -------------------------------------------------------------------------------- /packages/serverless-framework-schema/json/aws/functions/events/cloudwatchLog.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "additionalProperties": false, 4 | "properties": { 5 | "cloudwatchLog": { 6 | "type": "object", 7 | "additionalProperties": false, 8 | "properties": { 9 | "logGroup": { 10 | "type": "string", 11 | "default": "/aws/lambda/hello" 12 | }, 13 | "filter": { 14 | "type": "string", 15 | "default": "{$.userIdentity.type = Root}" 16 | } 17 | }, 18 | "require": [ 19 | "logGroup", 20 | "filter" 21 | ] 22 | } 23 | }, 24 | "required": [ 25 | "cloudwatchLog" 26 | ] 27 | } -------------------------------------------------------------------------------- /packages/serverless-framework-schema/json/aws/functions/events/cognitoUserPool.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "additionalProperties": false, 4 | "properties": { 5 | "cognitoUserPool": { 6 | "type": "object", 7 | "additionalProperties": false, 8 | "properties": { 9 | "pool": { 10 | "type": "string", 11 | "default": "MyUserPool" 12 | }, 13 | "trigger": { 14 | "type": "string", 15 | "default": "PreSignUp" 16 | }, 17 | "existing": { 18 | "type": "boolean", 19 | "default": false 20 | } 21 | }, 22 | "require": [ 23 | "logGroup", 24 | "filter" 25 | ] 26 | } 27 | }, 28 | "required": [ 29 | "cognitoUserPool" 30 | ] 31 | } -------------------------------------------------------------------------------- /packages/serverless-framework-schema/json/aws/functions/events/eventBridge.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "additionalProperties": false, 4 | "properties": { 5 | "eventBridge": { 6 | "type": "object" 7 | } 8 | }, 9 | "required": [ 10 | "eventBridge" 11 | ] 12 | } -------------------------------------------------------------------------------- /packages/serverless-framework-schema/json/aws/functions/events/httpApi.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "This creates an API Gateway HTTP endpoint which can be used to trigger this function.", 3 | "type": "object", 4 | "additionalProperties": false, 5 | "properties": { 6 | "httpApi": { 7 | "oneOf": [ 8 | { 9 | "type": "object", 10 | "additionalProperties": true, 11 | "properties": { 12 | "method": { 13 | "type": "string", 14 | "description": "HTTP method for this endpoint", 15 | "enum": [ 16 | "get", 17 | "head", 18 | "patch", 19 | "post", 20 | "put", 21 | "delete", 22 | "options", 23 | "trace", 24 | "connect", 25 | "any", 26 | "*", 27 | "GET", 28 | "HEAD", 29 | "PATCH", 30 | "POST", 31 | "PUT", 32 | "DELETE", 33 | "OPTIONS", 34 | "TRACE", 35 | "CONNECT", 36 | "ANY" 37 | ], 38 | "default": "get" 39 | }, 40 | "path": { 41 | "type": "string", 42 | "description": "Path for this endpoint", 43 | "default": "users/create" 44 | }, 45 | "cors": { 46 | "description": "Turn on CORS for this endpoint, but don't forget to return the right header in your response", 47 | "oneOf": [ 48 | { 49 | "type": "boolean" 50 | }, 51 | { 52 | "type": "object" 53 | } 54 | ] 55 | }, 56 | "authorizer": { 57 | "$ref": "#/definitions/aws:common:authorizer" 58 | } 59 | }, 60 | "require": [ 61 | "path", 62 | "method" 63 | ] 64 | }, 65 | { 66 | "type": "string" 67 | } 68 | ] 69 | } 70 | }, 71 | "required": [ 72 | "httpApi" 73 | ] 74 | } -------------------------------------------------------------------------------- /packages/serverless-framework-schema/json/aws/functions/events/iot.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "additionalProperties": false, 4 | "properties": { 5 | "iot": { 6 | "type": "object", 7 | "additionalProperties": false, 8 | "properties": { 9 | "name": { 10 | "type": "string", 11 | "default": "myIoTEvent" 12 | }, 13 | "description": { 14 | "type": "string", 15 | "default": "An IoT event" 16 | }, 17 | "sql": { 18 | "type": "string", 19 | "default": "SELECT * FROM 'some_topic'" 20 | }, 21 | "sqlVersion": { 22 | "type": "string", 23 | "default": "beta" 24 | }, 25 | "enabled": { 26 | "type": "boolean", 27 | "default": false 28 | } 29 | }, 30 | "require": [ 31 | "name", 32 | "sql" 33 | ] 34 | } 35 | }, 36 | "required": [ 37 | "iot" 38 | ] 39 | } -------------------------------------------------------------------------------- /packages/serverless-framework-schema/json/aws/functions/events/s3.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "additionalProperties": false, 4 | "properties": { 5 | "s3": { 6 | "type": "object", 7 | "additionalProperties": false, 8 | "properties": { 9 | "bucket": { 10 | "type": "string", 11 | "default": "photos" 12 | }, 13 | "event": { 14 | "type": "string", 15 | "default": "s3:ObjectCreated:*" 16 | }, 17 | "rules": { 18 | "type": "array", 19 | "items": { 20 | "type": "object" 21 | } 22 | }, 23 | "existing": { 24 | "type": "boolean" 25 | }, 26 | "forceDeploy": { 27 | "type": "boolean" 28 | } 29 | }, 30 | "require": [ 31 | "bucket", 32 | "event" 33 | ] 34 | } 35 | }, 36 | "required": [ 37 | "s3" 38 | ] 39 | } -------------------------------------------------------------------------------- /packages/serverless-framework-schema/json/aws/functions/events/schedule.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "additionalProperties": false, 4 | "properties": { 5 | "schedule": { 6 | "oneOf": [ 7 | { 8 | "type": "object", 9 | "additionalProperties": false, 10 | "properties": { 11 | "name": { 12 | "type": "string", 13 | "default": "my scheduled event" 14 | }, 15 | "description": { 16 | "type": "string", 17 | "default": "a description of my scheduled event's purpose" 18 | }, 19 | "rate": { 20 | "type": "string", 21 | "default": "rate(10 minutes)" 22 | }, 23 | "enabled": { 24 | "type": "boolean", 25 | "default": true 26 | }, 27 | "input": { 28 | "oneOf": [ 29 | { 30 | "type": "string" 31 | }, 32 | { 33 | "type": "object" 34 | } 35 | ] 36 | }, 37 | "inputPath": { 38 | "type": "string", 39 | "default": "$.stageVariables" 40 | }, 41 | "inputTransformer": { 42 | "type": "object" 43 | } 44 | }, 45 | "require": [ 46 | "rate" 47 | ] 48 | }, 49 | { 50 | "type": "string" 51 | } 52 | ] 53 | } 54 | }, 55 | "required": [ 56 | "schedule" 57 | ] 58 | } -------------------------------------------------------------------------------- /packages/serverless-framework-schema/json/aws/functions/events/sns.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "additionalProperties": false, 4 | "properties": { 5 | "sns": { 6 | "type": "object", 7 | "additionalProperties": true, 8 | "properties": { 9 | "topicName": { 10 | "type": "string", 11 | "default": "aggregate" 12 | }, 13 | "displayName": { 14 | "type": "string", 15 | "default": "Data aggregation pipeline" 16 | }, 17 | "filterPolicy": { 18 | "type": "object" 19 | }, 20 | "redrivePolicy": { 21 | "type": "object", 22 | "oneOf": [ 23 | { 24 | "properties": { 25 | "deadLetterTargetArn": { 26 | "type": "string", 27 | "description": "ARN" 28 | } 29 | } 30 | }, 31 | { 32 | "properties": { 33 | "deadLetterTargetRef": { 34 | "type": "string", 35 | "description": "Ref (resource defined in same CF stack)" 36 | } 37 | } 38 | }, 39 | { 40 | "properties": { 41 | "deadLetterTargetImport": { 42 | "type": "object", 43 | "description": "Import (resource defined in outer CF stack)" 44 | } 45 | } 46 | } 47 | ] 48 | } 49 | }, 50 | "require": [ 51 | "topicName", 52 | "displayName" 53 | ] 54 | } 55 | }, 56 | "required": [ 57 | "sns" 58 | ] 59 | } -------------------------------------------------------------------------------- /packages/serverless-framework-schema/json/aws/functions/events/sqs.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "additionalProperties": false, 4 | "properties": { 5 | "sqs": { 6 | "type": "object", 7 | "additionalProperties": true, 8 | "properties": { 9 | "arn": { 10 | "$ref": "#/definitions/aws:common:arn", 11 | "default": "arn:aws:sqs:region:XXXXXX:myQueue" 12 | }, 13 | "batchSize": { 14 | "type": "number", 15 | "default": 10 16 | }, 17 | "maximumRetryAttempts": { 18 | "type": "number", 19 | "default": 10 20 | } 21 | }, 22 | "require": [ 23 | "arn" 24 | ] 25 | } 26 | }, 27 | "required": [ 28 | "sqs" 29 | ] 30 | } -------------------------------------------------------------------------------- /packages/serverless-framework-schema/json/aws/functions/events/stream.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "additionalProperties": false, 4 | "properties": { 5 | "stream": { 6 | "oneOf": [ 7 | { 8 | "type": "object", 9 | "additionalProperties": false, 10 | "properties": { 11 | "type": { 12 | "type": "string", 13 | "enum": [ 14 | "dynamodb", 15 | "kinesis" 16 | ], 17 | "default": "dynamodb" 18 | }, 19 | "arn": { 20 | "$ref": "#/definitions/aws:common:arn", 21 | "default": "arn:aws:kinesis:region:XXXXXX:stream/foo" 22 | }, 23 | "batchSize": { 24 | "type": "number" 25 | }, 26 | "batchWindow": { 27 | "type": "number" 28 | }, 29 | "bisectBatchOnFunctionError": { 30 | "type": "boolean" 31 | }, 32 | "startingPosition": { 33 | "type": "string", 34 | "default": "LATEST" 35 | }, 36 | "maximumRetryAttempts": { 37 | "type": "number" 38 | }, 39 | "parallelizationFactor": { 40 | "type": "number" 41 | }, 42 | "enabled": { 43 | "type": "boolean" 44 | }, 45 | "consumer": { 46 | "type": "boolean" 47 | }, 48 | "destinations": { 49 | "type": "object" 50 | } 51 | }, 52 | "require": [ 53 | "type", 54 | "arn" 55 | ] 56 | }, 57 | { 58 | "type": "string", 59 | "default": "arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1901T00:00:00.000" 60 | } 61 | ] 62 | } 63 | }, 64 | "required": [ 65 | "stream" 66 | ] 67 | } -------------------------------------------------------------------------------- /packages/serverless-framework-schema/json/aws/functions/events/websocket.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "additionalProperties": false, 4 | "properties": { 5 | "websocket": { 6 | "type": "object", 7 | "additionalProperties": true, 8 | "properties": { 9 | "route": { 10 | "type": "string", 11 | "default": "$connect" 12 | }, 13 | "authorizer": { 14 | "$ref": "#/definitions/aws:common:authorizer" 15 | }, 16 | "routeResponseSelectionExpression": { 17 | "type": "string", 18 | "description": "optional, setting this enables callbacks on websocket requests for two-way communication", 19 | "default": "$default" 20 | } 21 | }, 22 | "require": [ 23 | "route" 24 | ] 25 | } 26 | }, 27 | "required": [ 28 | "websocket" 29 | ] 30 | } -------------------------------------------------------------------------------- /packages/serverless-framework-schema/json/aws/functions/functions.json: -------------------------------------------------------------------------------- 1 | { 2 | "oneOf": [ 3 | { 4 | "type": "object", 5 | "patternProperties": { 6 | "^[a-zA-Z0-9]+$": { 7 | "$ref": "#/definitions/aws:functions:function" 8 | } 9 | } 10 | }, 11 | { 12 | "type": "array", 13 | "items": { 14 | "type": "object", 15 | "patternProperties": { 16 | "^[a-zA-Z0-9]+$": { 17 | "$ref": "#/definitions/aws:functions:function" 18 | } 19 | } 20 | } 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /packages/serverless-framework-schema/json/aws/layers.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "patternProperties": { 4 | "^[a-zA-Z0-9]+$": { 5 | "type": "object", 6 | "additionalProperties": false, 7 | "description": "A lambda layer", 8 | "properties": { 9 | "path": { 10 | "type": "string", 11 | "description": "required, path to layer contents on disk", 12 | "default": "layer-dir" 13 | }, 14 | "name": { 15 | "type": "string", 16 | "description": "optional, Deployed Lambda layer name", 17 | "default": "${self:provider.stage}-layerName" 18 | }, 19 | "description": { 20 | "type": "string", 21 | "description": "optional, Description to publish to AWS" 22 | }, 23 | "compatibleRuntimes": { 24 | "type": "array", 25 | "items": { 26 | "$ref": "#/definitions/aws:common:runtime" 27 | }, 28 | "description": "optional, a list of runtimes this layer is compatible with" 29 | }, 30 | "compatibleArchitectures": { 31 | "type": "array", 32 | "items": { 33 | "type": "string", 34 | "enum": [ 35 | "x86_64", 36 | "arm64" 37 | ] 38 | } 39 | }, 40 | "licenseInfo": { 41 | "type": "string", 42 | "description": "optional, a string specifying license information", 43 | "default": "GPLv3" 44 | }, 45 | "allowedAccounts": { 46 | "type": "array", 47 | "description": "optional, a list of AWS account IDs allowed to access this layer.", 48 | "items": { 49 | "type": "string", 50 | "default": "'*'" 51 | } 52 | }, 53 | "retain": { 54 | "type": "boolean", 55 | "description": "optional, false by default. If true, layer versions are not deleted as new ones are created", 56 | "default": false 57 | }, 58 | "package": { 59 | "$ref": "#/definitions/common:package-config" 60 | } 61 | }, 62 | "anyOf": [ 63 | { 64 | "required": [ 65 | "path" 66 | ] 67 | }, 68 | { 69 | "required": [ 70 | "package" 71 | ] 72 | } 73 | ] 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /packages/serverless-framework-schema/json/aws/service.json: -------------------------------------------------------------------------------- 1 | { 2 | "oneOf": [ 3 | { 4 | "type": "string" 5 | }, 6 | { 7 | "type": "object", 8 | "additionalProperties": true, 9 | "properties": { 10 | "name": { 11 | "type": "string", 12 | "default": "myService" 13 | }, 14 | "awsKmsKeyArn": { 15 | "type": "string", 16 | "description": "Optional KMS key arn which will be used for encryption for all functions" 17 | } 18 | }, 19 | "required": [ 20 | "name" 21 | ] 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /packages/serverless-framework-schema/json/common/package-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "additionalProperties": false, 4 | "description": "Optional deployment packaging configuration", 5 | "properties": { 6 | "patterns": { 7 | "type": "array", 8 | "description": "Specify patterns to include or exclude files from the deployment package", 9 | "items": { 10 | "type": "string" 11 | } 12 | }, 13 | "include": { 14 | "type": "array", 15 | "description": "Specify the directories and files which should be included in the deployment package", 16 | "items": { 17 | "type": "string" 18 | } 19 | }, 20 | "exclude": { 21 | "type": "array", 22 | "description": "Specify the directories and files which should be excluded in the deployment package", 23 | "items": { 24 | "type": "string" 25 | } 26 | }, 27 | "excludeDevDependencies": { 28 | "type": "boolean", 29 | "description": "Config if Serverless should automatically exclude dev dependencies in the deployment package. Defaults to true", 30 | "default": false 31 | }, 32 | "artifact": { 33 | "type": "string", 34 | "description": "Own package that should be used. You must provide this file." 35 | }, 36 | "individually": { 37 | "type": "boolean", 38 | "description": "Enables individual packaging for each function. If true you must provide package for each function. Defaults to false" 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /packages/serverless-framework-schema/json/common/tags.json: -------------------------------------------------------------------------------- 1 | { 2 | "patternProperties": { 3 | "^[a-zA-Z0-9]+$": { 4 | "type": "string" 5 | } 6 | }, 7 | "type": "object" 8 | } -------------------------------------------------------------------------------- /packages/serverless-framework-schema/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@serverless-ide/serverless-framework-schema", 3 | "version": "0.6.5", 4 | "description": "Json schema for AWS SAM template configuration", 5 | "main": "index.js", 6 | "repository": "git@github.com:threadheap/aws-sam-json-schema.git", 7 | "author": "Pavel Vlasov ", 8 | "keywords": [ 9 | "aws", 10 | "serverless", 11 | "serverless framework", 12 | "sls", 13 | "cloudformation", 14 | "json", 15 | "schema", 16 | "validation" 17 | ], 18 | "license": "MIT", 19 | "private": false, 20 | "devDependencies": { 21 | "@serverless-ide/sam-schema": "^0.6.5", 22 | "@types/glob": "^7.1.1", 23 | "@types/json-stable-stringify": "^1.0.32", 24 | "@types/lodash": "^4.14.119", 25 | "@types/node": "^10.12.18", 26 | "@types/request": "^2.48.1", 27 | "@types/request-promise": "^4.1.42", 28 | "glob": "^7.1.4", 29 | "json-stable-stringify": "^1.0.1", 30 | "lodash": "^4.17.11", 31 | "request": "^2.88.0", 32 | "request-promise": "^4.2.2", 33 | "typescript": "^4.9.4" 34 | }, 35 | "scripts": { 36 | "build": "npm run clean && npm run compile && node dist/index.js", 37 | "clean": "rm -rf ./dist", 38 | "compile": "tsc", 39 | "lint:types": "tsc --noEmit" 40 | }, 41 | "publishConfig": { 42 | "access": "public" 43 | }, 44 | "gitHead": "390fa05ac004e80dd92b96e08eded82c162ffcd9" 45 | } 46 | -------------------------------------------------------------------------------- /packages/serverless-framework-schema/src/index.ts: -------------------------------------------------------------------------------- 1 | import glob = require("glob") 2 | import { readFileSync, writeFileSync } from "fs" 3 | import { forEach } from "lodash" 4 | import * as path from "path" 5 | import samSchema = require("@serverless-ide/sam-schema/schema.json") 6 | 7 | const readDefinitions = () => { 8 | const files = glob.sync("./json/**/*.json") 9 | 10 | const hash: { [key: string]: string } = {} 11 | 12 | files.forEach(file => { 13 | hash[ 14 | file 15 | .replace("./json/", "") 16 | .replace(".json", "") 17 | .replace(/\//g, ":") 18 | ] = JSON.parse( 19 | readFileSync(file, { 20 | encoding: "utf8" 21 | }) 22 | ) 23 | }) 24 | 25 | return hash 26 | } 27 | 28 | const buildSchema = async () => { 29 | const definitions = readDefinitions() 30 | 31 | const resourcesProperties = { 32 | type: "object", 33 | properties: { 34 | ...samSchema.properties, 35 | Transform: { 36 | type: ["object", "string"] 37 | }, 38 | extensions: { 39 | type: "object", 40 | description: 41 | "Override Properties or other attributes of Framework-created resources. \nSee https://serverless.com/framework/docs/providers/aws/guide/resources#override-aws-cloudformation-resource for more details" 42 | } 43 | }, 44 | additionalProperties: true 45 | } 46 | 47 | forEach(samSchema.definitions, (definition: any) => { 48 | if (definition.properties && definition.properties.DependsOn) { 49 | definition.properties.DependsOn = { 50 | oneOf: [ 51 | { 52 | type: "string" 53 | }, 54 | { 55 | type: "array", 56 | items: { 57 | type: "string" 58 | } 59 | } 60 | ] 61 | } 62 | } 63 | }) 64 | 65 | return { 66 | $id: "http://json-schema.org/draft-04/schema#", 67 | additionalProperties: true, 68 | definitions: { 69 | ...definitions, 70 | ...samSchema.definitions 71 | }, 72 | properties: { 73 | app: { 74 | type: "string" 75 | }, 76 | org: { 77 | type: "string" 78 | }, 79 | service: { 80 | $ref: "#/definitions/aws:service" 81 | }, 82 | frameworkVersion: { 83 | type: "string" 84 | }, 85 | provider: { 86 | $ref: "#/definitions/aws:provider:provider" 87 | }, 88 | package: { 89 | $ref: "#/definitions/common:package-config" 90 | }, 91 | functions: { 92 | $ref: "#/definitions/aws:functions:functions" 93 | }, 94 | layers: { 95 | $ref: "#/definitions/aws:layers" 96 | }, 97 | resources: { 98 | oneOf: [ 99 | resourcesProperties, 100 | { 101 | type: "array", 102 | item: resourcesProperties 103 | } 104 | ] 105 | }, 106 | custom: { 107 | type: "object" 108 | }, 109 | plugins: { 110 | oneOf: [ 111 | { 112 | type: "array" 113 | }, 114 | { 115 | type: "object", 116 | properties: { 117 | localPath: { 118 | type: "string" 119 | }, 120 | modules: { 121 | type: "array" 122 | } 123 | } 124 | } 125 | ] 126 | } 127 | }, 128 | required: ["service", "provider"] 129 | } 130 | } 131 | 132 | const generate = async () => { 133 | writeFileSync( 134 | path.resolve("./schema.json"), 135 | JSON.stringify(await buildSchema(), null, 2), 136 | { encoding: "utf8" } 137 | ) 138 | } 139 | 140 | generate() 141 | -------------------------------------------------------------------------------- /packages/serverless-framework-schema/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "include": [ 7 | "src" 8 | ] 9 | } -------------------------------------------------------------------------------- /packages/vscode/.npmignore: -------------------------------------------------------------------------------- 1 | icon 2 | src 3 | .vscodeignore 4 | tsconfig.json 5 | -------------------------------------------------------------------------------- /packages/vscode/.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | typings/** 3 | out/test/** 4 | test/** 5 | src/** 6 | demo/** 7 | **/*.map 8 | .gitignore 9 | .github/** 10 | tsconfig.json 11 | vsc-extension-quickstart.md 12 | undefined/** 13 | CONTRIBUTING.md 14 | .vscode-test/** 15 | **/**.vsix 16 | **/**.tar.gz 17 | -------------------------------------------------------------------------------- /packages/vscode/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | 3 | ## v0.5.33 4 | - Schema updates 5 | 6 | ## v0.5.32 7 | - Add support for package patterns property 8 | - Other small schema updates 9 | 10 | ## v0.5.30 11 | - Update schemas, small bug fixes 12 | 13 | ## v0.5.29 14 | - Fix "Matches multiple schemas when only one must validate" bug when using authorizers config 15 | 16 | ## v0.5.28 17 | - Small updates and fixes for base json schemas 18 | 19 | ## v0.5.27 20 | - Small updates and fixes for base json schemas 21 | 22 | ## v0.5.25 23 | - Update Cloudformation schema 24 | - Minor bugfixes in Serverless framework schema 25 | 26 | ## v0.5.20 27 | - Fixed the bug with validation of subsequent files in serverless.yml 28 | - Small improvemants of serverless json schema 29 | 30 | ## v0.5.17 31 | - Improved intrinsic functions support 32 | - HTTP API support for serverless framework 33 | - Fix bug that prevents VSCode failure when cfn-lint fails with an error 34 | - Small improvements and fixes 35 | 36 | ## v0.5.14 37 | - Cloudformation resources updates 38 | - Minor changes and bugfixes 39 | 40 | ## v0.5.11 41 | - Bundle lodash into all lerna packages 42 | - Small schemas updates 43 | 44 | ## v0.5.7 45 | - Update Cloudformation and serverless framework schemas 46 | 47 | ## v0.5.6 48 | - Update supported runtime values, resolves [#55](https://github.com/threadheap/serverless-ide-vscode/issues/55) 49 | - Allow for multiple imports in serverless functions definitions, resolves [#54](https://github.com/threadheap/serverless-ide-vscode/issues/54) 50 | - Fix go to definition and references for conditions statements 51 | 52 | ## v0.5.2 53 | - Fix required properties for serverless framework schedule event 54 | 55 | ## v0.5.1 56 | - Fix error thrown by hover method, when a path contains number 57 | 58 | ## v0.5.0 59 | - Various schemas bugfixes and improvements for Cloudformation and Serverless framework 60 | - Autocompletion for conditions properties and functions 61 | - Support for Lambda@Edge in Serverless framework config 62 | - Minor bug fixes 63 | 64 | ## v0.4.12 65 | - Allow OPTION as a valid HTTP method in Serverless framework schema 66 | 67 | ## v0.4.11 68 | - Update SAM/Cloudformation schema with recent updates 69 | - Small serverless framework schema improvements 70 | - Allow additional properties for serverless framework schema for better plugins support 71 | 72 | ## v0.4.10 73 | - Turns off schema validation, when `cfn-lint` option is selected 74 | 75 | ## v0.4.9 76 | - Gracefully handle exceptions when interacting unsupported yaml documents 77 | 78 | ## v0.4.8 79 | - Rename `org` to `tenant` in serverless framework schema 80 | 81 | ## v0.4.6 82 | - Workplace capabilities for validation 83 | - Fix runtime handling for serverless framework 84 | - Minor changes and fixes 85 | 86 | ## v0.4.5 87 | - Quick installation dialog for cfn-lint 88 | - Remove fallback to default schema validation when cfn-lint validation failed 89 | 90 | ## v0.4.4 91 | - Bug fixes and improvements for Serverless Framework 92 | 93 | ## v0.4.2 94 | - Go to definition 95 | - References 96 | - Add default SAM resources 97 | - Minor performance improvements 98 | 99 | ## v0.4.0 100 | - Validation and autocompletion for resources references 101 | - Improved support of AWS Intrinsic Functions 102 | - Basic support for Serverless Framework 103 | 104 | ## v0.3.8 105 | - Fix SAM schema to support MethodSettings property in AWS::Serverless::Api resource type 106 | - Additional logging around cfn-lint validation 107 | 108 | ## v0.3.7 109 | - Delete sentry global handlers 110 | 111 | ## v0.3.6 112 | - less verbose logging 113 | - telemetry and crash reporting 114 | 115 | ## v0.3.4 116 | - cfn-lint support fix 117 | - update dependencies 118 | 119 | ## v0.3.2 (May 12, 2019) 120 | - cfn-lint support 121 | 122 | ## v0.2.1 (March 3, 2019) 123 | 124 | - Add support for global SAM configuration (fix [#5](https://github.com/threadheap/serverless-ide-vscode/issues/5)) 125 | 126 | ## v0.2.0 (February 26, 2019) 127 | 128 | - Support for CloudFormation templates 129 | - At glance documentation for AWS resources 130 | 131 | ## 0.3.1 (April 27, 2019) 132 | 133 | - Add support of cfn-lint 134 | - Simplified configuration 135 | 136 | ## 0.3.2 (May 12, 2019) 137 | 138 | - Basic telemtry 139 | -------------------------------------------------------------------------------- /packages/vscode/NOTICE.txt: -------------------------------------------------------------------------------- 1 | @serverless-ide/client 2 | 3 | THIRD-PARTY SOFTWARE NOTICES AND INFORMATION 4 | Do Not Translate or Localize 5 | 6 | This project incorporates components from the projects listed below. The original copyright notices and the licenses under which Red Hat received such components are set forth below. Red Hat reserves all rights not expressly granted herein, whether by implication, estoppel or otherwise. 7 | 8 | 1. textmate/yaml.tmbundle (https://github.com/textmate/yaml.tmbundle) 9 | 2. microsoft/vscode (https://github.com/Microsoft/vscode) 10 | 11 | %% textmate/yaml.tmbundle NOTICES AND INFORMATION BEGIN HERE 12 | ========================================= 13 | Copyright (c) 2019 FichteFoll 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy 16 | of this software and associated documentation files (the "Software"), to deal 17 | in the Software without restriction, including without limitation the rights 18 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | copies of the Software, and to permit persons to whom the Software is 20 | furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in all 23 | copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 31 | ========================================= 32 | END OF textmate/yaml.tmbundle NOTICES AND INFORMATION 33 | 34 | %% vscode NOTICES AND INFORMATION BEGIN HERE 35 | ========================================= 36 | MIT License 37 | 38 | Copyright (c) 2019 - present Microsoft Corporation 39 | 40 | All rights reserved. 41 | 42 | Permission is hereby granted, free of charge, to any person obtaining a copy 43 | of this software and associated documentation files (the "Software"), to deal 44 | in the Software without restriction, including without limitation the rights 45 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 46 | copies of the Software, and to permit persons to whom the Software is 47 | furnished to do so, subject to the following conditions: 48 | 49 | The above copyright notice and this permission notice shall be included in all 50 | copies or substantial portions of the Software. 51 | 52 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 53 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 54 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 55 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 56 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 57 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 58 | SOFTWARE. 59 | ========================================= 60 | END OF vscode NOTICES AND INFORMATION 61 | 62 | ========================================================================= 63 | NOTICE file for use with, and corresponding to Section 4 of, 64 | the Apache License, Version 2.0, 65 | in this case for the @serverless-ide/client project. 66 | ========================================================================= 67 | 68 | This product includes software developed by 69 | Amazon.com, Inc. or its affiliates. 70 | Copyright (c) 2019 Amazon.com, Inc. All rights reserved. 71 | -------------------------------------------------------------------------------- /packages/vscode/demo/autocomplete.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/threadheap/serverless-ide-vscode/14a7e18f7963eca231b959080f0faa51f0c27475/packages/vscode/demo/autocomplete.gif -------------------------------------------------------------------------------- /packages/vscode/demo/documentation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/threadheap/serverless-ide-vscode/14a7e18f7963eca231b959080f0faa51f0c27475/packages/vscode/demo/documentation.gif -------------------------------------------------------------------------------- /packages/vscode/demo/serverless_framework.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/threadheap/serverless-ide-vscode/14a7e18f7963eca231b959080f0faa51f0c27475/packages/vscode/demo/serverless_framework.gif -------------------------------------------------------------------------------- /packages/vscode/demo/validation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/threadheap/serverless-ide-vscode/14a7e18f7963eca231b959080f0faa51f0c27475/packages/vscode/demo/validation.gif -------------------------------------------------------------------------------- /packages/vscode/icon/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/threadheap/serverless-ide-vscode/14a7e18f7963eca231b959080f0faa51f0c27475/packages/vscode/icon/icon.png -------------------------------------------------------------------------------- /packages/vscode/language-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | "lineComment": "#" 4 | }, 5 | "brackets": [ 6 | ["{", "}"], 7 | ["[", "]"], 8 | ["(", ")"] 9 | ], 10 | "autoClosingPairs": [ 11 | ["{", "}"], 12 | ["[", "]"], 13 | ["(", ")"], 14 | ["\"", "\""], 15 | ["'", "'"] 16 | ], 17 | "surroundingPairs": [ 18 | ["{", "}"], 19 | ["[", "]"], 20 | ["(", ")"], 21 | ["\"", "\""], 22 | ["'", "'"] 23 | ], 24 | "folding": { 25 | "offSide": true, 26 | "markers": { 27 | "start": "^\\s*#\\s*region\\b", 28 | "end": "^\\s*#\\s*endregion\\b" 29 | } 30 | }, 31 | "indentationRules": { 32 | "increaseIndentPattern": "^\\s*.*(:|-) ?(&\\w+)?(\\{[^}\"']*|\\([^)\"']*)?$", 33 | "decreaseIndentPattern": "^\\s+\\}$" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/vscode/src/analytics/index.ts: -------------------------------------------------------------------------------- 1 | import * as Sentry from "@sentry/node" 2 | import * as crypto from "crypto" 3 | import { machineIdSync } from "node-machine-id" 4 | import { userInfo } from "os" 5 | import { AnalyticsReporter, IAnalyticsClient } from "vscode-extension-analytics" 6 | import { Event as AnalyticsEvent, Exception } from "vscode-extension-analytics" 7 | 8 | import * as packageJson from "../../package.json" 9 | import { AmplitudeClient, AmplitudeEventData } from "./amplitude" 10 | export { AnalyticsEvent, Exception } 11 | 12 | class AnalyticsClient implements IAnalyticsClient { 13 | private amplitudeInstance: AmplitudeClient 14 | private deviceId: string 15 | private sessionId: number 16 | private userId: string 17 | 18 | constructor(apiKey: string) { 19 | const user = userInfo({ encoding: "utf8" }) 20 | this.amplitudeInstance = new AmplitudeClient(apiKey) 21 | this.deviceId = machineIdSync() 22 | this.userId = crypto 23 | .createHash("md5") 24 | .update(user.username) 25 | .digest("hex") 26 | this.sessionId = Date.now() 27 | } 28 | 29 | initialise() { 30 | Sentry.init({ 31 | dsn: "https://710778be7bd847558250574eb19e52e9@sentry.io/1509685", 32 | integrations: function(integrations) { 33 | return integrations.filter(integration => { 34 | return ( 35 | integration.name !== "OnUncaughtException" && 36 | integration.name !== "OnUnhandledRejection" 37 | ) 38 | }) 39 | }, 40 | release: `${packageJson.name}@${packageJson.version}` 41 | }) 42 | 43 | Sentry.configureScope(scope => { 44 | scope.setUser({ 45 | id: this.userId 46 | }) 47 | 48 | scope.setTags({ 49 | deviceId: this.deviceId, 50 | sessionId: this.sessionId.toString() 51 | }) 52 | }) 53 | } 54 | 55 | async flush(): Promise { 56 | await this.amplitudeInstance.dispose() 57 | } 58 | 59 | async sendEvent(event: AnalyticsEvent) { 60 | /* eslint-disable @typescript-eslint/camelcase */ 61 | const amplitudeEvent: AmplitudeEventData = { 62 | event_type: event.action, 63 | user_id: this.userId, 64 | device_id: this.deviceId, 65 | session_id: this.sessionId, 66 | event_properties: event.toJSON() 67 | } 68 | /* eslint-enable @typescript-eslint/camelcase */ 69 | 70 | await this.amplitudeInstance.track(amplitudeEvent) 71 | } 72 | 73 | async sendException(event: Exception) { 74 | await Sentry.captureException(event.error) 75 | } 76 | } 77 | 78 | export const createReporter = ( 79 | extensionId: string, 80 | extensionVersion: string, 81 | apiKey: string 82 | ): AnalyticsReporter => { 83 | const client = new AnalyticsClient(apiKey) 84 | 85 | return new AnalyticsReporter(extensionId, extensionVersion, client, { 86 | configId: "serverlessIDE.telemetry", 87 | configEnabledId: "enableTelemetry" 88 | }) 89 | } 90 | -------------------------------------------------------------------------------- /packages/vscode/src/commands/index.ts: -------------------------------------------------------------------------------- 1 | import * as os from "os" 2 | import * as vscode from "vscode" 3 | 4 | export const INSTALL_CFN_LITN = "ServerlessIDE.installCfnLint" 5 | 6 | export default (context: vscode.ExtensionContext) => { 7 | context.subscriptions.push( 8 | vscode.commands.registerCommand(INSTALL_CFN_LITN, () => { 9 | switch (os.platform()) { 10 | case "darwin": { 11 | const terminal = vscode.window.createTerminal() 12 | terminal.show() 13 | terminal.sendText("brew install cfn-lint\n") 14 | break 15 | } 16 | case "linux": 17 | case "win32": { 18 | const terminal = vscode.window.createTerminal() 19 | terminal.show() 20 | terminal.sendText("pip install cfn-lint\n") 21 | break 22 | } 23 | default: 24 | vscode.commands.executeCommand( 25 | "vscode.open", 26 | vscode.Uri.parse( 27 | "https://github.com/threadheap/serverless-ide-vscode#settings" 28 | ) 29 | ) 30 | break 31 | } 32 | }) 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /packages/vscode/src/settings/index.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode" 2 | 3 | export class Settings { 4 | readonly prefix: string 5 | 6 | constructor(prefix: string) { 7 | this.prefix = prefix 8 | } 9 | 10 | get(key: string, defaultValue?: T): T | undefined { 11 | // tslint:disable-next-line:no-null-keyword 12 | const settings = vscode.workspace.getConfiguration(this.prefix, null) 13 | if (settings) { 14 | const val = settings.get(key) 15 | if (val) { 16 | return val 17 | } 18 | } 19 | 20 | return defaultValue || undefined 21 | } 22 | 23 | async set( 24 | key: string, 25 | value: T, 26 | target: vscode.ConfigurationTarget 27 | ): Promise { 28 | const settings = vscode.workspace.getConfiguration(this.prefix, null) 29 | 30 | await settings.update(key, value, target) 31 | } 32 | } 33 | 34 | export default new Settings("serverlessIDE") 35 | -------------------------------------------------------------------------------- /packages/vscode/src/validation-error-dialog/index.ts: -------------------------------------------------------------------------------- 1 | import * as os from "os" 2 | import * as vscode from "vscode" 3 | import { AnalyticsReporter } from "vscode-extension-analytics" 4 | import * as nls from "vscode-nls" 5 | 6 | import { AnalyticsEvent } from "../analytics" 7 | import { INSTALL_CFN_LITN } from "../commands" 8 | const localize = nls.loadMessageBundle() 9 | 10 | const MISSING_CFN_LINT_MESSAGE = ` 11 | cfn-lint is not installed or could not be found in $PATH 12 | ` 13 | 14 | const RESPONSE_YES = "Install now" 15 | 16 | const RESPONSE_LEARN_MORE = "Learn more" 17 | 18 | const SUPPORTED_PLATFORMS = ["darwin", "linux", "win32"] 19 | 20 | export class CfnValidationMessage { 21 | private context: vscode.ExtensionContext 22 | private analytics: AnalyticsReporter 23 | private isVisible: boolean = false 24 | 25 | constructor( 26 | context: vscode.ExtensionContext, 27 | analytics: AnalyticsReporter 28 | ) { 29 | this.context = context 30 | this.analytics = analytics 31 | } 32 | 33 | async showNotification(): Promise { 34 | this.analytics.sendEvent( 35 | new AnalyticsEvent("cfnLintErrorDialogShown", { 36 | isVisible: this.isVisible.toString() 37 | }) 38 | ) 39 | 40 | if (this.isVisible) { 41 | return 42 | } 43 | 44 | this.isVisible = true 45 | 46 | const notificationMessage: string = localize( 47 | "ServerlessIDE.validation.missingCfnLintMessage", 48 | MISSING_CFN_LINT_MESSAGE 49 | ) 50 | 51 | return vscode.window 52 | .showWarningMessage( 53 | notificationMessage, 54 | RESPONSE_YES, 55 | RESPONSE_LEARN_MORE 56 | ) 57 | .then((response: string | void) => { 58 | switch (response) { 59 | case RESPONSE_YES: { 60 | this.onYesAnswer() 61 | break 62 | } 63 | case RESPONSE_LEARN_MORE: 64 | this.onLearnMore() 65 | break 66 | default: { 67 | this.analytics.sendEvent( 68 | new AnalyticsEvent( 69 | "cfnLintErrorDialogDismissed", 70 | {} 71 | ) 72 | ) 73 | } 74 | } 75 | 76 | this.isVisible = false 77 | }) 78 | } 79 | 80 | private onYesAnswer() { 81 | this.analytics.sendEvent( 82 | new AnalyticsEvent("cfnLintErrorDialogYesAnswered", {}) 83 | ) 84 | 85 | if (SUPPORTED_PLATFORMS.includes(os.platform())) { 86 | vscode.commands.executeCommand(INSTALL_CFN_LITN) 87 | } else { 88 | vscode.commands.executeCommand( 89 | "vscode.open", 90 | vscode.Uri.parse( 91 | "https://github.com/aws-cloudformation/cfn-python-lint#install" 92 | ) 93 | ) 94 | } 95 | } 96 | 97 | private onLearnMore() { 98 | this.analytics.sendEvent( 99 | new AnalyticsEvent("cfnLintErrorDialogLearnMoreAnswered", {}) 100 | ) 101 | 102 | vscode.commands.executeCommand( 103 | "vscode.open", 104 | vscode.Uri.parse( 105 | "https://github.com/threadheap/serverless-ide-vscode#settings" 106 | ) 107 | ) 108 | } 109 | } 110 | 111 | export default ( 112 | context: vscode.ExtensionContext, 113 | analytics: AnalyticsReporter 114 | ) => { 115 | return new CfnValidationMessage(context, analytics) 116 | } 117 | -------------------------------------------------------------------------------- /packages/vscode/src/workplace/find.ts: -------------------------------------------------------------------------------- 1 | import { exec } from "child_process" 2 | import join = require("lodash/join") 3 | import { Uri, workspace } from "vscode" 4 | 5 | export const filterGitIgnoredFiles = async (uris: Uri[]): Promise => { 6 | const workspaceRelativePaths = uris.map(uri => 7 | workspace.asRelativePath(uri, false) 8 | ) 9 | for (const workspaceDirectory of workspace.workspaceFolders) { 10 | const workspaceDirectoryPath = workspaceDirectory.uri.fsPath 11 | try { 12 | const { stdout, stderr } = await new Promise<{ 13 | stdout: string 14 | stderr: string 15 | }>((resolve, reject) => { 16 | exec( 17 | `git check-ignore ${workspaceRelativePaths.join(" ")}`, 18 | { cwd: workspaceDirectoryPath }, 19 | (error: Error & { code?: 0 | 1 | 128 }, stdout, stderr) => { 20 | if (error && (error.code !== 0 && error.code !== 1)) { 21 | reject(error) 22 | return 23 | } 24 | 25 | resolve({ stdout, stderr }) 26 | } 27 | ) 28 | }) 29 | 30 | if (stderr) { 31 | throw new Error(stderr) 32 | } 33 | 34 | for (const relativePath of stdout.split("\n")) { 35 | const uri = Uri.file( 36 | join(workspaceDirectoryPath, relativePath.slice(1, -1)) 37 | ) 38 | const index = uris.findIndex(u => u.fsPath === uri.fsPath) 39 | if (index > -1) { 40 | uris.splice(index, 1) 41 | } 42 | } 43 | } catch (error) {} 44 | } 45 | 46 | return uris 47 | } 48 | -------------------------------------------------------------------------------- /packages/vscode/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "include": [ 7 | "src" 8 | ] 9 | } -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "lib": [ 7 | "es2016", 8 | "dom" 9 | ], 10 | "sourceMap": true, 11 | "resolveJsonModule": true, 12 | "strict": false, 13 | "declaration": true, 14 | "skipLibCheck": true 15 | }, 16 | "exclude": [ 17 | "node_modules", 18 | "**/node_modules/**", 19 | "**/__tests__/**" 20 | ] 21 | } --------------------------------------------------------------------------------