diff --git a/package-lock.json b/package-lock.json index 0eb6af759a..4ee9a51e57 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36448,9 +36448,19 @@ } }, "packages/infrastructure/lens-link": { + "name": "@k8slens/lens-link", "version": "1.0.0-alpha.0", "license": "MIT", - "devDependencies": {} + "dependencies": { + "@ogre-tools/fp": "^15.3.1", + "@ogre-tools/injectable": "^15.3.1", + "@ogre-tools/injectable-extension-for-auto-registration": "^15.3.1", + "fast-glob": "^3.2.12", + "fs-extra": "^9.0.1" + }, + "devDependencies": { + "@async-fn/jest": "^1.6.4" + } }, "packages/infrastructure/typescript": { "name": "@k8slens/typescript", diff --git a/packages/infrastructure/lens-link/package.json b/packages/infrastructure/lens-link/package.json index 3c3efe659f..16cc9122a4 100644 --- a/packages/infrastructure/lens-link/package.json +++ b/packages/infrastructure/lens-link/package.json @@ -24,7 +24,8 @@ "@ogre-tools/fp": "^15.3.1", "@ogre-tools/injectable": "^15.3.1", "@ogre-tools/injectable-extension-for-auto-registration": "^15.3.1", - "fs-extra": "^9.0.1" + "fs-extra": "^9.0.1", + "fast-glob": "^3.2.12" }, "devDependencies": { diff --git a/packages/infrastructure/lens-link/src/fs/glob.injectable.ts b/packages/infrastructure/lens-link/src/fs/glob.injectable.ts new file mode 100644 index 0000000000..b6111530ef --- /dev/null +++ b/packages/infrastructure/lens-link/src/fs/glob.injectable.ts @@ -0,0 +1,9 @@ +import { getInjectable } from "@ogre-tools/injectable"; +import glob from "fast-glob"; + +export type Glob = typeof glob; + +export const globInjectable = getInjectable({ + id: "glob", + instantiate: (): Glob => glob, +}); diff --git a/packages/infrastructure/lens-link/src/lens-link.injectable.ts b/packages/infrastructure/lens-link/src/lens-link.injectable.ts index d6c507422c..ac50127b5e 100644 --- a/packages/infrastructure/lens-link/src/lens-link.injectable.ts +++ b/packages/infrastructure/lens-link/src/lens-link.injectable.ts @@ -1,6 +1,7 @@ +import { partition } from "lodash/fp"; import { dirname } from "path"; import { pipeline } from "@ogre-tools/fp"; -import { flatMap, map } from "lodash/fp"; +import { flatten, map } from "lodash/fp"; import { removeExistingLensLinkDirectoriesInjectable } from "./remove-existing-lens-link-directories.injectable"; import { createLensLinkDirectoriesInjectable } from "./create-lens-link-directories.injectable"; import { getMissingPackageJsonsInjectable } from "./get-missing-package-jsons.injectable"; @@ -13,9 +14,16 @@ import { existsInjectable } from "./fs/exists.injectable"; import { writeJsonFileInjectable } from "./fs/write-json-file.injectable"; import { createSymlinkInjectable } from "./fs/create-symlink.injectable"; import { workingDirectoryInjectable } from "./working-directory.injectable"; +import { globInjectable } from "./fs/glob.injectable"; +import { awaitAll } from "./await-all"; export type LensLink = () => Promise; +const shouldBeGlobbed = (possibleGlobString: string) => possibleGlobString.includes("*"); + +const simplifyGlobbing = new RegExp("(\\/\\*\\/\\*\\*|\\/\\*\\*|\\/\\*\\*\\/\\*|\\/\\*)$"); +const toAvoidableGlobStrings = (reference: string) => reference.replace(simplifyGlobbing, ""); + const lensLinkInjectable = getInjectable({ id: "lens-link", @@ -31,6 +39,7 @@ const lensLinkInjectable = getInjectable({ const writeJsonFile = di.inject(writeJsonFileInjectable); const createSymlink = di.inject(createSymlinkInjectable); const workingDirectory = di.inject(workingDirectoryInjectable); + const glob = di.inject(globInjectable); return async () => { const configFilePath = resolvePath(workingDirectory, ".lens-links.json"); @@ -61,12 +70,24 @@ const lensLinkInjectable = getInjectable({ await createLensLinkDirectories(packageJsons); - pipeline( + await pipeline( packageJsons, - flatMap(({ packageJsonPath, content }) => { + map(async ({ packageJsonPath, content }) => { const lensLinkDirectory = getLensLinkDirectory(content.name); + const fileStrings = content.files.map(toAvoidableGlobStrings); + + const [toBeGlobbed, toNotBeGlobbed] = partition(shouldBeGlobbed)(fileStrings); + + const moduleDirectory = dirname(packageJsonPath); + + let globbeds: string[] = []; + + if (toBeGlobbed.length) { + globbeds = await glob(toBeGlobbed, { cwd: moduleDirectory }); + } + return [ { target: packageJsonPath, @@ -74,15 +95,27 @@ const lensLinkInjectable = getInjectable({ type: "file" as const, }, - ...content.files.map((x) => ({ - target: resolvePath(dirname(packageJsonPath), x), - source: resolvePath(lensLinkDirectory, x), + ...globbeds.map((fileString) => ({ + target: resolvePath(moduleDirectory, fileString), + source: resolvePath(lensLinkDirectory, fileString), + type: "file" as const, + })), + + ...toNotBeGlobbed.map((fileOrDirectory) => ({ + target: resolvePath(moduleDirectory, fileOrDirectory), + source: resolvePath(lensLinkDirectory, fileOrDirectory), type: "dir" as const, })), ]; }), + awaitAll, + + flatten, + map(({ target, source, type }) => createSymlink(target, source, type)), + + awaitAll, ); }; }, diff --git a/packages/infrastructure/lens-link/src/lens-link.test.ts b/packages/infrastructure/lens-link/src/lens-link.test.ts index 57a0bebc5c..89b80f53e5 100644 --- a/packages/infrastructure/lens-link/src/lens-link.test.ts +++ b/packages/infrastructure/lens-link/src/lens-link.test.ts @@ -19,6 +19,8 @@ import { createSymlinkInjectable } from "./fs/create-symlink.injectable"; import { ensureDirectoryInjectable } from "./fs/ensure-directory.injectable"; import { removeDirectoryInjectable } from "./fs/remove-directory.injectable"; import { getDi } from "./get-di"; +import type { Glob } from "./fs/glob.injectable"; +import { globInjectable } from "./fs/glob.injectable"; describe("lens-link", () => { let lensLink: LensLink; @@ -28,6 +30,7 @@ describe("lens-link", () => { let createSymlinkMock: AsyncFnMock; let ensureDirectoryMock: AsyncFnMock; let removeDirectoryMock: AsyncFnMock; + let globMock: AsyncFnMock; beforeEach(() => { existsMock = asyncFn(); @@ -36,6 +39,7 @@ describe("lens-link", () => { createSymlinkMock = asyncFn(); ensureDirectoryMock = asyncFn(); removeDirectoryMock = asyncFn(); + globMock = asyncFn(); const di = getDi(); @@ -47,6 +51,7 @@ describe("lens-link", () => { di.override(createSymlinkInjectable, () => createSymlinkMock); di.override(ensureDirectoryInjectable, () => ensureDirectoryMock); di.override(removeDirectoryInjectable, () => removeDirectoryMock); + di.override(globInjectable, () => globMock); lensLink = di.inject(lensLinkInjectable); }); @@ -264,6 +269,112 @@ describe("lens-link", () => { }); }); + describe("given some of the packages have globs as files, when all contents resolve", () => { + beforeEach(async () => { + existsMock.mockClear(); + + await readJsonFileMock.resolveSpecific(([path]) => path === "/some-directory/some-module/package.json", { + name: "@some-scope/some-module", + files: [ + "some-build-directory-with-asterisk/*", + "some-build-directory-with-wild-card/**", + "some-build-directory-with-wild-card-before-asterisk/**/*", + "some-build-directory-with-asterisk-and-file-suffix/*.some-file-suffix", + "some-build-directory-with-file-name-and-asterisk/some-filename.*", + "some-build-directory-with-wild-card-and-asterisk-and-file-suffix/**/*.some-file-suffix", + ], + main: "some-build-directory/index.js", + }); + + await readJsonFileMock.resolveSpecific( + ([path]) => path === "/some-other-directory/some-other-module/package.json", + { + name: "@some-scope/some-other-module", + files: [], + main: "some-other-build-directory/index.js", + }, + ); + }); + + describe("given Lens link directories are handled", () => { + beforeEach(async () => { + await existsMock.resolve(false); + await existsMock.resolve(false); + + await ensureDirectoryMock.resolve(); + await ensureDirectoryMock.resolve(); + }); + + it("does not create symlinks yet", () => { + expect(createSymlinkMock).not.toHaveBeenCalled(); + }); + + it("calls for glob of file-strings for which glob cannot be avoided", () => { + expect(globMock.mock.calls).toEqual([ + [ + [ + "some-build-directory-with-asterisk-and-file-suffix/*.some-file-suffix", + "some-build-directory-with-file-name-and-asterisk/some-filename.*", + "some-build-directory-with-wild-card-and-asterisk-and-file-suffix/**/*.some-file-suffix", + ], + + { cwd: "/some-directory/some-module" }, + ], + ]); + }); + + it("doesn't create symlinks yet", () => { + expect(createSymlinkMock).not.toHaveBeenCalled(); + }); + + describe("when globbing resolves", () => { + beforeEach(async () => { + await globMock.resolve(["/some-directory/some-module/some-file-from-glob.txt"]); + }); + + it("creates the symlinks to files and directories that were both globbed and that avoided globbing", () => { + expect(createSymlinkMock.mock.calls).toEqual([ + [ + "/some-directory/some-module/package.json", + "/some-directory/some-project/node_modules/@some-scope/some-module/package.json", + "file", + ], + + [ + "/some-directory/some-module/some-file-from-glob.txt", + "/some-directory/some-module/some-file-from-glob.txt", + "file", + ], + + [ + "/some-directory/some-module/some-build-directory-with-asterisk", + "/some-directory/some-project/node_modules/@some-scope/some-module/some-build-directory-with-asterisk", + "dir", + ], + + [ + "/some-directory/some-module/some-build-directory-with-wild-card", + "/some-directory/some-project/node_modules/@some-scope/some-module/some-build-directory-with-wild-card", + "dir", + ], + + [ + "/some-directory/some-module/some-build-directory-with-wild-card-before-asterisk", + "/some-directory/some-project/node_modules/@some-scope/some-module/some-build-directory-with-wild-card-before-asterisk", + "dir", + ], + + [ + "/some-other-directory/some-other-module/package.json", + "/some-directory/some-project/node_modules/@some-scope/some-other-module/package.json", + "file", + ], + ]); + }); + }); + }); + }); + describe("when all contents resolve", () => { beforeEach(async () => { existsMock.mockClear(); @@ -375,6 +486,27 @@ describe("lens-link", () => { ], ]); }); + + it("given all symlink creations have not resolved, does not resolve yet", async () => { + createSymlinkMock.resolve(); + createSymlinkMock.resolve(); + createSymlinkMock.resolve(); + + const promiseStatus = await getPromiseStatus(actualPromise); + + expect(promiseStatus.fulfilled).toBe(false); + }); + + it("when symlink creations resolve, ends script", async () => { + createSymlinkMock.resolve(); + createSymlinkMock.resolve(); + createSymlinkMock.resolve(); + createSymlinkMock.resolve(); + + const promiseStatus = await getPromiseStatus(actualPromise); + + expect(promiseStatus.fulfilled).toBe(true); + }); }); }); });