diff --git a/.lint-roller.schema.json b/.lint-roller.schema.json new file mode 100644 index 0000000..72ef844 --- /dev/null +++ b/.lint-roller.schema.json @@ -0,0 +1,34 @@ +{ + "title": "JSON schema for @electron/lint-roller config files", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "$schema": { + "type": "string", + "format": "uri-reference" + }, + "markdown-ts-check": { + "description": "Configuration for the lint-roller-markdown-ts-check script", + "type": "object", + "properties": { + "defaultImports": { + "description": "Default imports to include when type checking a TypeScript code block", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "typings": { + "description": ".d.ts files (paths relative to root) to include when type checking a code block", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + }, + "additionalProperties": false + } + } +} diff --git a/README.md b/README.md index c655b63..f61ca35 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Markdown with `standard`, like `standard-markdown` does, but with better detection of code blocks. Linting can be disabled for specific code blocks by adding `@nolint` to the info string. -`electron-lint-markdown-ts-check` is a command to type check JS/TS code blocks +`lint-roller-markdown-ts-check` is a command to type check JS/TS code blocks in Markdown with `tsc`. Type checking can be disabled for specific code blocks by adding `@ts-nocheck` to the info string, specific lines can be ignored by adding `@ts-ignore=[,]` to the info string, and additional diff --git a/bin/lint-markdown-ts-check.ts b/bin/lint-markdown-ts-check.ts index 3c12535..22a365e 100644 --- a/bin/lint-markdown-ts-check.ts +++ b/bin/lint-markdown-ts-check.ts @@ -12,61 +12,30 @@ import { URI } from 'vscode-uri'; import { chunkFilenames, findCurlyBracedDirectives, + loadConfig, spawnAsync, wrapOrphanObjectInParens, + LintRollerConfig, } from '../lib/helpers'; import { getCodeBlocks, DocsWorkspace } from '../lib/markdown'; interface Options { + config?: LintRollerConfig; ignoreGlobs?: string[]; } -const ELECTRON_MODULES = [ - 'app', - 'autoUpdater', - 'contextBridge', - 'crashReporter', - 'dialog', - 'BrowserWindow', - 'ipcMain', - 'ipcRenderer', - 'Menu', - 'MessageChannelMain', - 'nativeImage', - 'net', - 'protocol', - 'session', - 'systemPreferences', - 'Tray', - 'utilityProcess', - 'webFrame', - 'webFrameMain', -]; - -const NODE_IMPORTS = - "const childProcess = require('node:child_process'); const fs = require('node:fs'); const path = require('node:path')"; - -const DEFAULT_IMPORTS = `${NODE_IMPORTS}; const { ${ELECTRON_MODULES.join( - ', ', -)} } = require('electron');`; - async function typeCheckFiles( tempDir: string, filenameMapping: Map, filenames: string[], + typings: string[], ) { const tscExec = path.join(require.resolve('typescript'), '..', '..', 'bin', 'tsc'); const options = ['--noEmit', '--pretty', '--moduleDetection', 'force']; if (filenames.find((filename) => filename.endsWith('.js'))) { options.push('--checkJs'); } - const args = [ - tscExec, - ...options, - path.join(tempDir, 'electron.d.ts'), - path.join(tempDir, 'ambient.d.ts'), - ...filenames, - ]; + const args = [tscExec, ...options, ...typings, ...filenames]; const { status, stderr, stdout } = await spawnAsync(process.execPath, args); if (stderr) { @@ -105,21 +74,19 @@ function parseDirectives(directive: string, value: string) { .filter((parsed): parsed is RegExpMatchArray => parsed !== null); } -// TODO(dsanders11): Refactor to make this script general purpose and -// not tied to Electron - will require passing in the list of modules -// as a CLI option, probably a file since there's a lot of info -async function main(workspaceRoot: string, globs: string[], { ignoreGlobs = [] }: Options) { +async function main( + workspaceRoot: string, + globs: string[], + { config = undefined, ignoreGlobs = [] }: Options, +) { const workspace = new DocsWorkspace(workspaceRoot, globs, ignoreGlobs); - const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'electron-ts-check-')); + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lint-roller-ts-check-')); try { const filenames: string[] = []; const originalFilenames = new Map(); const isolateFilenames = new Set(); - // Copy over the typings so that a relative path can be used - fs.copyFileSync(path.join(process.cwd(), 'electron.d.ts'), path.join(tempDir, 'electron.d.ts')); - let ambientModules = ''; let errors = false; @@ -244,11 +211,11 @@ async function main(workspaceRoot: string, globs: string[], { ignoreGlobs = [] } // This isn't foolproof and might cause name conflicts const imports = codeBlock.value.match(/^\s*(?:import .* from )|(?:.* = require())/m) ? '' - : DEFAULT_IMPORTS; + : (config?.['markdown-ts-check']?.defaultImports?.join(';') ?? '') + ';'; // Insert the necessary number of blank lines so that the line // numbers in output from tsc is accurate to the original file - const blankLines = '\n'.repeat(insertedInitialLine ? line - 3 : line - 2); + const blankLines = '\n'.repeat(Math.max(0, insertedInitialLine ? line - 3 : line - 2)); // Filename is unique since it is the name of the original Markdown // file, with the starting line number of the codeblock appended @@ -299,7 +266,17 @@ async function main(workspaceRoot: string, globs: string[], { ignoreGlobs = [] } } } - fs.writeFileSync(path.join(tempDir, 'ambient.d.ts'), ambientModules); + const ambientTypings = path.join(tempDir, 'ambient.d.ts'); + fs.writeFileSync(ambientTypings, ambientModules); + + const typings = [ambientTypings]; + + // Copy over the typings so that a relative path can be used + for (const typing of config?.['markdown-ts-check']?.typings ?? []) { + const tempPath = path.join(tempDir, path.basename(typing)); + fs.copyFileSync(path.join(path.resolve(workspaceRoot), typing), tempPath); + typings.push(tempPath); + } // Files for code blocks with window type directives or 'declare global' need // to be processed separately since window types are by nature global, and @@ -312,13 +289,13 @@ async function main(workspaceRoot: string, globs: string[], { ignoreGlobs = [] } filenames.unshift(windowTypesFilename); } catch {} - const status = await typeCheckFiles(tempDir, originalFilenames, filenames); + const status = await typeCheckFiles(tempDir, originalFilenames, filenames, typings); errors = errors || status !== 0; } // For the rest of the files, run them all at once so it doesn't take forever for (const chunk of chunkFilenames(filenames)) { - const status = await typeCheckFiles(tempDir, originalFilenames, chunk); + const status = await typeCheckFiles(tempDir, originalFilenames, chunk, typings); errors = errors || status !== 0; } @@ -332,8 +309,8 @@ function parseCommandLine() { const showUsage = (arg?: string): boolean => { if (!arg || arg.startsWith('-')) { console.log( - 'Usage: electron-lint-markdown-ts-check [--root ] [-h|--help]' + - '[--ignore ] [--ignore-path ]', + 'Usage: lint-roller-markdown-ts-check [--root ] [-h|--help]' + + '[--ignore ] [--ignore-path ] [--config ]', ); process.exit(1); } @@ -343,7 +320,7 @@ function parseCommandLine() { const opts = minimist(process.argv.slice(2), { boolean: ['help'], - string: ['root', 'ignore', 'ignore-path'], + string: ['config', 'root', 'ignore', 'ignore-path'], unknown: showUsage, }); @@ -373,7 +350,12 @@ if (require.main === module) { } } + const config = loadConfig( + opts.config ? path.resolve(opts.config) : path.resolve('.lint-roller.json'), + ); + main(path.resolve(process.cwd(), opts.root), opts._, { + config, ignoreGlobs: opts.ignore, }) .then((errors) => { diff --git a/lib/helpers.ts b/lib/helpers.ts index 7657ca6..43a038e 100644 --- a/lib/helpers.ts +++ b/lib/helpers.ts @@ -1,4 +1,5 @@ import * as childProcess from 'node:child_process'; +import * as fs from 'node:fs'; import * as os from 'node:os'; import { range as balancedRange } from 'balanced-match'; @@ -94,3 +95,26 @@ export function findCurlyBracedDirectives(directive: string, str: string) { return matches; } + +export interface LintRollerTsCheckConfig { + defaultImports?: string[]; + typings?: string[]; +} + +export interface LintRollerConfig { + 'markdown-ts-check'?: LintRollerTsCheckConfig; +} + +export function loadConfig(path: string) { + if (!fs.existsSync(path)) { + return undefined; + } + + const config = fs.readFileSync(path, 'utf8'); + + try { + return JSON.parse(config) as LintRollerConfig; + } catch { + throw new Error(`Couldn't parse config at ${path}`); + } +} diff --git a/package.json b/package.json index 3a91729..71d0b47 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "electron-markdownlint": "./dist/bin/markdownlint-cli-wrapper.js", "electron-lint-markdown-links": "./dist/bin/lint-markdown-links.js", "electron-lint-markdown-standard": "./dist/bin/lint-markdown-standard.js", - "electron-lint-markdown-ts-check": "./dist/bin/lint-markdown-ts-check.js" + "lint-roller-markdown-ts-check": "./dist/bin/lint-markdown-ts-check.js" }, "files": [ "configs", diff --git a/tests/__snapshots__/electron-lint-markdown-ts-check.spec.ts.snap b/tests/__snapshots__/lint-roller-markdown-ts-check.spec.ts.snap similarity index 97% rename from tests/__snapshots__/electron-lint-markdown-ts-check.spec.ts.snap rename to tests/__snapshots__/lint-roller-markdown-ts-check.spec.ts.snap index 398d283..edce19e 100644 --- a/tests/__snapshots__/electron-lint-markdown-ts-check.spec.ts.snap +++ b/tests/__snapshots__/lint-roller-markdown-ts-check.spec.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`electron-lint-markdown-ts-check should type check code blocks 1`] = ` +exports[`lint-roller-markdown-ts-check should type check code blocks 1`] = ` "ts-check.md:49:1: Code block has both @ts-nocheck and @ts-expect-error/@ts-ignore/@ts-type/@ts-window-type, they conflict ts-check.md:55:1: Code block has both @ts-nocheck and @ts-expect-error/@ts-ignore/@ts-type/@ts-window-type, they conflict ts-check.md:128:8 - error TS2339: Property 'myAwesomeAPI' does not exist on type 'Window & typeof globalThis'. diff --git a/tests/fixtures/.lint-roller.json b/tests/fixtures/.lint-roller.json new file mode 100644 index 0000000..a47c771 --- /dev/null +++ b/tests/fixtures/.lint-roller.json @@ -0,0 +1,14 @@ +{ + "$schema": "../../.lint-roller.schema.json", + "markdown-ts-check": { + "defaultImports": [ + "import * as childProcess from 'node:child_process'", + "import * as fs from 'node:fs'", + "import * as path from 'node:path'", + "import { app, BrowserWindow } from 'electron'" + ], + "typings": [ + "./electron.d.ts" + ] + } +} diff --git a/tests/fixtures/custom-config.json b/tests/fixtures/custom-config.json new file mode 100644 index 0000000..4bd5b09 --- /dev/null +++ b/tests/fixtures/custom-config.json @@ -0,0 +1,8 @@ +{ + "$schema": "../../.lint-roller.schema.json", + "markdown-ts-check": { + "defaultImports": [ + "import { performance } from 'node:perf_hooks'" + ] + } +} diff --git a/tests/fixtures/ts-check-custom-config.md b/tests/fixtures/ts-check-custom-config.md new file mode 100644 index 0000000..23178b0 --- /dev/null +++ b/tests/fixtures/ts-check-custom-config.md @@ -0,0 +1,15 @@ +```js +performance.measure('Start to Now') +``` + +```js @ts-expect-error=[1] +performance.Xmeasure('Start to Now') +``` + +```ts +performance.measure('Start to Now') +``` + +```ts @ts-expect-error=[1] +performance.Xmeasure('Start to Now') +``` diff --git a/tests/fixtures/ts-check.md b/tests/fixtures/ts-check.md index da1badf..5ba7cf5 100644 --- a/tests/fixtures/ts-check.md +++ b/tests/fixtures/ts-check.md @@ -212,3 +212,11 @@ declare global { window.AwesomeAPI.foo(42) window.OtherAwesomeAPI.bar('baz') ``` + +```js @ts-expect-error=[1] +fs.wrongApi('hello') +``` + +```ts @ts-expect-error=[1] +fs.wrongApi('hello') +``` diff --git a/tests/electron-lint-markdown-ts-check.spec.ts b/tests/lint-roller-markdown-ts-check.spec.ts similarity index 81% rename from tests/electron-lint-markdown-ts-check.spec.ts rename to tests/lint-roller-markdown-ts-check.spec.ts index c182600..6703777 100644 --- a/tests/electron-lint-markdown-ts-check.spec.ts +++ b/tests/lint-roller-markdown-ts-check.spec.ts @@ -11,7 +11,7 @@ function runLintMarkdownTsCheck(...args: string[]) { ); } -describe('electron-lint-markdown-ts-check', () => { +describe('lint-roller-markdown-ts-check', () => { it('should run clean when there are no errors', () => { const { status, stdout } = runLintMarkdownTsCheck('--root', FIXTURES_DIR, 'ts-check-clean.md'); @@ -63,4 +63,18 @@ describe('electron-lint-markdown-ts-check', () => { expect(stdout.replace(FIXTURES_DIR, '')).toMatchSnapshot(); expect(status).toEqual(1); }); + + it('can use a custom config', () => { + const { status, stdout, stderr } = runLintMarkdownTsCheck( + '--root', + FIXTURES_DIR, + '--config', + 'custom-config.json', + 'ts-check-custom-config.md', + ); + + expect(stdout).toEqual(''); + expect(stderr).toEqual(''); + expect(status).toEqual(0); + }); });