@ben_12/eslint-plugin-dprint 1.20.0 → 1.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -123,6 +123,96 @@ module.exports = {
123
123
 
124
124
  Then run ESLint with `--fix`!
125
125
 
126
+ ### Providing a pre-resolved formatter via `settings`
127
+
128
+ By default the plugin loads each dprint formatter at module-load time by doing a dynamic `require()`
129
+ of the corresponding peer package (`@dprint/typescript`, `dprint-plugin-yaml`, …). That works for
130
+ most setups, but it relies on the optional peer dependency being installed _and_ discoverable by
131
+ Node's resolver. Some bundlers and bundled config packages (anything that does static module-graph
132
+ analysis, e.g. [Packtory](https://github.com/lukas-bischof/packtory)) only follow real `import`
133
+ statements, so the dprint plugins never end up in the bundle. In that case the plugin logs
134
+ `Plugin not found: …` for every linted file and the rule silently becomes a no-op.
135
+
136
+ You can avoid this by passing a pre-resolved formatter through ESLint's `settings`. Settings are not
137
+ serialized or `structuredClone`d by ESLint, so they can carry runtime objects (modules, buffers,
138
+ pre-built formatters). When a formatter is provided this way, the plugin uses it directly and skips
139
+ the dynamic `require()` lookup (and the associated "Plugin not found" warning).
140
+
141
+ Accepted shapes for each formatter entry:
142
+
143
+ - an object with `getPath(): string` (the shape exported by `@dprint/typescript`, `@dprint/json`,
144
+ `@dprint/markdown`, `@dprint/toml`, `@dprint/dockerfile`)
145
+ - an object with `getBuffer(): Buffer | Uint8Array` (legacy shape, previously used by some
146
+ `@dprint/*` packages)
147
+ - a raw `Buffer` / `Uint8Array` / `ArrayBuffer` of the plugin's wasm bytes
148
+ - a pre-built `Formatter` returned by `@dprint/formatter`'s `createFromBuffer`
149
+
150
+ Example (flat config):
151
+
152
+ ```mjs
153
+ import { defineConfig } from "eslint/config";
154
+ import dprint from "@ben_12/eslint-plugin-dprint";
155
+ import typescriptFormatter from "@dprint/typescript";
156
+
157
+ export default defineConfig([
158
+ {
159
+ files: ["**/*.ts"],
160
+ plugins: { "@ben_12/dprint": dprint },
161
+ settings: {
162
+ "@ben_12/dprint": {
163
+ formatters: {
164
+ typescript: typescriptFormatter,
165
+ },
166
+ },
167
+ },
168
+ rules: {
169
+ "@ben_12/dprint/typescript": ["error", { config: { /* dprint config */ } }],
170
+ },
171
+ },
172
+ ]);
173
+ ```
174
+
175
+ The settings key is the plugin namespace (`@ben_12/dprint`). Inside `formatters`, the key is the
176
+ rule name (`typescript`, `json`, `markdown`, `toml`, `dockerfile`, `malva`, `markup`, `yaml`,
177
+ `graphql`).
178
+
179
+ #### g-plane plugins (`yaml`, `malva`, `markup`, `graphql`)
180
+
181
+ The `@dprint/*` packages can be imported and passed straight through, as in the example above.
182
+ The g-plane packages (`dprint-plugin-yaml`, `dprint-plugin-malva`, `dprint-plugin-markup`,
183
+ `dprint-plugin-graphql`) have no JS entrypoint and ship only `plugin.wasm`, so callers have to
184
+ resolve and read the wasm file themselves and pass the result through `createFromBuffer` (or as
185
+ a raw `BufferSource`). Example for `dprint-plugin-graphql`:
186
+
187
+ ```mjs
188
+ import { defineConfig } from "eslint/config";
189
+ import dprint from "@ben_12/eslint-plugin-dprint";
190
+ import { createFromBuffer } from "@dprint/formatter";
191
+ import fs from "node:fs";
192
+
193
+ const graphqlFormatter = createFromBuffer(
194
+ fs.readFileSync(new URL(import.meta.resolve("dprint-plugin-graphql/plugin.wasm"))),
195
+ );
196
+
197
+ export default defineConfig([
198
+ {
199
+ files: ["**/*.graphql"],
200
+ plugins: { "@ben_12/dprint": dprint },
201
+ settings: {
202
+ "@ben_12/dprint": {
203
+ formatters: { graphql: graphqlFormatter },
204
+ },
205
+ },
206
+ rules: {
207
+ "@ben_12/dprint/graphql": ["error", { config: { /* dprint config */ } }],
208
+ },
209
+ },
210
+ ]);
211
+ ```
212
+
213
+ The same pattern (with the matching package and rule name) applies to `yaml`, `malva`, and
214
+ `markup`.
215
+
126
216
  For unparsed eslint file like markdown or dockerfile, you can use [@ben_12/eslint-simple-parser](https://www.npmjs.com/package/@ben_12/eslint-simple-parser) as parser.
127
217
 
128
218
  ```mjs
@@ -1,10 +1,37 @@
1
- import { FormatRequest } from "@dprint/formatter";
1
+ import { FormatRequest, Formatter } from "@dprint/formatter";
2
+ /**
3
+ * Caller-provided formatter, used as the pre-resolved bypass of the module-level
4
+ * plugin lookup. The `@dprint/*` packages expose `getPath()` and can be passed directly;
5
+ * the `g-plane` packages (`dprint-plugin-yaml`, `-malva`, `-markup`, `-graphql`) ship
6
+ * only `plugin.wasm` with no JS entrypoint, so callers must read the wasm file and
7
+ * pass the buffer (or a pre-built `Formatter`) instead.
8
+ *
9
+ * Supported shapes:
10
+ * - `{ getPath(): string }`: shape exported by current `@dprint/*` plugin packages
11
+ * - `{ getBuffer(): BufferSource }`: legacy shape, used by older `@dprint/*` versions
12
+ * - raw wasm bytes (`Buffer` / `Uint8Array` / `ArrayBuffer`)
13
+ * - a pre-built `Formatter` from `@dprint/formatter`'s `createFromBuffer`
14
+ */
15
+ export type FormatterInput = {
16
+ getPath(): string;
17
+ } | {
18
+ getBuffer(): BufferSource;
19
+ } | BufferSource | Formatter;
2
20
  /**
3
21
  * Format the given text with the given config.
4
- * @param config The config object.
22
+ * @param configFile Path to a dprint configuration file (or empty string for none).
23
+ * @param overrideConfig Inline config for the originating plugin (applied per-call via FormatRequest.overrideConfig).
24
+ * @param hostConfigs Per-sibling-plugin config. Merged into FormatRequest.overrideConfig on each
25
+ * delegated call when the originating plugin invokes a sibling via the host callback
26
+ * (e.g. fenced code blocks inside markdown). Keys are plugin names ("typescript", "json", ...).
27
+ * Without this, host-invoked plugins fall back to the on-disk configFile only.
5
28
  * @param filePath The path to the file.
6
29
  * @param fileText The content of the file.
7
- * @returns The formatted text or undefined. It's undefined if the formatter doesn't change the text.
30
+ * @param configName The plugin name of the originating formatter.
31
+ * @param formatterInput Optional pre-resolved dprint formatter (or its source) to use instead of the
32
+ * module-level plugin lookup. When supplied, the global formatter table is bypassed and no
33
+ * "Plugin not found" warning is logged.
34
+ * @returns The formatted text. Returns the input fileText if no formatter applies.
8
35
  */
9
- export declare function format(configFile: string, overrideConfig: Record<string, unknown>, filePath: string, fileText: string, configName: string): string;
10
- export declare function formatWithHost(request: FormatRequest, configFile: string, fromConfigName: string): string;
36
+ export declare function format(configFile: string, overrideConfig: Record<string, unknown>, hostConfigs: Record<string, Record<string, unknown>>, filePath: string, fileText: string, configName: string, formatterInput?: FormatterInput): string;
37
+ export declare function formatWithHost(request: FormatRequest, configFile: string, hostConfigs: Record<string, Record<string, unknown>>, fromConfigName: string): string;
@@ -99,16 +99,57 @@ const formatters = Object.entries(plugins).reduce((formatters, [name, getBuffer]
99
99
  }
100
100
  return formatters;
101
101
  }, {});
102
- /** Cache to reduce copies of config values. */
103
- const lastConfigFile = {};
104
- function getFormatter(filePath, configName, configFile, log = true) {
105
- const formatter = formatters[configName];
106
- if (formatter) {
102
+ function isBufferSource(value) {
103
+ return value instanceof ArrayBuffer || ArrayBuffer.isView(value);
104
+ }
105
+ function isFormatter(value) {
106
+ return typeof value === "object" && value !== null &&
107
+ typeof value.formatText === "function" &&
108
+ typeof value.getPluginInfo === "function" &&
109
+ typeof value.setConfig === "function";
110
+ }
111
+ /** Cache of resolved Formatters keyed by the caller-provided FormatterInput identity. */
112
+ const resolvedFormatters = new WeakMap();
113
+ function resolveFormatterInput(input) {
114
+ const cached = resolvedFormatters.get(input);
115
+ if (cached) {
116
+ return cached;
117
+ }
118
+ let formatter;
119
+ if (isFormatter(input)) {
120
+ formatter = input;
121
+ }
122
+ else if (isBufferSource(input)) {
123
+ formatter = (0, formatter_1.createFromBuffer)(input);
124
+ }
125
+ else if (typeof input.getPath === "function") {
126
+ formatter = (0, formatter_1.createFromBuffer)(Buffer.from(fs.readFileSync(input.getPath())));
127
+ }
128
+ else if (typeof input.getBuffer === "function") {
129
+ formatter = (0, formatter_1.createFromBuffer)(input.getBuffer());
130
+ }
131
+ else {
132
+ throw new TypeError("Invalid `formatter` option: expected a Formatter, BufferSource, " +
133
+ "or an object with getPath()/getBuffer().");
134
+ }
135
+ resolvedFormatters.set(input, formatter);
136
+ return formatter;
137
+ }
138
+ /** Cache of the last configFile applied to a given Formatter, to avoid redundant setConfig calls. */
139
+ const lastConfigFileByFormatter = new WeakMap();
140
+ function applyConfigIfNeeded(formatter, configFile) {
141
+ if (lastConfigFileByFormatter.get(formatter) !== configFile) {
142
+ lastConfigFileByFormatter.set(formatter, configFile);
107
143
  const configKey = formatter.getPluginInfo().configKey;
108
- if (configFile !== lastConfigFile[configKey]) {
109
- lastConfigFile[configKey] = configFile;
110
- setConfig(formatter, configKey, configFile);
111
- }
144
+ setConfig(formatter, configKey, configFile);
145
+ }
146
+ }
147
+ function getFormatter(filePath, configName, configFile, log, formatterInput) {
148
+ const formatter = formatterInput === undefined
149
+ ? formatters[configName]
150
+ : resolveFormatterInput(formatterInput);
151
+ if (formatter) {
152
+ applyConfigIfNeeded(formatter, configFile);
112
153
  const fileMatchingInfo = formatter.getFileMatchingInfo();
113
154
  const fileExtensions = fileMatchingInfo.fileExtensions || [];
114
155
  const fileNames = fileMatchingInfo.fileNames || [];
@@ -131,37 +172,52 @@ function getFormatter(filePath, configName, configFile, log = true) {
131
172
  function getFormatterByExt(filePath, configFile, exclude) {
132
173
  const configNames = Object.keys(formatters).filter(name => name !== exclude);
133
174
  for (const configName of configNames) {
134
- const formatter = getFormatter(filePath, configName, configFile, false);
175
+ const formatter = getFormatter(filePath, configName, configFile, false, undefined);
135
176
  if (formatter) {
136
- return [configFile, formatter];
177
+ return [configName, formatter];
137
178
  }
138
179
  }
139
180
  return [];
140
181
  }
141
182
  /**
142
183
  * Format the given text with the given config.
143
- * @param config The config object.
184
+ * @param configFile Path to a dprint configuration file (or empty string for none).
185
+ * @param overrideConfig Inline config for the originating plugin (applied per-call via FormatRequest.overrideConfig).
186
+ * @param hostConfigs Per-sibling-plugin config. Merged into FormatRequest.overrideConfig on each
187
+ * delegated call when the originating plugin invokes a sibling via the host callback
188
+ * (e.g. fenced code blocks inside markdown). Keys are plugin names ("typescript", "json", ...).
189
+ * Without this, host-invoked plugins fall back to the on-disk configFile only.
144
190
  * @param filePath The path to the file.
145
191
  * @param fileText The content of the file.
146
- * @returns The formatted text or undefined. It's undefined if the formatter doesn't change the text.
192
+ * @param configName The plugin name of the originating formatter.
193
+ * @param formatterInput Optional pre-resolved dprint formatter (or its source) to use instead of the
194
+ * module-level plugin lookup. When supplied, the global formatter table is bypassed and no
195
+ * "Plugin not found" warning is logged.
196
+ * @returns The formatted text. Returns the input fileText if no formatter applies.
147
197
  */
148
- function format(configFile, overrideConfig, filePath, fileText, configName) {
149
- const formatter = getFormatter(filePath, configName, configFile);
198
+ function format(configFile, overrideConfig, hostConfigs, filePath, fileText, configName, formatterInput) {
199
+ const formatter = getFormatter(filePath, configName, configFile, true, formatterInput);
150
200
  if (formatter) {
151
201
  const request = {
152
202
  filePath,
153
203
  fileText,
154
204
  overrideConfig,
155
205
  };
156
- return formatter.formatText(request, hostRequest => formatWithHost(hostRequest, configFile, configName));
206
+ return formatter.formatText(request, hostRequest => formatWithHost(hostRequest, configFile, hostConfigs, configName));
157
207
  }
158
208
  return fileText;
159
209
  }
160
210
  exports.format = format;
161
- function formatWithHost(request, configFile, fromConfigName) {
211
+ function formatWithHost(request, configFile, hostConfigs, fromConfigName) {
162
212
  const [configName, formatter] = getFormatterByExt(request.filePath, configFile, fromConfigName);
163
213
  if (configName && formatter) {
164
- return formatter.formatText(request, hostRequest => formatWithHost(hostRequest, configFile, configName));
214
+ const hostConfig = hostConfigs[configName];
215
+ if (hostConfig && typeof hostConfig === "object") {
216
+ const overrideConfig = { ...request.overrideConfig };
217
+ extractConfig(hostConfig, overrideConfig);
218
+ request = { ...request, overrideConfig };
219
+ }
220
+ return formatter.formatText(request, hostRequest => formatWithHost(hostRequest, configFile, hostConfigs, configName));
165
221
  }
166
222
  return request.fileText;
167
223
  }
@@ -1,4 +1,24 @@
1
1
  import { Rule } from "eslint";
2
+ import { FormatterInput } from "../dprint/dprint";
3
+ /**
4
+ * Shape of the per-plugin entry under ESLint's `settings`.
5
+ *
6
+ * Example:
7
+ * settings: {
8
+ * "@ben_12/dprint": {
9
+ * formatters: { typescript: require("@dprint/typescript") },
10
+ * },
11
+ * }
12
+ *
13
+ * `settings` is deep-merged without being structured-cloned, so the formatter values
14
+ * can be runtime objects (modules, pre-built Formatters, raw buffers) — see
15
+ * `FormatterInput` in lib/dprint/dprint.ts.
16
+ */
17
+ export interface DprintPluginSettings {
18
+ readonly formatters?: {
19
+ readonly [ruleName: string]: FormatterInput | undefined;
20
+ };
21
+ }
2
22
  export declare const dprintRules: {
3
23
  [name: string]: Rule.RuleModule;
4
24
  };
@@ -173,59 +173,85 @@ function createMessage(d) {
173
173
  createMoveMessage(d) ||
174
174
  createRepaceMessage(d);
175
175
  }
176
- const defaultOptions = { configFile: "dprint.json", config: {} };
177
- exports.dprintRules = configSchemas.map((config) => ({
178
- [config.name]: {
179
- meta: {
180
- docs: {
181
- description: `Format ${config.name} with dprint`,
182
- url: `https://github.com/ben12/eslint-plugin-dprint/blob/master/docs/rules/dprint-${config.name}.md`,
183
- recommended: true,
184
- },
185
- fixable: "code",
186
- messages,
187
- schema: {
188
- definitions: config.configSchema.definitions,
189
- type: "array",
190
- items: [{
191
- type: "object",
192
- properties: {
193
- configFile: {
194
- type: "string",
195
- default: "dprint.json",
196
- description: "dprint configuration file (default 'dprint.json')",
197
- },
198
- config: {
199
- type: "object",
200
- properties: config.configSchema.properties,
201
- additionalProperties: false,
176
+ const defaultOptions = { configFile: "dprint.json", config: {}, hostConfigs: {} };
177
+ const SETTINGS_KEY = "@ben_12/dprint";
178
+ function readFormatterFromSettings(settings, ruleName) {
179
+ const pluginSettings = (settings?.[SETTINGS_KEY] ?? undefined);
180
+ return pluginSettings?.formatters?.[ruleName];
181
+ }
182
+ exports.dprintRules = configSchemas.map((config) => {
183
+ // Allow per-sibling-plugin config to flow into setConfig for host-invoked formatters
184
+ // (e.g. fenced code blocks inside markdown). Keys are sibling plugin names; the current
185
+ // plugin is excluded because its own config already goes through `config`/overrideConfig.
186
+ // Inner values are not strictly validated by schema — dprint's WASM setConfig handles that.
187
+ const hostConfigsProperties = configSchemas.reduce((acc, sibling) => {
188
+ if (sibling.name !== config.name) {
189
+ acc[sibling.name] = { type: "object", additionalProperties: true };
190
+ }
191
+ return acc;
192
+ }, {});
193
+ return {
194
+ [config.name]: {
195
+ meta: {
196
+ docs: {
197
+ description: `Format ${config.name} with dprint`,
198
+ url: `https://github.com/ben12/eslint-plugin-dprint/blob/master/docs/rules/dprint-${config.name}.md`,
199
+ recommended: true,
200
+ },
201
+ fixable: "code",
202
+ messages,
203
+ schema: {
204
+ definitions: config.configSchema.definitions,
205
+ type: "array",
206
+ items: [{
207
+ type: "object",
208
+ properties: {
209
+ configFile: {
210
+ type: "string",
211
+ default: "dprint.json",
212
+ description: "dprint configuration file (default 'dprint.json')",
213
+ },
214
+ config: {
215
+ type: "object",
216
+ properties: config.configSchema.properties,
217
+ additionalProperties: false,
218
+ },
219
+ hostConfigs: {
220
+ type: "object",
221
+ properties: hostConfigsProperties,
222
+ additionalProperties: false,
223
+ description: "Config applied to sibling plugins when this plugin delegates to them " +
224
+ "(e.g. fenced code blocks). Keys are sibling plugin names.",
225
+ },
202
226
  },
203
- },
204
- additionalProperties: false,
205
- }],
206
- additionalItems: false,
227
+ additionalProperties: false,
228
+ }],
229
+ additionalItems: false,
230
+ },
231
+ type: "layout",
207
232
  },
208
- type: "layout",
233
+ create: (context) => ({
234
+ Program() {
235
+ const sourceCode = context.sourceCode ?? context.getSourceCode();
236
+ const filePath = context.filename ?? context.getFilename();
237
+ const fileText = sourceCode.getText();
238
+ const options = context.options[0] ?? defaultOptions;
239
+ const configFile = options.configFile ?? "dprint.json";
240
+ const configOpt = options.config || {};
241
+ const hostConfigs = options.hostConfigs || {};
242
+ // Needs an absolute path
243
+ if (!filePath || !node_path_1.default.isAbsolute(filePath)) {
244
+ return;
245
+ }
246
+ // Does format
247
+ const formatterInput = readFormatterFromSettings(context.settings, config.name);
248
+ const formattedText = (0, dprint_1.format)(configFile, configOpt, hostConfigs, filePath, fileText, config.name, formatterInput);
249
+ generateLintReports(fileText, formattedText, sourceCode, context);
250
+ },
251
+ }),
209
252
  },
210
- create: (context) => ({
211
- Program() {
212
- const sourceCode = context.sourceCode ?? context.getSourceCode();
213
- const filePath = context.filename ?? context.getFilename();
214
- const fileText = sourceCode.getText();
215
- const options = context.options[0] ?? defaultOptions;
216
- const configFile = options.configFile ?? "dprint.json";
217
- const configOpt = options.config || {};
218
- // Needs an absolute path
219
- if (!filePath || !node_path_1.default.isAbsolute(filePath)) {
220
- return;
221
- }
222
- // Does format
223
- const formattedText = (0, dprint_1.format)(configFile, configOpt, filePath, fileText, config.name);
224
- generateLintReports(fileText, formattedText, sourceCode, context);
225
- },
226
- }),
227
- },
228
- })).reduce((r1, r2) => ({ ...r1, ...r2 }), {});
253
+ };
254
+ }).reduce((r1, r2) => ({ ...r1, ...r2 }), {});
229
255
  function generateLintReports(fileText, formattedText, sourceCode, context) {
230
256
  for (const d of difference_iterator_1.DifferenceIterator.iterate(fileText, formattedText)) {
231
257
  const loc = d.type === "add"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ben_12/eslint-plugin-dprint",
3
- "version": "1.20.0",
3
+ "version": "1.21.1",
4
4
  "description": "An ESLint plugin that fixes code with dprint",
5
5
  "engines": {
6
6
  "node": ">=18.0.0"
@@ -18,7 +18,7 @@
18
18
  "@dprint/typescript": "^0.96.1",
19
19
  "dprint-plugin-graphql": "^0.2.3",
20
20
  "dprint-plugin-malva": "^0.16.0",
21
- "dprint-plugin-markup": "^0.27.2",
21
+ "dprint-plugin-markup": "^0.27.3",
22
22
  "dprint-plugin-yaml": "^0.6.0",
23
23
  "eslint": ">=7.0.0"
24
24
  },
@@ -72,7 +72,7 @@
72
72
  "axios": "^1.7.9",
73
73
  "dprint-plugin-graphql": "^0.2.3",
74
74
  "dprint-plugin-malva": "^0.16.0",
75
- "dprint-plugin-markup": "^0.27.2",
75
+ "dprint-plugin-markup": "^0.27.3",
76
76
  "dprint-plugin-yaml": "^0.6.0",
77
77
  "eslint": "^9.0.0",
78
78
  "mocha": "^11.0.0",