diff --git a/oxlint.config.ts b/oxlint.config.ts index 37983778e0..eb317c814a 100644 --- a/oxlint.config.ts +++ b/oxlint.config.ts @@ -7,7 +7,7 @@ const typegpuRules = typegpuPreset && 'rules' in typegpuPreset ? typegpuPreset.r export default defineConfig({ plugins: ['eslint', 'typescript', 'import', 'unicorn', 'oxc'], - jsPlugins: ['eslint-plugin-typegpu', 'eslint-plugin-eslint-plugin'], + jsPlugins: ['eslint-plugin-typegpu', 'eslint-plugin-eslint-plugin', 'eslint-plugin-internal'], categories: { correctness: 'warn', suspicious: 'warn', @@ -24,6 +24,7 @@ export default defineConfig({ 'eslint-plugin-import/no-named-as-default': 'off', 'eslint-plugin-import/no-named-as-default-member': 'off', 'eslint-plugin-import/extensions': ['error', 'always', { ignorePackages: true }], + 'eslint-plugin-internal/no-useless-path-segments': 'error', }, ignorePatterns: ['**/*.astro', '**/*.mjs'], overrides: [ @@ -45,6 +46,10 @@ export default defineConfig({ ...(eslintPlugin.configs.recommended.rules as Record), }, }, + { + files: ['apps/typegpu-docs/src/examples/**/*.ts'], + rules: { 'eslint-plugin-internal/no-long-imports': 'error' }, + }, ], env: { builtin: true, diff --git a/package.json b/package.json index d9c199c59d..7711ca82a6 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@webgpu/types": "catalog:types", "dpdm": "^3.14.0", "eslint-plugin-eslint-plugin": "^7.3.2", + "eslint-plugin-internal": "workspace:*", "eslint-plugin-typegpu": "workspace:*", "jiti": "catalog:build", "oxfmt": "^0.35.0", diff --git a/packages/eslint-plugin-internal/README.md b/packages/eslint-plugin-internal/README.md new file mode 100644 index 0000000000..4381d343a0 --- /dev/null +++ b/packages/eslint-plugin-internal/README.md @@ -0,0 +1,7 @@ +
+ +# eslint-plugin-internal + +Internal ESLint rules used by this repository. + +
diff --git a/packages/eslint-plugin-internal/package.json b/packages/eslint-plugin-internal/package.json new file mode 100644 index 0000000000..e2b1dc379a --- /dev/null +++ b/packages/eslint-plugin-internal/package.json @@ -0,0 +1,25 @@ +{ + "name": "eslint-plugin-internal", + "version": "0.10.0", + "private": true, + "license": "MIT", + "type": "module", + "main": "./src/index.ts", + "scripts": { + "test:types": "pnpm tsc --p ./tsconfig.json --noEmit", + "test": "vitest" + }, + "dependencies": { + "@typescript-eslint/utils": "^8.57.2" + }, + "devDependencies": { + "@types/node": "catalog:types", + "@typescript-eslint/rule-tester": "^8.57.2", + "eslint": "^9.39.2", + "typescript": "^5.9.3", + "vitest": "^4.0.17" + }, + "peerDependencies": { + "eslint": "^9.0.0" + } +} diff --git a/packages/eslint-plugin-internal/src/index.ts b/packages/eslint-plugin-internal/src/index.ts new file mode 100644 index 0000000000..5fd5be5883 --- /dev/null +++ b/packages/eslint-plugin-internal/src/index.ts @@ -0,0 +1,17 @@ +import pkg from '../package.json' with { type: 'json' }; +import type { TSESLint } from '@typescript-eslint/utils'; +import { noUselessPathSegments } from './rules/noUselessPathSegments.ts'; +import { noLongImports } from './rules/noLongImports.ts'; + +const plugin = { + meta: { + name: pkg.name, + version: pkg.version, + }, + rules: { + 'no-useless-path-segments': noUselessPathSegments, + 'no-long-imports': noLongImports, + }, +} satisfies TSESLint.FlatConfig.Plugin; + +export default plugin; diff --git a/packages/eslint-plugin-internal/src/ruleCreator.ts b/packages/eslint-plugin-internal/src/ruleCreator.ts new file mode 100644 index 0000000000..d4fa2d5089 --- /dev/null +++ b/packages/eslint-plugin-internal/src/ruleCreator.ts @@ -0,0 +1,5 @@ +import { ESLintUtils } from '@typescript-eslint/utils'; + +export const createRule = ESLintUtils.RuleCreator( + () => `https://docs.swmansion.com/TypeGPU/getting-started/`, +); diff --git a/packages/eslint-plugin-internal/src/rules/noLongImports.ts b/packages/eslint-plugin-internal/src/rules/noLongImports.ts new file mode 100644 index 0000000000..936164240c --- /dev/null +++ b/packages/eslint-plugin-internal/src/rules/noLongImports.ts @@ -0,0 +1,32 @@ +import { createRule } from '../ruleCreator.ts'; + +export const noLongImports = createRule({ + name: 'no-long-imports', + meta: { + type: 'suggestion', + docs: { + description: 'Disallow long import paths (to be used in TypeGPU examples), except common.', + }, + messages: { + unexpected: + "Import path '{{path}}' probably won't work on StackBlitz, use imports from packages instead", + }, + schema: [], + }, + defaultOptions: [], + + create(context) { + return { + ImportDeclaration(node) { + const importPath = node.source.value; + if (importPath.startsWith('../../') && !importPath.startsWith('../../common/')) { + context.report({ + node, + messageId: 'unexpected', + data: { path: importPath }, + }); + } + }, + }; + }, +}); diff --git a/packages/eslint-plugin-internal/src/rules/noUselessPathSegments.ts b/packages/eslint-plugin-internal/src/rules/noUselessPathSegments.ts new file mode 100644 index 0000000000..5e9717d007 --- /dev/null +++ b/packages/eslint-plugin-internal/src/rules/noUselessPathSegments.ts @@ -0,0 +1,52 @@ +import { createRule } from '../ruleCreator.ts'; +import * as path from 'node:path'; + +export const noUselessPathSegments = createRule({ + name: 'no-useless-path-segments', + meta: { + type: 'suggestion', + fixable: 'code', + docs: { + description: 'Disallow redundant parent folder lookups in relative import paths', + }, + messages: { + redundant: "Import path '{{path}}' can be simplified to '{{simplified}}'", + }, + schema: [], + }, + defaultOptions: [], + + create(context) { + return { + ImportDeclaration(node) { + const importPath = node.source.value; + if (!importPath.startsWith('.')) { + return; + } + + const filename = context.filename; // e.g. `/Users/me/typegpu-monorepo/packages/typegpu/tests/buffer.test.ts` + const dir = path.dirname(filename); // e.g. `/Users/me/typegpu-monorepo/packages/typegpu/tests` + const resolved = path.resolve(dir, importPath); // e.g. `/Users/me/typegpu-monorepo/packages/typegpu/src/data/index.ts` + let simplified = path + .relative(dir, resolved) // e.g. `../src/data/index.ts`, or `subfolder/helper.ts` + .replaceAll('\\', '/'); // Windows compatibility + + if (!simplified.startsWith('..')) { + simplified = `./${simplified}`; + } + + if (importPath !== simplified) { + context.report({ + node, + messageId: 'redundant', + data: { path: importPath, simplified }, + fix(fixer) { + const quote = context.sourceCode.getText(node.source)[0]; + return fixer.replaceText(node.source, `${quote}${simplified}${quote}`); + }, + }); + } + }, + }; + }, +}); diff --git a/packages/eslint-plugin-internal/tests/rules/noLongImports.test.ts b/packages/eslint-plugin-internal/tests/rules/noLongImports.test.ts new file mode 100644 index 0000000000..5ed1e99489 --- /dev/null +++ b/packages/eslint-plugin-internal/tests/rules/noLongImports.test.ts @@ -0,0 +1,24 @@ +import { describe } from 'vitest'; +import { ruleTester } from '../utils/ruleTester.ts'; +import { noLongImports } from '../../src/rules/noLongImports.ts'; + +describe('noLongImports', () => { + ruleTester.run('noLongImports', noLongImports, { + valid: [ + { code: "import item from './file.ts';" }, + { code: "import item from '../file.ts';" }, + { code: "import item from '../../common/file.ts';" }, + ], + invalid: [ + { + code: "import item from '../../file.ts';", + errors: [ + { + messageId: 'unexpected', + data: { path: '../../file.ts' }, + }, + ], + }, + ], + }); +}); diff --git a/packages/eslint-plugin-internal/tests/rules/noUselessPathSegments.test.ts b/packages/eslint-plugin-internal/tests/rules/noUselessPathSegments.test.ts new file mode 100644 index 0000000000..db67522d87 --- /dev/null +++ b/packages/eslint-plugin-internal/tests/rules/noUselessPathSegments.test.ts @@ -0,0 +1,66 @@ +import { describe } from 'vitest'; +import { ruleTester } from '../utils/ruleTester.ts'; +import { noUselessPathSegments } from '../../src/rules/noUselessPathSegments.ts'; +import path from 'path'; + +const filename = path.join(process.cwd(), 'packages', 'typegpu', 'tests', 'buffer.test.ts'); + +describe('noUselessPathSegments', () => { + ruleTester.run('noUselessPathSegments', noUselessPathSegments, { + valid: [ + { code: "import item from './file.ts';", filename }, + { code: "import item from '../file.ts';", filename }, + { code: "import item from '../../file.ts';", filename }, + { code: "import item from '../folder/file.ts';", filename }, + + { code: "import item from 'eslint-plugin-typegpu';", filename }, + { code: "import item from '@eslint-plugin/typegpu';", filename }, + ], + invalid: [ + { + code: "import item from '../tests/file.ts';", + filename, + errors: [ + { + messageId: 'redundant', + data: { path: '../tests/file.ts', simplified: './file.ts' }, + }, + ], + output: "import item from './file.ts';", + }, + { + code: 'import item from "../tests/file.ts";', + filename, + errors: [ + { + messageId: 'redundant', + data: { path: '../tests/file.ts', simplified: './file.ts' }, + }, + ], + output: 'import item from "./file.ts";', + }, + { + code: "import item from './../file.ts';", + filename, + errors: [ + { + messageId: 'redundant', + data: { path: './../file.ts', simplified: '../file.ts' }, + }, + ], + output: "import item from '../file.ts';", + }, + { + code: "import item from '../../typegpu/folder/file.ts';", + filename, + errors: [ + { + messageId: 'redundant', + data: { path: '../../typegpu/folder/file.ts', simplified: '../folder/file.ts' }, + }, + ], + output: "import item from '../folder/file.ts';", + }, + ], + }); +}); diff --git a/packages/eslint-plugin-internal/tests/utils/ruleTester.ts b/packages/eslint-plugin-internal/tests/utils/ruleTester.ts new file mode 100644 index 0000000000..3efee657d3 --- /dev/null +++ b/packages/eslint-plugin-internal/tests/utils/ruleTester.ts @@ -0,0 +1,10 @@ +import { RuleTester } from '@typescript-eslint/rule-tester'; +import { afterAll, describe, it } from 'vitest'; + +// RuleTester relies on global hooks for tests. +// Vitest doesn't make the hooks available globally, so we need to bind them. +RuleTester.describe = describe; +RuleTester.it = it; +RuleTester.afterAll = afterAll; + +export const ruleTester = new RuleTester(); diff --git a/packages/eslint-plugin-internal/tsconfig.json b/packages/eslint-plugin-internal/tsconfig.json new file mode 100644 index 0000000000..88024df5ec --- /dev/null +++ b/packages/eslint-plugin-internal/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["src/**/*", "tests/**/*"], + "exclude": ["node_modules"] +} diff --git a/packages/eslint-plugin-internal/vitest.config.ts b/packages/eslint-plugin-internal/vitest.config.ts new file mode 100644 index 0000000000..b0e95a5a89 --- /dev/null +++ b/packages/eslint-plugin-internal/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['tests/**/*.test.ts'], + environment: 'node', + }, +}); diff --git a/packages/typegpu/src/core/pipeline/computePipeline.ts b/packages/typegpu/src/core/pipeline/computePipeline.ts index c9b31a076f..1b2d5e3035 100644 --- a/packages/typegpu/src/core/pipeline/computePipeline.ts +++ b/packages/typegpu/src/core/pipeline/computePipeline.ts @@ -1,5 +1,5 @@ import type { AnyComputeBuiltin } from '../../builtin.ts'; -import type { TgpuQuerySet } from '../../core/querySet/querySet.ts'; +import type { TgpuQuerySet } from '../querySet/querySet.ts'; import { type ResolvedSnippet, snip } from '../../data/snippet.ts'; import { sizeOf } from '../../data/sizeOf.ts'; import type { AnyWgslData } from '../../data/wgslTypes.ts'; diff --git a/packages/typegpu/src/core/pipeline/renderPipeline.ts b/packages/typegpu/src/core/pipeline/renderPipeline.ts index 55b784ce60..bddf18c9ac 100644 --- a/packages/typegpu/src/core/pipeline/renderPipeline.ts +++ b/packages/typegpu/src/core/pipeline/renderPipeline.ts @@ -1,6 +1,6 @@ import type { AnyBuiltin, OmitBuiltins } from '../../builtin.ts'; -import type { IndexFlag, TgpuBuffer, VertexFlag } from '../../core/buffer/buffer.ts'; -import type { TgpuQuerySet } from '../../core/querySet/querySet.ts'; +import type { IndexFlag, TgpuBuffer, VertexFlag } from '../buffer/buffer.ts'; +import type { TgpuQuerySet } from '../querySet/querySet.ts'; import { isBuiltin } from '../../data/attributes.ts'; import { type Disarray, getCustomLocation, type UndecorateRecord } from '../../data/dataTypes.ts'; import { sizeOf } from '../../data/sizeOf.ts'; diff --git a/packages/typegpu/src/core/querySet/querySet.ts b/packages/typegpu/src/core/querySet/querySet.ts index 71335d2648..d895c70d57 100644 --- a/packages/typegpu/src/core/querySet/querySet.ts +++ b/packages/typegpu/src/core/querySet/querySet.ts @@ -1,5 +1,5 @@ import { setName, type TgpuNamable } from '../../shared/meta.ts'; -import type { ExperimentalTgpuRoot } from '../../core/root/rootTypes.ts'; +import type { ExperimentalTgpuRoot } from '../root/rootTypes.ts'; import { $internal } from '../../shared/symbols.ts'; export interface TgpuQuerySet extends TgpuNamable { diff --git a/packages/typegpu/src/core/root/init.ts b/packages/typegpu/src/core/root/init.ts index bc84ad408b..0a27e2f78e 100644 --- a/packages/typegpu/src/core/root/init.ts +++ b/packages/typegpu/src/core/root/init.ts @@ -1,9 +1,5 @@ import { type AnyComputeBuiltin, builtin, type OmitBuiltins } from '../../builtin.ts'; -import { - INTERNAL_createQuerySet, - isQuerySet, - type TgpuQuerySet, -} from '../../core/querySet/querySet.ts'; +import { INTERNAL_createQuerySet, isQuerySet, type TgpuQuerySet } from '../querySet/querySet.ts'; import type { AnyData, Disarray } from '../../data/dataTypes.ts'; import type { AnyWgslData, BaseData, v3u, Vec3u, WgslArray } from '../../data/wgslTypes.ts'; import { invariant } from '../../errors.ts'; diff --git a/packages/typegpu/src/core/root/rootTypes.ts b/packages/typegpu/src/core/root/rootTypes.ts index 05b9c3db32..5a2b1167ec 100644 --- a/packages/typegpu/src/core/root/rootTypes.ts +++ b/packages/typegpu/src/core/root/rootTypes.ts @@ -1,5 +1,5 @@ import type { AnyComputeBuiltin, AnyFragmentInputBuiltin, OmitBuiltins } from '../../builtin.ts'; -import type { TgpuQuerySet } from '../../core/querySet/querySet.ts'; +import type { TgpuQuerySet } from '../querySet/querySet.ts'; import type { AnyData, Disarray, UndecorateRecord } from '../../data/dataTypes.ts'; import type { WgslComparisonSamplerProps, WgslSamplerProps } from '../../data/sampler.ts'; import type { @@ -51,7 +51,7 @@ import type { LayoutToAllowedAttribs, } from '../vertexLayout/vertexAttribute.ts'; import type { TgpuVertexLayout } from '../vertexLayout/vertexLayout.ts'; -import type { TgpuComputeFn } from './../function/tgpuComputeFn.ts'; +import type { TgpuComputeFn } from '../function/tgpuComputeFn.ts'; import type { TgpuNamable } from '../../shared/meta.ts'; import type { AnyAutoCustoms, diff --git a/packages/typegpu/src/core/slot/slotTypes.ts b/packages/typegpu/src/core/slot/slotTypes.ts index 80cab9197a..8fce92c90d 100644 --- a/packages/typegpu/src/core/slot/slotTypes.ts +++ b/packages/typegpu/src/core/slot/slotTypes.ts @@ -5,7 +5,7 @@ import type { GPUValueOf, Infer, InferGPU } from '../../shared/repr.ts'; import { $gpuValueOf, $internal, $providing } from '../../shared/symbols.ts'; import type { UnwrapRuntimeConstructor } from '../../tgpuBindGroupLayout.ts'; import type { TgpuBufferShorthand } from '../buffer/bufferShorthand.ts'; -import type { TgpuBufferUsage } from './../buffer/bufferUsage.ts'; +import type { TgpuBufferUsage } from '../buffer/bufferUsage.ts'; import type { TgpuConst } from '../constant/tgpuConstant.ts'; import type { Withable } from '../root/rootTypes.ts'; import type { TgpuTextureView } from '../texture/texture.ts'; diff --git a/packages/typegpu/src/core/unroll/tgpuUnroll.ts b/packages/typegpu/src/core/unroll/tgpuUnroll.ts index 0a1756824b..6d3da4bd6d 100644 --- a/packages/typegpu/src/core/unroll/tgpuUnroll.ts +++ b/packages/typegpu/src/core/unroll/tgpuUnroll.ts @@ -1,9 +1,9 @@ import { stitch } from '../resolve/stitch.ts'; -import { $gpuCallable, $internal, $resolve } from '../../../src/shared/symbols.ts'; -import { setName } from '../../../src/shared/meta.ts'; -import type { DualFn } from '../../../src/types.ts'; -import { type ResolvedSnippet, snip, type Snippet } from '../../../src/data/snippet.ts'; -import type { ResolutionCtx, SelfResolvable } from '../../../src/types.ts'; +import { $gpuCallable, $internal, $resolve } from '../../shared/symbols.ts'; +import { setName } from '../../shared/meta.ts'; +import type { DualFn } from '../../types.ts'; +import { type ResolvedSnippet, snip, type Snippet } from '../../data/snippet.ts'; +import type { ResolutionCtx, SelfResolvable } from '../../types.ts'; import type { BaseData } from '../../data/wgslTypes.ts'; /** diff --git a/packages/typegpu/src/tgsl/generationHelpers.ts b/packages/typegpu/src/tgsl/generationHelpers.ts index d4900b86e7..4e2c74f595 100644 --- a/packages/typegpu/src/tgsl/generationHelpers.ts +++ b/packages/typegpu/src/tgsl/generationHelpers.ts @@ -26,9 +26,9 @@ import { type SelfResolvable, } from '../types.ts'; import type { ShelllessRepository } from './shellless.ts'; -import { stitch } from '../../src/core/resolve/stitch.ts'; -import { WgslTypeError } from '../../src/errors.ts'; -import { $internal, $resolve } from '../../src/shared/symbols.ts'; +import { stitch } from '../core/resolve/stitch.ts'; +import { WgslTypeError } from '../errors.ts'; +import { $internal, $resolve } from '../shared/symbols.ts'; export function numericLiteralToSnippet(value: number): Snippet { if (value >= 2 ** 63 || value < -(2 ** 63)) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b87cffc0fe..d6cc58d7e8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -73,6 +73,9 @@ importers: eslint-plugin-eslint-plugin: specifier: ^7.3.2 version: 7.3.2(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-internal: + specifier: workspace:* + version: link:packages/eslint-plugin-internal eslint-plugin-typegpu: specifier: workspace:* version: link:packages/eslint-plugin @@ -428,7 +431,29 @@ importers: version: tsover@5.9.11 vitest: specifier: ^4.0.17 - version: 4.0.18(@types/node@24.10.0)(@vitest/browser-preview@4.1.2)(esbuild@0.27.5)(jiti@2.6.1)(jsdom@27.0.0(canvas@3.2.0)(postcss@8.5.8))(msw@2.10.2(@types/node@24.10.0)(tsover@5.9.11))(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.3) + version: 4.0.18(@types/node@24.10.0)(@vitest/browser-preview@4.1.2(msw@2.10.2(@types/node@24.10.0)(tsover@5.9.11))(vite@8.0.2(@types/node@24.10.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.3))(vitest@4.1.2))(esbuild@0.27.5)(jiti@2.6.1)(jsdom@27.0.0(canvas@3.2.0)(postcss@8.5.8))(msw@2.10.2(@types/node@24.10.0)(tsover@5.9.11))(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.3) + + packages/eslint-plugin-internal: + dependencies: + '@typescript-eslint/utils': + specifier: ^8.57.2 + version: 8.57.2(eslint@9.39.2(jiti@2.6.1))(tsover@5.9.11) + devDependencies: + '@types/node': + specifier: ^24.1.0 + version: 24.10.0 + '@typescript-eslint/rule-tester': + specifier: ^8.57.2 + version: 8.57.2(eslint@9.39.2(jiti@2.6.1))(tsover@5.9.11) + eslint: + specifier: ^9.39.2 + version: 9.39.2(jiti@2.6.1) + typescript: + specifier: npm:tsover@^5.9.11 + version: tsover@5.9.11 + vitest: + specifier: ^4.0.17 + version: 4.1.2(@types/node@24.10.0)(@vitest/browser-preview@4.1.2)(jsdom@27.0.0(canvas@3.2.0)(postcss@8.5.8))(msw@2.10.2(@types/node@24.10.0)(tsover@5.9.11))(vite@8.0.2(@types/node@24.10.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.3)) packages/tgpu-dev-cli: dependencies: @@ -16061,7 +16086,7 @@ snapshots: optionalDependencies: vite: 8.0.2(@types/node@24.10.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.3) - vitest@4.0.18(@types/node@24.10.0)(@vitest/browser-preview@4.1.2)(esbuild@0.27.5)(jiti@2.6.1)(jsdom@27.0.0(canvas@3.2.0)(postcss@8.5.8))(msw@2.10.2(@types/node@24.10.0)(tsover@5.9.11))(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.3): + vitest@4.0.18(@types/node@24.10.0)(@vitest/browser-preview@4.1.2(msw@2.10.2(@types/node@24.10.0)(tsover@5.9.11))(vite@8.0.2(@types/node@24.10.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.3))(vitest@4.1.2))(esbuild@0.27.5)(jiti@2.6.1)(jsdom@27.0.0(canvas@3.2.0)(postcss@8.5.8))(msw@2.10.2(@types/node@24.10.0)(tsover@5.9.11))(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.3): dependencies: '@vitest/expect': 4.0.18 '@vitest/mocker': 4.0.18(msw@2.10.2(@types/node@24.10.0)(tsover@5.9.11))(vite@8.0.2(@types/node@24.10.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.3))