-
Notifications
You must be signed in to change notification settings - Fork 116
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2360 from opral/add-cross-sell-sherlock-package
Add `@inlang/cross-sell-sherlock` package
- Loading branch information
Showing
11 changed files
with
411 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@inlang/cross-sell-sherlock": patch | ||
--- | ||
|
||
init package |
67 changes: 67 additions & 0 deletions
67
inlang/source-code/cross-sell/cross-sell-sherlock/README.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
# 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 | ||
|
||
Put this one into your `dependencies` in `package.json` & install it with `pnpm install`: | ||
|
||
```bash | ||
"@inlang/cross-sell-sherlock": "workspace:*" | ||
``` | ||
|
||
## Usage | ||
|
||
The module exports two main asynchronous functions: | ||
|
||
### `isAdopted(fs: NodeishFilesystem, workingDirectory?: string): Promise<boolean>` | ||
|
||
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<boolean>`: `true` if the extension is recommended, `false` otherwise. | ||
|
||
### `add(fs: NodeishFilesystem, workingDirectory?: string): Promise<void>` | ||
|
||
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 '@inlang/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). |
32 changes: 32 additions & 0 deletions
32
inlang/source-code/cross-sell/cross-sell-sherlock/package.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
{ | ||
"name": "@inlang/cross-sell-sherlock", | ||
"description": "A package to cross-sell Sherlock", | ||
"version": "0.0.0", | ||
"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", | ||
"@sinclair/typebox": "^0.31.17", | ||
"typescript": "^5.1.3", | ||
"@vitest/coverage-v8": "0.33.0", | ||
"vitest": "0.33.0" | ||
} | ||
} |
175 changes: 175 additions & 0 deletions
175
inlang/source-code/cross-sell/cross-sell-sherlock/src/index.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
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 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: ["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: ["some.other-extension", "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") | ||
}) | ||
}) |
16 changes: 16 additions & 0 deletions
16
inlang/source-code/cross-sell/cross-sell-sherlock/src/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { type NodeishFilesystem } from "@lix-js/fs" | ||
import { | ||
addRecommendationToWorkspace, | ||
isInWorkspaceRecommendation, | ||
} from "./recommendation/recommendation.js" | ||
|
||
export async function isAdopted( | ||
fs: NodeishFilesystem, | ||
workingDirectory?: string | ||
): Promise<boolean> { | ||
return isInWorkspaceRecommendation(fs, workingDirectory) | ||
} | ||
|
||
export async function add(fs: NodeishFilesystem, workingDirectory?: string): Promise<void> { | ||
await addRecommendationToWorkspace(fs, workingDirectory) | ||
} |
60 changes: 60 additions & 0 deletions
60
inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/recommendation.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import { normalizePath, type NodeishFilesystem } from "@lix-js/fs" | ||
import { joinPath } from "./utils/joinPath.js" | ||
import { Value } from "@sinclair/typebox/value" | ||
import { parse, type CommentJSONValue, stringify } from "comment-json" | ||
import { type ExtensionsJson as ExtensionsJsonType, ExtensionsJson } from "./utils/types.js" | ||
|
||
export async function addRecommendationToWorkspace( | ||
fs: NodeishFilesystem, | ||
workingDirectory?: string | ||
): Promise<void> { | ||
const vscodeFolderPath = normalizePath(joinPath(workingDirectory ?? "", "./.vscode")) | ||
const extensionsJsonPath = joinPath(vscodeFolderPath, "extensions.json") | ||
|
||
if (!(await fs.stat(vscodeFolderPath))) { | ||
await fs.mkdir(vscodeFolderPath) | ||
} | ||
|
||
let extensions: ExtensionsJsonType | ||
if (await fs.stat(extensionsJsonPath)) { | ||
try { | ||
const parsed = parse(await fs.readFile(extensionsJsonPath, { encoding: "utf-8" })) | ||
if (Value.Check(ExtensionsJson, 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<boolean> { | ||
const vscodeFolderPath = normalizePath(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 (!Value.Check(ExtensionsJson, extensions)) { | ||
return false | ||
} | ||
|
||
return extensions?.recommendations.includes("inlang.vs-code-extension") || false | ||
} |
3 changes: 3 additions & 0 deletions
3
inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/utils/joinPath.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export function joinPath(...parts: string[]): string { | ||
return parts.map((part) => part.replace(/\/$/, "")).join("/") | ||
} |
7 changes: 7 additions & 0 deletions
7
inlang/source-code/cross-sell/cross-sell-sherlock/src/recommendation/utils/types.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import { Type, type Static } from "@sinclair/typebox" | ||
|
||
export const ExtensionsJson = Type.Object({ | ||
recommendations: Type.Array(Type.String()), | ||
}) | ||
|
||
export type ExtensionsJson = Static<typeof ExtensionsJson> |
9 changes: 9 additions & 0 deletions
9
inlang/source-code/cross-sell/cross-sell-sherlock/tsconfig.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"extends": "../../tsconfig.base.json", | ||
"include": ["src/*", "./build.js"], | ||
"compilerOptions": { | ||
"resolveJsonModule": true, | ||
"outDir": "./dist", | ||
"rootDir": "./src" | ||
} | ||
} |
Oops, something went wrong.