diff --git a/.vscode/launch.json b/.vscode/launch.json index 5183bee..e7d5201 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -19,17 +19,11 @@ "request": "launch", "name": "End to end execution", "preLaunchTask": "npm: build:cjs", - "program": "./build/ng-vs-snippets", - "args": - [ - "--dir", - "./src", - "--output", - "./.vscode" - ], + "program": "./build/@roguib/ng-vs-snippets.js", + "args": ["--dir", "./src", "--output", "./.vscode"], "console": "integratedTerminal", "internalConsoleOptions": "neverOpen", - "disableOptimisticBPs": true, + "disableOptimisticBPs": true } ] } diff --git a/README.md b/README.md index 4f0acdc..af395bd 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # ng-vs-snippets -![ts](https://badgen.net/badge/Built%20With/TypeScript/blue) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![made-for-VSCode](https://img.shields.io/badge/Made%20for-VSCode-1f425f.svg)](https://code.visualstudio.com/) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](http://makeapullrequest.com) - +![ts](https://badgen.net/badge/Built%20With/TypeScript/blue) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![made-for-VSCode](https://img.shields.io/badge/Made%20for-VSCode-1f425f.svg)](https://code.visualstudio.com/) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](http://makeapullrequest.com) Automatic VS Code snippet file generation for Angular codebases. Seamlessly maintain up-to-date VS Code snippets of all of your code. @@ -9,27 +8,27 @@ Currently, we support snippet generation from the following Angular elements:
-| Element | Status | -|-----------|:-----------------------------------------:| -| Component | :white_check_mark: Supported | -| Directive | :construction_worker: Not yet supported | -| Pipe | :construction_worker: Not yet supported | +| Element | Status | +| --------- | :-------------------------------------: | +| Component | :white_check_mark: Supported | +| Directive | :construction_worker: Not yet supported | +| Pipe | :construction_worker: Not yet supported |
## Installation -Install ```ng-vs-snippets``` as a dev-dependency in your Angular project. To do so, run: +Install `ng-vs-snippets` as a dev-dependency in your Angular project. To do so, run: ``` -npm i ng-vs-snippets --save-dev +npm i @roguib/ng-vs-snippets --save-dev ``` -Create a ```package.json``` script to extract snippets from your codebase: +Create a `package.json` script to extract snippets from your codebase: ```json "scripts": { - "ng-vs-snippets": "ng-vs-snippets --dir ./src --output ./.vscode", + "ng-vs-snippets": "ng-vs-snippets --dir . --output ./.vscode", }, ``` @@ -39,16 +38,18 @@ Execute the script by running: npm run ng-vs-snippets ``` -The script will generate a ```out.code-snippets``` file containing all the definitions. **Make sure you don't have any file with the same name** since the data contained in that file **is going to be replaced**. +The script will generate a `out.code-snippets` file containing all the definitions. **Make sure you don't have any file with the same name** since the data contained in that file **is going to be replaced**. ## Troubleshooting -Sometimes, due to VS Code configuration issues, snippets don't appear in the suggestion's dropdown. Make sure to specify, in VS Code ```settings.json``` configuration file the following properties: + +Sometimes, due to VS Code configuration issues, snippets don't appear in the suggestion's dropdown. Make sure to specify, in VS Code `settings.json` configuration file the following properties: ```json "editor.tabCompletion": "on", "editor.snippetSuggestions": "top" ``` -If this doesn't fix the problem, open the command palette and search for ```Preferences: Configure User Snippets``` to ensure the editor is considering the fille where your generated snippets are defined. + +If this doesn't fix the problem, open the command palette and search for `Preferences: Configure User Snippets` to ensure the editor is considering the fille where your generated snippets are defined. ## Documentation @@ -56,4 +57,4 @@ You can find the [full document design at this url](). ## Contributing -Pull requests are welcome :) Make sure to create an issue first so anyone's work is overlaped. \ No newline at end of file +Pull requests are welcome :) Make sure to create an issue first so anyone's work is overlaped. diff --git a/package-lock.json b/package-lock.json index bf4f410..3d60285 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "ng-vs-snippets", + "name": "@roguib/ng-vs-snippets", "version": "0.1.0", "lockfileVersion": 1, "requires": true, diff --git a/package.json b/package.json index 990a819..a526e4a 100644 --- a/package.json +++ b/package.json @@ -14,15 +14,15 @@ "angular", "vs snippets" ], - "main": "build/ng-vs-snippets.js", - "types": "build/ng-vs-snippets.d.ts", + "main": "build/@roguib/ng-vs-snippets.js", + "types": "build/@roguib/ng-vs-snippets.d.ts", "bin": { - "ng-vs-snippets": "build/ng-vs-snippets.js" + "ng-vs-snippets": "build/@roguib/ng-vs-snippets.js" }, "scripts": { "build:cjs": "rollup -c", "check": "tsc", - "run": "node ./build/index.js", + "run": "node ./build/@roguib/ng-vs-snippets.js", "test": "npm run build:cjs && jest ./tests/*" }, "dependencies": { diff --git a/src/index.ts b/src/index.ts index bfc5539..eced47b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,4 @@ -const fs = require("fs"); const path = require("path"); -const readline = require("readline"); const argv = process.argv; import * as walker from "./walker"; import * as parser from "./parser"; @@ -17,6 +15,9 @@ let config: ICLIConfig = { const parseArgs = (args: string[]) => { // TODO: Include multiple options + + logger.log("args:", args); + while (args.length > 0) { const arg = args.shift(); @@ -52,25 +53,24 @@ export const verifyArgs = () => { if (config.outputDir == null) { logger.err("No output directory specified. Aborting."); } -} +}; export const run = async (args: string[]) => { parseArgs(args); verifyArgs(); - // const config = parseArgs(args); if (config.debug) { logger.enableDebugger(); } + + process.env.ROOT_PROJECT_PATH = config.workingDir || path.posix.resolve(); + let candidateFilePaths: Array = walker.walker( - config.workingDir || path.posix.resolve(), + process.env.ROOT_PROJECT_PATH as string, [] ); let fileData: Array = parser.parser(candidateFilePaths); - generator.generator( - fileData, - config.outputDir as string - ); + generator.generator(fileData, config.outputDir as string); }; run(argv.slice(2, argv.length)); diff --git a/src/parser.ts b/src/parser.ts index fd070d8..9a375c4 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -3,6 +3,7 @@ const path = require("path"); import { File, Input, Output } from "./shared/IFile"; import logger from "./shared/logger"; +import pathResolver from "./utils/path-resolver"; // selectors const componentSelector = /export(\s+)class(\s+)[a-zA-Z0-9-_]+/g; @@ -31,11 +32,11 @@ const regularOutputSelector = /@Output\(\)(\s+)[a-zA-Z0-9-_]+:(\s+)EventEmitter< // other const extendedClassSelector = /export(\s+)class(\s+)[a-zA-Z0-9-_]+(\s+)extends(\s+)[a-zA-Z0-9-_]+/g; -const extendedClassPathSelector = /import(\s+){(\s+)[a-zA-Z0-9-_]+(\s+)}(\s+)from(\s+)[\/A-Za-z0-9."'_-]+/g; +const extendedClassPathSelector = /import(\s+){(\s+)[a-zA-Z0-9-_]+(\s+)}(\s+)from(\s+)[\/\@A-Za-z0-9."'_-]+/g; // TODO: class implementation with inputs/outputs defined -// TODO: Test in in other OS (github actions) -// TODO: Read files synchronously +// TODO: Test in other OS (github actions) +// TODO: Read files asynchronously to improve performance? export const parser = (filePaths: Array): Array => { let result: Array = []; // a temporal variable used for storing @Inputs/@Outputs declared on a parent class @@ -61,8 +62,7 @@ export const parser = (filePaths: Array): Array => { continue; } - let fileNameData: Array = - file?.match(componentSelector) || []; + let fileNameData: Array = file?.match(componentSelector) || []; if (fileNameData.length === 0) { logger.warn("Component tag not defined by any class."); continue; @@ -93,15 +93,14 @@ export const parser = (filePaths: Array): Array => { // Input() foo: 'type1' | 'type2' let inputs: Array = []; let inputsData: Array = - file?.match( - regularInputLiteralTypeSelector - ) || []; + file?.match(regularInputLiteralTypeSelector) || []; for (let input of inputsData) { - logger.log('inputs parsed:', inputsData); + logger.log("inputs parsed:", inputsData); let tmp: Array = input.replace(/(\s)+/g, " ").split(" "); let type = tmp .slice(2, tmp.length) .join() + .replace(/\"/g, "'") .replace(";", "") .replace(/,/g, ""); inputs.push({ @@ -113,13 +112,9 @@ export const parser = (filePaths: Array): Array => { // @Input() variableName: type; and @Input() variableName: number = 9; inputsData = []; - inputsData = - file?.match( - regularInputWithTypeSelector - ) || []; + inputsData = file?.match(regularInputWithTypeSelector) || []; for (let input of inputsData) { let tmp: Array = input.replace(/(\s+)/g, " ").split(" "); - logger.log("input data", inputsData); inputs.push({ inputName: tmp[1].replace(":", ""), type: tmp[2].replace(";", ""), @@ -129,16 +124,11 @@ export const parser = (filePaths: Array): Array => { inputsData = []; // @Input('inputName') varName: type; and @Input("inputName") varName: type - inputsData = - file?.match( - customNameInputWithTypeSelector - ) || []; + inputsData = file?.match(customNameInputWithTypeSelector) || []; for (let input of inputsData) { let tmp: Array = input.replace(/(\s+)/g, " ").split(" "); - const inputName = (tmp[0].match(/('|")[a-zA-Z0-9-_]+('|")/g) || [])[0].replace( - /'|"/g, - "" - ); + const inputName = (tmp[0].match(/('|")[a-zA-Z0-9-_]+('|")/g) || + [])[0].replace(/'|"/g, ""); inputs.push({ inputName, type: tmp[2].replace(";", ""), @@ -149,10 +139,7 @@ export const parser = (filePaths: Array): Array => { // @Input('inputNameC') varName = 'adv'; // @Input("inputNameD") varName = 2354; inputsData = []; - inputsData = - file?.match( - setterInputCustomNameSelector - ) || []; + inputsData = file?.match(setterInputCustomNameSelector) || []; for (let input of inputsData) { let tmp: Array = input.replace(/(\s+)/g, " ").split(" "); const inputName = (tmp[0].match(/('|")[a-zA-Z0-9-_]+('|")/g) || [ @@ -167,10 +154,7 @@ export const parser = (filePaths: Array): Array => { //@Input() set foo(value) {} inputsData = []; - inputsData = - file?.match( - setterInputSelector - ) || []; + inputsData = file?.match(setterInputSelector) || []; for (let input of inputsData) { let tmp: Array = input.replace(/(\s+)/g, " ").split(" "); const inputName = tmp[2].replace(/(\s+)/g, "").split("(")[0]; @@ -183,10 +167,7 @@ export const parser = (filePaths: Array): Array => { //@Input() set foo(value: type) {} inputsData = []; - inputsData = - file?.match( - setterInputWithTypeSelector - ) || []; + inputsData = file?.match(setterInputWithTypeSelector) || []; for (let input of inputsData) { let tmp: Array = input.replace(/(\s+)/g, " ").split(" "); const inputName = tmp[2].replace(/(\s+)/g, "").split("(")[0]; @@ -200,18 +181,15 @@ export const parser = (filePaths: Array): Array => { //@Input() set foo(value: 'type1' | 'type2') {} inputsData = []; - inputsData = - file?.match( - setterInputLiteralTypeSelector - ) || []; + inputsData = file?.match(setterInputLiteralTypeSelector) || []; for (let input of inputsData) { let tmp: Array = input.replace(/(\s+)/g, " ").split(" "); const inputName = tmp[2].replace(/(\s+)/g, "").split("(")[0]; const type = tmp - .slice(3, tmp.length) - .join() - .replace(/'|"|\)/g, "") - .replace(/,/g, " "); + .slice(3, tmp.length) + .join() + .replace(/'|"|\)/g, "") + .replace(/,/g, " "); inputs.push({ inputName, type, @@ -239,10 +217,7 @@ export const parser = (filePaths: Array): Array => { let outputs: Array = []; // only @Output() buttonClick: EventEmitter = new EventEmitter(); for now - let outputsData: Array = - file?.match( - regularOutputSelector - ) || []; + let outputsData: Array = file?.match(regularOutputSelector) || []; for (let output of outputsData) { let tmp: Array = output.replace(/(\s+)/g, " ").split(" "); outputs.push({ @@ -257,33 +232,23 @@ export const parser = (filePaths: Array): Array => { logger.log("Outputs detected:", outputs); let extendedClassPath; - if ( - file?.match( - extendedClassSelector - ) - ) { + if (file?.match(extendedClassSelector)) { // we should see if the extended class is in tmp and if not extract the inputs defined inside let matchExtendedClass: Array = - file?.match( - extendedClassSelector - ) || []; + file?.match(extendedClassSelector) || []; // resolve the path of the class let extendedClass: string = matchExtendedClass[0] .replace(/(\s+)/g, " ") .split(" ")[4]; logger.log("extendedClassName:", extendedClass); let matchExtendedClassPath: Array = - file?.match( - extendedClassPathSelector - ) || []; - // TODO: Document this in notes. Notice that by using path.join(path.dirname(fullComponentClassPath), relative path of the base path from ComponentClassPath) resolves into the full path of the base path - extendedClassPath = path.join( - path.dirname(filePath), + file?.match(extendedClassPathSelector) || []; + + extendedClassPath = pathResolver.resolve( + filePath, matchExtendedClassPath[0] - .replace(/(\s+)/g, " ") - .replace(/"/g, "") - .split(" ")[5] + ".ts" ); + logger.log("path:", extendedClassPath); } @@ -297,8 +262,15 @@ export const parser = (filePaths: Array): Array => { extendedClassFilepath: extendedClassPath || undefined, }); } else { + /** + * TODO: Instead of working with relative paths and converting them + * to absolute when needed, we can start the exec by transforming every + * relative path to absolute, and then clean up the resolve calls in the program + * that transforms the code into an spaguetti one. Also it could help by reducing + * the amount of times we call path.join(path.posix.resolve(), path); + */ tmp.push({ - fileLocation: filePath, + fileLocation: path.resolve(filePath), inputs: inputs, outputs: outputs, extendedClassFilepath: undefined, diff --git a/src/utils/path-resolver.ts b/src/utils/path-resolver.ts new file mode 100644 index 0000000..4bc4fa4 --- /dev/null +++ b/src/utils/path-resolver.ts @@ -0,0 +1,83 @@ +const path = require("path"); +const fs = require("fs"); + +let tsconfigFile: { + compilerOptions: { + paths: any; + }; +} | null = null; + +/** + * @param {string} filePath Path of the file that extends the base class + * @param {string} importExpr The expression used to import the base class + * @returns {string} An absolute path to the imported file + */ +export const resolve = ( + filePath: string, + importExpr: string +): string | null => { + /** + * @param {string} importExpr The expression used to import the class in the file + * @returns {string} The the path in the import expression + */ + const extractPathFromImportExpr = (importExpr: string): string => { + let pathIndex = importExpr.indexOf('"'); + + if (pathIndex === -1) { + pathIndex = importExpr.indexOf("'"); + } + let path = importExpr.substr(pathIndex + 1); + return path.replace(/\"/g, "").replace(/'/g, ""); + }; + + let resolvedPath = "", + pathToFile = extractPathFromImportExpr(importExpr); + + if (pathToFile.startsWith("@")) { + const rootProjectDir = process.env.ROOT_PROJECT_PATH; + + if (tsconfigFile == null) { + tsconfigFile = JSON.parse( + fs.readFileSync(path.resolve(`${rootProjectDir}/tsconfig.json`), { + encoding: "utf8", + flag: "r", + }) + ); + } + + let compilerOptionsPathsKey = pathToFile.substr(0, pathToFile.indexOf("/")), + compilerOptionsPathsValue: Array = [""]; + if (compilerOptionsPathsKey + "/*" in tsconfigFile?.compilerOptions.paths) { + compilerOptionsPathsValue = + tsconfigFile?.compilerOptions.paths[compilerOptionsPathsKey + "/*"]; + } // TODO: else throw an exception + + compilerOptionsPathsValue[0] = compilerOptionsPathsValue[0].replace( + "/*", + "" + ); + + // Notice that by calling path.join with a relative path of the base + // path from ComponentClassPath and the full path of the file resolves into the + // full path of the base path + resolvedPath = path.join( + path.posix.resolve(), + pathToFile + .replace(compilerOptionsPathsKey, compilerOptionsPathsValue[0]) + .replace(/(\s+)/g, " ") + .replace(/"/g, "") + ".ts" + ); + } else { + resolvedPath = path.join( + path.dirname(filePath), + pathToFile.replace(/(\s+)/g, " ").replace(/"/g, "") + ".ts" + ); + } + + // TODO: Throw an error if the function is unable to resolve the path + return resolvedPath; +}; + +export default { + resolve, +}; diff --git a/tests/fixtures/parser/main.component.ts b/tests/fixtures/parser/main.component.ts index bd225ca..bf3abeb 100644 --- a/tests/fixtures/parser/main.component.ts +++ b/tests/fixtures/parser/main.component.ts @@ -7,31 +7,25 @@ import { BaseComponent } from "./base.component"; }) export class MainComponent extends BaseComponent { foo = false; - @Input() literalType1: 'type1' | 'type2' | 'type3'; - @Input() literalType2: 'type1' | 'type2' | 'type3' = 'type1'; - @Input() literal_Type3: 'type1' | 'type2' | 'type3' = 'type1'; + @Input() literalType1: "type1" | "type2" | "type3"; + @Input() literalType2: "type1" | "type2" | "type3" = "type1"; + @Input() literal_Type3: "type1" | "type2" | "type3" = "type1"; @Input() appName: MediaModel; @Input() foo: TypeError; @Input() numberInput: number = 9; @Input("inputNameA") varName: type; - @Input('inputNameB') varName: type; - @Input('inputNameC') varName = 'adv'; + @Input("inputNameB") varName: type; + @Input("inputNameC") varName = "adv"; @Input("inputNameD") varName = 2354; @Input() withoutType; @Input() withoutTypeNorSemicolon; - @Input() variableAssignedValue = 9 + @Input() variableAssignedValue = 9; @Input() variableAssignedValueAndSemicolon = value; @Output() buttonClick: EventEmitter = new EventEmitter(); @Output() fooVar: EventEmitter = new EventEmitter(); - @Input() set Foo(value) { - - } - @Input() set FooType(value: string) { - - } - @Input() set FooTypeLiteral(value: 'literal1' | 'literal2' | 'literal3') { - - } + @Input() set Foo(value) {} + @Input() set FooType(value: string) {} + @Input() set FooTypeLiteral(value: "literal1" | "literal2" | "literal3") {} someRandomFunction(action) { action = "action"; diff --git a/tests/fixtures/parser/special-path-tsconfig/special-base.component.ts b/tests/fixtures/parser/special-path-tsconfig/special-base.component.ts new file mode 100644 index 0000000..e3405f0 --- /dev/null +++ b/tests/fixtures/parser/special-path-tsconfig/special-base.component.ts @@ -0,0 +1,5 @@ +import { Input } from "@angular/core"; + +export class SpecialBaseComponent { + @Input() baseInputInSpecialBaseClass: "type1" | "type2" | "type3"; +} diff --git a/tests/fixtures/parser/special-path-tsconfig/special-path.component.ts b/tests/fixtures/parser/special-path-tsconfig/special-path.component.ts new file mode 100644 index 0000000..79ac015 --- /dev/null +++ b/tests/fixtures/parser/special-path-tsconfig/special-path.component.ts @@ -0,0 +1,10 @@ +import { Component, Input } from "@angular/core"; +import { SpecialBaseComponent } from "@special-base/special-base.component"; + +@Component({ + selector: "app-main", + templateUrl: "./app.main.component.html", +}) +export class SpecialPathComponent extends SpecialBaseComponent { + @Input() inputInChildClass: "type1" | "type2" | "type3"; +} diff --git a/tests/fixtures/parser/tsconfig.json b/tests/fixtures/parser/tsconfig.json new file mode 100644 index 0000000..2475b1c --- /dev/null +++ b/tests/fixtures/parser/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist/out-tsc", + "sourceMap": true, + "sourceRoot": "/", + "declaration": false, + "downlevelIteration": true, + "experimentalDecorators": true, + "module": "esnext", + "moduleResolution": "node", + "importHelpers": true, + "target": "es2015", + "lib": ["es2018", "dom"], + "paths": { + "@special-base/*": ["tests/fixtures/parser/special-path-tsconfig"] + } + }, + "angularCompilerOptions": { + "fullTemplateTypeCheck": true, + "strictInjectionParameters": true + } +} diff --git a/tests/parser.test.ts b/tests/parser.test.ts index 0101bd5..0821ac1 100644 --- a/tests/parser.test.ts +++ b/tests/parser.test.ts @@ -5,8 +5,7 @@ test("Parses the contents of the candidate files and returns an array of File ty const result = [ { componentName: "MainComponent", - extendedClassFilepath: - path.join( + extendedClassFilepath: path.join( path.posix.resolve(), "/tests/fixtures/parser/base.component.ts" ), @@ -118,3 +117,45 @@ test("Parses the contents of the candidate files and returns an array of File ty ]) ).toStrictEqual(result); }); + +test("Tests the parser when de file is imported using the @ special keyword path defined in tsconfig.json", async () => { + process.env.ROOT_PROJECT_PATH = + "C:/Users/roger/oos/angular-vs-snippets/tests/fixtures/parser"; + const result = [ + { + componentName: "SpecialPathComponent", + extendedClassFilepath: path.join( + path.posix.resolve(), + "/tests/fixtures/parser/special-path-tsconfig/special-base.component.ts" + ), + fileLocation: path.join( + path.posix.resolve(), + "/tests/fixtures/parser/special-path-tsconfig/special-path.component.ts" + ), + inputs: [ + { + inputName: "inputInChildClass", + type: "'type1'|'type2'|'type3'", + }, + { + inputName: "baseInputInSpecialBaseClass", + type: "'type1'|'type2'|'type3'", + }, + ], + outputs: [], + prefix: "app-main", + }, + ]; + expect( + parser.parser([ + path.join( + path.posix.resolve(), + "/tests/fixtures/parser/special-path-tsconfig/special-base.component.ts" + ), + path.join( + path.posix.resolve(), + "/tests/fixtures/parser/special-path-tsconfig/special-path.component.ts" + ), + ]) + ).toStrictEqual(result); +});