From 81986db0a0368201a8d4c9d1755a71e06bec5a3e Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Wed, 17 Jul 2024 10:19:26 -0400 Subject: [PATCH] feat: added project references, with more docs --- .prettierignore | 2 +- README.md | 30 +++++++++ cspell.json | 2 +- src/creators/createEvenCaseFiles.ts | 67 ------------------- src/creators/createProjectsCaseFiles.ts | 6 -- src/creators/createWideCaseFiles.ts | 43 ------------ src/creators/files.ts | 38 ----------- src/creators/files/createESLintConfigFile.ts | 33 +++++++++ src/creators/files/createPackageFile.ts | 18 +++++ .../files/createStandardTSConfigFile.ts | 12 ++++ src/creators/writeCaseFiles.ts | 15 +++++ src/data.ts | 8 +-- src/generate.ts | 59 +++++----------- src/measure.ts | 16 ++--- src/utils.ts | 2 +- 15 files changed, 138 insertions(+), 213 deletions(-) delete mode 100644 src/creators/createEvenCaseFiles.ts delete mode 100644 src/creators/createProjectsCaseFiles.ts delete mode 100644 src/creators/createWideCaseFiles.ts delete mode 100644 src/creators/files.ts create mode 100644 src/creators/files/createESLintConfigFile.ts create mode 100644 src/creators/files/createPackageFile.ts create mode 100644 src/creators/files/createStandardTSConfigFile.ts create mode 100644 src/creators/writeCaseFiles.ts diff --git a/.prettierignore b/.prettierignore index e17683e..f1102d5 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,3 @@ +./cases/ .all-contributorsrc .husky/ -cases/ diff --git a/README.md b/README.md index 4156792..1fd9352 100644 --- a/README.md +++ b/README.md @@ -15,12 +15,42 @@ ## Usage +You'll need [hyperfine](https://github.com/sharkdp/hyperfine) installed locally, such as with `brew install hyperfine` or `winget install hyperfine`. +See [sharkdp/hyperfine#installation](https://github.com/sharkdp/hyperfine#installation). + ```shell npm install npm generate npm measure ``` +### Measured Attributes + +The `caseEntries` values in `src/data.ts` can be modified to test: + +- `files`: roughly how many generated files should be linted +- `layout`: what rough shape of imports those files exhibit: + - `"even"`: a single root-level `index.ts` importing from roughly an even triangle shape of files + - `"references"`: a single root-level `tsconfig.json` with project references to a few projects + - `"wide"`: one root-level `index.ts` importing from all files in the project +- `singleRun`: whether to enable [single-run inference](https://v8--typescript-eslint.netlify.app/packages/parser#disallowautomaticsingleruninference) as a performance boost +- `types`: whether to use `parserOptions.project` or `parserOptions.projectService` for typed linting + +## Results + +Right now, `parserOptions.project` outperforms `parserOptions.projectService`. +This is a performance issue and we are investigating it as a critical bug for v8. + +```plaintext +┌───────┬──────────────────────┬──────────────────────┬──────────────────────┬──────────────────────┐ +│ files │ project (even) │ project (references) │ service (even) │ service (references) │ +┼───────┼──────────────────────┼──────────────────────┼──────────────────────┼──────────────────────┤ +│ 128 │ '1.149 s ± 0.030 s' │ '1.135 s ± 0.008 s' │ '1.178 s ± 0.010 s' │ '1.736 s ± 0.012 s' │ +│ 512 │ '1.636 s ± 0.009 s' │ '1.656 s ± 0.004 s' │ '1.895 s ± 0.007 s' │ '2.613 s ± 0.020 s' │ +│ 1024 │ '2.353 s ± 0.013 s' │ '2.399 s ± 0.016 s' │ '3.130 s ± 0.017 s' │ '4.034 s ± 0.061 s' │ +┴───────┴──────────────────────┴──────────────────────┴──────────────────────┴──────────────────────┘ +``` + ## Contributors diff --git a/cspell.json b/cspell.json index 472bfdc..e64e00c 100644 --- a/cspell.json +++ b/cspell.json @@ -1,5 +1,5 @@ { "dictionaries": ["typescript"], "ignorePaths": [".github", "cases", "node_modules"], - "words": ["execa", "knip", "packagejson", "tseslint"] + "words": ["execa", "knip", "packagejson", "sharkdp", "tseslint", "winget"] } diff --git a/src/creators/createEvenCaseFiles.ts b/src/creators/createEvenCaseFiles.ts deleted file mode 100644 index fb3e4e7..0000000 --- a/src/creators/createEvenCaseFiles.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { CaseData } from "../data.js"; -import { Structure } from "../writing/writeStructure.js"; -import { range } from "./utils.js"; - -function createExampleFile(index: number) { - return [ - index > 1 && - range(1, index) - .map((i) => `export * as nested${i} from "./nested${i}/index.js";`) - .join("\n\t\t"), - ` - export async function example${index}(prefix: string) { - await Promise.resolve(); - return prefix + "" + ${index}; - } - `, - ].join("\n\n"); -} - -function createExampleDirectory(index: number): Structure { - return { - "index.ts": [createExampleFile(index), "typescript"], - ...(index > 2 && - Object.fromEntries( - range(1, index).map((i) => [ - `nested${i}`, - createExampleDirectory(i - 1), - ]), - )), - }; -} - -function createIndexFile(count: number) { - const indices = range(0, count); - - return ` - import { example0 } from "./example0/index.js"; - - export async function root() { - // Lint report: no-floating-promises - example0(""); - - // No lint report - await example0(""); - } - - ${indices.map((index) => `export { example${index} } from "./example${index}/index.js";`).join("\n\t\t")} - `; -} - -export function writeEvenCaseFiles(data: CaseData): Structure { - const topLevelWidth = Math.floor(Math.log(data.files) * 1.7); - - return { - src: { - "index.ts": [createIndexFile(topLevelWidth), "typescript"], - ...Object.fromEntries( - new Array(topLevelWidth) - .fill(undefined) - .map((_, index) => [ - `example${index}`, - createExampleDirectory(index), - ]), - ), - }, - }; -} diff --git a/src/creators/createProjectsCaseFiles.ts b/src/creators/createProjectsCaseFiles.ts deleted file mode 100644 index 26c0247..0000000 --- a/src/creators/createProjectsCaseFiles.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Structure } from "../writing/writeStructure.js"; - -export function writeProjectsCaseFiles(): Structure { - // ... - return {}; -} diff --git a/src/creators/createWideCaseFiles.ts b/src/creators/createWideCaseFiles.ts deleted file mode 100644 index a6b0026..0000000 --- a/src/creators/createWideCaseFiles.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { CaseData } from "../data.js"; -import { Structure } from "../writing/writeStructure.js"; - -function createExampleFile(index: number) { - return ` - export async function example${index}(prefix: string) { - await Promise.resolve(); - return prefix + "" + ${index}; - } - `; -} - -function createIndexFile(count: number) { - const indices = new Array(count - 1).fill(undefined).map((_, index) => index); - - return ` - ${indices.map((index) => `import { example${index} } from "./example${index}.js";`).join("\n\t\t")} - - export async function root() { - // Lint report: no-floating-promises - example0(""); - - // No lint reports - ${indices.map((index) => `await example${index}("");`).join("\n\t\t\t")} - } - `; -} - -export function writeWideCaseFiles(data: CaseData): Structure { - return { - src: { - "index.ts": [createIndexFile(data.files), "typescript"], - ...Object.fromEntries( - new Array(data.files - 1) - .fill(undefined) - .map((_, index) => [ - `example${index}.ts`, - [createExampleFile(index), "typescript"], - ]), - ), - }, - }; -} diff --git a/src/creators/files.ts b/src/creators/files.ts deleted file mode 100644 index 3e1d541..0000000 --- a/src/creators/files.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { CaseData, CaseTypes } from "../data.js"; -import { writeStructure } from "../writing/writeStructure.js"; -import { writeEvenCaseFiles } from "./createEvenCaseFiles.js"; -import { writeProjectsCaseFiles } from "./createProjectsCaseFiles.js"; -import { writeWideCaseFiles } from "./createWideCaseFiles.js"; - -export function createESLintConfigFile(data: CaseData) { - return ` - import tseslint from "typescript-eslint"; - - export default tseslint.config( - tseslint.configs.base, - { - files: ["**/*.ts"], - languageOptions: { - parserOptions: { - ${data.singleRun ? "disallowAutomaticSingleRunInference: true," : ""} - ${data.types === "service" ? "projectService" : "project"}: true, - tsconfigRootDir: import.meta.dirname, - }, - }, - rules: { - "@typescript-eslint/no-floating-promises": "error" - } - }, - ); - `; -} - -const caseFileCreators = { - even: writeEvenCaseFiles, - projects: writeProjectsCaseFiles, - wide: writeWideCaseFiles, -}; - -export async function writeCaseFiles(data: CaseData, directory: string) { - return await writeStructure(directory, caseFileCreators[data.layout](data)); -} diff --git a/src/creators/files/createESLintConfigFile.ts b/src/creators/files/createESLintConfigFile.ts new file mode 100644 index 0000000..b55af21 --- /dev/null +++ b/src/creators/files/createESLintConfigFile.ts @@ -0,0 +1,33 @@ +export interface ESLintConfigFileOptions { + singleRun: boolean; + types: "tsconfig.eslint.json" | "projectService" | true; +} + +export function createESLintConfigFile({ + singleRun, + types, +}: ESLintConfigFileOptions) { + const [projectKey, projectValue] = + types === "projectService" ? ["projectService", true] : ["project", types]; + + return ` + import tseslint from "typescript-eslint"; + + export default tseslint.config( + tseslint.configs.base, + { + files: ["**/*.ts"], + languageOptions: { + parserOptions: { + ${singleRun ? "disallowAutomaticSingleRunInference: true," : ""} + ${projectKey}: ${typeof projectValue === "string" ? `"${projectValue}"` : projectValue}, + tsconfigRootDir: import.meta.dirname, + }, + }, + rules: { + "@typescript-eslint/no-floating-promises": "error" + } + }, + ); + `; +} diff --git a/src/creators/files/createPackageFile.ts b/src/creators/files/createPackageFile.ts new file mode 100644 index 0000000..4397128 --- /dev/null +++ b/src/creators/files/createPackageFile.ts @@ -0,0 +1,18 @@ +import { NamedCaseData } from "../../data.js"; + +export function createPackageFile(data: NamedCaseData) { + return { + devDependencies: { + "@eslint/js": "*", + eslint: "*", + typescript: "*", + "typescript-eslint": "rc-v8", + }, + name: data.name, + private: true, + scripts: { + lint: "eslint src", + }, + type: "module", + }; +} diff --git a/src/creators/files/createStandardTSConfigFile.ts b/src/creators/files/createStandardTSConfigFile.ts new file mode 100644 index 0000000..0a1ff25 --- /dev/null +++ b/src/creators/files/createStandardTSConfigFile.ts @@ -0,0 +1,12 @@ +export function createStandardTSConfigFile() { + return { + compilerOptions: { + module: "NodeNext", + noEmit: true, + skipLibCheck: true, + strict: true, + target: "ESNext", + }, + include: ["src"], + }; +} diff --git a/src/creators/writeCaseFiles.ts b/src/creators/writeCaseFiles.ts new file mode 100644 index 0000000..ce152b5 --- /dev/null +++ b/src/creators/writeCaseFiles.ts @@ -0,0 +1,15 @@ +import { CaseData } from "../data.js"; +import { writeStructure } from "../writing/writeStructure.js"; +import { writeEvenCaseFiles } from "./cases/createEvenCaseFiles.js"; +import { createReferencesCaseFiles } from "./cases/createReferencesCaseFiles.js"; +import { writeWideCaseFiles } from "./cases/createWideCaseFiles.js"; + +const caseFileCreators = { + even: writeEvenCaseFiles, + references: createReferencesCaseFiles, + wide: writeWideCaseFiles, +}; + +export async function writeCaseFiles(data: CaseData, directory: string) { + return await writeStructure(directory, caseFileCreators[data.layout](data)); +} diff --git a/src/data.ts b/src/data.ts index d456711..3a4a218 100644 --- a/src/data.ts +++ b/src/data.ts @@ -3,16 +3,16 @@ export const casesPath = "cases"; export const caseEntries = [ { label: "files", - // values: [1, 32, 64, 128, 256, 512, 1024], - values: [512, 1024], + values: [128, 512, 1024], }, { label: "layout", - values: ["even" /* , "projects" , "wide" */], + // values: ["even" /* , "references" , "wide" */], + values: ["even", "references" /* , "wide" */], }, { label: "singleRun", - values: [false, true], + values: [/* false, */ true], }, { label: "types", diff --git a/src/generate.ts b/src/generate.ts index bff19e6..f7d6465 100644 --- a/src/generate.ts +++ b/src/generate.ts @@ -2,56 +2,27 @@ import { execa } from "execa"; import fs from "node:fs/promises"; import path from "node:path"; -import { createESLintConfigFile, writeCaseFiles } from "./creators/files.js"; +import { writeCaseFiles } from "./creators/writeCaseFiles.js"; +import { createPackageFile } from "./creators/files/createPackageFile.js"; +import { createESLintConfigFile } from "./creators/files/createESLintConfigFile.js"; import { CaseData, NamedCaseData, caseEntries, casesPath } from "./data.js"; import { createProjectName } from "./utils.js"; import { writeFile } from "./writing/writeFile.js"; -async function createCase(data: CaseData): Promise { - const name = createProjectName(data); +async function createCase(data: NamedCaseData): Promise { + const name = createProjectName({ + files: data.files, + layout: data.layout, + singleRun: data.singleRun, + types: data.types, + }); const directory = path.join(casesPath, name); console.log(`Populating ${name}...`); await fs.mkdir(path.join(directory, "src"), { recursive: true }); - await writeFile(directory, "eslint.config.js", createESLintConfigFile(data)); - - await writeFile( - directory, - "package.json", - { - devDependencies: { - "@eslint/js": "*", - eslint: "*", - typescript: "*", - "typescript-eslint": "rc-v8", - }, - name, - private: true, - scripts: { - lint: "eslint src", - }, - type: "module", - }, - "json", - ); - - await writeFile( - directory, - "tsconfig.json", - { - compilerOptions: { - module: "NodeNext", - noEmit: true, - skipLibCheck: true, - strict: true, - target: "ESNext", - }, - include: ["src"], - }, - "json", - ); + await writeFile(directory, "package.json", createPackageFile(data), "json"); console.log("Created", await writeCaseFiles(data, directory), "files"); @@ -71,10 +42,10 @@ const cases: NamedCaseData[] = []; for (const files of caseEntries[0].values) { for (const layout of caseEntries[1].values) { - for (const singleRun of caseEntries[2].values) { - for (const types of caseEntries[3].values) { - cases.push(await createCase({ files, layout, singleRun, types })); - } + for (const types of caseEntries[3].values) { + const data: CaseData = { files, layout, singleRun: true, types }; + const name = createProjectName(data); + cases.push(await createCase({ ...data, name })); } } } diff --git a/src/measure.ts b/src/measure.ts index 89708b6..67fdeec 100644 --- a/src/measure.ts +++ b/src/measure.ts @@ -37,27 +37,27 @@ const results: unknown[] = []; for (const files of caseEntries[0].values) { results.push({ files, - "project (multi-run)": await runProjectLint({ + "project (even)": await runProjectLint({ files, layout: "even", - singleRun: false, + singleRun: true, types: "project", }), - "project (single-run)": await runProjectLint({ + "project (references)": await runProjectLint({ files, - layout: "even", + layout: "references", singleRun: true, types: "project", }), - "service (multi-run)": await runProjectLint({ + "service (even)": await runProjectLint({ files, layout: "even", - singleRun: false, + singleRun: true, types: "service", }), - "service (single-run)": await runProjectLint({ + "service (references)": await runProjectLint({ files, - layout: "even", + layout: "references", singleRun: true, types: "service", }), diff --git a/src/utils.ts b/src/utils.ts index fba7339..15330ff 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,5 @@ import { CaseData } from "./data.js"; export function createProjectName(data: CaseData) { - return Object.entries(data).flat().join("-"); + return Object.entries(data).flat().join("-").toLowerCase(); }