Skip to content

Commit

Permalink
feat: file matcher (#41)
Browse files Browse the repository at this point in the history
  • Loading branch information
privatenumber authored Feb 6, 2023
1 parent 307dd1c commit dc97538
Show file tree
Hide file tree
Showing 7 changed files with 1,654 additions and 0 deletions.
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,47 @@ console.log(parseTsconfig('./path/to/tsconfig.custom.json'))

---

### createFileMatcher(tsconfig: TsconfigResult, caseSensitivePaths?: boolean)

Given a `tsconfig.json` file, it returns a file-matcher function that determines whether it should apply to a file path.

```ts
type FileMatcher = (filePath: string) => TsconfigResult['config'] | undefined
```
#### tsconfig
Type: `TsconfigResult`
Pass in the return value from `getTsconfig`, or a `TsconfigResult` object.
#### caseSensitivePaths
Type: `boolean`
By default, it uses [`is-fs-case-sensitive`](https://github.com/privatenumber/is-fs-case-sensitive) to detect whether the file-system is case-sensitive.
Pass in `true` to make it case-sensitive.
#### Example
For example, if it's called with a `tsconfig.json` file that has `include`/`exclude`/`files` defined, the file-matcher will return the config for files that match `include`/`files`, and return `undefined` for files that don't match or match `exclude`.
```ts
const tsconfig = getTsconfig()
const fileMatcher = tsconfig && createFileMatcher(tsconfig)

/*
* Returns tsconfig.json if it matches the file,
* undefined if not
*/
const configForFile = fileMatcher?.('/path/to/file.ts')
const distCode = compileTypescript({
code: sourceCode,
tsconfig: configForFile
})
```

---

### createPathsMatcher(tsconfig: TsconfigResult)

Given a tsconfig with [`compilerOptions.paths`](https://www.typescriptlang.org/tsconfig#paths) defined, it returns a matcher function.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"eslint": "^8.30.0",
"execa": "^6.1.0",
"fs-fixture": "^1.2.0",
"is-fs-case-sensitive": "^1.0.0",
"jsonc-parser": "^3.2.0",
"manten": "^0.6.0",
"pkgroll": "^1.8.0",
Expand Down
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

221 changes: 221 additions & 0 deletions src/files-matcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import path from 'path';
import slash from 'slash';
import type { TsConfigJson } from 'type-fest';
import { isFsCaseSensitive } from 'is-fs-case-sensitive';
import type { TsConfigResult, TsConfigJsonResolved } from './types.js';

export type FileMatcher = (filePath: string) => (TsConfigJsonResolved | undefined);

const { join: pathJoin } = path.posix;

const baseExtensions = {
ts: ['.ts', '.tsx', '.d.ts'],
cts: ['.cts', '.d.cts'],
mts: ['.mts', '.d.mts'],
};

const getSupportedExtensions = (
compilerOptions: TsConfigJson['compilerOptions'],
) => {
const ts = [...baseExtensions.ts];
const cts = [...baseExtensions.cts];
const mts = [...baseExtensions.mts];

if (compilerOptions?.allowJs) {
ts.push('.js', '.jsx');
cts.push('.cjs');
mts.push('.mjs');
}

return [
...ts,
...cts,
...mts,
];
};

// https://github.com/microsoft/TypeScript/blob/acf854b636e0b8e5a12c3f9951d4edfa0fa73bcd/src/compiler/commandLineParser.ts#L3014-L3016
const getDefaultExcludeSpec = (
compilerOptions: TsConfigJson['compilerOptions'],
) => {
const excludesSpec: string[] = [];

if (!compilerOptions) {
return excludesSpec;
}

const { outDir, declarationDir } = compilerOptions;
if (outDir) {
excludesSpec.push(outDir);
}

if (declarationDir) {
excludesSpec.push(declarationDir);
}

return excludesSpec;
};

const escapeForRegexp = (string: string) => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');

const dependencyDirectories = ['node_modules', 'bower_components', 'jspm_packages'] as const;
const implicitExcludePathRegexPattern = `(?!(${dependencyDirectories.join('|')})(/|$))`;

/**
*
* File matchers
* replace *, ?, and ** / with regex
* https://github.com/microsoft/TypeScript/blob/acf854b636e0b8e5a12c3f9951d4edfa0fa73bcd/src/compiler/utilities.ts#L8088
*
* getSubPatternFromSpec
* https://github.com/microsoft/TypeScript/blob/acf854b636e0b8e5a12c3f9951d4edfa0fa73bcd/src/compiler/utilities.ts#L8165
*
* matchFiles
* https://github.com/microsoft/TypeScript/blob/acf854b636e0b8e5a12c3f9951d4edfa0fa73bcd/src/compiler/utilities.ts#L8291
*
* getFileMatcherPatterns
* https://github.com/microsoft/TypeScript/blob/acf854b636e0b8e5a12c3f9951d4edfa0fa73bcd/src/compiler/utilities.ts#L8267
*/

/**
* An "includes" path "foo" is implicitly a glob "foo/** /*" (without the space)
* if its last component has no extension, and does not contain any glob characters itself.
*/
const isImplicitGlobPattern = /(?:^|\/)[^.*?]+$/;

const matchAllGlob = '**/*';

const anyCharacter = '[^/]';

const noPeriodOrSlash = '[^./]';

const isWindows = process.platform === 'win32';

export const createFilesMatcher = (
{
config,
path: tsconfigPath,
}: TsConfigResult,
caseSensitivePaths = isFsCaseSensitive(),
): FileMatcher => {
if ('extends' in config) {
throw new Error('tsconfig#extends must be resolved. Use getTsconfig or parseTsconfig to resolve it.');
}

if (!path.isAbsolute(tsconfigPath)) {
throw new Error('The tsconfig path must be absolute');
}

if (isWindows) {
tsconfigPath = slash(tsconfigPath);
}

const projectDirectory = path.dirname(tsconfigPath);
const {
files, include, exclude, compilerOptions,
} = config;
const filesList = files?.map(file => pathJoin(projectDirectory, file));
const extensions = getSupportedExtensions(compilerOptions);
const regexpFlags = caseSensitivePaths ? '' : 'i';

/**
* Match entire directory for `exclude`
* https://github.com/microsoft/TypeScript/blob/acf854b636e0b8e5a12c3f9951d4edfa0fa73bcd/src/compiler/utilities.ts#L8135
*/
const excludeSpec = exclude || getDefaultExcludeSpec(compilerOptions);
const excludePatterns = excludeSpec
.map((filePath) => {
const projectFilePath = pathJoin(projectDirectory, filePath);
const projectFilePathPattern = escapeForRegexp(projectFilePath)

// Replace **/
.replace(/\\\*\\\*\//g, '(.+/)?')

// Replace *
.replace(/\\\*/g, `${anyCharacter}*`)

// Replace ?
.replace(/\\\?/g, anyCharacter);

return new RegExp(
`^${projectFilePathPattern}($|/)`,
regexpFlags,
);
});

// https://github.com/microsoft/TypeScript/blob/acf854b636e0b8e5a12c3f9951d4edfa0fa73bcd/src/compiler/commandLineParser.ts#LL3020C29-L3020C47
const includeSpec = !(files || include) ? [matchAllGlob] : include;
const includePatterns = includeSpec
? includeSpec.map((filePath) => {
let projectFilePath = pathJoin(projectDirectory, filePath);

// https://github.com/microsoft/TypeScript/blob/acf854b636e0b8e5a12c3f9951d4edfa0fa73bcd/src/compiler/utilities.ts#L8178
if (isImplicitGlobPattern.test(projectFilePath)) {
projectFilePath = pathJoin(projectFilePath, matchAllGlob);
}

const projectFilePathPattern = escapeForRegexp(projectFilePath)

// Replace /**
.replace(/\/\\\*\\\*/g, `(/${implicitExcludePathRegexPattern}${noPeriodOrSlash}${anyCharacter}*)*?`)

// Replace *
.replace(/(\/)?\\\*/g, (_, hasSlash) => {
const pattern = `(${noPeriodOrSlash}|(\\.(?!min\\.js$))?)*`;
if (hasSlash) {
return `/${implicitExcludePathRegexPattern}${noPeriodOrSlash}${pattern}`;
}

return pattern;
})

// Replace ?
.replace(/(\/)?\\\?/g, (_, hasSlash) => {
const pattern = anyCharacter;
if (hasSlash) {
return `/${implicitExcludePathRegexPattern}${pattern}`;
}

return pattern;
});

return new RegExp(
`^${projectFilePathPattern}$`,
regexpFlags,
);
})
: undefined;

return (
filePath: string,
) => {
if (!path.isAbsolute(filePath)) {
throw new Error('filePath must be absolute');
}

if (isWindows) {
filePath = slash(filePath);
}

if (filesList?.includes(filePath)) {
return config;
}

if (
// Invalid extension (case sensitive)
!extensions.some(extension => filePath.endsWith(extension))

// Matches exclude
|| excludePatterns.some(pattern => pattern.test(filePath))
) {
return;
}

if (
includePatterns
&& includePatterns.some(pattern => pattern.test(filePath))
) {
return config;
}
};
};
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './types.js';
export * from './get-tsconfig.js';
export * from './parse-tsconfig/index.js';
export * from './paths-matcher/index.js';
export * from './files-matcher.js';
1 change: 1 addition & 0 deletions tests/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ describe('get-tsconfig', ({ runTestSuite }) => {
runTestSuite(import('./specs/get-tsconfig.js'));
runTestSuite(import('./specs/parse-tsconfig/index.js'));
runTestSuite(import('./specs/create-paths-matcher.js'));
runTestSuite(import('./specs/create-files-matcher.js'));
});
Loading

0 comments on commit dc97538

Please sign in to comment.