mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-09-14 13:36:36 +00:00
chore: update lint rules, add tests
This commit is contained in:
@@ -43,6 +43,30 @@
|
|||||||
размеры в kit, не-Prefix токены в preview.css и т.п.) — техдолг: чинить
|
размеры в kit, не-Prefix токены в preview.css и т.п.) — техдолг: чинить
|
||||||
только по заведённым tasks, не игнорировать правилом.
|
только по заведённым tasks, не игнорировать правилом.
|
||||||
|
|
||||||
|
### Тесты кастомных линт-правил
|
||||||
|
|
||||||
|
Линт-правила в `web/eslint-plugins/` покрыты юнит-тестами (Vitest):
|
||||||
|
|
||||||
|
- `web/eslint-plugins/__tests__/design-tokens.test.ts` — три чистых правила
|
||||||
|
(`no-hardcoded-in-svelte`, `no-category-mismatch`,
|
||||||
|
`no-token-definition-in-svelte`) через `RuleTester` со строковыми кейсами;
|
||||||
|
`no-undefined-in-svelte` — через `Linter` API, т.к. читает словарь токенов из
|
||||||
|
`__fixtures__/src/app.css` (не из реального `src/app.css`).
|
||||||
|
- `web/eslint-plugins/__tests__/no-mixed-imports.test.ts` — isolation-правило
|
||||||
|
через `Linter` API с `cwd` на `__fixtures__`: правило резолвит импорты по
|
||||||
|
реальным файлам, поэтому цели импортов обязаны существовать на диске.
|
||||||
|
- Хелпер `__tests__/helpers.ts` собирает `Linter` с `cwd = __fixtures__` —
|
||||||
|
`process.cwd()` не трогается, реальные `src/` не читаются.
|
||||||
|
|
||||||
|
Запуск: `pnpm --dir web test:rules` (`vitest run eslint-plugins/__tests__`).
|
||||||
|
Полный `pnpm --dir web test` тоже их гоняет.
|
||||||
|
|
||||||
|
Фикстуры живут в `web/eslint-plugins/__fixtures__/` и сами прогоняются линтом
|
||||||
|
(`eslint .`), поэтому добавление/правка стабов — тоже работа с валидным кодом.
|
||||||
|
Помнить: `allowDefaultProject` в `eslint.config.js` перечисляет test-файлы и
|
||||||
|
фикстуры точечно (glob'ы с `**` там запрещены tseslint) — при добавлении
|
||||||
|
файлов в `__tests__/`/`__fixtures__/` дописать их туда же.
|
||||||
|
|
||||||
## Правила кода
|
## Правила кода
|
||||||
|
|
||||||
### Svelte 5: типизация props через `interface Props`
|
### Svelte 5: типизация props через `interface Props`
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
:root {
|
||||||
|
--color-fg: #101010;
|
||||||
|
|
||||||
|
--space-1: calc(var(--size-1) / 2);
|
||||||
|
|
||||||
|
--z-nav: 100;
|
||||||
|
|
||||||
|
--duration-fast: 120ms;
|
||||||
|
|
||||||
|
--text-md: 0.875rem;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<!-- Fixture stub for lint-rule tests: content is ignored, must exist for import resolution. -->
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<!-- Fixture stub for lint-rule tests: content is ignored, must exist for import resolution. -->
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
// Fixture stub for lint-rule tests: content is ignored, must exist for import resolution.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
// Fixture stub for lint-rule tests: content is ignored, must exist for import resolution.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
// Fixture stub for lint-rule tests: content is ignored, must exist for import resolution.
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
// Fixture stub for lint-rule tests: content is ignored (the test supplies the
|
||||||
|
// source code), this file only has to EXIST for import resolution.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<!-- Fixture stub for lint-rule tests: content is ignored, must exist for import resolution. -->
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<!-- Fixture stub for lint-rule tests: content is ignored, must exist for import resolution. -->
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
// Tests for the design-tokens plugin rules (web/eslint-plugins/design-tokens).
|
||||||
|
//
|
||||||
|
// Strategy:
|
||||||
|
// - no-hardcoded-in-svelte, no-category-mismatch, no-token-definition-in-svelte
|
||||||
|
// are pure AST checks -> standard RuleTester with string cases.
|
||||||
|
// - no-undefined-in-svelte reads the token dictionary from <cwd>/src/app.css,
|
||||||
|
// so it runs through the Linter API with cwd pinned to the fixtures dir
|
||||||
|
// (see __tests__/helpers.ts). The fixture dictionary has exactly five
|
||||||
|
// tokens; anything else must be reported even if it exists in the REAL
|
||||||
|
// web/src/app.css.
|
||||||
|
import { RuleTester, Linter } from "eslint";
|
||||||
|
import svelteParser from "svelte-eslint-parser";
|
||||||
|
import tseslint from "typescript-eslint";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import noHardcoded from "../design-tokens/no-hardcoded-in-svelte.js";
|
||||||
|
import noCategoryMismatch from "../design-tokens/no-category-mismatch.js";
|
||||||
|
import noTokenDefinition from "../design-tokens/no-token-definition-in-svelte.js";
|
||||||
|
import noUndefined from "../design-tokens/no-undefined-in-svelte.js";
|
||||||
|
import { verifyInFixtures, type FlatConfig } from "./helpers.js";
|
||||||
|
|
||||||
|
const parserOptions = {
|
||||||
|
parser: tseslint.parser,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Vitest doesn't expose describe/it as globals (no `globals: true` in the
|
||||||
|
// config), so RuleTester would fall back to its synchronous default handler.
|
||||||
|
// Register the real ones so each case becomes a proper vitest test.
|
||||||
|
RuleTester.describe = describe;
|
||||||
|
RuleTester.it = it;
|
||||||
|
|
||||||
|
const ruleTester = new RuleTester({
|
||||||
|
languageOptions: {
|
||||||
|
parser: svelteParser,
|
||||||
|
parserOptions,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Wrap CSS from a <style> block into a minimal Svelte component. */
|
||||||
|
const component = (style: string): string =>
|
||||||
|
`<div class="box">x</div>\n\n<style>\n${style}\n</style>\n`;
|
||||||
|
|
||||||
|
const frame = (style: string) => ({
|
||||||
|
code: component(style),
|
||||||
|
filename: "Component.svelte",
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("design-tokens/no-hardcoded-in-svelte", () => {
|
||||||
|
ruleTester.run("no-hardcoded-in-svelte", noHardcoded, {
|
||||||
|
valid: [
|
||||||
|
frame(".box { color: var(--color-fg); }"),
|
||||||
|
frame(".box { padding: var(--space-3); }"),
|
||||||
|
frame(".box { background-color: transparent; }"),
|
||||||
|
frame(".box { width: 100%; }"),
|
||||||
|
frame(".box { line-height: 1.5; }"),
|
||||||
|
frame(".box { transition: var(--duration-fast); }"),
|
||||||
|
frame(".box { z-index: var(--z-nav); }"),
|
||||||
|
frame(".box { font-size: var(--text-md); }"),
|
||||||
|
frame("<div>no style block at all</div>"),
|
||||||
|
],
|
||||||
|
invalid: [
|
||||||
|
{
|
||||||
|
...frame(".box { color: #ff0000; }"),
|
||||||
|
errors: [{ messageId: "hardcodedColor" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...frame(".box { padding: 16px; }"),
|
||||||
|
errors: [{ messageId: "hardcodedSize" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...frame(".box { transition: 200ms; }"),
|
||||||
|
errors: [{ messageId: "hardcodedDuration" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...frame(".box { z-index: 100; }"),
|
||||||
|
errors: [{ messageId: "hardcodedZIndex" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...frame(
|
||||||
|
"@media (max-width: 640px) { .box { width: var(--text-md); } }",
|
||||||
|
),
|
||||||
|
errors: [{ messageId: "hardcodedBreakpoint" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...frame(
|
||||||
|
"@media (min-width: var(--bp-m)) { .box { width: var(--text-md); } }",
|
||||||
|
),
|
||||||
|
errors: [{ messageId: "varInMedia" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...frame(
|
||||||
|
".box { color: color-mix(in srgb, var(--color-a), var(--color-b)); }",
|
||||||
|
),
|
||||||
|
errors: [{ messageId: "colorMix" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("design-tokens/no-category-mismatch", () => {
|
||||||
|
ruleTester.run("no-category-mismatch", noCategoryMismatch, {
|
||||||
|
valid: [
|
||||||
|
frame(".box { padding: var(--space-1); }"),
|
||||||
|
frame(".box { color: var(--color-fg); }"),
|
||||||
|
frame(".box { border: 1px solid var(--color-border); }"),
|
||||||
|
],
|
||||||
|
invalid: [
|
||||||
|
{
|
||||||
|
...frame(".box { padding: var(--color-fg); }"),
|
||||||
|
errors: [{ messageId: "categoryMismatch" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...frame(".box { color: var(--space-1); }"),
|
||||||
|
errors: [{ messageId: "categoryMismatch" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("design-tokens/no-token-definition-in-svelte", () => {
|
||||||
|
ruleTester.run("no-token-definition-in-svelte", noTokenDefinition, {
|
||||||
|
valid: [
|
||||||
|
frame(".box { --local: var(--color-fg); }"),
|
||||||
|
frame(".box { --local: calc(var(--space-1) * 2); }"),
|
||||||
|
frame(".box { --ratio: 1.5; }"),
|
||||||
|
],
|
||||||
|
invalid: [
|
||||||
|
{
|
||||||
|
...frame(".box { --local: #ff0000; }"),
|
||||||
|
errors: [{ messageId: "tokenPrimitive" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...frame(".box { --local: 12px; }"),
|
||||||
|
errors: [{ messageId: "tokenPrimitive" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...frame(".box { --local: oklch(0.5 0.1 240); }"),
|
||||||
|
errors: [{ messageId: "tokenPrimitive" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("design-tokens/no-undefined-in-svelte", () => {
|
||||||
|
const undefinedConfig: FlatConfig = [
|
||||||
|
{
|
||||||
|
files: ["**/*.svelte"],
|
||||||
|
plugins: {
|
||||||
|
"design-tokens": { rules: { "no-undefined-in-svelte": noUndefined } },
|
||||||
|
},
|
||||||
|
rules: { "design-tokens/no-undefined-in-svelte": "error" },
|
||||||
|
languageOptions: {
|
||||||
|
parser: svelteParser,
|
||||||
|
parserOptions,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
it("accepts tokens defined in the fixture app.css", () => {
|
||||||
|
const messages = verifyInFixtures(
|
||||||
|
undefinedConfig,
|
||||||
|
component(".box { color: var(--color-fg); }"),
|
||||||
|
"lib/components/Foo.svelte",
|
||||||
|
);
|
||||||
|
expect(messages).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts component-local overrides", () => {
|
||||||
|
const messages = verifyInFixtures(
|
||||||
|
undefinedConfig,
|
||||||
|
component(".box { --local-x: var(--color-fg); color: var(--local-x); }"),
|
||||||
|
"lib/components/Foo.svelte",
|
||||||
|
);
|
||||||
|
expect(messages).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("flags an unknown token even if it exists in the real web/src/app.css", () => {
|
||||||
|
const messages = verifyInFixtures(
|
||||||
|
undefinedConfig,
|
||||||
|
component(".box { color: var(--color-love); }"),
|
||||||
|
"lib/components/Foo.svelte",
|
||||||
|
);
|
||||||
|
expect(messages).toHaveLength(1);
|
||||||
|
expect(messages[0].messageId).toBe("undefinedToken");
|
||||||
|
expect(messages[0].message).toContain("--color-love");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("flags unknown tokens referenced inside @media params", () => {
|
||||||
|
const messages = verifyInFixtures(
|
||||||
|
undefinedConfig,
|
||||||
|
component(
|
||||||
|
"@media (width >= var(--bp-xs)) { .box { width: var(--text-md); } }",
|
||||||
|
),
|
||||||
|
"lib/components/Foo.svelte",
|
||||||
|
);
|
||||||
|
expect(messages).toHaveLength(1);
|
||||||
|
expect(messages[0].messageId).toBe("undefinedToken");
|
||||||
|
expect(messages[0].message).toContain("--bp-xs");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// Shared harness for testing the custom ESLint rules in eslint-plugins/.
|
||||||
|
//
|
||||||
|
// Two strategies, picked per rule in the test files:
|
||||||
|
// - RuleTester (eslint) — pure string cases, no filesystem. Best fit for the
|
||||||
|
// design-tokens rules that only inspect the postcss AST of Svelte `<style>`.
|
||||||
|
// - Linter API + fixtures — needed when a rule reads real files. The isolation
|
||||||
|
// rule resolves imports via fs (targets must exist on disk) and
|
||||||
|
// no-undefined-in-svelte reads <cwd>/src/app.css for the token dictionary.
|
||||||
|
// `verifyInFixtures` runs a rule with cwd fixed to `__fixtures__/`, so both
|
||||||
|
// work without touching process.cwd() or the real src/ tree.
|
||||||
|
import { Linter } from "eslint";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
export const FIXTURES_DIR = path.resolve(HERE, "../__fixtures__");
|
||||||
|
export const FIXTURES_SRC = path.join(FIXTURES_DIR, "src");
|
||||||
|
|
||||||
|
/** Flat-config shape the Linter.verify accepts. */
|
||||||
|
export type FlatConfig = Parameters<Linter["verify"]>[1];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a rule (or a config) against `code` as if it were the fixture file
|
||||||
|
* `relFile` under `__fixtures__/src`. Returns the lint messages.
|
||||||
|
*/
|
||||||
|
export function verifyInFixtures(
|
||||||
|
config: FlatConfig,
|
||||||
|
code: string,
|
||||||
|
relFile: string,
|
||||||
|
) {
|
||||||
|
const linter = new Linter({ configType: "flat", cwd: FIXTURES_DIR });
|
||||||
|
return linter.verify(code, config, path.join(FIXTURES_SRC, relFile));
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
// Tests for the isolation rule (web/eslint-plugins/isolation/no-mixed-imports.js).
|
||||||
|
//
|
||||||
|
// The rule RESOLVES import specifiers to real files (via $lib alias and
|
||||||
|
// relative ../ paths), so every import target must exist on disk. It therefore
|
||||||
|
// runs through the Linter API with cwd pinned to the committed fixture tree
|
||||||
|
// (__fixtures__/src) — see __tests__/helpers.ts.
|
||||||
|
//
|
||||||
|
// Side map (mirrors the defaults in no-mixed-imports.js):
|
||||||
|
// old -> routes/v1/**, lib/v1/**
|
||||||
|
// new -> routes/** (minus v1), lib/components/**, lib/registry/** + a few
|
||||||
|
// shared-> everything else: core/, i18n/, theme, ...
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import noMixedImports from "../isolation/no-mixed-imports.js";
|
||||||
|
import { verifyInFixtures, type FlatConfig } from "./helpers.js";
|
||||||
|
|
||||||
|
const isolationConfig: FlatConfig = [
|
||||||
|
{
|
||||||
|
files: ["**/*.{ts,svelte}"],
|
||||||
|
plugins: { isolation: { rules: { "no-mixed-imports": noMixedImports } } },
|
||||||
|
rules: { "isolation/no-mixed-imports": "error" },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const messagesFor = (relFile: string, code: string) =>
|
||||||
|
verifyInFixtures(isolationConfig, code, relFile);
|
||||||
|
|
||||||
|
/** Assert a single "noMixed" violation with the given sides. */
|
||||||
|
function expectViolation(
|
||||||
|
relFile: string,
|
||||||
|
code: string,
|
||||||
|
side: string,
|
||||||
|
target: string,
|
||||||
|
) {
|
||||||
|
const messages = messagesFor(relFile, code);
|
||||||
|
expect(messages).toHaveLength(1);
|
||||||
|
expect(messages[0].messageId).toBe("noMixed");
|
||||||
|
// ESLint interpolates `data` into the message text (no `data` field on the
|
||||||
|
// returned message), so assert on the rendered message instead.
|
||||||
|
expect(messages[0].message).toContain(`"${side}" file imports "${target}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Assert the import is allowed (no violations). */
|
||||||
|
function expectClean(relFile: string, code: string) {
|
||||||
|
expect(messagesFor(relFile, code)).toEqual([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("isolation/no-mixed-imports", () => {
|
||||||
|
it("flags an old file importing a new component", () => {
|
||||||
|
expectViolation(
|
||||||
|
"lib/v1/old.ts",
|
||||||
|
'import { c } from "../components/CheckerCanvas.svelte";',
|
||||||
|
"old",
|
||||||
|
"new",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("flags a new route importing an old module through the $lib alias", () => {
|
||||||
|
expectViolation(
|
||||||
|
"routes/+page.svelte",
|
||||||
|
'import { o } from "$lib/v1/old";',
|
||||||
|
"new",
|
||||||
|
"old",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("flags a new component importing old code via a relative path", () => {
|
||||||
|
expectViolation(
|
||||||
|
"lib/components/CheckerCanvas.svelte",
|
||||||
|
'import { o } from "../v1/old.ts";',
|
||||||
|
"new",
|
||||||
|
"old",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows old -> shared (core via relative path)", () => {
|
||||||
|
expectClean("lib/v1/old.ts", 'import { e } from "../core/errors";');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows old -> shared (i18n and theme via $lib)", () => {
|
||||||
|
expectClean("routes/v1/+layout.svelte", 'import { t } from "$lib/i18n/t";');
|
||||||
|
expectClean(
|
||||||
|
"routes/v1/+layout.svelte",
|
||||||
|
'import { g } from "$lib/theme.svelte";',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows new -> shared and new -> new", () => {
|
||||||
|
expectClean("routes/+page.svelte", 'import { t } from "$lib/i18n/t";');
|
||||||
|
expectClean(
|
||||||
|
"routes/+page.svelte",
|
||||||
|
'import { b } from "$lib/components/ui/Button.svelte";',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("leaves shared files unrestricted in both directions", () => {
|
||||||
|
expectClean("lib/i18n/t.ts", 'import { o } from "$lib/v1/old";');
|
||||||
|
expectClean(
|
||||||
|
"lib/core/errors.ts",
|
||||||
|
'import { c } from "$lib/components/CheckerCanvas.svelte";',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* Local ESLint plugin "isolation".
|
* Local ESLint plugin "isolation".
|
||||||
*
|
*
|
||||||
* Guarantees full isolation between the old UI branch and the new (preview)
|
* Guarantees full isolation between the old (v1, archived) UI branch and the
|
||||||
* branch. Unlike no-restricted-imports (which matches the literal import
|
* new one. Unlike no-restricted-imports (which matches the literal import
|
||||||
* specifier string only), these rules RESOLVE the specifier to a real file
|
* specifier string only), these rules RESOLVE the specifier to a real file
|
||||||
* (supporting both `$lib/...` aliases and relative `./`/`../` paths) and then
|
* (supporting both `$lib/...` aliases and relative `./`/`../` paths) and then
|
||||||
* classify both sides by their actual location on disk.
|
* classify both sides by their actual location on disk.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Isolation rule: forbid mixing the "old" and the "new" (preview) branches.
|
* Isolation rule: forbid mixing the "old" (v1) and the "new" branches.
|
||||||
*
|
*
|
||||||
* The rule RESOLVES every import specifier to a real file (handles both the
|
* The rule RESOLVES every import specifier to a real file (handles both the
|
||||||
* `$lib/...` alias and relative `./` / `../` paths), classifies the source
|
* `$lib/...` alias and relative `./` / `../` paths), classifies the source
|
||||||
@@ -17,13 +17,19 @@
|
|||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
const DEFAULT_OLD = ["routes/(old)/**", "lib/old/**"];
|
// NEW declared as an allowlist: `lib/**` descends into v1/ and the shared dirs
|
||||||
|
// (core/, i18n/, assets/, theme), so matching by prefix would misclassify them.
|
||||||
|
// Negations (`!`) carve old paths out of `routes/**`.
|
||||||
|
const DEFAULT_OLD = ["routes/v1/**", "lib/v1/**"];
|
||||||
|
|
||||||
const DEFAULT_NEW = [
|
const DEFAULT_NEW = [
|
||||||
"routes/preview/**",
|
"routes/**",
|
||||||
"lib/components/kit/**",
|
"!routes/v1/**",
|
||||||
"lib/preview/**",
|
"lib/components/**",
|
||||||
"lib/registry-new/**",
|
"lib/registry/**",
|
||||||
|
"lib/catalog.ts",
|
||||||
|
"lib/categories.ts",
|
||||||
|
"lib/tool-icons.ts",
|
||||||
"lib/registry-schema.ts",
|
"lib/registry-schema.ts",
|
||||||
"lib/registry-schema.test.ts",
|
"lib/registry-schema.test.ts",
|
||||||
];
|
];
|
||||||
@@ -92,7 +98,7 @@ export default {
|
|||||||
type: "problem",
|
type: "problem",
|
||||||
docs: {
|
docs: {
|
||||||
description:
|
description:
|
||||||
"Forbid imports between the old UI branch and the new (preview) branch.",
|
"Forbid imports between the old (v1) UI branch and the new branch.",
|
||||||
category: "Best Practices",
|
category: "Best Practices",
|
||||||
},
|
},
|
||||||
schema: [
|
schema: [
|
||||||
|
|||||||
+50
-11
@@ -1,16 +1,18 @@
|
|||||||
import js from "@eslint/js";
|
import js from "@eslint/js";
|
||||||
import prettier from "eslint-config-prettier";
|
import prettier from "eslint-config-prettier";
|
||||||
import svelte from "eslint-plugin-svelte";
|
import svelte from "eslint-plugin-svelte";
|
||||||
import designTokens from "./eslint-plugins/index.js";
|
|
||||||
import isolationPlugin from "./eslint-plugins/isolation/index.js";
|
|
||||||
import globals from "globals";
|
import globals from "globals";
|
||||||
import svelteParser from "svelte-eslint-parser";
|
import svelteParser from "svelte-eslint-parser";
|
||||||
import tseslint from "typescript-eslint";
|
import tseslint from "typescript-eslint";
|
||||||
|
import designTokens from "./eslint-plugins/index.js";
|
||||||
|
import isolationPlugin from "./eslint-plugins/isolation/index.js";
|
||||||
|
|
||||||
|
// FIXME: надо игнорировать старые файлы, после переноса пути новых компонентов включают старые
|
||||||
// Новый код редизайна: к нему применяем полные recommended-наборы уже сейчас.
|
// Новый код редизайна: к нему применяем полные recommended-наборы уже сейчас.
|
||||||
// Когда старый дизайн удалим (C19), этот scoped-блок убирается и recommended
|
// Когда старый дизайн удалим (C19), этот scoped-блок убирается и recommended
|
||||||
// включается на весь код (см. план-redesign §10, шаг 6).
|
// включается на весь код (см. план-redesign §10, шаг 6).
|
||||||
const newCode = ["**/src/lib/components/kit/**", "**/src/routes/preview/**"];
|
const newCode = ["**/src/lib/components/**", "**/src/routes/**"];
|
||||||
|
const oldCode = ["**/src/lib/v1/**", "**/src/routes/v1/**"];
|
||||||
|
|
||||||
// Полные recommended-наборы — только на новый код (см. ниже, блок перед prettier).
|
// Полные recommended-наборы — только на новый код (см. ниже, блок перед prettier).
|
||||||
const jsRecommended = Array.isArray(js.configs.recommended)
|
const jsRecommended = Array.isArray(js.configs.recommended)
|
||||||
@@ -21,10 +23,19 @@ const svelteRecommended = Array.isArray(svelte.configs["flat/recommended"])
|
|||||||
: [svelte.configs["flat/recommended"]];
|
: [svelte.configs["flat/recommended"]];
|
||||||
// Svelte-рекомендации применяем только к .svelte-файлам нового кода, иначе
|
// Svelte-рекомендации применяем только к .svelte-файлам нового кода, иначе
|
||||||
// svelte-eslint-parser "съедает" обычные .ts в тех же папках (напр. +page.ts).
|
// svelte-eslint-parser "съедает" обычные .ts в тех же папках (напр. +page.ts).
|
||||||
const svelteFiles = [
|
const newSvelteFiles = [
|
||||||
"**/src/lib/components/kit/**/*.svelte",
|
"**/src/lib/components/**/*.svelte",
|
||||||
"**/src/routes/preview/**/*.svelte",
|
"**/src/routes/**/*.svelte",
|
||||||
];
|
];
|
||||||
|
const oldSvelteFiles = [
|
||||||
|
"**/src/lib/v1/**/*.svelte",
|
||||||
|
"**/src/routes/v1/**/*.svelte",
|
||||||
|
];
|
||||||
|
|
||||||
|
// FIXME:
|
||||||
|
// The signature '(...configs: InfiniteDepthConfigWithExtends[]): ConfigArray' of 'tseslint.config' is deprecated.ts
|
||||||
|
// Migrate to defineConfig(...)
|
||||||
|
// The core defineConfig(...) helper is a nearly exact clone of tseslint.config(...)
|
||||||
|
|
||||||
export default tseslint.config(
|
export default tseslint.config(
|
||||||
{
|
{
|
||||||
@@ -60,8 +71,23 @@ export default tseslint.config(
|
|||||||
"e2e/tools-smoke.spec.ts",
|
"e2e/tools-smoke.spec.ts",
|
||||||
"e2e/generators.spec.ts",
|
"e2e/generators.spec.ts",
|
||||||
"e2e/known-issues.spec.ts",
|
"e2e/known-issues.spec.ts",
|
||||||
|
// Тесты и фикстуры кастомных линт-правил лежат вне src/ (не в
|
||||||
|
// tsconfig), поэтому для типизированного парсинга резолвятся
|
||||||
|
// через default-проект. Перечисляются точечно: `**` в
|
||||||
|
// allowDefaultProject запрещён tseslint.
|
||||||
|
"eslint-plugins/__tests__/design-tokens.test.ts",
|
||||||
|
"eslint-plugins/__tests__/helpers.ts",
|
||||||
|
"eslint-plugins/__tests__/no-mixed-imports.test.ts",
|
||||||
|
"eslint-plugins/__fixtures__/src/lib/v1/old.ts",
|
||||||
|
"eslint-plugins/__fixtures__/src/lib/core/errors.ts",
|
||||||
|
"eslint-plugins/__fixtures__/src/lib/i18n/t.ts",
|
||||||
|
"eslint-plugins/__fixtures__/src/lib/theme.svelte.ts",
|
||||||
|
"eslint-plugins/__fixtures__/src/lib/components/CheckerCanvas.svelte",
|
||||||
|
"eslint-plugins/__fixtures__/src/lib/components/ui/Button.svelte",
|
||||||
|
"eslint-plugins/__fixtures__/src/routes/+page.svelte",
|
||||||
|
"eslint-plugins/__fixtures__/src/routes/v1/+layout.svelte",
|
||||||
],
|
],
|
||||||
maximumDefaultProjectFileMatchCount_THIS_WILL_SLOW_DOWN_LINTING: 12,
|
maximumDefaultProjectFileMatchCount_THIS_WILL_SLOW_DOWN_LINTING: 32,
|
||||||
},
|
},
|
||||||
extraFileExtensions: [".svelte"],
|
extraFileExtensions: [".svelte"],
|
||||||
},
|
},
|
||||||
@@ -100,7 +126,8 @@ export default tseslint.config(
|
|||||||
// Применяется к новому коду редизайна (см. newCode выше). Когда старый дизайн
|
// Применяется к новому коду редизайна (см. newCode выше). Когда старый дизайн
|
||||||
// удалят, расширить glob на весь код, исключив (old)/.
|
// удалят, расширить glob на весь код, исключив (old)/.
|
||||||
{
|
{
|
||||||
files: svelteFiles,
|
files: newSvelteFiles,
|
||||||
|
ignores: oldSvelteFiles,
|
||||||
plugins: {
|
plugins: {
|
||||||
"design-tokens": designTokens,
|
"design-tokens": designTokens,
|
||||||
},
|
},
|
||||||
@@ -113,9 +140,21 @@ export default tseslint.config(
|
|||||||
},
|
},
|
||||||
// Полные recommended-наборы — только на новый код.
|
// Полные recommended-наборы — только на новый код.
|
||||||
...[
|
...[
|
||||||
...jsRecommended.map((cfg) => ({ ...cfg, files: newCode })),
|
...jsRecommended.map((cfg) => ({
|
||||||
...tseslint.configs.recommended.map((cfg) => ({ ...cfg, files: newCode })),
|
...cfg,
|
||||||
...svelteRecommended.map((cfg) => ({ ...cfg, files: svelteFiles })),
|
files: newCode,
|
||||||
|
ignores: oldCode,
|
||||||
|
})),
|
||||||
|
...tseslint.configs.recommended.map((cfg) => ({
|
||||||
|
...cfg,
|
||||||
|
files: newCode,
|
||||||
|
ignores: oldCode,
|
||||||
|
})),
|
||||||
|
...svelteRecommended.map((cfg) => ({
|
||||||
|
...cfg,
|
||||||
|
files: newSvelteFiles,
|
||||||
|
ignores: oldSvelteFiles,
|
||||||
|
})),
|
||||||
],
|
],
|
||||||
|
|
||||||
// В Svelte 5 пропсы деструктурируются через `let` (конвенция документации и
|
// В Svelte 5 пропсы деструктурируются через `let` (конвенция документации и
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
|
"test:rules": "vitest run eslint-plugins/__tests__",
|
||||||
"test:e2e": "playwright test",
|
"test:e2e": "playwright test",
|
||||||
"format": "prettier --write . --log-level warn",
|
"format": "prettier --write . --log-level warn",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { defineConfig } from "vitest/config";
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [svelte()],
|
plugins: [svelte()],
|
||||||
test: {
|
test: {
|
||||||
include: ["src/**/*.test.ts"],
|
include: ["src/**/*.test.ts", "eslint-plugins/**/*.test.ts"],
|
||||||
environment: "node",
|
environment: "node",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user