diff --git a/.oxlintrc.json b/.oxlintrc.json index e3d3374d31..798a617ba9 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -1,7 +1,7 @@ { "$schema": "./node_modules/oxlint/configuration_schema.json", "plugins": ["import", "oxc", "typescript"], - "jsPlugins": ["eslint-plugin-cypress"], + "jsPlugins": ["eslint-plugin-cypress", "eslint-plugin-local-import-ext"], "env": { "es2025": true, "browser": true, @@ -62,9 +62,17 @@ "typescript/no-non-null-assertion": "off", "typescript/no-unused-expressions": "off", "typescript/no-unused-vars": ["error", { "argsIgnorePattern": "^_", "destructuredArrayIgnorePattern": "^_", "caughtErrors": "none" }], - // blocked by https://github.com/oxc-project/oxc/issues/19431 - // use "lint:local-js-imports" npm script in the meantime to catch any missing local JS imports + // "import/extensions" rule is blocked by https://github.com/oxc-project/oxc/issues/19431 + // use custom "eslint-plugin-import-ext" custom eslint plugin in the meantime to catch any missing local import file extensions "import/extensions": "off", + "local-import-ext/require-local-extension": [ + "error", + { + "js": "always", + "ts": "never", + "excludedFolders": ["demos/aurelia/test", "demos/react/test", "demos/vue/test", "frameworks/angular-slickgrid", "test"] + } + ], "import/no-self-import": "error", "no-async-promise-executor": "off", "oxc/erasing-op": "off", diff --git a/package.json b/package.json index 7e618d6999..93bfa18521 100644 --- a/package.json +++ b/package.json @@ -48,9 +48,8 @@ "roll-new-release": "pnpm build && pnpm new-version && pnpm new-publish", "build:dev": "pnpm -r --filter=vanilla-demo build:dev", "serve:vite": "pnpm -r --filter=vanilla-demo dev", - "lint": "oxlint . && pnpm lint:local-js-imports", + "lint": "oxlint .", "lint:fix": "oxlint . --fix", - "lint:local-js-imports": "node ./scripts/lint-local-js-imports.mjs", "prettier:check": "prettier --check **/*.{html,js,ts,tsx,vue}", "prettier:write": "prettier --write **/*.{html,js,ts,tsx,vue}", "test": "vitest --config ./test/vitest.config.mts", @@ -129,6 +128,7 @@ "cypress": "catalog:", "cypress-real-events": "catalog:", "eslint-plugin-cypress": "^6.4.1", + "eslint-plugin-local-import-ext": "0.2.0", "globals": "catalog:", "jsdom": "catalog:", "jsdom-global": "catalog:", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7121592384..9acbaa4982 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -200,6 +200,9 @@ importers: eslint-plugin-cypress: specifier: ^6.4.1 version: 6.4.1(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-local-import-ext: + specifier: 0.2.0 + version: 0.2.0 globals: specifier: 'catalog:' version: 17.6.0 @@ -5452,6 +5455,10 @@ packages: '@typescript-eslint/parser': optional: true + eslint-plugin-local-import-ext@0.2.0: + resolution: {integrity: sha512-2DelnKhTzSuZ0Xd9a5o740w+CL01O7LgGiOJq+0+fo+nTxkCTVl13/3L6LlJfCYhKCb79s1UkR9pa2bFe3yP1w==} + engines: {node: ^20.0.0 || >=22.0.0} + eslint-scope@8.4.0: resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -12778,6 +12785,8 @@ snapshots: eslint: 9.39.2(jiti@2.6.1) globals: 17.6.0 + eslint-plugin-local-import-ext@0.2.0: {} + eslint-scope@8.4.0: dependencies: esrecurse: 4.3.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 91582f3a5b..57f5f802cd 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -9,8 +9,7 @@ minimumReleaseAge: 2880 # 2 days in minutes minimumReleaseAgeExclude: # exclude packages that I personally maintain and are safe to use on the same day - '@lerna-lite/*' - # Renovate security update: hono@4.12.18 - - hono@4.12.18 + - 'eslint-plugin-local-import-ext' catalog: '@4tw/cypress-drag-drop': ^2.3.1 diff --git a/scripts/lint-local-js-imports.mjs b/scripts/lint-local-js-imports.mjs deleted file mode 100644 index e9b6762483..0000000000 --- a/scripts/lint-local-js-imports.mjs +++ /dev/null @@ -1,111 +0,0 @@ -// temp script to check for missing ".js" extensions in relative local imports -// related oxlint issue: https://github.com/oxc-project/oxc/issues/19431 - -import fs from 'node:fs'; -import path from 'node:path'; -import { styleText } from 'node:util'; - -const roots = ['demos', 'frameworks', 'frameworks-plugins', 'packages']; -const excludedFolders = ['demos/aurelia/test', 'demos/react/test', 'demos/vue/test', 'frameworks/angular-slickgrid']; -const allowedExtPattern = /\.(html|js|json|mjs|png|vue)(\?.*)?$/; -const fromPattern = /from\s+['\"](\.\.?\/[^'\"]+)['\"]/g; - -function color(text, format) { - return process.stdout.isTTY ? styleText(format, text) : text; -} - -function normalizeSlashes(value) { - return value.replaceAll('\\', '/'); -} - -function isExcluded(fullPath) { - const normalized = normalizeSlashes(fullPath); - return excludedFolders.some((excludedPath) => normalized === excludedPath || normalized.startsWith(`${excludedPath}/`)); -} - -/** @param {string} dir */ -function walk(dir, acc) { - if (!fs.existsSync(dir)) { - return; - } - - for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { - const fullPath = path.join(dir, entry.name); - - if (entry.isDirectory()) { - if (isExcluded(fullPath)) { - continue; - } - if (entry.name === 'node_modules' || entry.name === 'generated-parser' || entry.name === 'dist') { - continue; - } - walk(fullPath, acc); - continue; - } - - if (entry.isFile() && entry.name.endsWith('.ts')) { - acc.push(fullPath); - } - } -} - -const startTime = performance.now(); -const files = []; -for (const root of roots) { - walk(root, files); -} - -const scopedFiles = files.filter((filePath) => { - if (filePath.startsWith(`packages${path.sep}`)) { - return filePath.includes(`${path.sep}src${path.sep}`); - } - return true; -}); - -const errors = []; -for (const filePath of scopedFiles) { - const content = fs.readFileSync(filePath, 'utf8'); - const lines = content.split(/\r?\n/); - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - fromPattern.lastIndex = 0; - - for (let match = fromPattern.exec(line); match !== null; match = fromPattern.exec(line)) { - const importPath = match[1]; - if (!allowedExtPattern.test(importPath)) { - errors.push({ - filePath, - lineNumber: i + 1, - line: line.trim(), - }); - } - } - } -} - -const endTime = performance.now(); -const elapsedMs = endTime - startTime; -const elapsedTime = elapsedMs < 1000 ? `${elapsedMs.toFixed(0)}ms` : `${(elapsedMs / 1000).toFixed(1)}s`; - -if (errors.length > 0) { - console.error(color(`Found ${errors.length} relative imports missing a file extension (.js, .json, .mjs, or .vue).`, ['red', 'bold'])); - console.error(color('Add the appropriate extension to the import path in the following lines:', 'yellow')); - console.error(''); - - for (const error of errors) { - console.error(`${color('', 'red')}${color(`${error.filePath}:${error.lineNumber}`, 'cyan')}`); - console.error(` - ${color(`${error.line}`, 'red')}`); - console.error(''); - } - - console.error(''); - console.error(color(`Total violations: ${errors.length}`, ['red', 'bold'])); - console.error(`Finished in ${elapsedTime} on ${scopedFiles.length} files.`); - process.exit(1); -} else { - console.log('Found 0 violations.'); - console.log(`Finished in ${elapsedTime} on ${scopedFiles.length} files.`); -} - -console.log('All relative imports have file extensions.');