Commit 9a03f0de authored by David Sveningsson's avatar David Sveningsson
Browse files

Merge branch 'feature/flat-transform' into 'master'

feat: handle transformers from plugins in flat config

See merge request !1407
parents 7b4a85e0 f1ad4c63
Loading
Loading
Loading
Loading
Loading
+39 −1
Original line number Diff line number Diff line
@@ -69,7 +69,7 @@ Each configuration object may include these properties:
- `ignores` - An array of glob patterns this configuration should not apply to. If not specified this object applies to all files matched by one or more `files` pattern. If specified without any other properties, then the patterns provided by `ignores` acts as global ignores and is applied to all other configuration objects.
- `elements` - An array of {@link usage/elements element metadata}, typically set to the bundled `html5` metadata.
- `plugins` - An array of plugins to use.
- `transform` - An object mapping transformers to use.
- `transform` - An object mapping filename patterns to transformers to use. See [transformers](#transformers) below.
- `rules` - An object containing the configured rules. When `files` or `ignores` are specified, these rule configurations only applies to the matching files.

## Specifying `files` and `ignores`
@@ -136,6 +136,44 @@ export default defineFlatConfig([
]);
```

## Transformers

The `transform` property maps a regular-expression pattern (matched against the filename) to a transformer used to extract HTML from the matching files.
See {@link usage/transformers transformers} for details about transformers in general.

A transformer can be set directly to a function:

```ts fake-require
import { defineFlatConfig } from "html-validate";
import myTransformer from "./my-transformer.js";

export default defineFlatConfig([
  {
    files: ["**/*.foo"],
    transform: {
      "^.*\\.foo$": myTransformer,
    },
  },
]);
```

Or can be set to the name of a transformer from a plugin:

```ts fake-require
import { defineFlatConfig } from "html-validate";
import myPlugin from "./my-plugin.js";

export default defineFlatConfig([
  {
    files: ["**/*.foo"],
    plugins: [myPlugin],
    transform: {
      "^.*\\.foo$": "my-plugin",
    },
  },
]);
```

## Global ignores

- When `ignores` is used without other properties, it acts as a global ignore (applying to all other configuration objects).
+1 −1
Original line number Diff line number Diff line
@@ -491,7 +491,7 @@ export interface FlatConfigObject {
    name?: string;
    plugins?: Plugin_2[];
    rules?: RuleConfig;
    transform?: Record<string, Transformer_2>;
    transform?: Record<string, Transformer_2 | string>;
}

// @public (undocumented)
+1 −1
Original line number Diff line number Diff line
@@ -580,7 +580,7 @@ export interface FlatConfigObject {
    name?: string;
    plugins?: Plugin_2[];
    rules?: RuleConfig;
    transform?: Record<string, Transformer_2>;
    transform?: Record<string, Transformer_2 | string>;
}

// @public (undocumented)
+9 −1
Original line number Diff line number Diff line
@@ -14,6 +14,7 @@ import { findFlatConfigFile } from "./find-flat-config-file.nodejs";
import { type FlatConfig, type FlatConfigObject } from "./flat-config";
import { loadFlatConfigFile } from "./load-flat-config-file.nodejs";
import { type MergedFlatConfig, mergeFlatConfig } from "./merge-flat-config";
import { resolveFlatConfigTransformer } from "./resolve-transformer";

function isGlobalIgnore(block: FlatConfigObject): block is { ignores: string[] } {
	return (
@@ -83,8 +84,15 @@ function buildResolvedConfig(merged: MergedFlatConfig, original: FlatConfig): Re
	}

	const transformers: TransformerEntry[] = Object.entries(merged.transform).map(
		([pattern, value]) => {
			const transformer = resolveFlatConfigTransformer(value, plugins);
			return {
				kind: "function" as const,
				/* eslint-disable-next-line security/detect-non-literal-regexp -- transform patterns are user-provided regexp strings */
		([pattern, fn]) => ({ kind: "function" as const, pattern: new RegExp(pattern), function: fn }),
				pattern: new RegExp(pattern),
				function: transformer,
			};
		},
	);

	const resolvedData = { metaTable, plugins, rules, transformers };
+50 −0
Original line number Diff line number Diff line
import { beforeEach, describe, expect, it, jest } from "@jest/globals";
import { fs, vol } from "memfs";
import { type Plugin } from "../plugin";
import { type Transformer } from "../transform";
import { type FlatConfig } from "./flat-config";
import { FlatConfigLoader } from "./flat-config-loader.nodejs";
@@ -448,6 +449,55 @@ describe("FlatConfigLoader", () => {
		expect(entry?.pattern).toEqual(/.*\.vue$/);
	});

	it("should build transform entries from an unnamed plugin transformer string", async () => {
		expect.assertions(2);
		const transformer = jest.fn() as unknown as Transformer;
		const plugin: Plugin = { name: "mock-plugin", transformer };
		mockLoadFlatConfigFile.mockResolvedValue([
			{
				plugins: [plugin],
				transform: { ".*\\.vue$": "mock-plugin" },
			},
		]);
		const loader = new FlatConfigLoader("/project/html-validate.config.js");
		const config = await loader.getConfigFor("/project/file.vue");
		const entry = config.findTransformer("file.vue");
		expect(entry).toMatchObject({ kind: "function", function: transformer });
		expect(entry?.pattern).toEqual(/.*\.vue$/);
	});

	it("should build transform entries from a named plugin transformer string", async () => {
		expect.assertions(2);
		const transformer = jest.fn() as unknown as Transformer;
		const plugin: Plugin = { name: "mock-plugin", transformer: { foobar: transformer } };
		mockLoadFlatConfigFile.mockResolvedValue([
			{
				plugins: [plugin],
				transform: { ".*\\.vue$": "mock-plugin:foobar" },
			},
		]);
		const loader = new FlatConfigLoader("/project/html-validate.config.js");
		const config = await loader.getConfigFor("/project/file.vue");
		const entry = config.findTransformer("file.vue");
		expect(entry).toMatchObject({ kind: "function", function: transformer });
		expect(entry?.pattern).toEqual(/.*\.vue$/);
	});

	it("should throw when a transform string does not match any loaded plugin", async () => {
		expect.assertions(1);
		mockLoadFlatConfigFile.mockResolvedValue([
			{
				transform: { ".*\\.vue$": "missing-plugin" },
			},
		]);
		const loader = new FlatConfigLoader("/project/html-validate.config.js");
		await expect(
			loader.getConfigFor("/project/file.vue"),
		).rejects.toThrowErrorMatchingInlineSnapshot(
			`"No plugin named "missing-plugin" has been loaded"`,
		);
	});

	it("should return the same object on repeated calls for the same filename", async () => {
		expect.assertions(2);
		vol.fromJSON({
Loading