diff --git a/.github/scripts/wait-for-npm-packages.ts b/.github/scripts/wait-for-npm-packages.ts index 1be99b9578..d30509daab 100644 --- a/.github/scripts/wait-for-npm-packages.ts +++ b/.github/scripts/wait-for-npm-packages.ts @@ -93,7 +93,7 @@ function propagationTimeoutError( pending: ReadonlyMap, timeoutSeconds: number, ): Error { - const packageList = [...pending].map(([name, version]) => `${name}@${version}`).join(', '); + const packageList = Array.from(pending, ([name, version]) => `${name}@${version}`).join(', '); return new Error( `Timed out after ${timeoutSeconds}s waiting for npm propagation: ${packageList}`, ); diff --git a/docs/.vitepress/theme/components/home/FeatureToolbar.vue b/docs/.vitepress/theme/components/home/FeatureToolbar.vue index c31a0a1d95..3316e456d4 100644 --- a/docs/.vitepress/theme/components/home/FeatureToolbar.vue +++ b/docs/.vitepress/theme/components/home/FeatureToolbar.vue @@ -124,7 +124,7 @@ const determineActiveSection = () => { // If no section found above, check if we're at the very bottom if (activeId === activeSection.value) { - const lastSection = sections[sections.length - 1]; + const lastSection = sections.at(-1); if ( lastSection && window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 100 diff --git a/docs/.vitepress/theme/components/home/TerminalTranscript.vue b/docs/.vitepress/theme/components/home/TerminalTranscript.vue index 6be3987b5a..261803339a 100644 --- a/docs/.vitepress/theme/components/home/TerminalTranscript.vue +++ b/docs/.vitepress/theme/components/home/TerminalTranscript.vue @@ -56,8 +56,9 @@ const restartAnimation = () => { visibleLineCount.value = lineIndex + 1; if (lineIndex === props.transcript.lines.length - 1) { const completionTimer = window.setTimeout( - () => emit('complete'), + emit, props.transcript.completionDelay ?? 900, + 'complete', ); timers.push(completionTimer); } diff --git a/package.json b/package.json index 62ddecc990..2440a68587 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "prepare": "vp config" }, "devDependencies": { + "@e18e/eslint-plugin": "catalog:", "@emnapi/core": "catalog:", "@emnapi/runtime": "catalog:", "@napi-rs/cli": "catalog:", diff --git a/packages/cli/src/migration/__tests__/vitest-v5-documentation.spec.ts b/packages/cli/src/migration/__tests__/vitest-v5-documentation.spec.ts index e1988a59dc..c63de98646 100644 --- a/packages/cli/src/migration/__tests__/vitest-v5-documentation.spec.ts +++ b/packages/cli/src/migration/__tests__/vitest-v5-documentation.spec.ts @@ -56,7 +56,7 @@ it('keeps local documentation anchors valid', () => { 'utf8', ); const localSections = source.slice(source.indexOf('const localSections')); - const headings = [...guide.matchAll(/^## (.+)$/gm)].map((match) => + const headings = Array.from(guide.matchAll(/^## (.+)$/gm), (match) => match[1].toLowerCase().replaceAll(' ', '-'), ); for (const match of localSections.matchAll(/'[^']+': '([^']+)'/g)) { diff --git a/packages/cli/src/migration/__tests__/vitest-v5.spec.ts b/packages/cli/src/migration/__tests__/vitest-v5.spec.ts index 0005d66f39..0de4c63f81 100644 --- a/packages/cli/src/migration/__tests__/vitest-v5.spec.ts +++ b/packages/cli/src/migration/__tests__/vitest-v5.spec.ts @@ -2232,9 +2232,9 @@ export default defineConfig(CONFIG);`, 'checks.ts': 'export default { test: {} };', }); const plan = planProject(root); - expect([...plan.projects[0].configFiles].map((file) => path.basename(file)).toSorted()).toEqual( - ['checks.ts', 'test-base.ts', 'vite.config.ts'], - ); + expect( + Array.from(plan.projects[0].configFiles, (file) => path.basename(file)).toSorted(), + ).toEqual(['checks.ts', 'test-base.ts', 'vite.config.ts']); expect(plan.findings.some(({ code }) => code === 'source-parse')).toBe(false); expect(plan.changes.some(({ file }) => /plugin-(?:options|base)\.ts$/.test(file))).toBe(false); }); diff --git a/packages/cli/src/migration/migrator/catalog.ts b/packages/cli/src/migration/migrator/catalog.ts index 26c7e59d5e..34a30c24da 100644 --- a/packages/cli/src/migration/migrator/catalog.ts +++ b/packages/cli/src/migration/migrator/catalog.ts @@ -348,7 +348,7 @@ export function rewritePnpmWorkspaceYaml( for (const key of updatedOverrideKeys) { if (key.value.includes('>')) { const splits = key.value.split('>'); - if (splits[splits.length - 1].trim() === 'vite') { + if (splits.at(-1)!.trim() === 'vite') { updatedOverrides.delete(key); } } @@ -1253,7 +1253,7 @@ export function rewriteRootWorkspacePackageJson( for (const key in pkg.pnpm?.overrides) { if (key.includes('>')) { const splits = key.split('>'); - if (splits[splits.length - 1].trim() === 'vite') { + if (splits.at(-1)!.trim() === 'vite') { delete pkg.pnpm.overrides[key]; } } diff --git a/packages/cli/src/migration/migrator/orchestrators.ts b/packages/cli/src/migration/migrator/orchestrators.ts index 269f5cb191..a551ba2f79 100644 --- a/packages/cli/src/migration/migrator/orchestrators.ts +++ b/packages/cli/src/migration/migrator/orchestrators.ts @@ -223,7 +223,7 @@ export function rewriteStandaloneProject( for (const key in pkg.pnpm?.overrides) { if (key.includes('>')) { const splits = key.split('>'); - if (splits[splits.length - 1].trim() === 'vite') { + if (splits.at(-1)!.trim() === 'vite') { delete pkg.pnpm.overrides[key]; } } diff --git a/packages/cli/src/migration/migrator/tsup.ts b/packages/cli/src/migration/migrator/tsup.ts index d72a61c984..7eff1306e4 100644 --- a/packages/cli/src/migration/migrator/tsup.ts +++ b/packages/cli/src/migration/migrator/tsup.ts @@ -528,10 +528,10 @@ export async function migrateTsupToTsdown( } } for (const [configPath, sharedConfig] of sharedConfigs) { - const message = `${displayRelative(configPath, projectPath)} is shared by ${[ - ...sharedConfig.consumers.values(), - ] - .map((consumer) => consumer.label) + const message = `${displayRelative(configPath, projectPath)} is shared by ${Array.from( + sharedConfig.consumers.values(), + (consumer) => consumer.label, + ) .toSorted() .join(', ')}. It was preserved and must be migrated manually.`; if (options?.report) { diff --git a/packages/cli/src/migration/migrator/vitest-v5.ts b/packages/cli/src/migration/migrator/vitest-v5.ts index 671bb44529..f9c91117ec 100644 --- a/packages/cli/src/migration/migrator/vitest-v5.ts +++ b/packages/cli/src/migration/migrator/vitest-v5.ts @@ -692,7 +692,7 @@ export function planVitestV5Migration( ]; const projects: ProjectPlan[] = []; const projectSources = new Map( - [...new Set(directories)].map((directory) => [ + Array.from(new Set(directories), (directory) => [ directory, new Map(filesInProject(directory).map((file) => [file, fs.readFileSync(file, 'utf8')])), ]), @@ -931,7 +931,7 @@ function currentProjectConfigs(plan: VitestV5MigrationPlan): Map (entry.file ? [entry.file] : [])), ); return new Map( - [...filesByProject].map(([directory, files]) => [ + Array.from(filesByProject, ([directory, files]) => [ directory, files.filter((file) => configs.has(file)), ]), diff --git a/packages/cli/src/migration/vitest-v5/ast.ts b/packages/cli/src/migration/vitest-v5/ast.ts index 7a313fa8ed..6f2f179812 100644 --- a/packages/cli/src/migration/vitest-v5/ast.ts +++ b/packages/cli/src/migration/vitest-v5/ast.ts @@ -539,7 +539,7 @@ export class SourceEditor { } const propertyIndent = multilinePrefix && object.properties.length ? indent : `${objectIndent} `; - const entries = [...additions.values()].map((property) => + const entries = Array.from(additions.values(), (property) => printAddedProperty(property, propertyIndent, newline), ); // A generated line comment must end before the original inline properties diff --git a/packages/cli/src/migration/vitest-v5/scopes.ts b/packages/cli/src/migration/vitest-v5/scopes.ts index e06d466be8..c1d28f6dfb 100644 --- a/packages/cli/src/migration/vitest-v5/scopes.ts +++ b/packages/cli/src/migration/vitest-v5/scopes.ts @@ -689,7 +689,7 @@ export function resolveVitestV5TestModes( } } const modes = new Map( - [...sources.keys()].map((file) => { + Array.from(sources.keys(), (file) => { const matching: Array<{ scope: TestScope; certain: boolean }> = []; let benchmarkFile = false; // Match Vitest's source-text check, including its exact spelling. diff --git a/packages/cli/src/utils/package-overrides.ts b/packages/cli/src/utils/package-overrides.ts index 9ca5e98579..f5a7234034 100644 --- a/packages/cli/src/utils/package-overrides.ts +++ b/packages/cli/src/utils/package-overrides.ts @@ -20,7 +20,7 @@ export function extractOverrideTargetName(key: string): string { // Yarn uses `from/target`; retain a trailing scoped target as one name. if (target.includes('/')) { const segments = target.split('/'); - const last = segments[segments.length - 1]; + const last = segments.at(-1)!; const scope = segments[segments.length - 2]; target = scope?.startsWith('@') ? `${scope}/${last}` : last; } diff --git a/packages/tools/src/sync-remote-deps.ts b/packages/tools/src/sync-remote-deps.ts index 3024c5c4df..9f56d8e820 100755 --- a/packages/tools/src/sync-remote-deps.ts +++ b/packages/tools/src/sync-remote-deps.ts @@ -499,7 +499,7 @@ function globToRegExp(pattern: string): RegExp { function mergeMinimumReleaseAgeExclude(entries: string[]): string[] { // Dedupe exact-string duplicates (Set preserves insertion order) and parse // each entry once. - const parsed = [...new Set(entries)].map((entry) => { + const parsed = Array.from(new Set(entries), (entry) => { const { name, version } = parseExcludeEntry(entry); return { entry, name, version }; }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6839ca4a89..cf1a9e39df 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -176,6 +176,9 @@ catalogs: '@clack/core': specifier: ^1.5.1 version: 1.5.1 + '@e18e/eslint-plugin': + specifier: ^0.8.1 + version: 0.8.1 '@emnapi/core': specifier: 2.0.0-alpha.5 version: 2.0.0-alpha.5 @@ -439,6 +442,9 @@ importers: .: devDependencies: + '@e18e/eslint-plugin': + specifier: 'catalog:' + version: 0.8.1(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(oxlint@1.88.0(oxlint-tsgolint@7.0.2004)(vite-plus@packages+cli)) '@emnapi/core': specifier: 'catalog:' version: 2.0.0-alpha.5 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index bbc7540092..b672cd68e7 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -11,6 +11,7 @@ catalog: '@babel/preset-env': ^7.29.7 '@babel/preset-typescript': ^7.29.7 '@clack/core': ^1.5.1 + '@e18e/eslint-plugin': ^0.8.1 '@emnapi/core': 2.0.0-alpha.5 '@emnapi/runtime': 2.0.0-alpha.5 '@iconify/vue': ^5.0.0 diff --git a/vite.config.ts b/vite.config.ts index 59c8d94943..f25b997d27 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -11,12 +11,33 @@ export default defineConfig({ typeCheck: true, }, plugins: ['unicorn', 'typescript', 'oxc'], + jsPlugins: ['@e18e/eslint-plugin'], categories: { correctness: 'error', perf: 'error', suspicious: 'error', }, rules: { + // e18e's recommended rules, with exceptions for existing integrations and regex style. + 'e18e/ban-dependencies': ['error', { allowed: ['cross-spawn', 'glob'] }], + 'e18e/prefer-array-at': 'error', + 'e18e/prefer-array-fill': 'error', + 'e18e/prefer-array-from-map': 'error', + 'e18e/prefer-array-some': 'error', + 'e18e/prefer-array-to-reversed': 'error', + 'e18e/prefer-array-to-sorted': 'error', + 'e18e/prefer-array-to-spliced': 'error', + 'e18e/prefer-date-now': 'error', + 'e18e/prefer-includes': 'error', + 'e18e/prefer-nullish-coalescing': 'error', + 'e18e/prefer-object-has-own': 'error', + 'e18e/prefer-regex-test': 'error', + // Its concat autofix can nest array arguments without TypeScript ESLint's type information. + 'e18e/prefer-spread-syntax': 'off', + 'e18e/prefer-static-regex': 'off', + 'e18e/prefer-string-fromcharcode': 'error', + 'e18e/prefer-timer-args': 'error', + 'e18e/prefer-url-canparse': 'error', 'eslint/no-await-in-loop': 'off', 'no-console': ['error', { allow: ['error'] }], 'no-shadow': 'off',