-
-
Notifications
You must be signed in to change notification settings - Fork 724
chore(oxlint/napi): add createWorkspace & destroyWorkspace js callback function
#16121
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,3 @@ | ||
| export { lintFile } from "./lint.js"; | ||
| export { loadPlugin } from "./load.js"; | ||
| export { createWorkspace, destroyWorkspace } from "./workspace.js"; |
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,13 +1,13 @@ | ||||||||||||||
| import { getErrorMessage } from "../utils/utils.js"; | ||||||||||||||
| import { createContext } from "./context.js"; | ||||||||||||||
| import { DEFAULT_OPTIONS } from "./options.js"; | ||||||||||||||
| import { getErrorMessage } from "../utils/utils.js"; | ||||||||||||||
|
|
||||||||||||||
| import type { Writable } from "type-fest"; | ||||||||||||||
| import type { SetNullable } from "../utils/types.ts"; | ||||||||||||||
| import type { Context } from "./context.ts"; | ||||||||||||||
| import type { Options } from "./options.ts"; | ||||||||||||||
| import type { RuleMeta } from "./rule_meta.ts"; | ||||||||||||||
| import type { AfterHook, BeforeHook, Visitor, VisitorWithHooks } from "./types.ts"; | ||||||||||||||
| import type { SetNullable } from "../utils/types.ts"; | ||||||||||||||
|
|
||||||||||||||
| const ObjectKeys = Object.keys, | ||||||||||||||
| { isArray } = Array; | ||||||||||||||
|
|
@@ -78,11 +78,11 @@ interface CreateOnceRuleDetails extends RuleDetailsBase { | |||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // Absolute paths of plugins which have been loaded | ||||||||||||||
| const registeredPluginUrls = new Set<string>(); | ||||||||||||||
| export const registeredPluginUrls = new Map<string, Set<string>>(); | ||||||||||||||
|
|
||||||||||||||
| // Rule objects for loaded rules. | ||||||||||||||
| // Indexed by `ruleId`, which is passed to `lintFile`. | ||||||||||||||
| export const registeredRules: RuleDetails[] = []; | ||||||||||||||
| export const registeredRules: Map<string, RuleDetails[]> = new Map(); | ||||||||||||||
|
|
||||||||||||||
| // `before` hook which makes rule never run. | ||||||||||||||
| const neverRunBeforeHook: BeforeHook = () => false; | ||||||||||||||
|
|
@@ -102,13 +102,18 @@ interface PluginDetails { | |||||||||||||
| * | ||||||||||||||
| * Main logic is in separate function `loadPluginImpl`, because V8 cannot optimize functions containing try/catch. | ||||||||||||||
| * | ||||||||||||||
| * @param workspaceDir - Workspace root directory | ||||||||||||||
| * @param url - Absolute path of plugin file as a `file://...` URL | ||||||||||||||
| * @param packageName - Optional package name from `package.json` (fallback if `plugin.meta.name` is not defined) | ||||||||||||||
| * @returns Plugin details or error serialized to JSON string | ||||||||||||||
| */ | ||||||||||||||
| export async function loadPlugin(url: string, packageName: string | null): Promise<string> { | ||||||||||||||
| export async function loadPlugin( | ||||||||||||||
| workspaceDir: string, | ||||||||||||||
| url: string, | ||||||||||||||
| packageName: string | null, | ||||||||||||||
| ): Promise<string> { | ||||||||||||||
| try { | ||||||||||||||
| const res = await loadPluginImpl(url, packageName); | ||||||||||||||
| const res = await loadPluginImpl(workspaceDir, url, packageName); | ||||||||||||||
| return JSON.stringify({ Success: res }); | ||||||||||||||
| } catch (err) { | ||||||||||||||
| return JSON.stringify({ Failure: getErrorMessage(err) }); | ||||||||||||||
|
|
@@ -118,19 +123,27 @@ export async function loadPlugin(url: string, packageName: string | null): Promi | |||||||||||||
| /** | ||||||||||||||
| * Load a plugin. | ||||||||||||||
| * | ||||||||||||||
| * @param workspaceDir - Workspace root directory | ||||||||||||||
| * @param url - Absolute path of plugin file as a `file://...` URL | ||||||||||||||
| * @param packageName - Optional package name from `package.json` (fallback if `plugin.meta.name` is not defined) | ||||||||||||||
| * @returns - Plugin details | ||||||||||||||
| * @throws {Error} If workspaceDir is invalid | ||||||||||||||
| * @throws {Error} If plugin has already been registered | ||||||||||||||
| * @throws {Error} If plugin has no name | ||||||||||||||
| * @throws {TypeError} If one of plugin's rules is malformed, or its `createOnce` method returns invalid visitor | ||||||||||||||
| * @throws {TypeError} if `plugin.meta.name` is not a string | ||||||||||||||
| * @throws {*} If plugin throws an error during import | ||||||||||||||
| */ | ||||||||||||||
| async function loadPluginImpl(url: string, packageName: string | null): Promise<PluginDetails> { | ||||||||||||||
| async function loadPluginImpl( | ||||||||||||||
| workspaceDir: string, | ||||||||||||||
| url: string, | ||||||||||||||
| packageName: string | null, | ||||||||||||||
| ): Promise<PluginDetails> { | ||||||||||||||
| if (DEBUG) { | ||||||||||||||
| if (registeredPluginUrls.has(url)) throw new Error("This plugin has already been registered"); | ||||||||||||||
| registeredPluginUrls.add(url); | ||||||||||||||
| if (!registeredRules.has(workspaceDir)) throw new Error("Invalid workspaceDir"); | ||||||||||||||
| if (registeredPluginUrls.get(workspaceDir)?.has(url)) | ||||||||||||||
| throw new Error("This plugin has already been registered"); | ||||||||||||||
| registeredPluginUrls.get(workspaceDir)?.add(url); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| const { default: plugin } = (await import(url)) as { default: Plugin }; | ||||||||||||||
|
|
@@ -139,7 +152,7 @@ async function loadPluginImpl(url: string, packageName: string | null): Promise< | |||||||||||||
|
|
||||||||||||||
| const pluginName = getPluginName(plugin, packageName); | ||||||||||||||
|
|
||||||||||||||
| const offset = registeredRules.length; | ||||||||||||||
| const offset = registeredRules.get(workspaceDir)?.length ?? 0; | ||||||||||||||
| const { rules } = plugin; | ||||||||||||||
| const ruleNames = ObjectKeys(rules); | ||||||||||||||
| const ruleNamesLen = ruleNames.length; | ||||||||||||||
|
|
@@ -232,7 +245,7 @@ async function loadPluginImpl(url: string, packageName: string | null): Promise< | |||||||||||||
| (ruleDetails as unknown as Writable<CreateOnceRuleDetails>).afterHook = afterHook; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| registeredRules.push(ruleDetails); | ||||||||||||||
| registeredRules.get(workspaceDir)?.push(ruleDetails); | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Silent failure when pushing rule. If the workspace doesn't exist, the optional chaining causes const rules = registeredRules.get(workspaceDir);
if (!rules) {
throw new Error(`Workspace "${workspaceDir}" has not been created`);
}
rules.push(ruleDetails);The workspace should be validated to exist, or an error should be thrown.
Suggested change
Spotted by Graphite Agent |
||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| return { name: pluginName, offset, ruleNames }; | ||||||||||||||
|
|
||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| /* | ||
| * Isolated Workspaces for linting plugins. | ||
| * | ||
| * Every Workspace starts with a "workspace root" directory. This directory is | ||
| * used to isolate the plugin's dependencies from other plugins and the main | ||
| * application. | ||
| * | ||
| * Each workspace can be created, used, and then cleared to free up resources. | ||
| */ | ||
|
|
||
| import { registeredPluginUrls, registeredRules } from "./load.js"; | ||
|
|
||
| /** | ||
| * Set of workspace root directories. | ||
| */ | ||
| const workspaceRoots = new Set<string>(); | ||
|
|
||
| /** | ||
| * Create a new workspace. | ||
| */ | ||
| export const createWorkspace = async (rootDir: string): Promise<undefined> => { | ||
Sysix marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if (DEBUG) { | ||
| if (workspaceRoots.has(rootDir)) | ||
| throw new Error(`Workspace for rootDir "${rootDir}" already exists`); | ||
| } | ||
|
|
||
| workspaceRoots.add(rootDir); | ||
| registeredPluginUrls.set(rootDir, new Set<string>()); | ||
| registeredRules.set(rootDir, []); | ||
| }; | ||
|
|
||
Sysix marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| /** | ||
| * Destroy a workspace. | ||
| */ | ||
| export const destroyWorkspace = (rootDir: string): undefined => { | ||
| if (DEBUG) { | ||
| if (!workspaceRoots.has(rootDir)) | ||
| throw new Error(`Workspace for rootDir "${rootDir}" does not exist`); | ||
| if (!registeredPluginUrls.has(rootDir)) throw new Error("Invalid workspaceDir"); | ||
| if (!registeredRules.has(rootDir)) throw new Error("Invalid workspaceDir"); | ||
| } | ||
| workspaceRoots.delete(rootDir); | ||
| registeredPluginUrls.delete(rootDir); | ||
| registeredRules.delete(rootDir); | ||
| }; | ||
Uh oh!
There was an error while loading. Please reload this page.