From 45cffb3470546aab540cf7e2c30e68d7dddd1bb9 Mon Sep 17 00:00:00 2001 From: Felix Haeberle Date: Mon, 11 Mar 2024 14:55:07 +0100 Subject: [PATCH 01/17] create cross-sell-sherlock package --- .../cross-sell-sherlock/package.json | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 inlang/source-code/cross-sell/cross-sell-sherlock/package.json diff --git a/inlang/source-code/cross-sell/cross-sell-sherlock/package.json b/inlang/source-code/cross-sell/cross-sell-sherlock/package.json new file mode 100644 index 0000000000..013a661f14 --- /dev/null +++ b/inlang/source-code/cross-sell/cross-sell-sherlock/package.json @@ -0,0 +1,30 @@ +{ + "name": "cross-sell-sherlock", + "description": "A package to cross-sell Sherlock", + "type": "module", + "exports": { + ".": "./dist/index.js" + }, + "files": [ + "./dist", + "./src" + ], + "scripts": { + "build": "tsc --build", + "dev": "tsc --watch", + "test": "tsc --noEmit && vitest run --passWithNoTests --coverage", + "lint": "eslint ./src --fix", + "format": "prettier ./src --write", + "clean": "rm -rf ./dist ./node_modules" + }, + "devDependencies": { + "@lix-js/fs": "workspace:*", + "@inlang/sdk": "workspace:*", + "@types/vscode": "^1.84.2", + "comment-json": "^4.2.3", + "patch-package": "6.5.1", + "typescript": "^5.1.3", + "@vitest/coverage-v8": "0.33.0", + "vitest": "0.33.0" + } +} From b09da4704e184f9f433805a7138cd760c6709648 Mon Sep 17 00:00:00 2001 From: Felix Haeberle Date: Mon, 11 Mar 2024 14:55:20 +0100 Subject: [PATCH 02/17] add README --- .../cross-sell/cross-sell-sherlock/README.md | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 inlang/source-code/cross-sell/cross-sell-sherlock/README.md diff --git a/inlang/source-code/cross-sell/cross-sell-sherlock/README.md b/inlang/source-code/cross-sell/cross-sell-sherlock/README.md new file mode 100644 index 0000000000..bf551ae539 --- /dev/null +++ b/inlang/source-code/cross-sell/cross-sell-sherlock/README.md @@ -0,0 +1,65 @@ +# Cross-sell Sherlock package + +## Features + +- **Check for Existing Recommendations**: Quickly verify if an extension is already recommended in the workspace's `.vscode/extensions.json` file. +- **Add New Recommendations**: Automatically add new recommendations to the `.vscode/extensions.json` file if they are not already present. + +## Installation + +```bash +"@inlang/cross-sell-sherlock": "workspace:*" +``` + +## Usage + +The module exports two main asynchronous functions: + +### `isAdopted(fs: NodeishFilesystem, workingDirectory?: string): Promise` + +Checks whether the `inlang.vs-code-extension` is recommended in the workspace. + +#### Parameters + +- `fs`: A `NodeishFilesystem` object to interact with the file system. +- `workingDirectory`: (Optional) The working directory path. + +#### Returns + +- `Promise`: `true` if the extension is recommended, `false` otherwise. + +### `add(fs: NodeishFilesystem, workingDirectory?: string): Promise` + +Adds the `inlang.vs-code-extension` recommendation to the workspace if it's not already present. + +#### Parameters + +- `fs`: A `NodeishFilesystem` object to interact with the file system. +- `workingDirectory`: (Optional) The working directory path. + +## Example + +```typescript +import { isAdopted, add } from 'cross-sell-sherlock'; +import { NodeishFilesystem } from '@lix-js/fs'; + +async function addSherlock(fs: NodeishFilesystem) { + const isExtensionAdopted = await isAdopted(fs); + + if (!isExtensionAdopted) { + // prompt for user confirmation + const userConfirmed = await promptUser(); + + if (userConfirmed) { + await add(fs); + console.log('Extension recommendation added.'); + } else { + console.log('User declined to add extension recommendation.'); + } + } +} +``` + +## Contributing + +Contributions are welcome! If you have a feature request, bug report, or proposal, please open an issue or submit a pull request. Our Discord can be found [here](https://discord.gg/VXHw44ux). \ No newline at end of file From 247d1c1a75ae95a9ffb0a1fc3a92008b87f83f4e Mon Sep 17 00:00:00 2001 From: Felix Haeberle Date: Mon, 11 Mar 2024 14:55:28 +0100 Subject: [PATCH 03/17] add typescript config --- .../cross-sell/cross-sell-sherlock/tsconfig.json | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 inlang/source-code/cross-sell/cross-sell-sherlock/tsconfig.json diff --git a/inlang/source-code/cross-sell/cross-sell-sherlock/tsconfig.json b/inlang/source-code/cross-sell/cross-sell-sherlock/tsconfig.json new file mode 100644 index 0000000000..f135cfd1a0 --- /dev/null +++ b/inlang/source-code/cross-sell/cross-sell-sherlock/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src/*", "./build.js"], + "compilerOptions": { + "resolveJsonModule": true, + "outDir": "./dist", + "rootDir": "./src" + } +} From b6644854812086939478cf67ad0f9e0f251233be Mon Sep 17 00:00:00 2001 From: Felix Haeberle Date: Mon, 11 Mar 2024 14:55:47 +0100 Subject: [PATCH 04/17] add to pnpm workspace --- pnpm-workspace.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 9dded5b8b3..2ac44b15e4 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,6 +1,7 @@ packages: - 'lix/source-code/*' - 'inlang/source-code/*' + - 'inlang/source-code/cross-sell/*' - 'inlang/source-code/plugins/*' - 'inlang/source-code/paraglide/*' - 'inlang/source-code/paraglide/paraglide-js-adapter-vite/tests/*' From 1baa4e6cee408d0667d422086f4c85d65743a545 Mon Sep 17 00:00:00 2001 From: Felix Haeberle Date: Mon, 11 Mar 2024 14:55:59 +0100 Subject: [PATCH 05/17] update pnpm lock --- pnpm-lock.yaml | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3631f12646..9927c5aca7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -204,6 +204,33 @@ importers: specifier: 0.34.6 version: 0.34.6(jsdom@22.1.0) + inlang/source-code/cross-sell/cross-sell-sherlock: + devDependencies: + '@inlang/sdk': + specifier: workspace:* + version: link:../../sdk + '@lix-js/fs': + specifier: workspace:* + version: link:../../../../lix/source-code/fs + '@types/vscode': + specifier: ^1.84.2 + version: 1.84.2 + '@vitest/coverage-v8': + specifier: 0.34.6 + version: 0.34.6(vitest@0.34.6) + comment-json: + specifier: ^4.2.3 + version: 4.2.3 + patch-package: + specifier: 6.5.1 + version: 6.5.1 + typescript: + specifier: ^5.1.3 + version: 5.3.3 + vitest: + specifier: 0.34.6 + version: 0.34.6(jsdom@22.1.0) + inlang/source-code/design-system: dependencies: '@ctrl/tinycolor': @@ -13337,6 +13364,7 @@ packages: /esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} + hasBin: true /esquery@1.5.0: resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} @@ -15303,6 +15331,7 @@ packages: /is-ci@2.0.0: resolution: {integrity: sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==} + hasBin: true dependencies: ci-info: 2.0.0 dev: true @@ -15336,6 +15365,7 @@ packages: /is-docker@3.0.0: resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true /is-extendable@0.1.1: resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} @@ -18871,6 +18901,7 @@ packages: /patch-package@6.5.1: resolution: {integrity: sha512-I/4Zsalfhc6bphmJTlrLoOcAF87jcxko4q0qsv4bGcurbr8IskEOtdnt9iCmsQVGL1B+iUhSQqweyTLJfCF9rA==} engines: {node: '>=10', npm: '>5'} + hasBin: true dependencies: '@yarnpkg/lockfile': 1.1.0 chalk: 4.1.2 @@ -20318,6 +20349,7 @@ packages: /rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + hasBin: true dependencies: glob: 7.2.3 @@ -20574,6 +20606,7 @@ packages: /semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true /semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} From eee0604519a3f5e4c4261d27ee6ed22132daef16 Mon Sep 17 00:00:00 2001 From: Felix Haeberle Date: Mon, 11 Mar 2024 14:56:27 +0100 Subject: [PATCH 06/17] add main API --- .../cross-sell/cross-sell-sherlock/src/index.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 inlang/source-code/cross-sell/cross-sell-sherlock/src/index.ts diff --git a/inlang/source-code/cross-sell/cross-sell-sherlock/src/index.ts b/inlang/source-code/cross-sell/cross-sell-sherlock/src/index.ts new file mode 100644 index 0000000000..80c9a79457 --- /dev/null +++ b/inlang/source-code/cross-sell/cross-sell-sherlock/src/index.ts @@ -0,0 +1,16 @@ +import { type NodeishFilesystem } from "@lix-js/fs" +import { + addRecommendationToWorkspace, + isInWorkspaceRecommendation, +} from "./recommendation/index.js" + +export async function isAdopted( + fs: NodeishFilesystem, + workingDirectory?: string +): Promise { + return isInWorkspaceRecommendation(fs, workingDirectory) +} + +export async function add(fs: NodeishFilesystem, workingDirectory?: string): Promise { + await addRecommendationToWorkspace(fs, workingDirectory) +} From 0ff375b61b8d500989724c35b1ca1b6229fc8895 Mon Sep 17 00:00:00 2001 From: Felix Haeberle Date: Mon, 11 Mar 2024 14:56:41 +0100 Subject: [PATCH 07/17] add recommendation logic --- .../src/recommendation/index.ts | 60 +++++++++++++++++++ .../src/recommendation/isExtensionJson.ts | 10 ++++ .../src/recommendation/joinPath.ts | 3 + .../src/recommendation/types.ts | 3 + 4 files changed, 76 insertions(+) create mode 100644 inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/index.ts create mode 100644 inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/isExtensionJson.ts create mode 100644 inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/joinPath.ts create mode 100644 inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/types.ts diff --git a/inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/index.ts b/inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/index.ts new file mode 100644 index 0000000000..4ea85849dc --- /dev/null +++ b/inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/index.ts @@ -0,0 +1,60 @@ +import type { NodeishFilesystem } from "@lix-js/fs" +import { joinPath } from "./joinPath.js" +import { isExtensionsJson } from "./isExtensionJson.js" +import { parse, type CommentJSONValue, stringify } from "comment-json/index.js" +import type { ExtensionsJson } from "./types.js" + +export async function addRecommendationToWorkspace( + fs: NodeishFilesystem, + workingDirectory?: string +): Promise { + const vscodeFolderPath = joinPath(workingDirectory ?? "", ".vscode") + const extensionsJsonPath = joinPath(vscodeFolderPath, "extensions.json") + + if (!(await fs.stat(vscodeFolderPath))) { + await fs.mkdir(vscodeFolderPath) + } + + let extensions: ExtensionsJson + if (await fs.stat(extensionsJsonPath)) { + try { + const parsed = parse(await fs.readFile(extensionsJsonPath, { encoding: "utf-8" })) + if (isExtensionsJson(parsed)) { + extensions = parsed + } else { + extensions = { recommendations: [] } + } + } catch (error) { + extensions = { recommendations: [] } + } + } else { + extensions = { recommendations: [] } + } + + if (!extensions.recommendations.includes("inlang.vs-code-extension")) { + extensions.recommendations.push("inlang.vs-code-extension") + await fs.writeFile(extensionsJsonPath, stringify(extensions, undefined, 2)) + } +} + +export async function isInWorkspaceRecommendation( + fs: NodeishFilesystem, + workingDirectory?: string +): Promise { + const vscodeFolderPath = joinPath(workingDirectory ?? "", ".vscode") + const extensionsJsonPath = joinPath(vscodeFolderPath, "extensions.json") + + if (!(await fs.stat(extensionsJsonPath)) || !(await fs.stat(vscodeFolderPath))) { + return false + } + + const extensions = parse( + await fs.readFile(extensionsJsonPath, { encoding: "utf-8" }) + ) as CommentJSONValue + + if (!isExtensionsJson(extensions)) { + return false + } + + return extensions?.recommendations.includes("inlang.vs-code-extension") || false +} diff --git a/inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/isExtensionJson.ts b/inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/isExtensionJson.ts new file mode 100644 index 0000000000..5d199c3df1 --- /dev/null +++ b/inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/isExtensionJson.ts @@ -0,0 +1,10 @@ +import type { ExtensionsJson } from "./types.js" + +export function isExtensionsJson(object: any): object is ExtensionsJson { + return ( + object !== null && + typeof object === "object" && + Array.isArray(object.recommendations) && + object.recommendations.every((item: any) => typeof item === "string") + ) +} diff --git a/inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/joinPath.ts b/inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/joinPath.ts new file mode 100644 index 0000000000..ef69f9a785 --- /dev/null +++ b/inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/joinPath.ts @@ -0,0 +1,3 @@ +export function joinPath(...parts: string[]): string { + return parts.map((part) => part.replace(/\/$/, "")).join("/") +} diff --git a/inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/types.ts b/inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/types.ts new file mode 100644 index 0000000000..362a958972 --- /dev/null +++ b/inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/types.ts @@ -0,0 +1,3 @@ +export type ExtensionsJson = { + recommendations: string[] +} From 0a2c864931bb813b6484cea3672ff2f25c8f7bda Mon Sep 17 00:00:00 2001 From: Felix Haeberle Date: Mon, 11 Mar 2024 14:56:47 +0100 Subject: [PATCH 08/17] add tests --- .../cross-sell-sherlock/src/index.test.ts | 169 ++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 inlang/source-code/cross-sell/cross-sell-sherlock/src/index.test.ts diff --git a/inlang/source-code/cross-sell/cross-sell-sherlock/src/index.test.ts b/inlang/source-code/cross-sell/cross-sell-sherlock/src/index.test.ts new file mode 100644 index 0000000000..e99d850a45 --- /dev/null +++ b/inlang/source-code/cross-sell/cross-sell-sherlock/src/index.test.ts @@ -0,0 +1,169 @@ +import { add, isAdopted } from "./index.js" +import { describe, it, expect, vi } from "vitest" + +describe("Cross-sell Sherlock app", () => { + it("should recognize when extension is already adopted", async () => { + const fsMock: any = { + stat: vi.fn().mockResolvedValue(true), + readFile: vi + .fn() + .mockResolvedValue(JSON.stringify({ recommendations: ["inlang.vs-code-extension"] })), + } + expect(await isAdopted(fsMock, "/mock/path")).toBe(true) + }) + + it("should recognize when extension is not adopted", async () => { + const fsMock: any = { + stat: vi.fn().mockResolvedValue(true), + readFile: vi.fn().mockResolvedValue(JSON.stringify({ recommendations: [] })), + } + expect(await isAdopted(fsMock, "/mock/path")).toBe(false) + }) + + it("should add recommendation when not present", async () => { + const fsMock: any = { + stat: vi.fn().mockResolvedValue(true), + readFile: vi.fn().mockResolvedValue(JSON.stringify({ recommendations: [] })), + writeFile: vi.fn(), + mkdir: vi.fn(), + } + await add(fsMock, "/mock/path") + expect(fsMock.writeFile).toHaveBeenCalledWith( + "/mock/path/.vscode/extensions.json", + JSON.stringify({ recommendations: ["inlang.vs-code-extension"] }, undefined, 2) + ) + }) + + it("should handle when the .vscode directory does not exist", async () => { + const fsMock: any = { + stat: vi.fn((path) => + path.includes(".vscode") ? Promise.resolve(false) : Promise.resolve(true) + ), + readFile: vi.fn(), + writeFile: vi.fn(), + mkdir: vi.fn(), + } + await add(fsMock, "/mock/path") + expect(fsMock.mkdir).toHaveBeenCalledWith("/mock/path/.vscode") + expect(fsMock.writeFile).toHaveBeenCalledWith( + "/mock/path/.vscode/extensions.json", + expect.any(String) + ) + }) + + it("should handle when extensions.json does not exist", async () => { + const fsMock: any = { + stat: vi.fn((path) => + path.includes("extensions.json") ? Promise.resolve(false) : Promise.resolve(true) + ), + readFile: vi.fn(), + writeFile: vi.fn(), + mkdir: vi.fn(), + } + await add(fsMock, "/mock/path") + expect(fsMock.writeFile).toHaveBeenCalledWith( + "/mock/path/.vscode/extensions.json", + expect.any(String) + ) + }) + + it("should return false if extensions.json does not exist", async () => { + const fsMock: any = { + stat: vi.fn((path) => { + if (path.includes("extensions.json")) return Promise.resolve(false) + return Promise.resolve(true) + }), + readFile: vi.fn(), + writeFile: vi.fn(), + mkdir: vi.fn(), + } + expect(await isAdopted(fsMock, "/mock/path")).toBe(false) + }) + + it("should return false for invalid extensions.json content", async () => { + const fsMock: any = { + stat: vi.fn().mockResolvedValue(true), + readFile: vi.fn().mockResolvedValue(JSON.stringify({ invalid: "content" })), + writeFile: vi.fn(), + mkdir: vi.fn(), + } + expect(await isAdopted(fsMock, "/mock/path")).toBe(false) + }) + + it("should create extensions.json in the .vscode folder if it does not exist", async () => { + const fsMock: any = { + stat: vi.fn((path) => { + // Simulate .vscode directory exists but extensions.json does not + if (path.includes("extensions.json")) return Promise.resolve(false) + return Promise.resolve(true) + }), + readFile: vi.fn(), + writeFile: vi.fn(), + mkdir: vi.fn(), + } + + await add(fsMock, "/mock/path") + + expect(fsMock.writeFile).toHaveBeenCalledWith( + "/mock/path/.vscode/extensions.json", + expect.any(String) + ) + }) + + it("should not modify extensions.json if extension is already present", async () => { + const fsMock: any = { + stat: vi.fn().mockResolvedValue(true), + readFile: vi + .fn() + .mockResolvedValue(JSON.stringify({ recommendations: ["inlang.vs-code-extension"] })), + writeFile: vi.fn(), + mkdir: vi.fn(), + } + await add(fsMock, "/mock/path") + expect(fsMock.writeFile).not.toHaveBeenCalled() + }) + + it("should handle errors in file operations", async () => { + const error = new Error("File operation failed") + const fsMock: any = { + stat: vi.fn().mockRejectedValue(error), + readFile: vi.fn().mockRejectedValue(error), + writeFile: vi.fn().mockRejectedValue(error), + mkdir: vi.fn().mockRejectedValue(error), + } + + await expect(add(fsMock, "/mock/path")).rejects.toThrow("File operation failed") + }) + + it("should reset extensions.json if it contains malformed content", async () => { + const malformedContent = "this is not valid json" + + const fsMock = { + stat: vi.fn().mockResolvedValue(true), + readFile: vi.fn().mockResolvedValue(malformedContent), + writeFile: vi.fn(), + mkdir: vi.fn(), + } + + // @ts-expect-error + await add(fsMock, "/test/dir") + expect(fsMock.writeFile).toHaveBeenCalledWith( + "/test/dir/.vscode/extensions.json", + expect.any(String) + ) + }) + + it("should handle read/write errors gracefully", async () => { + const error = new Error("File operation failed") + + const fsMock = { + stat: vi.fn().mockRejectedValue(error), + readFile: vi.fn().mockRejectedValue(error), + writeFile: vi.fn().mockRejectedValue(error), + mkdir: vi.fn().mockRejectedValue(error), + } + + // @ts-expect-error + await expect(add(fsMock, "/test/dir")).rejects.toThrow("File operation failed") + }) +}) From 3e5063449503d3452ea7b18819ae03a3d7b62d01 Mon Sep 17 00:00:00 2001 From: Felix Haeberle Date: Mon, 11 Mar 2024 15:01:03 +0100 Subject: [PATCH 09/17] add changeset and version to package --- .changeset/short-steaks-call.md | 5 +++++ .../source-code/cross-sell/cross-sell-sherlock/package.json | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 .changeset/short-steaks-call.md diff --git a/.changeset/short-steaks-call.md b/.changeset/short-steaks-call.md new file mode 100644 index 0000000000..4496b667d2 --- /dev/null +++ b/.changeset/short-steaks-call.md @@ -0,0 +1,5 @@ +--- +"@inlang/cross-sell-sherlock": patch +--- + +init package diff --git a/inlang/source-code/cross-sell/cross-sell-sherlock/package.json b/inlang/source-code/cross-sell/cross-sell-sherlock/package.json index 013a661f14..ac75e19d5f 100644 --- a/inlang/source-code/cross-sell/cross-sell-sherlock/package.json +++ b/inlang/source-code/cross-sell/cross-sell-sherlock/package.json @@ -1,6 +1,7 @@ { - "name": "cross-sell-sherlock", + "name": "@inlang/cross-sell-sherlock", "description": "A package to cross-sell Sherlock", + "version": "0.0.0", "type": "module", "exports": { ".": "./dist/index.js" From 6dd00abd14be5251c8bc13d18bdf09b9445fff05 Mon Sep 17 00:00:00 2001 From: Felix Haeberle Date: Mon, 11 Mar 2024 15:05:04 +0100 Subject: [PATCH 10/17] move files to have better structure --- .../cross-sell-sherlock/src/recommendation/index.ts | 6 +++--- .../src/recommendation/{ => utils}/isExtensionJson.ts | 0 .../src/recommendation/{ => utils}/joinPath.ts | 0 .../src/recommendation/{ => utils}/types.ts | 0 4 files changed, 3 insertions(+), 3 deletions(-) rename inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/{ => utils}/isExtensionJson.ts (100%) rename inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/{ => utils}/joinPath.ts (100%) rename inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/{ => utils}/types.ts (100%) diff --git a/inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/index.ts b/inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/index.ts index 4ea85849dc..8082ea8141 100644 --- a/inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/index.ts +++ b/inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/index.ts @@ -1,8 +1,8 @@ import type { NodeishFilesystem } from "@lix-js/fs" -import { joinPath } from "./joinPath.js" -import { isExtensionsJson } from "./isExtensionJson.js" +import { joinPath } from "./utils/joinPath.js" +import { isExtensionsJson } from "./utils/isExtensionJson.js" import { parse, type CommentJSONValue, stringify } from "comment-json/index.js" -import type { ExtensionsJson } from "./types.js" +import type { ExtensionsJson } from "./utils/types.js" export async function addRecommendationToWorkspace( fs: NodeishFilesystem, diff --git a/inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/isExtensionJson.ts b/inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/utils/isExtensionJson.ts similarity index 100% rename from inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/isExtensionJson.ts rename to inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/utils/isExtensionJson.ts diff --git a/inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/joinPath.ts b/inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/utils/joinPath.ts similarity index 100% rename from inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/joinPath.ts rename to inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/utils/joinPath.ts diff --git a/inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/types.ts b/inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/utils/types.ts similarity index 100% rename from inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/types.ts rename to inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/utils/types.ts From b7f046d8511f18890ee9ed489fc17605163ecddb Mon Sep 17 00:00:00 2001 From: Felix Haeberle Date: Mon, 11 Mar 2024 15:05:45 +0100 Subject: [PATCH 11/17] rename file to have better observability --- inlang/source-code/cross-sell/cross-sell-sherlock/src/index.ts | 2 +- .../src/recommendation/{index.ts => recommendation.ts} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/{index.ts => recommendation.ts} (100%) diff --git a/inlang/source-code/cross-sell/cross-sell-sherlock/src/index.ts b/inlang/source-code/cross-sell/cross-sell-sherlock/src/index.ts index 80c9a79457..d41c071ecb 100644 --- a/inlang/source-code/cross-sell/cross-sell-sherlock/src/index.ts +++ b/inlang/source-code/cross-sell/cross-sell-sherlock/src/index.ts @@ -2,7 +2,7 @@ import { type NodeishFilesystem } from "@lix-js/fs" import { addRecommendationToWorkspace, isInWorkspaceRecommendation, -} from "./recommendation/index.js" +} from "./recommendation/recommendation.js" export async function isAdopted( fs: NodeishFilesystem, diff --git a/inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/index.ts b/inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/recommendation.ts similarity index 100% rename from inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/index.ts rename to inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/recommendation.ts From af8ae76f78123ed7dd9cb1b2a86cd864098ed9a7 Mon Sep 17 00:00:00 2001 From: Felix Haeberle Date: Mon, 11 Mar 2024 15:07:11 +0100 Subject: [PATCH 12/17] update 'README --- inlang/source-code/cross-sell/cross-sell-sherlock/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inlang/source-code/cross-sell/cross-sell-sherlock/README.md b/inlang/source-code/cross-sell/cross-sell-sherlock/README.md index bf551ae539..f596f27fcb 100644 --- a/inlang/source-code/cross-sell/cross-sell-sherlock/README.md +++ b/inlang/source-code/cross-sell/cross-sell-sherlock/README.md @@ -40,7 +40,7 @@ Adds the `inlang.vs-code-extension` recommendation to the workspace if it's not ## Example ```typescript -import { isAdopted, add } from 'cross-sell-sherlock'; +import { isAdopted, add } from '@inlang/cross-sell-sherlock'; import { NodeishFilesystem } from '@lix-js/fs'; async function addSherlock(fs: NodeishFilesystem) { From 0797d16c2ed6b9c7e07d3689bbe75abf48c1b149 Mon Sep 17 00:00:00 2001 From: Felix Haeberle Date: Mon, 11 Mar 2024 15:09:40 +0100 Subject: [PATCH 13/17] update README --- inlang/source-code/cross-sell/cross-sell-sherlock/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/inlang/source-code/cross-sell/cross-sell-sherlock/README.md b/inlang/source-code/cross-sell/cross-sell-sherlock/README.md index f596f27fcb..4c9300493c 100644 --- a/inlang/source-code/cross-sell/cross-sell-sherlock/README.md +++ b/inlang/source-code/cross-sell/cross-sell-sherlock/README.md @@ -7,6 +7,8 @@ ## Installation +Put this one into your `dependencies` in `package.json` & install it with `pnpm install`: + ```bash "@inlang/cross-sell-sherlock": "workspace:*" ``` From 88f806ea54fff05e9678824c6d53f2019ff6ceac Mon Sep 17 00:00:00 2001 From: Felix Haeberle Date: Mon, 11 Mar 2024 15:20:10 +0100 Subject: [PATCH 14/17] update import --- .../cross-sell-sherlock/src/recommendation/recommendation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/recommendation.ts b/inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/recommendation.ts index 8082ea8141..dafbf986b1 100644 --- a/inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/recommendation.ts +++ b/inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/recommendation.ts @@ -1,7 +1,7 @@ import type { NodeishFilesystem } from "@lix-js/fs" import { joinPath } from "./utils/joinPath.js" import { isExtensionsJson } from "./utils/isExtensionJson.js" -import { parse, type CommentJSONValue, stringify } from "comment-json/index.js" +import { parse, type CommentJSONValue, stringify } from "comment-json" import type { ExtensionsJson } from "./utils/types.js" export async function addRecommendationToWorkspace( From ff323dbf5c78a52fda15c98791510c0f3e6773bd Mon Sep 17 00:00:00 2001 From: Felix Haeberle Date: Mon, 11 Mar 2024 15:29:03 +0100 Subject: [PATCH 15/17] update test --- .../cross-sell/cross-sell-sherlock/src/index.test.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/inlang/source-code/cross-sell/cross-sell-sherlock/src/index.test.ts b/inlang/source-code/cross-sell/cross-sell-sherlock/src/index.test.ts index e99d850a45..40298196c5 100644 --- a/inlang/source-code/cross-sell/cross-sell-sherlock/src/index.test.ts +++ b/inlang/source-code/cross-sell/cross-sell-sherlock/src/index.test.ts @@ -20,17 +20,23 @@ describe("Cross-sell Sherlock app", () => { expect(await isAdopted(fsMock, "/mock/path")).toBe(false) }) - it("should add recommendation when not present", async () => { + it("should merge with other extensions & add recommendation when not present", async () => { const fsMock: any = { stat: vi.fn().mockResolvedValue(true), - readFile: vi.fn().mockResolvedValue(JSON.stringify({ recommendations: [] })), + readFile: vi + .fn() + .mockResolvedValue(JSON.stringify({ recommendations: ["some.other-extension"] })), writeFile: vi.fn(), mkdir: vi.fn(), } await add(fsMock, "/mock/path") expect(fsMock.writeFile).toHaveBeenCalledWith( "/mock/path/.vscode/extensions.json", - JSON.stringify({ recommendations: ["inlang.vs-code-extension"] }, undefined, 2) + JSON.stringify( + { recommendations: ["some.other-extension", "inlang.vs-code-extension"] }, + undefined, + 2 + ) ) }) From 0995046e0633c70b5a85d440419e682950d3770c Mon Sep 17 00:00:00 2001 From: Felix Haeberle Date: Mon, 11 Mar 2024 15:34:17 +0100 Subject: [PATCH 16/17] fix `./.vscode` root dir --- .../src/recommendation/recommendation.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/recommendation.ts b/inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/recommendation.ts index dafbf986b1..63715e861c 100644 --- a/inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/recommendation.ts +++ b/inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/recommendation.ts @@ -1,4 +1,4 @@ -import type { NodeishFilesystem } from "@lix-js/fs" +import { normalizePath, type NodeishFilesystem } from "@lix-js/fs" import { joinPath } from "./utils/joinPath.js" import { isExtensionsJson } from "./utils/isExtensionJson.js" import { parse, type CommentJSONValue, stringify } from "comment-json" @@ -8,7 +8,7 @@ export async function addRecommendationToWorkspace( fs: NodeishFilesystem, workingDirectory?: string ): Promise { - const vscodeFolderPath = joinPath(workingDirectory ?? "", ".vscode") + const vscodeFolderPath = normalizePath(joinPath(workingDirectory ?? "", "./.vscode")) const extensionsJsonPath = joinPath(vscodeFolderPath, "extensions.json") if (!(await fs.stat(vscodeFolderPath))) { @@ -41,7 +41,7 @@ export async function isInWorkspaceRecommendation( fs: NodeishFilesystem, workingDirectory?: string ): Promise { - const vscodeFolderPath = joinPath(workingDirectory ?? "", ".vscode") + const vscodeFolderPath = normalizePath(joinPath(workingDirectory ?? "", "./.vscode")) const extensionsJsonPath = joinPath(vscodeFolderPath, "extensions.json") if (!(await fs.stat(extensionsJsonPath)) || !(await fs.stat(vscodeFolderPath))) { From 140d80395363f6c6ea7f2b80eed0cce1125b186b Mon Sep 17 00:00:00 2001 From: Felix Haeberle Date: Mon, 11 Mar 2024 15:48:21 +0100 Subject: [PATCH 17/17] add typebox for type validation --- .../cross-sell/cross-sell-sherlock/package.json | 1 + .../src/recommendation/recommendation.ts | 10 +++++----- .../src/recommendation/utils/isExtensionJson.ts | 10 ---------- .../src/recommendation/utils/types.ts | 10 +++++++--- pnpm-lock.yaml | 3 +++ 5 files changed, 16 insertions(+), 18 deletions(-) delete mode 100644 inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/utils/isExtensionJson.ts diff --git a/inlang/source-code/cross-sell/cross-sell-sherlock/package.json b/inlang/source-code/cross-sell/cross-sell-sherlock/package.json index ac75e19d5f..a4934ea026 100644 --- a/inlang/source-code/cross-sell/cross-sell-sherlock/package.json +++ b/inlang/source-code/cross-sell/cross-sell-sherlock/package.json @@ -24,6 +24,7 @@ "@types/vscode": "^1.84.2", "comment-json": "^4.2.3", "patch-package": "6.5.1", + "@sinclair/typebox": "^0.31.17", "typescript": "^5.1.3", "@vitest/coverage-v8": "0.33.0", "vitest": "0.33.0" diff --git a/inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/recommendation.ts b/inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/recommendation.ts index 63715e861c..7ab08acaf5 100644 --- a/inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/recommendation.ts +++ b/inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/recommendation.ts @@ -1,8 +1,8 @@ import { normalizePath, type NodeishFilesystem } from "@lix-js/fs" import { joinPath } from "./utils/joinPath.js" -import { isExtensionsJson } from "./utils/isExtensionJson.js" +import { Value } from "@sinclair/typebox/value" import { parse, type CommentJSONValue, stringify } from "comment-json" -import type { ExtensionsJson } from "./utils/types.js" +import { type ExtensionsJson as ExtensionsJsonType, ExtensionsJson } from "./utils/types.js" export async function addRecommendationToWorkspace( fs: NodeishFilesystem, @@ -15,11 +15,11 @@ export async function addRecommendationToWorkspace( await fs.mkdir(vscodeFolderPath) } - let extensions: ExtensionsJson + let extensions: ExtensionsJsonType if (await fs.stat(extensionsJsonPath)) { try { const parsed = parse(await fs.readFile(extensionsJsonPath, { encoding: "utf-8" })) - if (isExtensionsJson(parsed)) { + if (Value.Check(ExtensionsJson, parsed)) { extensions = parsed } else { extensions = { recommendations: [] } @@ -52,7 +52,7 @@ export async function isInWorkspaceRecommendation( await fs.readFile(extensionsJsonPath, { encoding: "utf-8" }) ) as CommentJSONValue - if (!isExtensionsJson(extensions)) { + if (!Value.Check(ExtensionsJson, extensions)) { return false } diff --git a/inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/utils/isExtensionJson.ts b/inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/utils/isExtensionJson.ts deleted file mode 100644 index 5d199c3df1..0000000000 --- a/inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/utils/isExtensionJson.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { ExtensionsJson } from "./types.js" - -export function isExtensionsJson(object: any): object is ExtensionsJson { - return ( - object !== null && - typeof object === "object" && - Array.isArray(object.recommendations) && - object.recommendations.every((item: any) => typeof item === "string") - ) -} diff --git a/inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/utils/types.ts b/inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/utils/types.ts index 362a958972..f900cf4681 100644 --- a/inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/utils/types.ts +++ b/inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/utils/types.ts @@ -1,3 +1,7 @@ -export type ExtensionsJson = { - recommendations: string[] -} +import { Type, type Static } from "@sinclair/typebox" + +export const ExtensionsJson = Type.Object({ + recommendations: Type.Array(Type.String()), +}) + +export type ExtensionsJson = Static diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9927c5aca7..e52c9a73cd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -212,6 +212,9 @@ importers: '@lix-js/fs': specifier: workspace:* version: link:../../../../lix/source-code/fs + '@sinclair/typebox': + specifier: ^0.31.17 + version: 0.31.28 '@types/vscode': specifier: ^1.84.2 version: 1.84.2