diff --git a/lib/base-package-manager.ts b/lib/base-package-manager.ts index 5ee9a96abd..daa7141578 100644 --- a/lib/base-package-manager.ts +++ b/lib/base-package-manager.ts @@ -13,6 +13,14 @@ import { IHostInfo, } from "./common/declarations"; +// Options for the CLI itself. npm 12 fails on flags it doesn't know. +const CLI_ONLY_OPTIONS = [ + "path", + "frameworkPath", + "disableNpmInstall", + "ignoreScripts", +]; + export abstract class BasePackageManager implements INodePackageManager { public abstract install( packageName: string, @@ -136,6 +144,10 @@ export abstract class BasePackageManager implements INodePackageManager { protected getFlagsString(config: any, asArray: boolean): any { const array: Array = []; for (const flag in config) { + if (CLI_ONLY_OPTIONS.includes(flag)) { + continue; + } + if ( flag === "global" && this.packageManager !== "yarn" && diff --git a/lib/services/ios-project-service.ts b/lib/services/ios-project-service.ts index 7ac6f30c0e..c1b4b27db8 100644 --- a/lib/services/ios-project-service.ts +++ b/lib/services/ios-project-service.ts @@ -1532,7 +1532,11 @@ export class IOSProjectService projectData: IProjectData, ): Promise { const project = this.createPbxProj(projectData); - const group = await this.getRootGroup(groupName, sourceFolderPath); + const group = await this.getRootGroup( + groupName, + sourceFolderPath, + projectData.projectDir, + ); // pin the sources to the main app target: without an explicit target the // underlying xcode lib picks whichever "Sources" build phase it finds // first, which can be an extension target (e.g. a widget) once one @@ -1543,16 +1547,26 @@ export class IOSProjectService filesRelativeToProject: true, target: project.getFirstTarget()?.uuid, }); - project.addToHeaderSearchPaths(group.path); - const headerFiles = this.$fs.exists(sourceFolderPath) + // A glob is no use as a search path, the folders of its headers are. + const headerSearchPaths = fastGlob.isDynamicPattern(sourceFolderPath) + ? _.uniq( + group.files + .filter((file) => path.extname(file) === ".h") + .map((file) => path.dirname(file)), + ) + : [group.path]; + headerSearchPaths.forEach((searchPath) => + project.addToHeaderSearchPaths(searchPath), + ); + const headerFiles = this.$fs.exists(group.path) ? this.$fs.enumerateFilesInDirectorySync( - sourceFolderPath, + group.path, (file, stat) => stat.isDirectory() || path.extname(file) === ".h", ) : []; if ( headerFiles.length > 0 && - !this.$fs.exists(path.join(sourceFolderPath, "module.modulemap")) + !this.$fs.exists(path.join(group.path, "module.modulemap")) ) { this.$logger.warn( `warning: Directory ${sourceFolderPath} with native iOS source code doesn't contain a modulemap file. Metadata for it will not be generated and it will not be accessible from JavaScript. To learn more see https://docs.nativescript.org/guides/ios-source-code`, @@ -1597,7 +1611,11 @@ export class IOSProjectService } } - private async getRootGroup(name: string, rootPath: string) { + private async getRootGroup( + name: string, + rootPath: string, + projectRoot: string, + ) { const filePathsArr: string[] = []; const rootGroup: INativeSourceCodeGroup = { name: name, @@ -1606,17 +1624,19 @@ export class IOSProjectService }; if (fastGlob.isDynamicPattern(rootPath)) { - const projectRoot = this.$projectDataService.getProjectData().projectDir; - const filePaths = await fastGlob(rootPath); + // Relative to the project: --path can run the CLI from anywhere. + const filePaths = await fastGlob(rootPath, { cwd: projectRoot }); for (const filePath of filePaths) { const sourceFilePath = path.normalize(path.join(projectRoot, filePath)); filePathsArr.push(sourceFilePath); } } else { - if (this.$fs.exists(rootPath)) { - const stats = this.$fs.getFsStats(rootPath); - if (stats.isDirectory() && !this.$fs.isEmptyDir(rootPath)) { - this.$fs.readDirectory(rootPath).forEach((fileName) => { + const folderPath = path.resolve(projectRoot, rootPath); + rootGroup.path = folderPath; + if (this.$fs.exists(folderPath)) { + const stats = this.$fs.getFsStats(folderPath); + if (stats.isDirectory() && !this.$fs.isEmptyDir(folderPath)) { + this.$fs.readDirectory(folderPath).forEach((fileName) => { const filePath = path.join(rootGroup.path, fileName); filePathsArr.push(filePath); }); @@ -1684,6 +1704,7 @@ export class IOSProjectService const group = await this.getRootGroup( pluginData.name, pluginPlatformsFolderPath, + projectData.projectDir, ); project.removePbxGroup(group.name, group.path); project.removeFromHeaderSearchPaths(group.path); diff --git a/test/base-package-manager.ts b/test/base-package-manager.ts new file mode 100644 index 0000000000..d94cf1aced --- /dev/null +++ b/test/base-package-manager.ts @@ -0,0 +1,32 @@ +import { assert } from "chai"; +import { BasePackageManager } from "../lib/base-package-manager"; + +class TestPackageManager extends (BasePackageManager) { + constructor() { + super({}, {}, {}, {}, {}, "npm"); + } +} + +describe("BasePackageManager", () => { + const flags = (config: object): string[] => + (new TestPackageManager()).getFlagsString(config, true); + + it("doesn't pass the CLI's own options to the package manager", () => { + assert.deepEqual( + flags({ + path: "apps/app", + frameworkPath: "runtime.tgz", + disableNpmInstall: true, + ignoreScripts: true, + }), + [], + ); + }); + + it("passes the package manager's options on", () => { + assert.deepEqual(flags({ "ignore-scripts": true, "save-dev": true }), [ + "--ignore-scripts", + "--save-dev", + ]); + }); +}); diff --git a/test/ios-project-service-native-source.ts b/test/ios-project-service-native-source.ts new file mode 100644 index 0000000000..dd8f692126 --- /dev/null +++ b/test/ios-project-service-native-source.ts @@ -0,0 +1,96 @@ +import { assert } from "chai"; +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; +import { FileSystem } from "../lib/common/file-system"; +import { IOSProjectService } from "../lib/services/ios-project-service"; + +describe("iOSProjectService getRootGroup", () => { + let projectDir: string; + let sourcesDir: string; + let cwd: string; + + beforeEach(() => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "native-source-")); + projectDir = path.join(root, "apps", "app"); + sourcesDir = path.join(root, "libs", "native"); + fs.mkdirSync(projectDir, { recursive: true }); + fs.mkdirSync(sourcesDir, { recursive: true }); + fs.writeFileSync(path.join(sourcesDir, "Source.swift"), ""); + cwd = process.cwd(); + process.chdir(root); + }); + + afterEach(() => { + process.chdir(cwd); + }); + + const getFiles = async (rootPath: string): Promise => { + const { files } = await (IOSProjectService.prototype).getRootGroup.call( + { $fs: new FileSystem({}) }, + "Native", + rootPath, + projectDir, + ); + return files.map((file: string) => fs.realpathSync(file)); + }; + + const expectedSource = () => + fs.realpathSync(path.join(sourcesDir, "Source.swift")); + + it("resolves a glob against the project, not the working directory", async () => { + assert.deepEqual(await getFiles("../../libs/native/*.swift"), [ + expectedSource(), + ]); + }); + + it("resolves a folder against the project, not the working directory", async () => { + assert.deepEqual(await getFiles("../../libs/native"), [expectedSource()]); + }); + + describe("header search paths", () => { + const prepare = async (rootPath: string): Promise => { + const searchPaths: string[] = []; + const proto = IOSProjectService.prototype; + await proto.prepareNativeSourceCode.call( + { + $fs: new FileSystem({}), + $logger: { warn: () => {} }, + getRootGroup: proto.getRootGroup, + savePbxProj: () => {}, + createPbxProj: () => ({ + addPbxGroup: () => {}, + getFirstTarget: () => ({ uuid: "target" }), + addToHeaderSearchPaths: (searchPath: string) => + searchPaths.push(searchPath), + }), + }, + "Native", + rootPath, + { projectDir }, + ); + return searchPaths.map((searchPath) => fs.realpathSync(searchPath)); + }; + + it("uses the folders of the headers a glob matches", async () => { + const nested = path.join(sourcesDir, "nested"); + fs.mkdirSync(nested); + fs.writeFileSync(path.join(nested, "One.h"), ""); + fs.writeFileSync(path.join(nested, "Two.h"), ""); + + assert.deepEqual(await prepare("../../libs/native/**/*.{h,swift}"), [ + fs.realpathSync(nested), + ]); + }); + + it("adds nothing for a glob without headers", async () => { + assert.deepEqual(await prepare("../../libs/native/*.swift"), []); + }); + + it("uses the folder itself, against the project", async () => { + assert.deepEqual(await prepare("../../libs/native"), [ + fs.realpathSync(sourcesDir), + ]); + }); + }); +});