From f50f22d73a0c44715c30450dbe49e4f237ad2f04 Mon Sep 17 00:00:00 2001 From: rosen-vladimirov Date: Mon, 18 Feb 2019 19:55:07 +0200 Subject: [PATCH] feat: detect and report short imports used in application In NativeScript 5.2.0 we've deprecated support for short imports. Add ability in CLI to check if there are such imports in the application's code and report them to users. The logic will be executed whenever application is prepared for build/livesync or when `tns doctor` is executed inside project dir. Create class with static variables for ProjectTypes. These values must not be changed as they are used in Analytics, so changing them will break the current reports. --- lib/common/declarations.d.ts | 7 + lib/common/helpers.ts | 7 +- lib/common/test/unit-tests/helpers.ts | 36 +++++ lib/constants.ts | 6 + lib/definitions/project.d.ts | 13 +- lib/project-data.ts | 8 +- lib/services/doctor-service.ts | 63 ++++++++- lib/services/platform-service.ts | 3 + lib/services/project-data-service.ts | 28 +++- test/npm-support.ts | 3 + test/platform-commands.ts | 3 + test/platform-service.ts | 3 + test/services/doctor-service.ts | 190 ++++++++++++++++++++++++++ test/services/project-data-service.ts | 163 +++++++++++++++++++++- test/stubs.ts | 6 +- 15 files changed, 524 insertions(+), 15 deletions(-) create mode 100644 test/services/doctor-service.ts diff --git a/lib/common/declarations.d.ts b/lib/common/declarations.d.ts index cd9c9e5b94..a132c8d25b 100644 --- a/lib/common/declarations.d.ts +++ b/lib/common/declarations.d.ts @@ -1168,6 +1168,13 @@ interface IDoctorService { * @returns {Promise} true if the environment is properly configured for local builds */ canExecuteLocalBuild(platform?: string, projectDir?: string, runtimeVersion?: string): Promise; + + /** + * Checks and notifies users for deprecated short imports in their applications. + * @param {string} projectDir Path to the application. + * @returns {void} + */ + checkForDeprecatedShortImportsInAppDir(projectDir: string): void; } interface IUtils { diff --git a/lib/common/helpers.ts b/lib/common/helpers.ts index 1152026caf..29089ee08f 100644 --- a/lib/common/helpers.ts +++ b/lib/common/helpers.ts @@ -8,6 +8,12 @@ import * as crypto from "crypto"; import * as _ from "lodash"; const Table = require("cli-table"); +const STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; + +export function stripComments(content: string): string { + const newContent = content.replace(STRIP_COMMENTS, ""); + return newContent; +} export function doesCurrentNpmCommandMatch(patterns?: RegExp[]): boolean { const currentNpmCommandArgv = getCurrentNpmCommandArgv(); @@ -682,7 +688,6 @@ const CONSTRUCTOR_ARGS = /constructor\s*([^\(]*)\(\s*([^\)]*)\)/m; const FN_NAME_AND_ARGS = /^(?:function)?\s*([^\(]*)\(\s*([^\)]*)\)\s*(=>)?\s*[{_]/m; const FN_ARG_SPLIT = /,/; const FN_ARG = /^\s*(_?)(\S+?)\1\s*$/; -const STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; export function annotate(fn: any) { let $inject: any, diff --git a/lib/common/test/unit-tests/helpers.ts b/lib/common/test/unit-tests/helpers.ts index b7f3a1f34e..f21c1db2ec 100644 --- a/lib/common/test/unit-tests/helpers.ts +++ b/lib/common/test/unit-tests/helpers.ts @@ -825,4 +825,40 @@ describe("helpers", () => { }); }); }); + + describe("stripComments", () => { + const testData: ITestData[] = [ + { + input: `// this is comment, +const test = require("./test");`, + expectedResult: `\nconst test = require("./test");` + }, + { + input: `/* this is multiline +comment */ +const test = require("./test");`, + expectedResult: `\nconst test = require("./test");` + }, + { + input: `/* this is multiline +comment +// with inner one line comment inside it +the multiline comment finishes here +*/ +const test = require("./test");`, + expectedResult: `\nconst test = require("./test");` + }, + + { + input: `const test /*inline comment*/ = require("./test");`, + expectedResult: `const test = require("./test");` + }, + ]; + + it("strips comments correctly", () => { + testData.forEach(testCase => { + assertTestData(testCase, helpers.stripComments); + }); + }); + }); }); diff --git a/lib/constants.ts b/lib/constants.ts index 2e82a9c2ea..ffc869fcba 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -116,6 +116,12 @@ export const NgFlavorName = "Angular"; export const VueFlavorName = "Vue.js"; export const TsFlavorName = "Plain TypeScript"; export const JsFlavorName = "Plain JavaScript"; +export class ProjectTypes { + public static NgFlavorName = NgFlavorName; + public static VueFlavorName = VueFlavorName; + public static TsFlavorName = "Pure TypeScript"; + public static JsFlavorName = "Pure JavaScript"; +} export const BUILD_OUTPUT_EVENT_NAME = "buildOutput"; export const CONNECTION_ERROR_EVENT_NAME = "connectionError"; export const USER_INTERACTION_NEEDED_EVENT_NAME = "userInteractionNeeded"; diff --git a/lib/definitions/project.d.ts b/lib/definitions/project.d.ts index 4af5382abc..017253cbfc 100644 --- a/lib/definitions/project.d.ts +++ b/lib/definitions/project.d.ts @@ -172,6 +172,13 @@ interface IProjectDataService { * @returns {Promise} An object describing the current asset structure for Android. */ getAndroidAssetsStructure(opts: IProjectDir): Promise; + + /** + * Returns array with paths to all `.js` or `.ts` files in application's app directory. + * @param {string} projectDir Path to application. + * @returns {string[]} Array of paths to `.js` or `.ts` files. + */ + getAppExecutableFiles(projectDir: string): string[]; } interface IAssetItem { @@ -535,9 +542,9 @@ interface ICocoaPodsService { /** * Merges pod's xcconfig file into project's xcconfig file - * @param projectData - * @param platformData - * @param opts + * @param projectData + * @param platformData + * @param opts */ mergePodXcconfigFile(projectData: IProjectData, platformData: IPlatformData, opts: IRelease): Promise; } diff --git a/lib/project-data.ts b/lib/project-data.ts index 2fe31b0f2f..07a73475f8 100644 --- a/lib/project-data.ts +++ b/lib/project-data.ts @@ -16,19 +16,19 @@ export class ProjectData implements IProjectData { */ private static PROJECT_TYPES: IProjectType[] = [ { - type: "Pure JavaScript", + type: constants.ProjectTypes.JsFlavorName, isDefaultProjectType: true }, { - type: constants.NgFlavorName, + type: constants.ProjectTypes.NgFlavorName, requiredDependencies: ["@angular/core", "nativescript-angular"] }, { - type: constants.VueFlavorName, + type: constants.ProjectTypes.VueFlavorName, requiredDependencies: ["nativescript-vue"] }, { - type: "Pure TypeScript", + type: constants.ProjectTypes.TsFlavorName, requiredDependencies: ["typescript", "nativescript-dev-typescript"] } ]; diff --git a/lib/services/doctor-service.ts b/lib/services/doctor-service.ts index 4e93ff0124..0e9d6bae5e 100644 --- a/lib/services/doctor-service.ts +++ b/lib/services/doctor-service.ts @@ -1,10 +1,10 @@ import { EOL } from "os"; import * as path from "path"; import * as helpers from "../common/helpers"; -import { TrackActionNames } from "../constants"; +import { TrackActionNames, NODE_MODULES_FOLDER_NAME, TNS_CORE_MODULES_NAME } from "../constants"; import { doctor, constants } from "nativescript-doctor"; -class DoctorService implements IDoctorService { +export class DoctorService implements IDoctorService { private static DarwinSetupScriptLocation = path.join(__dirname, "..", "..", "setup", "mac-startup-shell-script.sh"); private static WindowsSetupScriptExecutable = "powershell.exe"; private static WindowsSetupScriptArguments = ["start-process", "-FilePath", "PowerShell.exe", "-NoNewWindow", "-Wait", "-ArgumentList", '"-NoProfile -ExecutionPolicy Bypass -Command iex ((new-object net.webclient).DownloadString(\'https://www.nativescript.org/setup/win\'))"']; @@ -14,10 +14,12 @@ class DoctorService implements IDoctorService { private $logger: ILogger, private $childProcess: IChildProcess, private $injector: IInjector, + private $projectDataService: IProjectDataService, + private $fs: IFileSystem, private $terminalSpinnerService: ITerminalSpinnerService, private $versionsService: IVersionsService) { } - public async printWarnings(configOptions?: { trackResult: boolean , projectDir?: string, runtimeVersion?: string, options?: IOptions }): Promise { + public async printWarnings(configOptions?: { trackResult: boolean, projectDir?: string, runtimeVersion?: string, options?: IOptions }): Promise { const infos = await this.$terminalSpinnerService.execute({ text: `Getting environment information ${EOL}` }, () => doctor.getInfos({ projectDir: configOptions && configOptions.projectDir, androidRuntimeVersion: configOptions && configOptions.runtimeVersion })); @@ -48,6 +50,8 @@ class DoctorService implements IDoctorService { this.$logger.error("Cannot get the latest versions information from npm. Please try again later."); } + this.checkForDeprecatedShortImportsInAppDir(configOptions.projectDir); + await this.$injector.resolve("platformEnvironmentRequirements").checkEnvironmentRequirements({ platform: null, projectDir: configOptions && configOptions.projectDir, @@ -113,6 +117,59 @@ class DoctorService implements IDoctorService { return !hasWarnings; } + public checkForDeprecatedShortImportsInAppDir(projectDir: string): void { + if (projectDir) { + try { + const files = this.$projectDataService.getAppExecutableFiles(projectDir); + const shortImports = this.getDeprecatedShortImportsInFiles(files, projectDir); + if (shortImports.length) { + this.$logger.printMarkdown("Detected short imports in your application. Please note that `short imports are deprecated` since NativeScript 5.2.0. More information can be found in this blogpost https://www.nativescript.org/blog/say-goodbye-to-short-imports-in-nativescript"); + shortImports.forEach(shortImport => { + this.$logger.printMarkdown(`In file \`${shortImport.file}\` line \`${shortImport.line}\` is short import. Add \`tns-core-modules/\` in front of the required/imported module.`); + }); + } + } catch (err) { + this.$logger.trace(`Unable to validate if project has short imports. Error is`, err); + } + } + } + + protected getDeprecatedShortImportsInFiles(files: string[], projectDir: string): { file: string, line: string }[] { + const shortImportRegExps = this.getShortImportRegExps(projectDir); + const shortImports: { file: string, line: string }[] = []; + + for (const file of files) { + const fileContent = this.$fs.readText(file); + const strippedComments = helpers.stripComments(fileContent); + const linesWithRequireStatements = strippedComments + .split(/\r?\n/) + .filter(line => /\btns-core-modules\b/.exec(line) === null && (/\bimport\b/.exec(line) || /\brequire\b/.exec(line))); + + for (const line of linesWithRequireStatements) { + for (const regExp of shortImportRegExps) { + const matches = line.match(regExp); + + if (matches && matches.length) { + shortImports.push({ file, line }); + break; + } + } + } + } + + return shortImports; + } + + private getShortImportRegExps(projectDir: string): RegExp[] { + const pathToTnsCoreModules = path.join(projectDir, NODE_MODULES_FOLDER_NAME, TNS_CORE_MODULES_NAME); + const contents = this.$fs.readDirectory(pathToTnsCoreModules) + .filter(entry => this.$fs.getFsStats(path.join(pathToTnsCoreModules, entry)).isDirectory()); + + const regExps = contents.map(c => new RegExp(`[\"\']${c}[\"\'/]`, "g")); + + return regExps; + } + private async runSetupScriptCore(executablePath: string, setupScriptArgs: string[]): Promise { return this.$childProcess.spawnFromEvent(executablePath, setupScriptArgs, "close", { stdio: "inherit" }); } diff --git a/lib/services/platform-service.ts b/lib/services/platform-service.ts index eda6449b9f..58cf1ea65d 100644 --- a/lib/services/platform-service.ts +++ b/lib/services/platform-service.ts @@ -27,6 +27,7 @@ export class PlatformService extends EventEmitter implements IPlatformService { private $errors: IErrors, private $fs: IFileSystem, private $logger: ILogger, + private $doctorService: IDoctorService, private $packageInstallationManager: IPackageInstallationManager, private $platformsData: IPlatformsData, private $projectDataService: IProjectDataService, @@ -237,6 +238,8 @@ export class PlatformService extends EventEmitter implements IPlatformService { await this.cleanDestinationApp(platformInfo); } + this.$doctorService.checkForDeprecatedShortImportsInAppDir(platformInfo.projectData.projectDir); + await this.preparePlatformCore( platformInfo.platform, platformInfo.appFilesUpdaterOptions, diff --git a/lib/services/project-data-service.ts b/lib/services/project-data-service.ts index de5e873d44..65578062ee 100644 --- a/lib/services/project-data-service.ts +++ b/lib/services/project-data-service.ts @@ -1,7 +1,7 @@ import * as path from "path"; import { ProjectData } from "../project-data"; import { exported } from "../common/decorators"; -import { NATIVESCRIPT_PROPS_INTERNAL_DELIMITER, AssetConstants, SRC_DIR, RESOURCES_DIR, MAIN_DIR, CLI_RESOURCES_DIR_NAME } from "../constants"; +import { NATIVESCRIPT_PROPS_INTERNAL_DELIMITER, AssetConstants, SRC_DIR, RESOURCES_DIR, MAIN_DIR, CLI_RESOURCES_DIR_NAME, ProjectTypes } from "../constants"; interface IProjectFileData { projectData: any; @@ -116,6 +116,32 @@ export class ProjectDataService implements IProjectDataService { }; } + public getAppExecutableFiles(projectDir: string): string[] { + const projectData = this.getProjectData(projectDir); + + let supportedFileExtension = ".js"; + if (projectData.projectType === ProjectTypes.NgFlavorName || projectData.projectType === ProjectTypes.TsFlavorName) { + supportedFileExtension = ".ts"; + } + + const files = this.$fs.enumerateFilesInDirectorySync( + projectData.appDirectoryPath, + (filePath, fstat) => { + if (filePath.indexOf(projectData.appResourcesDirectoryPath) !== -1) { + return false; + } + + if (fstat.isDirectory()) { + return true; + } + + return path.extname(filePath) === supportedFileExtension; + } + ); + + return files; + } + private getImageDefinitions(): IImageDefinitionsStructure { const pathToImageDefinitions = path.join(__dirname, "..", "..", CLI_RESOURCES_DIR_NAME, AssetConstants.assets, AssetConstants.imageDefinitionsFileName); const imageDefinitions = this.$fs.readJson(pathToImageDefinitions); diff --git a/test/npm-support.ts b/test/npm-support.ts index cb6bb86eed..7092a306f4 100644 --- a/test/npm-support.ts +++ b/test/npm-support.ts @@ -107,6 +107,9 @@ function createTestInjector(): IInjector { extractPackage: async (packageName: string, destinationDirectory: string, options?: IPacoteExtractOptions): Promise => undefined }); testInjector.register("usbLiveSyncService", () => ({})); + testInjector.register("doctorService", { + checkForDeprecatedShortImportsInAppDir: (projectDir: string): void => undefined + }); return testInjector; } diff --git a/test/platform-commands.ts b/test/platform-commands.ts index dd2d2074e7..fa19182278 100644 --- a/test/platform-commands.ts +++ b/test/platform-commands.ts @@ -176,6 +176,9 @@ function createTestInjector() { trackOptions: () => Promise.resolve(null) }); testInjector.register("usbLiveSyncService", ({})); + testInjector.register("doctorService", { + checkForDeprecatedShortImportsInAppDir: (projectDir: string): void => undefined + }); return testInjector; } diff --git a/test/platform-service.ts b/test/platform-service.ts index 98a725657b..9b85af1675 100644 --- a/test/platform-service.ts +++ b/test/platform-service.ts @@ -118,6 +118,9 @@ function createTestInjector() { } }); testInjector.register("usbLiveSyncService", () => ({})); + testInjector.register("doctorService", { + checkForDeprecatedShortImportsInAppDir: (projectDir: string): void => undefined + }); return testInjector; } diff --git a/test/services/doctor-service.ts b/test/services/doctor-service.ts new file mode 100644 index 0000000000..31617b009a --- /dev/null +++ b/test/services/doctor-service.ts @@ -0,0 +1,190 @@ +import { DoctorService } from "../../lib/services/doctor-service"; +import { Yok } from "../../lib/common/yok"; +import { LoggerStub } from "../stubs"; +import { assert } from "chai"; +import * as path from "path"; + +class DoctorServiceInheritor extends DoctorService { + constructor($analyticsService: IAnalyticsService, + $hostInfo: IHostInfo, + $logger: ILogger, + $childProcess: IChildProcess, + $injector: IInjector, + $projectDataService: IProjectDataService, + $fs: IFileSystem, + $terminalSpinnerService: ITerminalSpinnerService, + $versionsService: IVersionsService) { + super($analyticsService, $hostInfo, $logger, $childProcess, $injector, $projectDataService, $fs, $terminalSpinnerService, $versionsService); + } + + public getDeprecatedShortImportsInFiles(files: string[], projectDir: string): { file: string, line: string }[] { + return super.getDeprecatedShortImportsInFiles(files, projectDir); + } +} + +describe("doctorService", () => { + const createTestInjector = (): IInjector => { + const testInjector = new Yok(); + testInjector.register("doctorService", DoctorServiceInheritor); + testInjector.register("analyticsService", {}); + testInjector.register("hostInfo", {}); + testInjector.register("logger", LoggerStub); + testInjector.register("childProcess", {}); + testInjector.register("projectDataService", {}); + testInjector.register("fs", {}); + testInjector.register("terminalSpinnerService", {}); + testInjector.register("versionsService", {}); + + return testInjector; + }; + + describe("checkForDeprecatedShortImportsInAppDir", () => { + const tnsCoreModulesDirs = [ + "application", + "data" + ]; + + const testData: { filesContents: IStringDictionary, expectedShortImports: any[] }[] = [ + { + filesContents: { + file1: 'const application = require("application");' + }, + expectedShortImports: [{ file: "file1", line: 'const application = require("application");' }] + }, + { + filesContents: { + file1: 'const application = require("tns-core-modules/application");' + }, + expectedShortImports: [] + }, + { + filesContents: { + file1: 'const Observable = require("data/observable").Observable;' + }, + expectedShortImports: [{ file: "file1", line: 'const Observable = require("data/observable").Observable;' }] + }, + { + filesContents: { + file1: 'const Observable = require("tns-core-modules/data/observable").Observable;' + }, + expectedShortImports: [] + }, + { + filesContents: { + file1: 'import * as application from "application";' + }, + expectedShortImports: [{ file: "file1", line: 'import * as application from "application";' }] + }, + { + filesContents: { + file1: 'import * as application from "tns-core-modules/application";' + }, + expectedShortImports: [] + }, + { + filesContents: { + file1: 'import { run } from "application";' + }, + expectedShortImports: [{ file: "file1", line: 'import { run } from "application";' }] + }, + { + filesContents: { + file1: 'import { run } from "tns-core-modules/application";' + }, + expectedShortImports: [] + }, + { + // Using single quotes + filesContents: { + file1: "import { run } from 'application';" + }, + expectedShortImports: [{ file: "file1", line: "import { run } from 'application';" }] + }, + { + // Using single quotes + filesContents: { + file1: "import { run } from 'tns-core-modules/application';" + }, + expectedShortImports: [] + }, + { + filesContents: { + file1: `const application = require("application"); +const Observable = require("data/observable").Observable; +` + }, + expectedShortImports: [ + { file: "file1", line: 'const application = require("application");' }, + { file: "file1", line: 'const Observable = require("data/observable").Observable;' }, + ] + }, + { + filesContents: { + file1: `const application = require("application"); +const Observable = require("data/observable").Observable; +` + }, + expectedShortImports: [ + { file: "file1", line: 'const application = require("application");' }, + { file: "file1", line: 'const Observable = require("data/observable").Observable;' }, + ] + }, + { + filesContents: { + file1: `const application = require("application"); +const Observable = require("tns-core-modules/data/observable").Observable; +` + }, + expectedShortImports: [ + { file: "file1", line: 'const application = require("application");' }, + ] + }, + { + filesContents: { + file1: `const application = require("application"); +const Observable = require("tns-core-modules/data/observable").Observable; +`, + file2: `const application = require("tns-core-modules/application"); +const Observable = require("data/observable").Observable;` + }, + expectedShortImports: [ + { file: "file1", line: 'const application = require("application");' }, + { file: "file2", line: 'const Observable = require("data/observable").Observable;' }, + ] + }, + { + filesContents: { + // this is not from tns-core-modules + file1: `const application = require("application1"); +const Observable = require("tns-core-modules/data/observable").Observable; +`, + file2: `const application = require("some-name-tns-core-modules-widgets/application"); +const Observable = require("tns-core-modules-widgets/data/observable").Observable;` + }, + expectedShortImports: [ ] + }, + ]; + + it("getDeprecatedShortImportsInFiles returns correct results", () => { + const testInjector = createTestInjector(); + const doctorService = testInjector.resolve("doctorService"); + const fs = testInjector.resolve("fs"); + fs.getFsStats = (file) => ({ + isDirectory: () => true + }); + + fs.readDirectory = (dirPath) => { + if (dirPath.indexOf(path.join("node_modules", "tns-core-modules"))) { + return tnsCoreModulesDirs; + } + }; + + testData.forEach(({ filesContents, expectedShortImports }) => { + fs.readText = (filePath) => filesContents[filePath]; + + const shortImports = doctorService.getDeprecatedShortImportsInFiles(_.keys(filesContents), "projectDir"); + assert.deepEqual(shortImports, expectedShortImports); + }); + }); + }); +}); diff --git a/test/services/project-data-service.ts b/test/services/project-data-service.ts index e1f68149fb..414276b1e8 100644 --- a/test/services/project-data-service.ts +++ b/test/services/project-data-service.ts @@ -2,9 +2,10 @@ import { Yok } from "../../lib/common/yok"; import { assert } from "chai"; import { ProjectDataService } from "../../lib/services/project-data-service"; import { LoggerStub } from "../stubs"; -import { NATIVESCRIPT_PROPS_INTERNAL_DELIMITER, PACKAGE_JSON_FILE_NAME, AssetConstants } from '../../lib/constants'; +import { NATIVESCRIPT_PROPS_INTERNAL_DELIMITER, PACKAGE_JSON_FILE_NAME, AssetConstants, ProjectTypes } from '../../lib/constants'; import { DevicePlatformsConstants } from "../../lib/common/mobile/device-platforms-constants"; -import { basename } from "path"; +import { basename, join } from "path"; +import { FileSystem } from "../../lib/common/file-system"; const CLIENT_NAME_KEY_IN_PROJECT_FILE = "nativescript"; @@ -299,4 +300,162 @@ describe("projectDataService", () => { assert.deepEqual(assetStructure, { ios: emptyAssetStructure, android: _.merge(_.cloneDeep(emptyAssetStructure), { splashImages: null }) }); }); }); + + describe("getAppExecutableFiles", () => { + const appDirectoryPath = "appDirPath"; + const appResourcesDirectoryPath = join(appDirectoryPath, "App_Resources"); + + const getAppExecutableFilesTestData = [ + { + projectType: ProjectTypes.NgFlavorName, + appFiles: [ + "component1.ts", + "component1.js", + "component2.ts", + "App_Resources" + ], + expectedResult: [ + join(appDirectoryPath, "component1.ts"), + join(appDirectoryPath, "component2.ts"), + ] + }, + { + projectType: ProjectTypes.TsFlavorName, + appFiles: [ + "component1.ts", + "component1.js", + "component2.ts", + "App_Resources" + ], + expectedResult: [ + join(appDirectoryPath, "component1.ts"), + join(appDirectoryPath, "component2.ts"), + ] + }, + { + projectType: ProjectTypes.JsFlavorName, + appFiles: [ + "component1.ts", + "component1.js", + "component2.ts", + "App_Resources" + ], + expectedResult: [ + join(appDirectoryPath, "component1.js"), + ] + }, + { + projectType: ProjectTypes.VueFlavorName, + appFiles: [ + "component1.ts", + "component1.js", + "component2.ts", + "App_Resources" + ], + expectedResult: [ + join(appDirectoryPath, "component1.js"), + ] + } + ]; + + const setupTestCase = (testCase: any): { projectDataService: IProjectDataService, testInjector: IInjector } => { + const testInjector = createTestInjector(); + const fs = testInjector.resolve("fs"); + const realFileSystemInstance = testInjector.resolve(FileSystem); + fs.enumerateFilesInDirectorySync = realFileSystemInstance.enumerateFilesInDirectorySync; + + const appResourcesContent: string[] = [ + "file1.ts", + "file1.js" + ]; + + fs.exists = (filePath: string) => true; + fs.getFsStats = (filePath: string) => { + if (filePath === appDirectoryPath || filePath === appResourcesDirectoryPath) { + return { isDirectory: () => true }; + } + + return { isDirectory: () => false }; + }; + + fs.readDirectory = (dirPath: string) => { + if (dirPath === appDirectoryPath) { + return testCase.appFiles; + } + + if (dirPath === appResourcesDirectoryPath) { + return appResourcesContent; + } + + return []; + }; + + const projectDataService = testInjector.resolve("projectDataService"); + projectDataService.getProjectData = () => ({ + appDirectoryPath, + appResourcesDirectoryPath, + projectType: testCase.projectType + }); + + return { projectDataService, testInjector }; + }; + + getAppExecutableFilesTestData.forEach(testCase => { + it(`returns correct files for application type ${testCase.projectType}`, () => { + const { projectDataService } = setupTestCase(testCase); + const appExecutableFiles = projectDataService.getAppExecutableFiles("projectDir"); + assert.deepEqual(appExecutableFiles, testCase.expectedResult); + }); + }); + + it("returns correct files when inner dirs exist in app dir", () => { + const innerDirName = "innerDir"; + const innerDirPath = join(appDirectoryPath, innerDirName); + const testCase = { + projectType: ProjectTypes.NgFlavorName, + appFiles: [ + "component1.ts", + "component1.js", + "component2.ts", + "App_Resources", + innerDirName + ], + expectedResult: [ + join(appDirectoryPath, "component1.ts"), + join(appDirectoryPath, "component2.ts"), + join(innerDirPath, "subcomponent1.ts"), + join(innerDirPath, "subcomponent2.ts"), + ] + }; + const { projectDataService, testInjector } = setupTestCase(testCase); + const fs = testInjector.resolve("fs"); + const baseFsReadDirectory = fs.readDirectory; + const innerDirContents = [ + "subcomponent1.ts", + "subcomponent1.js", + "subcomponent2.ts", + "subcomponent2.js", + ]; + + fs.readDirectory = (dirPath) => { + if (dirPath === innerDirPath) { + return innerDirContents; + } + + return baseFsReadDirectory(dirPath); + }; + + const baseFsGetFsStats = fs.getFsStats; + fs.getFsStats = (filePath: string) => { + if (filePath === innerDirPath) { + return { isDirectory: () => true }; + } + + return baseFsGetFsStats(filePath); + }; + + const appExecutableFiles = projectDataService.getAppExecutableFiles("projectDir"); + assert.deepEqual(appExecutableFiles, testCase.expectedResult); + }); + }); }); diff --git a/test/stubs.ts b/test/stubs.ts index 45c04dd3c9..dc24d3e6d8 100644 --- a/test/stubs.ts +++ b/test/stubs.ts @@ -527,6 +527,10 @@ export class ProjectDataService implements IProjectDataService { async getAndroidAssetsStructure(opts: IProjectDir): Promise { return null; } + + getAppExecutableFiles(projectDir: string): string[] { + return []; + } } export class ProjectHelperStub implements IProjectHelper { @@ -913,7 +917,7 @@ export class AndroidBundleValidatorHelper implements IAndroidBundleValidatorHelp export class PerformanceService implements IPerformanceService { now(): number { return 10; } - processExecutionData() {} + processExecutionData() { } } export class InjectorStub extends Yok implements IInjector {