diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..99ef3b7 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "tabWidth": 2, + "useTabs": false, + "printWidth": 160 +} diff --git a/DESIGN.md b/DESIGN.md index d707e35..1d7fa1f 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -11,10 +11,10 @@ Once every file has been parsed, generate a JSON with the information gathered a ## High-level implementation ```ts -public walker(root: string, filesExploredPath: Array): Array +public reader(root: string): Array ``` -The function `walker` will start at the root directory and save those files candidates of having components defined inside of them. Only will consider files with `.ts` extension. Should traverse all the file tree from the root up to the bottom and return the full path to every candidate file. +The function `reader` will start at the root directory and save those files candidates of having components defined inside of them. Only will consider files with `.ts` extension. Should traverse all the file tree from the root up to the bottom and return the full path to every candidate file, the content of the file and the type of definition that contains that file (component, pipe or directive). ```ts public parser(filePaths: Array): Array diff --git a/src/generator.ts b/src/generator.ts index 108eec2..f2488d2 100644 --- a/src/generator.ts +++ b/src/generator.ts @@ -3,23 +3,26 @@ import logger from "./shared/logger"; const fs = require("fs"); const path = require("path"); +/** + * + * @param {Array} files An array of tokens generated after parsing the content of every file + * @param {string} outputPath The location in which the function has to write the file with the deffinitions + * of the snippets + */ export const generator = (files: Array, outputPath: string): void => { // scope will be html only for now let json = Object(); for (const file of files) { - let component = Object(), inputs = "", index = 1; + let component = Object(), + inputs = "", + index = 1; component.scope = "html"; component.prefix = file.prefix; for (let input of file.inputs) { - if (input.type?.indexOf('|') != -1 && input.type) { - inputs += - ` [${input.inputName}]=\"$\{${index}\|${input.type.replace(/(\s)\|(\s)/g, ',') - .replace(/'/g, '')}\|\}\"`; - } - else { - inputs += input.type === "string" ? - ` ${input.inputName}=` : - ` [${input.inputName}]=`; + if (input.type?.indexOf("|") != -1 && input.type) { + inputs += ` [${input.inputName}]=\"$\{${index}\|${input.type.replace(/(\s)\|(\s)/g, ",").replace(/'/g, "")}\|\}\"`; + } else { + inputs += input.type === "string" ? ` ${input.inputName}=` : ` [${input.inputName}]=`; inputs += `\"$${index}\"`; } ++index; @@ -30,11 +33,9 @@ export const generator = (files: Array, outputPath: string): void => { outputs += ` (${output.outputName})=\"$${index}\"`; ++index; } - component.body = [ - `<${file.prefix}` + inputs + outputs + `>`, - ]; + component.body = [`<${file.prefix}` + inputs + outputs + `>`]; json[file.componentName] = { - ...component + ...component, }; } const dir = path.join(outputPath, "/out.code-snippets"); diff --git a/src/index.ts b/src/index.ts index eced47b..5e2ee15 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,13 @@ const path = require("path"); const argv = process.argv; -import * as walker from "./walker"; +import * as reader from "./reader"; import * as parser from "./parser"; import * as generator from "./generator"; import { File } from "./shared/IFile"; import logger from "./shared/logger"; import { ICLIConfig } from "./shared/ICLIConfig"; +import { FileType } from "./shared/constants"; +import { IFileData } from "./shared/IFileData"; let config: ICLIConfig = { workingDir: null, @@ -65,10 +67,7 @@ export const run = async (args: string[]) => { process.env.ROOT_PROJECT_PATH = config.workingDir || path.posix.resolve(); - let candidateFilePaths: Array = walker.walker( - process.env.ROOT_PROJECT_PATH as string, - [] - ); + let candidateFilePaths: Array = reader.reader(process.env.ROOT_PROJECT_PATH as string); let fileData: Array = parser.parser(candidateFilePaths); generator.generator(fileData, config.outputDir as string); }; diff --git a/src/parser.ts b/src/parser.ts index 9a375c4..93147be 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1,302 +1,321 @@ const fs = require("fs"); const path = require("path"); +import { FileType } from "./shared/constants"; import { File, Input, Output } from "./shared/IFile"; +import { IFileData } from "./shared/IFileData"; import logger from "./shared/logger"; import pathResolver from "./utils/path-resolver"; +import { REGEX_SELECTORS } from "./utils/regexSelectors"; -// selectors -const componentSelector = /export(\s+)class(\s+)[a-zA-Z0-9-_]+/g; -const componentHTMLselector = /selector:(\s+)(\"|')[a-zA-Z-_]+(\"|')/g; - -// inputs -// @Input() variableName; and @Input() variableName -const regularInputSelector = /@Input\(\)(\s+)[a-zA-Z0-9-_]+(;|)/g; -// @Input() variableName: type; and @Input() variableName: number = 9; -const regularInputWithTypeSelector = /@Input\(\)(\s+)[a-zA-Z0-9-_]+:(\s+)[a-zA-Z0-9-_]+((;|)|(\s+)[a-zA-Z0-9-_]+(\s+)=(\s+)[a-zA-Z0-9-_]+(;|))/g; -// @Input('inputName') varName: type; and @Input("inputName") varName: type -const customNameInputWithTypeSelector = /@Input\(('|")[a-zA-Z0-9-_]+('|")\)(\s+)[a-zA-Z0-9-_]+:(\s+)[a-zA-Z0-9-_]+\;/g; -const regularInputLiteralTypeSelector = /@Input\(\)(\s+)[a-zA-Z0-9-_]+:((\s+)(('|")[a-zA-Z0-9-_]+('|")((\s+)\|)))+(\s+)('|")[a-zA-Z0-9-_]+('|")(;|:|)/g; -//@Input() set foo(value) {} -const setterInputSelector = /@Input\(\)(\s+)set(\s+)[a-zA-Z0-9-_]+\([a-zA-Z0-9-_]+\)(\s+)/g; -//@Input() set foo(value: type) {} -const setterInputWithTypeSelector = /@Input\(\)(\s+)set(\s+)[a-zA-Z0-9-_]+\([a-zA-Z0-9-_]+:(\s+)[a-zA-Z0-9-_]+\)(\s+)/g; -// @Input('inputNameC') varName = 'adv'; -const setterInputCustomNameSelector = /@Input\(("|')[a-zA-Z0-9-_]+("|')\)(\s+)[a-zA-Z0-9-_]+(\s+)=(\s+)[A-Za-z0-9"']+(;|)/g; -//@Input() set foo(value: 'type1' | 'type2') {} -const setterInputLiteralTypeSelector = /@Input\(\)(\s+)set(\s+)[a-zA-Z0-9-_]+\([a-zA-Z0-9-_]+:((\s+)('|")[a-zA-Z0-9-_]+('|")(\s+)\|)+(\s)('|")[a-zA-Z0-9-_]+('|")\)/g; +// TODO: Test in other OS (github actions) +/** + * + * @param {IFIleData} data An array of files that can contain component or class definitions + * @returns {File} An array filed of tokens that contains information about each component parsed + */ +export const parser = (data: Array): Array => { + let result: Array = [], + tmp: Array> = []; // a temporal variable used for storing @Inputs/@Outputs declared on a parent class -// outputs -// @Output() buttonClick: EventEmitter = new EventEmitter() -const regularOutputSelector = /@Output\(\)(\s+)[a-zA-Z0-9-_]+:(\s+)EventEmitter<[a-zA-Z0-9-_]+>(\s+)=(\s+)new(\s+)EventEmitter\(\);/g; + for (const item of data) { + let parsedData; + switch (item.type) { + case "CLASS": + parsedData = parseClassDefinition(item); + if (parsedData) { + tmp.push(parsedData); + } + break; + default: + parsedData = parseComponentDefinition(item); + if (parsedData) { + result.push(parsedData); + } + break; + } + } + // we're asuming there won't be a lot of classes extending others + // we could make this algorithm more efficient by storing tmp variables on a dicc + // instead of iterating over the array every time + // TODO: Make it efficient, its O(n^m)!!! + for (let i = 0; i < result.length; ++i) { + if (result[i].extendedClassFilepath) { + for (let j = 0; j < tmp.length; ++j) { + if (result[i].extendedClassFilepath === tmp[j].filePath) { + result[i].inputs = [...result[i].inputs, ...(tmp[j].inputs as [])] as Input[]; + result[i].outputs = [...result[i].outputs, ...(tmp[j].outputs as [])] as Output[]; + break; + } + } + } + } + return result; +}; -// 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; -// TODO: class implementation with inputs/outputs defined +/** + * @private + * @param {IFIleData} file A file that is CLASS type + * @returns {string | undefined} A token with the data extracted from the class definition file + */ +const parseClassDefinition = (file: IFileData): Partial | undefined => { + if (file.fileData?.match(/@Input/g) == null && file.fileData?.match(/@Output/g) == null) { + logger.log("No Inputs or Outputs defined in this file class:", file.filePath); + return undefined; + } -// 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 - let tmp: Array> = []; + const inputs: Array = parseInputs(file.fileData), + outputs: Array = parseOutputs(file.fileData), + extendedClassFilepath = parseExtendedClassPath(file); + /** + * 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); + */ + return { + filePath: file.filePath, + inputs, + outputs, + extendedClassFilepath, + }; +}; - for (const filePath of filePaths) { - let file: string = fs.readFileSync(filePath, { - encoding: "utf8", - flag: "r", - }); +/** + * @private + * @param {IFIleData} file A file that is COMPONENT type + * @returns {string | undefined} A token with the data extracted from the class definition file + */ +const parseComponentDefinition = (file: IFileData): File | undefined => { + if (file.fileData?.match(/@Input/g) == null && file.fileData?.match(/@Output/g) == null) { + logger.log("No component, Inputs or Outputs defined in this file:", file.filePath); + return undefined; + } - let containsComponentDef = false; - if (file?.match(/@Component/g)?.length || 0 > 0) { - containsComponentDef = true; - } + const componentName = parseComponentName(file.fileData), + prefix = parseComponentSelector(file); + if (!componentName || !prefix) { + return undefined; + } + const inputs: Array = parseInputs(file.fileData), + outputs: Array = parseOutputs(file.fileData), + extendedClassFilepath = parseExtendedClassPath(file); - if ( - !containsComponentDef && - file?.match(/@Input/g) == null && - file?.match(/@Output/g) == null - ) { - logger.log("No component, Inputs or Outputs defined in this file"); - continue; - } + return { + filePath: file.filePath, + prefix, + componentName, + inputs, + outputs, + extendedClassFilepath, + }; +}; - let fileNameData: Array = file?.match(componentSelector) || []; - if (fileNameData.length === 0) { - logger.warn("Component tag not defined by any class."); - continue; - } +/** + * @private + * @param {string} file a string where to look for input definitions + * @returns {Array} An array containing all the inputs defined in the given string + */ +const parseInputs = (file: string): Array => { + // notice we ignore the default value of the input in the regex + // Input() foo: 'type1' | 'type2' + let inputsData: Array = file?.match(REGEX_SELECTORS.regularInputLiteralTypeSelector) || [], + inputs: Array = []; + for (let input of 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({ + inputName: tmp[1].replace(":", ""), + type, + }); + } + file = file.replace(REGEX_SELECTORS.regularInputLiteralTypeSelector, ""); - // we consider only one component per file - fileNameData[0].replace(/(\s+)/, " "); - const componentName: string = fileNameData[0].split(" ")[2]; - logger.log("Name of the component:", componentName); + // @Input() variableName: type; and @Input() variableName: number = 9; + inputsData = []; + inputsData = file?.match(REGEX_SELECTORS.regularInputWithTypeSelector) || []; + for (let input of inputsData) { + let tmp: Array = input.replace(/(\s+)/g, " ").split(" "); + inputs.push({ + inputName: tmp[1].replace(":", ""), + type: tmp[2].replace(";", ""), + }); + } + file = file.replace(REGEX_SELECTORS.regularInputWithTypeSelector, ""); - let selector = ""; - if (containsComponentDef) { - // match returns a string not an array - let componentSelectorData: Array = - file?.match(componentHTMLselector) || []; - if (componentSelectorData.length === 0) { - logger.warn( - "Component doesn't define any selector but contains @Component anotation." - ); - continue; - } - componentSelectorData[0].replace(/(\s+)/g, " "); - selector = componentSelectorData[0].split(" ")[1].replace(/('|")/g, ""); - logger.log("Selector:", selector); - } + inputsData = []; + // @Input('inputName') varName: type; and @Input("inputName") varName: type + inputsData = file?.match(REGEX_SELECTORS.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, ""); + inputs.push({ + inputName, + type: tmp[2].replace(";", ""), + }); + } + file = file.replace(REGEX_SELECTORS.customNameInputWithTypeSelector, ""); - // notice we ignore the default value of the input in the regex - // Input() foo: 'type1' | 'type2' - let inputs: Array = []; - let inputsData: Array = - file?.match(regularInputLiteralTypeSelector) || []; - for (let input of 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({ - inputName: tmp[1].replace(":", ""), - type, - }); - } - file = file.replace(regularInputLiteralTypeSelector, ""); + // @Input('inputNameC') varName = 'adv'; + // @Input("inputNameD") varName = 2354; + inputsData = []; + inputsData = file?.match(REGEX_SELECTORS.setterInputCustomNameSelector) || []; + 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, ""); + inputs.push({ + inputName, + type: undefined, + }); + } + file = file.replace(REGEX_SELECTORS.setterInputCustomNameSelector, ""); - // @Input() variableName: type; and @Input() variableName: number = 9; - inputsData = []; - inputsData = file?.match(regularInputWithTypeSelector) || []; - for (let input of inputsData) { - let tmp: Array = input.replace(/(\s+)/g, " ").split(" "); - inputs.push({ - inputName: tmp[1].replace(":", ""), - type: tmp[2].replace(";", ""), - }); - } - file = file.replace(regularInputWithTypeSelector, ""); + //@Input() set foo(value) {} + inputsData = []; + inputsData = file?.match(REGEX_SELECTORS.setterInputSelector) || []; + for (let input of inputsData) { + let tmp: Array = input.replace(/(\s+)/g, " ").split(" "); + const inputName = tmp[2].replace(/(\s+)/g, "").split("(")[0]; + inputs.push({ + inputName, + type: undefined, + }); + } + file = file.replace(REGEX_SELECTORS.setterInputSelector, ""); - inputsData = []; - // @Input('inputName') varName: type; and @Input("inputName") varName: type - 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, ""); - inputs.push({ - inputName, - type: tmp[2].replace(";", ""), - }); - } - file = file.replace(customNameInputWithTypeSelector, ""); + //@Input() set foo(value: type) {} + inputsData = []; + inputsData = file?.match(REGEX_SELECTORS.setterInputWithTypeSelector) || []; + 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[3].replace(/(\s+)/g, "").split(")")[0]; + inputs.push({ + inputName, + type, + }); + } + file = file.replace(REGEX_SELECTORS.setterInputWithTypeSelector, ""); - // @Input('inputNameC') varName = 'adv'; - // @Input("inputNameD") varName = 2354; - inputsData = []; - 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) || [ - "", - ])[0].replace(/'|"/g, ""); - inputs.push({ - inputName, - type: undefined, - }); - } - file = file.replace(setterInputCustomNameSelector, ""); + //@Input() set foo(value: 'type1' | 'type2') {} + inputsData = []; + inputsData = file?.match(REGEX_SELECTORS.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, " "); + inputs.push({ + inputName, + type, + }); + } + file = file.replace(REGEX_SELECTORS.setterInputLiteralTypeSelector, ""); - //@Input() set foo(value) {} - inputsData = []; - 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]; - inputs.push({ - inputName, - type: undefined, - }); + // @Input() variableName; and @Input() variableName. Also for now we will parse + // in this part of the code @Input() variableName = value and @Input() variableName = value; + inputsData = []; + inputsData = file?.match(REGEX_SELECTORS.regularInputSelector) || []; + for (let input of inputsData) { + let tmp: Array = input.replace(/(\s+)/g, " ").split(" "); + const inputName = tmp[1].replace(";", ""); + if (inputs.filter((elem) => elem.inputName == inputName).length > 0) { + continue; } - file = file.replace(setterInputSelector, ""); + inputs.push({ + inputName, + type: undefined, + }); + } + file = file.replace(REGEX_SELECTORS.regularInputSelector, ""); + logger.log("Inputs detected:", inputs); + return inputs; +}; - //@Input() set foo(value: type) {} - inputsData = []; - 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]; - const type = tmp[3].replace(/(\s+)/g, "").split(")")[0]; - inputs.push({ - inputName, - type, - }); - } - file = file.replace(setterInputWithTypeSelector, ""); +/** + * @private + * @param {string} file a string where to look for output definitions + * @returns {Array} An array containing all the outputs defined in the given string + */ +const parseOutputs = (file: string): Array => { + let outputs: Array = []; + // only @Output() buttonClick: EventEmitter = new EventEmitter(); for now + let outputsData: Array = file?.match(REGEX_SELECTORS.regularOutputSelector) || []; + for (let output of outputsData) { + let tmp: Array = output.replace(/(\s+)/g, " ").split(" "); + outputs.push({ + outputName: tmp[1].replace(":", ""), + type: tmp[2].substr(tmp[2].indexOf("<"), tmp[2].indexOf(">")).replace(">", "").replace("<", ""), + }); + } + file = file.replace(REGEX_SELECTORS.regularOutputSelector, ""); + logger.log("Outputs detected:", outputs); - //@Input() set foo(value: 'type1' | 'type2') {} - inputsData = []; - 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, " "); - inputs.push({ - inputName, - type, - }); - } - file = file.replace(setterInputLiteralTypeSelector, ""); + return outputs; +}; - // @Input() variableName; and @Input() variableName. Also for now we will parse - // in this part of the code @Input() variableName = value and @Input() variableName = value; - inputsData = []; - inputsData = file?.match(regularInputSelector) || []; - for (let input of inputsData) { - let tmp: Array = input.replace(/(\s+)/g, " ").split(" "); - const inputName = tmp[1].replace(";", ""); - if (inputs.filter((elem) => elem.inputName == inputName).length > 0) { - continue; - } - inputs.push({ - inputName, - type: undefined, - }); - } - file = file.replace(regularInputSelector, ""); - logger.log("Inputs detected:", inputs); +/** + * @private + * @param {IFIleData} file The class definition that is candidate to extend another class + * @returns {string | undefined} The absolute resolved extended class path or undefined if doesn´t extend any class + */ +const parseExtendedClassPath = (file: IFileData): string | undefined => { + let extendedClassPath: string | undefined; + if (file.fileData?.match(REGEX_SELECTORS.extendedClassSelector)) { + // we should see if the extended class is in tmp and if not extract the inputs defined inside + let matchExtendedClass: Array = file.fileData?.match(REGEX_SELECTORS.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.fileData?.match(REGEX_SELECTORS.extendedClassPathSelector) || []; - let outputs: Array = []; - // only @Output() buttonClick: EventEmitter = new EventEmitter(); for now - let outputsData: Array = file?.match(regularOutputSelector) || []; - for (let output of outputsData) { - let tmp: Array = output.replace(/(\s+)/g, " ").split(" "); - outputs.push({ - outputName: tmp[1].replace(":", ""), - type: tmp[2] - .substr(tmp[2].indexOf("<"), tmp[2].indexOf(">")) - .replace(">", "") - .replace("<", ""), - }); - } - file = file.replace(regularOutputSelector, ""); - logger.log("Outputs detected:", outputs); + extendedClassPath = pathResolver.resolve(file.filePath, matchExtendedClassPath[0]) as string; - let extendedClassPath; - 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) || []; - // 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) || []; + logger.log("path:", extendedClassPath); + } - extendedClassPath = pathResolver.resolve( - filePath, - matchExtendedClassPath[0] - ); - - logger.log("path:", extendedClassPath); - } + return extendedClassPath; +}; - if (containsComponentDef) { - result.push({ - fileLocation: filePath, - prefix: selector, - componentName: componentName, - inputs: inputs, - outputs: outputs, - 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: path.resolve(filePath), - inputs: inputs, - outputs: outputs, - extendedClassFilepath: undefined, - }); - } +/** + * @private + * @param {string} file + * @returns {string | undefined} The name of the component + */ +const parseComponentName = (file: string): string | undefined => { + let fileNameData: Array = file?.match(REGEX_SELECTORS.componentSelector) || []; + if (fileNameData.length === 0) { + logger.warn("Component tag not defined by any class."); + return undefined; } - // we're asuming there won't be a lot of classes extending others - // we could make this algorithm more efficient by storing tmp variables on a dicc - // instead of iterating over the array every time - // TODO: Make it efficient, its O(n^m)!!! - for (let i = 0; i < result.length; ++i) { - if (result[i].extendedClassFilepath) { - for (let j = 0; j < tmp.length; ++j) { - if (result[i].extendedClassFilepath === tmp[j].fileLocation) { - result[i].inputs = [ - ...result[i].inputs, - ...(tmp[j].inputs as []), - ] as Input[]; - result[i].outputs = [ - ...result[i].outputs, - ...(tmp[j].outputs as []), - ] as Output[]; - break; - } - } - } + + // we consider only one component per file + fileNameData[0].replace(/(\s+)/, " "); + const componentName: string = fileNameData[0].split(" ")[2]; + logger.log("Name of the component:", componentName); + return componentName; +}; + +/** + * @private + * @param {IFIleData} file + * @returns {string | undefined} The component selector + */ +const parseComponentSelector = (file: IFileData): string | undefined => { + let selector; + // match returns a string not an array + let componentSelectorData: Array = file.fileData?.match(REGEX_SELECTORS.componentHTMLselector) || []; + if (componentSelectorData.length === 0) { + logger.warn("Component doesn't define any selector but contains @Component anotation:", file.filePath); + return undefined; } - return result; + componentSelectorData[0].replace(/(\s+)/g, " "); + selector = componentSelectorData[0].split(" ")[1].replace(/('|")/g, ""); + logger.log("Selector:", selector); + return selector; }; diff --git a/src/reader.ts b/src/reader.ts new file mode 100644 index 0000000..60e54a3 --- /dev/null +++ b/src/reader.ts @@ -0,0 +1,57 @@ +import { Dirent } from "fs"; +import { FileType } from "./shared/constants"; +import { IFileData } from "./shared/IFileData"; +import logger from "./shared/logger"; + +const fs = require("fs"); +const path = require("path"); + +/** + * + * @param {string} root The root of the directory in which the function has + * to start reading files + * @returns {Array} An array that contains all the files that are candidates of having + * components or class definitions + */ +export const reader = (root: string): Array => { + let pendingDir: Array = [root], + result: Array = []; + + while (pendingDir.length > 0) { + let currentDir = pendingDir.shift(); + + if (currentDir == null) break; + + const files: Dirent[] = fs.readdirSync(currentDir, { + encoding: "utf8", + withFileTypes: true, + }); + + for (let file of files) { + if (file.isFile() && file.name.endsWith(".ts")) { + logger.log("Candidate file to contain component definition:", file.name); + const filePath = path.join(currentDir, file.name); + const fileData: string = fs.readFileSync(filePath, { + encoding: "utf8", + flag: "r", + }); + + let type: FileType = "CLASS"; + if (fileData?.match(/@Component/g)?.length || 0 > 0) { + type = "COMPONENT"; + } + + // In the future we will add FileType PIPE & FileType DIRECTIVE + + result.push({ + type, + filePath, + fileData, + }); + } else if (file.isDirectory() && file.name != "node_modules" && file.name != ".git") { + pendingDir.push(path.join(currentDir, file.name)); + } + } + } + return result; +}; diff --git a/src/shared/IFile.ts b/src/shared/IFile.ts index c40ef7b..423417f 100644 --- a/src/shared/IFile.ts +++ b/src/shared/IFile.ts @@ -1,5 +1,5 @@ export interface File { - fileLocation: string; + filePath: string; prefix: string; componentName: string; inputs: Array; diff --git a/src/shared/IFileData.ts b/src/shared/IFileData.ts new file mode 100644 index 0000000..6df8157 --- /dev/null +++ b/src/shared/IFileData.ts @@ -0,0 +1,7 @@ +import { FileType } from "./constants"; + +export interface IFileData { + type: FileType; + filePath: string; + fileData: string; +} diff --git a/src/shared/constants.ts b/src/shared/constants.ts new file mode 100644 index 0000000..4fa46ad --- /dev/null +++ b/src/shared/constants.ts @@ -0,0 +1 @@ +export type FileType = "CLASS" | "COMPONENT"; diff --git a/src/shared/logger.ts b/src/shared/logger.ts index ec2a7c5..2863b8d 100644 --- a/src/shared/logger.ts +++ b/src/shared/logger.ts @@ -12,26 +12,25 @@ let debuggerEnabled = false; export const enableDebugger = () => { debuggerEnabled = true; -} +}; export const disableDebugger = () => { debuggerEnabled = false; -} +}; export const log = (message: string, args?: any[] | any) => { - if (!debuggerEnabled) return; + if (!debuggerEnabled) return; console.log(logPrefix + message, args); }; export const warn = (message: string, args?: any[] | any) => { - if (!debuggerEnabled) return; + if (!debuggerEnabled) return; console.log(warnPrefix + message, args); }; export const err = (message: string, args?: any[] | any) => { - console.log(errorPrefix + message, args); process.exit(); }; @@ -41,4 +40,4 @@ export default { warn, err, enableDebugger, -}; \ No newline at end of file +}; diff --git a/src/utils/path-resolver.ts b/src/utils/path-resolver.ts index 4bc4fa4..7ff9e1b 100644 --- a/src/utils/path-resolver.ts +++ b/src/utils/path-resolver.ts @@ -12,10 +12,7 @@ let tsconfigFile: { * @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 => { +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 @@ -37,8 +34,9 @@ export const resolve = ( const rootProjectDir = process.env.ROOT_PROJECT_PATH; if (tsconfigFile == null) { + const tsconfigFilePath = path.resolve(`${rootProjectDir}/tsconfig.json`); tsconfigFile = JSON.parse( - fs.readFileSync(path.resolve(`${rootProjectDir}/tsconfig.json`), { + fs.readFileSync(tsconfigFilePath, { encoding: "utf8", flag: "r", }) @@ -48,30 +46,20 @@ export const resolve = ( let compilerOptionsPathsKey = pathToFile.substr(0, pathToFile.indexOf("/")), compilerOptionsPathsValue: Array = [""]; if (compilerOptionsPathsKey + "/*" in tsconfigFile?.compilerOptions.paths) { - compilerOptionsPathsValue = - tsconfigFile?.compilerOptions.paths[compilerOptionsPathsKey + "/*"]; + compilerOptionsPathsValue = tsconfigFile?.compilerOptions.paths[compilerOptionsPathsKey + "/*"]; } // TODO: else throw an exception - compilerOptionsPathsValue[0] = compilerOptionsPathsValue[0].replace( - "/*", - "" - ); + 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" + 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" - ); + 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 diff --git a/src/utils/regexSelectors.ts b/src/utils/regexSelectors.ts new file mode 100644 index 0000000..0a21823 --- /dev/null +++ b/src/utils/regexSelectors.ts @@ -0,0 +1,30 @@ +// selectors +export const REGEX_SELECTORS = { + componentSelector: /export(\s+)class(\s+)[a-zA-Z0-9-_]+/g, + componentHTMLselector: /selector:(\s+)(\"|')[a-zA-Z-_]+(\"|')/g, + // inputs + // @Input() variableName; and @Input() variableName + regularInputSelector: /@Input\(\)(\s+)[a-zA-Z0-9-_]+(;|)/g, + // @Input() variableName: type; and @Input() variableName: number = 9; + regularInputWithTypeSelector: /@Input\(\)(\s+)[a-zA-Z0-9-_]+:(\s+)[a-zA-Z0-9-_]+((;|)|(\s+)[a-zA-Z0-9-_]+(\s+)=(\s+)[a-zA-Z0-9-_]+(;|))/g, + // @Input('inputName') varName: type; and @Input("inputName") varName: type + customNameInputWithTypeSelector: /@Input\(('|")[a-zA-Z0-9-_]+('|")\)(\s+)[a-zA-Z0-9-_]+:(\s+)[a-zA-Z0-9-_]+\;/g, + regularInputLiteralTypeSelector: /@Input\(\)(\s+)[a-zA-Z0-9-_]+:((\s+)(('|")[a-zA-Z0-9-_]+('|")((\s+)\|)))+(\s+)('|")[a-zA-Z0-9-_]+('|")(;|:|)/g, + //@Input() set foo(value) {} + setterInputSelector: /@Input\(\)(\s+)set(\s+)[a-zA-Z0-9-_]+\([a-zA-Z0-9-_]+\)(\s+)/g, + //@Input() set foo(value: type) {} + setterInputWithTypeSelector: /@Input\(\)(\s+)set(\s+)[a-zA-Z0-9-_]+\([a-zA-Z0-9-_]+:(\s+)[a-zA-Z0-9-_]+\)(\s+)/g, + // @Input('inputNameC') varName = 'adv'; + setterInputCustomNameSelector: /@Input\(("|')[a-zA-Z0-9-_]+("|')\)(\s+)[a-zA-Z0-9-_]+(\s+)=(\s+)[A-Za-z0-9"']+(;|)/g, + //@Input() set foo(value: 'type1' | 'type2') {} + setterInputLiteralTypeSelector: + /@Input\(\)(\s+)set(\s+)[a-zA-Z0-9-_]+\([a-zA-Z0-9-_]+:((\s+)('|")[a-zA-Z0-9-_]+('|")(\s+)\|)+(\s)('|")[a-zA-Z0-9-_]+('|")\)/g, + + // outputs + // @Output() buttonClick: EventEmitter = new EventEmitter() + regularOutputSelector: /@Output\(\)(\s+)[a-zA-Z0-9-_]+:(\s+)EventEmitter<[a-zA-Z0-9-_]+>(\s+)=(\s+)new(\s+)EventEmitter\(\);/g, + + // other + extendedClassSelector: /export(\s+)class(\s+)[a-zA-Z0-9-_]+(\s+)extends(\s+)[a-zA-Z0-9-_]+/g, + extendedClassPathSelector: /import(\s+){(\s+)[a-zA-Z0-9-_]+(\s+)}(\s+)from(\s+)[\/\@A-Za-z0-9."'_-]+/g, +}; diff --git a/src/walker.ts b/src/walker.ts deleted file mode 100644 index 6887dda..0000000 --- a/src/walker.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Dirent } from "fs"; -import logger from "./shared/logger"; - -const fs = require("fs"); -const path = require("path"); - -// TODO: Parse files synchronously -export const walker = ( - root: string, - filesExploredPath: Array -): Array => { - let pendingDir: Array = [root]; - - while (pendingDir.length > 0) { - let currentDir = pendingDir.shift(); - - if (currentDir == null) break; - - const files: Dirent[] = fs.readdirSync(currentDir, { - encoding: "utf8", - withFileTypes: true, - }); - - for (let file of files) { - if (file.isFile() && file.name.endsWith(".ts")) { - logger.log("Candidate file to contain component definition:", file.name); - filesExploredPath.push(path.join(currentDir, file.name)); - } else if ( - file.isDirectory() && - file.name != "node_modules" && - file.name != ".git" - ) { - pendingDir.push(path.join(currentDir, file.name)); - } - } - } - return filesExploredPath; -}; diff --git a/tests/fixtures/ts-files/foo.ts b/tests/fixtures/ts-files/foo.ts index e69de29..e5df803 100644 --- a/tests/fixtures/ts-files/foo.ts +++ b/tests/fixtures/ts-files/foo.ts @@ -0,0 +1,5 @@ +import { Input } from "@angular/core"; + +export class BaseComponent { + @Input() baseInput: "type1" | "type2" | "type3"; +} diff --git a/tests/fixtures/ts-files/index.ts b/tests/fixtures/ts-files/index.ts deleted file mode 100644 index e69de29..0000000 diff --git a/tests/fixtures/ts-files/var.ts b/tests/fixtures/ts-files/var.ts new file mode 100644 index 0000000..1b76cee --- /dev/null +++ b/tests/fixtures/ts-files/var.ts @@ -0,0 +1,8 @@ +import { Input } from "@angular/core"; +@Component({ + selector: "app-main", + templateUrl: "./app.main.component.html", +}) +export class BaseComponent { + @Input() baseInput: "type1" | "type2" | "type3"; +} diff --git a/tests/generator.test.ts b/tests/generator.test.ts index 2f4694d..f5f7a67 100644 --- a/tests/generator.test.ts +++ b/tests/generator.test.ts @@ -3,37 +3,32 @@ const fs = require("fs"); const path = require("path"); test("JSON file generation", async () => { - generator.generator([ - { - componentName: "MainComponent", - prefix: "app-main", - inputs: [ - { inputName: "appName", type: "MediaModel" }, - { inputName: "var", type: "'type1' | 'type2'" }, - { inputName: "foo", type: "TypeError" }, - { inputName: "fooStr", type: "string" }, - ], - outputs: [ - { outputName: "buttonClick", type: "any" }, - { outputName: "fooVar", type: "number" }, - ], - fileLocation: path.join( - path.posix.resolve(), - "/tests/fixtures/parser/main.component.ts" - ), - extendedClassFilepath: undefined - } - ], path.join(path.posix.resolve(), "/out")); - const expectedResult = '{"MainComponent": { "scope": "html", "prefix": "app-main", "body": [ "" ] } }'.replace( - /\s/g, - "" + generator.generator( + [ + { + componentName: "MainComponent", + prefix: "app-main", + inputs: [ + { inputName: "appName", type: "MediaModel" }, + { inputName: "var", type: "'type1' | 'type2'" }, + { inputName: "foo", type: "TypeError" }, + { inputName: "fooStr", type: "string" }, + ], + outputs: [ + { outputName: "buttonClick", type: "any" }, + { outputName: "fooVar", type: "number" }, + ], + fileLocation: path.join(path.posix.resolve(), "/tests/fixtures/parser/main.component.ts"), + extendedClassFilepath: undefined, + }, + ], + path.join(path.posix.resolve(), "/out") ); - const data = fs - .readFileSync( - path.join(path.posix.resolve(), "/out/out.code-snippets"), - "utf-8" - ) - .replace(/\s+/g, "") - .replace(/\\/g, ""); + const expectedResult = + '{"MainComponent": { "scope": "html", "prefix": "app-main", "body": [ "" ] } }'.replace( + /\s/g, + "" + ); + const data = fs.readFileSync(path.join(path.posix.resolve(), "/out/out.code-snippets"), "utf-8").replace(/\s+/g, "").replace(/\\/g, ""); expect(data).toEqual(expectedResult); }); diff --git a/tests/index.test.ts b/tests/index.test.ts index 0a16199..4813289 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -3,5 +3,5 @@ const fs = require("fs"); const path = require("path"); test("End to end test", async () => { - // TODO + // TODO }); diff --git a/tests/parser.test.ts b/tests/parser.test.ts index 0821ac1..6292771 100644 --- a/tests/parser.test.ts +++ b/tests/parser.test.ts @@ -1,18 +1,19 @@ import * as parser from "../src/parser"; const path = require("path"); +// TODO: Rewrite the tests so we have: +// 1. For normal inputs +// 2. For complex inputs +// 3. For normal outputs +// 4. For class inheritance + test("Parses the contents of the candidate files and returns an array of File type", async () => { + process.env.ROOT_PROJECT_PATH = "C:/Users/roger/oos/angular-vs-snippets/tests/fixtures/parser"; const result = [ { componentName: "MainComponent", - extendedClassFilepath: path.join( - path.posix.resolve(), - "/tests/fixtures/parser/base.component.ts" - ), - fileLocation: path.join( - path.posix.resolve(), - "/tests/fixtures/parser/main.component.ts" - ), + extendedClassFilepath: path.join(path.posix.resolve(), "/tests/fixtures/parser/base.component.ts"), + filePath: path.join(path.posix.resolve(), "/tests/fixtures/parser/main.component.ts"), inputs: [ { inputName: "literalType1", @@ -102,36 +103,75 @@ test("Parses the contents of the candidate files and returns an array of File ty ]; expect( parser.parser([ - path.join( - path.posix.resolve(), - "/tests/fixtures/parser/main.component.ts" - ), - path.join( - path.posix.resolve(), - "/tests/fixtures/parser/base.component.ts" - ), - path.join( - path.posix.resolve(), - "/tests/fixtures/parser/component.module.ts" - ), + { + fileData: `import { Input } from \"@angular/core\", + export class BaseComponent { + @Input() baseInput: 'type1' | 'type2' | 'type3'; + }`, + filePath: "C:\\Users\\roger\\oos\\angular-vs-snippets\\tests\\fixtures\\parser\\base.component.ts", + type: "CLASS", + }, + { + fileData: `import { CommonModule } from '@angular/common'; + import { NgModule } from '@angular/core'; + + @NgModule({ + declarations: [Component], + imports: [CommonModule], + exports: [Component], + }) + export class ComponentModule {}`, + filePath: "C:\\Users\\roger\\oos\\angular-vs-snippets\\tests\\fixtures\\parser\\component.module.ts", + type: "CLASS", + }, + { + fileData: `import { Component, Input } from \"@angular/core\"; + import { BaseComponent } from \"./base.component\"; + + @Component({ + selector: \"app-main\", + templateUrl: \"./app.main.component.html\", + }) + 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() appName: MediaModel; + @Input() foo: TypeError; + @Input() numberInput: number = 9; + @Input(\"inputNameA\") varName: type; + @Input(\"inputNameB\") varName: type; + @Input(\"inputNameC\") varName = \"adv\"; + @Input(\"inputNameD\") varName = 2354; + @Input() withoutType; + @Input() withoutTypeNorSemicolon; + @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\") {} + + someRandomFunction(action) { + action = \"action\"; + } + }`, + filePath: "C:\\Users\\roger\\oos\\angular-vs-snippets\\tests\\fixtures\\parser\\main.component.ts", + type: "COMPONENT", + }, ]) ).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"; + 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" - ), + extendedClassFilepath: path.join(path.posix.resolve(), "/tests/fixtures/parser/special-path-tsconfig/special-base.component.ts"), + filePath: path.join(path.posix.resolve(), "/tests/fixtures/parser/special-path-tsconfig/special-path.component.ts"), inputs: [ { inputName: "inputInChildClass", @@ -148,14 +188,29 @@ test("Tests the parser when de file is imported using the @ special keyword path ]; 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" - ), + { + fileData: `import { Input } from \"@angular/core\"; + + export class SpecialBaseComponent { + @Input() baseInputInSpecialBaseClass: \"type1\" | \"type2\" | \"type3\"; + }`, + filePath: "C:\\Users\\roger\\oos\\angular-vs-snippets\\tests\\fixtures\\parser\\special-path-tsconfig\\special-base.component.ts", + type: "CLASS", + }, + { + fileData: `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\"; + }`, + filePath: "C:\\Users\\roger\\oos\\angular-vs-snippets\\tests\\fixtures\\parser\\special-path-tsconfig\\special-path.component.ts", + type: "COMPONENT", + }, ]) ).toStrictEqual(result); }); diff --git a/tests/reader.test.ts b/tests/reader.test.ts new file mode 100644 index 0000000..fb71747 --- /dev/null +++ b/tests/reader.test.ts @@ -0,0 +1,30 @@ +import * as reader from "../src/reader"; +const path = require("path"); + +test("Inspects /tests/fixtures/ts-files with reader function", () => { + expect(reader.reader(path.join(path.posix.resolve(), "tests/fixtures/ts-files"))).toStrictEqual([ + { + type: "CLASS", + filePath: "C:\\Users\\roger\\oos\\angular-vs-snippets\\tests\\fixtures\\ts-files\\foo.ts", + fileData: + 'import { Input } from "@angular/core";\r\n' + + "\r\n" + + "export class BaseComponent {\r\n" + + ' @Input() baseInput: "type1" | "type2" | "type3";\r\n' + + "}\r\n", + }, + { + type: "COMPONENT", + filePath: "C:\\Users\\roger\\oos\\angular-vs-snippets\\tests\\fixtures\\ts-files\\var.ts", + fileData: + 'import { Input } from "@angular/core";\r\n' + + "@Component({\r\n" + + ' selector: "app-main",\r\n' + + ' templateUrl: "./app.main.component.html",\r\n' + + "})\r\n" + + "export class BaseComponent {\r\n" + + ' @Input() baseInput: "type1" | "type2" | "type3";\r\n' + + "}\r\n", + }, + ]); +}); diff --git a/tests/walker.test.ts b/tests/walker.test.ts deleted file mode 100644 index d7eeb4b..0000000 --- a/tests/walker.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { markAsUntransferable } from "worker_threads"; -import * as walker from "../src/walker"; -const path = require("path"); - -test("Inspects /tests/fixtures/ts-files with walker function", () => { - expect( - walker.walker( - path.join(path.posix.resolve(), "tests/fixtures/ts-files"), - [] - ) - ).toStrictEqual([ - path.join(path.posix.resolve(), "/tests/fixtures/ts-files/foo.ts"), - path.join(path.posix.resolve(), "/tests/fixtures/ts-files/index.ts"), - ]); -});