3 Commits
11 changed files with 84 additions and 77 deletions

No files matched your search

+3 -16
View File
@@ -1,16 +1,8 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
coverage
node_modules node_modules
dist dist
dist-ssr coverage
*.log
*.tsbuildinfo
*.local *.local
# Editor directories and files # Editor directories and files
@@ -20,8 +12,3 @@ dist-ssr
!.vscode/tasks.json !.vscode/tasks.json
.idea .idea
.DS_Store .DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
-1
View File
@@ -10,5 +10,4 @@ src/
.node-version .node-version
cspell.json cspell.json
lefthook.yml lefthook.yml
LICENSE
commit-message-template commit-message-template
+8 -1
View File
@@ -13,7 +13,14 @@
"eslint/no-undefined": "off", "eslint/no-undefined": "off",
"eslint/sort-keys": "off", "eslint/sort-keys": "off",
"eslint/id-length": "off", "eslint/id-length": "off",
"import/no-named-export": "off" "import/no-named-export": "off",
"eslint/one-var": "off",
"import/group-exports": "off",
"import/exports-last": "off",
"eslint/sort-imports": "off",
"import/consistent-type-specifier-style": "off",
"unicorn/prefer-export-from": "off",
"typescript/method-signature-style": "off"
}, },
"env": { "env": {
"builtin": true, "builtin": true,
+2 -2
View File
@@ -21,12 +21,12 @@ Pattern matching for TypeScript/ESM environments (F#-style, not regex).
### Requirements ### Requirements
- Node.js >= 22.6 (>= 24 recommended; `--experimental-strip-types` is unflagged on 24). - Node.js >= 26 (engines field; pinned via `.node-version`).
## VSCode integration ## VSCode integration
- Recommended extensions: see `.vscode/extensions.json` (oxc, cspell). - Recommended extensions: see `.vscode/extensions.json` (oxc, cspell).
- TypeScript 7 is used via `typescript.tsdk` pointing at `node_modules/typescript/lib`. - TypeScript 7 is used via the `typescriptteam.native-preview` extension.
- oxc extension provides oxlint squiggles and oxfmt format-on-save. - oxc extension provides oxlint squiggles and oxfmt format-on-save.
## Workflows ## Workflows
+1 -8
View File
@@ -17,12 +17,5 @@
"dbaeumer", "dbaeumer",
"msvc" "msvc"
], ],
"ignorePaths": [ "ignorePaths": ["dist", "node_modules", "coverage", "*.svg", ".gitignore"]
"dist",
"node_modules",
"public",
"coverage",
"*.svg",
".gitignore"
]
} }
+3 -3
View File
@@ -5,11 +5,11 @@ pre-commit:
commands: commands:
oxlint: oxlint:
glob: "*.{ts,tsx,js,jsx,mjs,cjs}" glob: "*.{ts,tsx,js,jsx,mjs,cjs}"
run: sh -c 'LEFTHOOK_FILES="$0" npm run check:oxlint' {staged_files} run: sh -c 'LEFTHOOK_FILES="$*" npm run check:oxlint' sh {staged_files}
oxfmt: oxfmt:
glob: "*.{ts,tsx,js,jsx,mjs,cjs,json,jsonc,yaml,yml,md,mdx}" glob: "*.{ts,tsx,js,jsx,mjs,cjs,json,jsonc,yaml,yml,md,mdx}"
run: sh -c 'LEFTHOOK_FILES="$0" npm run check:oxfmt' {staged_files} run: sh -c 'LEFTHOOK_FILES="$*" npm run check:oxfmt' sh {staged_files}
cspell: cspell:
run: sh -c 'LEFTHOOK_FILES="$0" npm run check:cspell' {staged_files} run: sh -c 'LEFTHOOK_FILES="$*" npm run check:cspell' sh {staged_files}
typecheck: typecheck:
run: npm run check:tsc run: npm run check:tsc
+19 -6
View File
@@ -27,6 +27,22 @@ typescript-lib-starter-tiny => https://github.com/tmueller/typescript-lib-starte
- `eslint/sort-keys` — handler/case order is semantic, not alphabetical. - `eslint/sort-keys` — handler/case order is semantic, not alphabetical.
- `eslint/id-length` — `T`, `R`, `U`, `V` are standard TS generics. - `eslint/id-length` — `T`, `R`, `U`, `V` are standard TS generics.
- `import/no-named-export` — false positive on library entry re-exports. - `import/no-named-export` — false positive on library entry re-exports.
- Stylistic rules superseded by oxfmt (oxfmt is the canonical
formatter; these rules either conflict with its output or
duplicate features oxfmt already provides):
- `eslint/one-var` — oxfmt uses comma-joined `const`
declarations; oxlint wanted to split them.
- `import/group-exports` — oxfmt keeps separate `export`
statements as-is.
- `import/exports-last` — statement ordering is up to oxfmt.
- `eslint/sort-imports` — replaced by oxfmt's built-in
`sortImports` (enabled in `.oxfmtrc.json`).
- `import/consistent-type-specifier-style` — inline
`import { type X, Y }` is intentional for grouping.
- `unicorn/prefer-export-from` — conflicts with how the
barrel `src/index.ts` re-exports through `src/match.ts`.
- `typescript/method-signature-style` — method signatures
in `interface` are conventional TS ergonomics.
For `*.test.ts` files additionally: `no-unused-expressions` (for For `*.test.ts` files additionally: `no-unused-expressions` (for
`expectTypeOf(...)` calls), `no-empty-file` (we have a single import `expectTypeOf(...)` calls), `no-empty-file` (we have a single import
per test file in some cases), `import/no-nodejs-modules` (we use per test file in some cases), `import/no-nodejs-modules` (we use
@@ -186,7 +202,7 @@ typescript-lib-starter-tiny => https://github.com/tmueller/typescript-lib-starte
alphabetized (replaces the former `sort-package-json` tool). alphabetized (replaces the former `sort-package-json` tool).
Built-in import sorting (enabled via `sortImports: true`) replaces Built-in import sorting (enabled via `sortImports: true`) replaces
any external import-sort plugin. any external import-sort plugin.
- `check:tsc`: `tsc --noEmit`. - `check:tsc`: `tsc` (noEmit is set in `tsconfig.json`).
- `check:cspell`: `cspell lint ${LEFTHOOK_FILES:-.}` — walks the - `check:cspell`: `cspell lint ${LEFTHOOK_FILES:-.}` — walks the
project root by default; from lefthook, only the staged files. project root by default; from lefthook, only the staged files.
- `check:outdated`: `check-outdated --ignore-pre-releases --ignore-packages @oxfmt/binding-*,@oxlint/binding-*`. The oxc native bindings are declared as `optionalDependencies` so the correct one is selected per platform automatically; the `*`-platform bindings show as "not installed" on the current platform and are explicitly ignored here. - `check:outdated`: `check-outdated --ignore-pre-releases --ignore-packages @oxfmt/binding-*,@oxlint/binding-*`. The oxc native bindings are declared as `optionalDependencies` so the correct one is selected per platform automatically; the `*`-platform bindings show as "not installed" on the current platform and are explicitly ignored here.
@@ -403,8 +419,6 @@ Resolves #...
```json ```json
{ {
"typescript.tsdk": "node_modules/typescript/lib",
"js/ts.tsdk.path": "node_modules/typescript/lib",
"[typescript]": { "[typescript]": {
"editor.defaultFormatter": "oxc.oxc-vscode", "editor.defaultFormatter": "oxc.oxc-vscode",
"editor.formatOnSave": true "editor.formatOnSave": true
@@ -458,6 +472,5 @@ Resolves #...
(build, test, lint, typecheck, format, spell, check:outdated) are (build, test, lint, typecheck, format, spell, check:outdated) are
run via the npm scripts in `package.json` from the integrated run via the npm scripts in `package.json` from the integrated
terminal. terminal.
- VSCode uses the workspace TypeScript version via - VSCode uses TypeScript 7 via the `typescriptteam.native-preview`
`typescript.tsdk` (and the explicit `js/ts.tsdk.path`), with extension, with `oxc.oxc-vscode` for formatting and linting.
`oxc.oxc-vscode` for formatting and linting.
+1 -1
View File
@@ -3,7 +3,7 @@ import { test } from "node:test";
import { expectTypeOf } from "expect-type"; import { expectTypeOf } from "expect-type";
import { match, P, type Matcher } from "./index.ts"; import { type Matcher, P, match } from "./index.ts";
test("match returns a builder", () => { test("match returns a builder", () => {
const builder = match("x"); const builder = match("x");
+1 -3
View File
@@ -1,8 +1,6 @@
import { P, type Matcher, type Pattern } from "./pattern.ts"; import { P, type Matcher, type Pattern } from "./pattern.ts";
type Cases<R> = ReadonlyArray< type Cases<R> = readonly (readonly [Matcher<unknown>, (value: unknown) => R])[];
readonly [Matcher<unknown>, (value: unknown) => R]
>;
interface MatchBuilder<T, R> { interface MatchBuilder<T, R> {
with<U extends T, V>( with<U extends T, V>(
+45 -34
View File
@@ -14,7 +14,6 @@ const literalMatcher = <
): Matcher<L> => ({ ): Matcher<L> => ({
matches: (candidate): candidate is L => candidate === value, matches: (candidate): candidate is L => candidate === value,
}); });
const typeMatcher = <T>( const typeMatcher = <T>(
type: type:
| "string" | "string"
@@ -25,58 +24,70 @@ const typeMatcher = <T>(
| "undefined" | "undefined"
| "object" | "object"
| "function", | "function",
): Matcher<T> => { ): Matcher<T> => {
const matches = (value: unknown): value is T => { const matches = (value: unknown): value is T => {
if (type === "undefined") return value === undefined; if (type === "undefined") {
if (type === "object") return value === undefined;
}
if (type === "object") {
return ( return (
(typeof value === "object" && value !== null) || (typeof value === "object" && value !== null) ||
typeof value === "function" typeof value === "function"
); );
}
return typeof value === type; return typeof value === type;
}; };
return { matches }; return { matches };
}; },
whenMatcher = <T>(
const whenMatcher = <T>(
predicate: (value: unknown) => value is T, predicate: (value: unknown) => value is T,
): Matcher<T> => ({ ): Matcher<T> => ({
matches: predicate, matches: predicate,
}); }),
whenMatcherAny = <T>(
const whenMatcherAny = <T>(
predicate: (value: unknown) => boolean, predicate: (value: unknown) => boolean,
): Matcher<T> => ({ ): Matcher<T> => ({
matches: (value: unknown): value is T => predicate(value), matches: (value: unknown): value is T => predicate(value),
}); }),
isNestedMatcher = (expected: unknown): expected is Matcher<unknown> =>
const structuralMatcher = <S extends object, T extends S>(
shape: S,
refine?: (value: S) => value is T,
): Matcher<T> => ({
matches: (value: unknown): value is T => {
if (typeof value !== "object" || value === null) return false;
const candidate = value as S;
for (const key of Object.keys(shape) as Array<keyof S>) {
if (!(key in candidate)) return false;
const expected = shape[key];
const actual = candidate[key];
if (
typeof expected === "object" && typeof expected === "object" &&
expected !== null && expected !== null &&
"matches" in expected "matches" in expected,
) { keysMatch = <S extends object>(shape: S, candidate: object): boolean => {
const nested = expected as unknown as Matcher<unknown>; for (const key of Object.keys(shape) as (keyof S)[]) {
if (!nested.matches(actual)) return false; if (!(key in candidate)) {
return false;
}
const expected = shape[key],
actual = candidate[key as keyof object];
if (isNestedMatcher(expected)) {
if (!expected.matches(actual)) {
return false;
}
} else if (actual !== expected) { } else if (actual !== expected) {
return false; return false;
} }
} }
return refine return true;
? refine(candidate)
: (true as T extends S ? true : never);
}, },
}); structuralMatcher = <S extends object, T extends S>(
shape: S,
refine?: (value: S) => value is T,
): Matcher<T> => ({
matches: (value: unknown): value is T => {
if (typeof value !== "object" || value === null) {
return false;
}
const candidate = value as S;
if (!keysMatch(shape, candidate)) {
return false;
}
if (refine && !refine(candidate)) {
return false;
}
return true;
},
});
export const P = { export const P = {
literal: literalMatcher, literal: literalMatcher,
+1 -2
View File
@@ -3,8 +3,7 @@
"compilerOptions": { "compilerOptions": {
"noEmit": false, "noEmit": false,
"rewriteRelativeImportExtensions": true, "rewriteRelativeImportExtensions": true,
"rootDir": "src", "rootDir": "src"
"outDir": "dist"
}, },
"exclude": ["src/**/*.test.ts", "src/**/__tests__/**"] "exclude": ["src/**/*.test.ts", "src/**/__tests__/**"]
} }