diff --git a/.github/workflows/on-push-to-main.yml b/.github/workflows/on-push-to-main.yml index 43a6f405..718f6799 100644 --- a/.github/workflows/on-push-to-main.yml +++ b/.github/workflows/on-push-to-main.yml @@ -61,6 +61,7 @@ jobs: with: title: 'chore: version packages' commit: 'chore: version packages' + cwd: "./dev" publish: pnpm changeset:release version: pnpm changeset:version env: diff --git a/.changeset/README.md b/dev/.changeset/README.md similarity index 100% rename from .changeset/README.md rename to dev/.changeset/README.md diff --git a/.changeset/config.json b/dev/.changeset/config.json similarity index 100% rename from .changeset/config.json rename to dev/.changeset/config.json diff --git a/dev/.changeset/tidy-drinks-glow.md b/dev/.changeset/tidy-drinks-glow.md new file mode 100644 index 00000000..56e55de7 --- /dev/null +++ b/dev/.changeset/tidy-drinks-glow.md @@ -0,0 +1,5 @@ +--- +"abitype": patch +--- + +Moves configuration files into dedicated `dev` folder diff --git a/rome.json b/dev/configs/rome.json similarity index 100% rename from rome.json rename to dev/configs/rome.json diff --git a/tsconfig.base.json b/dev/configs/tsconfig.base.json similarity index 100% rename from tsconfig.base.json rename to dev/configs/tsconfig.base.json diff --git a/tsconfig.build.json b/dev/configs/tsconfig.build.json similarity index 57% rename from tsconfig.build.json rename to dev/configs/tsconfig.build.json index 76907b36..0cc535d0 100644 --- a/tsconfig.build.json +++ b/dev/configs/tsconfig.build.json @@ -1,15 +1,15 @@ { // This file is used to compile the for cjs and esm (see package.json build scripts). It should exclude all test files. "extends": "./tsconfig.base.json", - "include": ["src"], + "include": ["../../src"], "exclude": [ - "src/**/*.test.ts", - "src/**/*.test-d.ts", - "src/**/*.bench.ts", - "src/_test" + "../../src/**/*.test.ts", + "../../src/**/*.test-d.ts", + "../../src/**/*.bench.ts", + "../../src/_test" ], "compilerOptions": { "sourceMap": true, - "rootDir": "./src" + "rootDir": "../../src" } } diff --git a/tsconfig.node.json b/dev/configs/tsconfig.node.json similarity index 66% rename from tsconfig.node.json rename to dev/configs/tsconfig.node.json index a46e511f..b01afd3b 100644 --- a/tsconfig.node.json +++ b/dev/configs/tsconfig.node.json @@ -1,11 +1,12 @@ { // This configuration is used for local development and type checking of configuration and script files that are not part of the build. - "include": ["vite.config.ts", "scripts"], + "include": ["vite.config.ts", "../scripts/**.ts", "../utils/**.ts"], "compilerOptions": { "strict": true, "composite": true, "module": "ESNext", "moduleResolution": "Node", - "allowSyntheticDefaultImports": true + "allowSyntheticDefaultImports": true, + "rootDir": "../" } } diff --git a/vitest.config.ts b/dev/configs/vitest.config.ts similarity index 90% rename from vitest.config.ts rename to dev/configs/vitest.config.ts index 07e8bd36..cdec24ac 100644 --- a/vitest.config.ts +++ b/dev/configs/vitest.config.ts @@ -12,6 +12,6 @@ export default defineConfig({ }, environment: 'node', include: ['src/**/*.test.ts'], - setupFiles: ['./src/_test/setup.ts'], + setupFiles: ['src/_test/setup.ts'], }, }) diff --git a/dev/scripts/changeset.ts b/dev/scripts/changeset.ts new file mode 100644 index 00000000..79582631 --- /dev/null +++ b/dev/scripts/changeset.ts @@ -0,0 +1,14 @@ +import { repoDirs } from '../utils/path.js' +import { fromHere, shell } from '../utils/utils.js' + +shell( + `node ${fromHere( + '..', + '..', + 'node_modules', + '@changesets', + 'cli', + 'bin.js', + )} ${process.argv.slice(2).join(' ')}`, + { cwd: repoDirs.dev }, +) diff --git a/scripts/prepublishOnly.ts b/dev/scripts/prepublishOnly.ts similarity index 94% rename from scripts/prepublishOnly.ts rename to dev/scripts/prepublishOnly.ts index 52f383bb..fb5c4bcd 100644 --- a/scripts/prepublishOnly.ts +++ b/dev/scripts/prepublishOnly.ts @@ -5,7 +5,7 @@ generatePackageJson() // Generates a package.json to be published to NPM with only the necessary fields. function generatePackageJson() { - const packageJsonPath = path.join(__dirname, '../package.json') + const packageJsonPath = path.join(__dirname, '../../package.json') const tmpPackageJson = readJsonSync(packageJsonPath) writeJsonSync(`${packageJsonPath}.tmp`, tmpPackageJson, { spaces: 2 }) diff --git a/scripts/updateVersion.ts b/dev/scripts/updateVersion.ts similarity index 67% rename from scripts/updateVersion.ts rename to dev/scripts/updateVersion.ts index 4b0da0b2..ea44d340 100644 --- a/scripts/updateVersion.ts +++ b/dev/scripts/updateVersion.ts @@ -2,8 +2,8 @@ import { readJsonSync, writeFileSync } from 'fs-extra' import path from 'path' // Writes the current package.json version to `./src/version.ts`. -const versionFilePath = path.join(__dirname, '../src/version.ts') -const packageJsonPath = path.join(__dirname, '../package.json') +const versionFilePath = path.join(__dirname, '../../src/version.ts') +const packageJsonPath = path.join(__dirname, '../../package.json') const packageVersion = readJsonSync(packageJsonPath).version writeFileSync(versionFilePath, `export const version = '${packageVersion}'\n`) diff --git a/dev/utils/caller.ts b/dev/utils/caller.ts new file mode 100644 index 00000000..136c3b4a --- /dev/null +++ b/dev/utils/caller.ts @@ -0,0 +1,103 @@ +// Copied from arktype https://github.com/arktypeio/arktype/tree/main/dev/attest/src + +import { getCurrentLine, getFramesFromError } from './getCurrentLine.js' +import path from 'node:path' +import * as process from 'node:process' +import { fileURLToPath } from 'node:url' +import { isDeepStrictEqual } from 'node:util' + +export type GetCallStackOptions = { + offset?: number +} + +export const getCallStack = ({ offset = 0 }: GetCallStackOptions = {}) => { + const frames = getFramesFromError(new Error()) + frames.splice(1, 1 + offset) + return frames +} + +export type LinePosition = { + line: number + char: number +} + +export type SourcePosition = LinePosition & { + file: string + method: string +} + +export type CallerOfOptions = { + formatPath?: FormatFilePathOptions + upStackBy?: number + skip?: (position: SourcePosition) => boolean + methodName?: string +} + +const nonexistentCurrentLine = { + line: -1, + char: -1, + method: '', + file: '', +} + +export type FormatFilePathOptions = { + relative?: string | boolean + seperator?: string +} + +export const formatFilePath = ( + original: string, + { relative, seperator }: FormatFilePathOptions, +) => { + let formatted = original + if (original.startsWith('file:///')) { + formatted = fileURLToPath(original) + } + if (relative) { + formatted = path.relative( + typeof relative === 'string' ? relative : process.cwd(), + formatted, + ) + } + if (seperator) { + formatted = formatted.replace(new RegExp(`\\${path.sep}`, 'g'), seperator) + } + return formatted +} + +export const caller = (options: CallerOfOptions = {}): SourcePosition => { + let upStackBy = options.upStackBy ?? 0 + if (!options.methodName) { + upStackBy = 3 + } + let match: SourcePosition | undefined + while (!match) { + const location = getCurrentLine({ + method: options.methodName as string, + frames: upStackBy, + }) + + if (!location || isDeepStrictEqual(location, nonexistentCurrentLine)) { + throw new Error( + `No caller of '${ + options.methodName + }' matches given options: ${JSON.stringify(options, null, 4)}.`, + ) + } + const candidate = { + ...location, + file: formatFilePath(location.file, options.formatPath ?? {}), + } + if (options.skip?.(candidate)) { + upStackBy++ + } else { + match = candidate + } + } + return match +} + +export const callsAgo = ( + num: number, + options: Omit = {}, +) => caller({ methodName: 'callsAgo', upStackBy: num, ...options }) diff --git a/dev/utils/getCurrentLine.ts b/dev/utils/getCurrentLine.ts new file mode 100644 index 00000000..58939315 --- /dev/null +++ b/dev/utils/getCurrentLine.ts @@ -0,0 +1,261 @@ +// Copied from unmaintained package https://github.com/bevry/get-current-line to fix imports + +/** The combination of location information about the line that was executing at the time */ +export type Location = { + /** the location of the line that was executing at the time */ + line: number + /** the location of the character that was executing at the time */ + char: number + /** the method name that was executing at the time */ + method: string + /** the file path that was executing at the time */ + file: string +} + +/** + * If provided, continue skipping until: + * + * 1. The file or method is found + * 2. Once found, will continue until neither the file nor method are found anymore + * 3. Once exited, the frame offset will then apply + * + * If you wish to capture the found method or the file, combine them with `frames: -1` or `immediate: true`. + * + * If you wish for more customisation than this, create an issue requesting passing a custom skip handler function, as more variance to this interface is too much customisation complexity. + */ +type Offset = { + /** + * if provided, continue until a method containing or matching this string is exited + * if provided alongside a file, will continue until neither the file nor method are found + * this allows file and method to act as fallbacks for each other, such that if one is not found, it doesn't skip everything + */ + method?: RegExp | string | null + /** + * if provided, continue until a file containing or matching this string is exited + * if provided alongside a method, will continue until neither the file nor method are found + * this allows file and method to act as fallbacks for each other, such that if one is not found, it doesn't skip everything + */ + file?: RegExp | string | null + /** + * once we have satisfied the found condition, if any, then apply this index offset to the frames + * e.g. 1 would mean next frame, and -1 would mean the previous frame + * Use -1 to go back to the found method or file + */ + frames?: number + /** + * once we have satisfied the found condition, should we apply the frame offset immediately, or wait until the found condition has exited + */ + immediate?: boolean +} + +/** + * For an error instance, return its stack frames as an array. + */ +export const getFramesFromError = (error: Error): string[] => { + // Create an error + let stack: Error['stack'] | null + let frames: any[] + + // And attempt to retrieve it's stack + // https://github.com/winstonjs/winston/issues/401#issuecomment-61913086 + try { + stack = error.stack + } catch (_error1) { + try { + // @ts-ignore + const previous = err.__previous__ || err.__previous + // rome-ignore lint/complexity/useOptionalChain: + stack = previous && previous.stack + } catch (_error2) { + stack = null + } + } + + // Handle different stack formats + if (stack) { + if (Array.isArray(stack)) { + frames = Array(stack) + } else { + frames = stack.toString().split('\n') + } + } else { + frames = [] + } + + // Parse our frames + return frames +} + +const lineRegex = + /\s+at\s(?:(?.+?)\s\()?(?.+?):(?\d+):(?\d+)\)?\s*$/ + +const otherRegex = + /(?.+?)@(?.+?):(?\d+):(?\d+)\)?\s*$/ + +/** + * Get the locations from a list of error stack frames. + */ +const getLocationsFromFrames = (frames: string[]): Location[] => { + // Prepare + const locations: Location[] = [] + + // Cycle through the lines + for (const frame of frames) { + // ensure each line is a string + const line = (frame || '').toString() + + // skip empty lines + if (line.length === 0) { + continue + } + + // Error + // at file:///Users/balupton/Projects/active/get-current-line/asd.js:1:13 + // at ModuleJob.run (internal/modules/esm/module_job.js:140:23) + // at async Loader.import (internal/modules/esm/loader.js:165:24) + // at async Object.loadESM (internal/process/esm_loader.js:68:5) + const match = line.match(lineRegex) + + if (match?.groups) { + locations.push({ + method: match.groups.method || '', + file: match.groups.file || '', + line: Number(match.groups.line), + char: Number(match.groups.char), + }) + + continue + } + + // Node can also have a error pattern of method@filepath:line:char + const otherMatch = line.match(otherRegex) + + if (otherMatch?.groups) { + locations.push({ + method: otherMatch.groups.method || '', + file: otherMatch.groups.file || '', + line: Number(otherMatch.groups.line), + char: Number(otherMatch.groups.char), + }) + } + } + + return locations +} + +/** + * If a location is not found, this is the result that is used. + */ +const failureLocation: Location = { + line: -1, + char: -1, + method: '', + file: '', +} + +/** + * From a list of locations, get the location that is determined by the offset. + * If none are found, return the failure location + */ +const getLocationWithOffset = (locations: Location[], offset: Offset) => { + // Continue + let found: boolean = !offset.file && !offset.method + + // use while loop so we can skip ahead + let i = 0 + while (i < locations.length) { + const location = locations[i] + + // the current location matches the offset + if ( + (offset.file && + (typeof offset.file === 'string' + ? location.file.includes(offset.file) + : offset.file.test(location.file))) || + (offset.method && + (typeof offset.method === 'string' + ? location.method.includes(offset.method) + : offset.method.test(location.method))) + ) { + // we are found, and we should exit immediately, so return with the frame offset applied + if (offset.immediate) { + // apply frame offset + i += offset.frames || 0 + // and return the result + return locations[i] + } + // otherwise, continue until the found condition has exited + else { + found = true + ++i + // rome-ignore lint/correctness/noUnnecessaryContinue: + continue + } + } + // has been found, and the found condition has exited, so return with the frame offset applied + else if (found) { + // apply frame offset + i += offset.frames || 0 + // and return the result + return locations[i] + } + // nothing has been found yet, so continue until we find the offset + else { + ++i + // rome-ignore lint/correctness/noUnnecessaryContinue: + continue + } + } + + // return failure + return failureLocation +} + +/** + * Get each error stack frame's location information. + */ +const getLocationsFromError = (error: Error): Location[] => { + const frames = getFramesFromError(error) + return getLocationsFromFrames(frames) +} + +/** + * Get first determined location information that appears in the stack of the error. + * If no offset is provided, then the offset used will determine the first location information. + */ +const getLocationFromError = ( + error: Error, + offset: Offset = { + immediate: true, + }, +): Location => { + const locations = getLocationsFromError(error) + return getLocationWithOffset(locations, offset) +} + +/** + * Get the location information about the line that called this method. + * If no offset is provided, then continue until the caller of the `getCurrentLine` is found. + * @example Input + * ``` javascript + * console.log(getCurrentLine()) + * ``` + * @example Result + * ``` json + * { + * "line": "1", + * "char": "12", + * "method": "Object.", + * "file": "/Users/balupton/some-project/calling-file.js" + * } + * ``` + */ +export const getCurrentLine = ( + offset: Offset = { + method: 'getCurrentLine', + frames: 0, + immediate: false, + }, +): Location => { + return getLocationFromError(new Error(), offset) +} diff --git a/dev/utils/path.ts b/dev/utils/path.ts new file mode 100644 index 00000000..6ebe3065 --- /dev/null +++ b/dev/utils/path.ts @@ -0,0 +1,16 @@ +import { fromHere } from './utils' +import { join } from 'node:path' + +const root = fromHere('..', '..') +const dev = join(root, 'dev') +const configs = join(dev, 'configs') +const srcRoot = join(root, 'src') +const outRoot = join(root, 'dist') + +export const repoDirs = { + root, + dev, + configs, + srcRoot, + outRoot, +} diff --git a/dev/utils/utils.ts b/dev/utils/utils.ts new file mode 100644 index 00000000..7af7a341 --- /dev/null +++ b/dev/utils/utils.ts @@ -0,0 +1,47 @@ +// Copied from arktype https://github.com/arktypeio/arktype/tree/main/dev/attest/src + +import { caller } from './caller.js' +import { execSync } from 'node:child_process' +import { dirname, join } from 'node:path' +import { URL, fileURLToPath } from 'node:url' + +export type Prettify = { + [K in keyof T]: T[K] +} & {} + +export type ShellOptions = Prettify< + Parameters[1] & { + env?: Record + stdio?: 'pipe' | 'inherit' + returnOutput?: boolean + } +> + +const dirOfCaller = () => + dirname(filePath(caller({ methodName: 'dirOfCaller', upStackBy: 1 }).file)) + +export const filePath = (path: string) => { + let file + if (path.includes('://')) { + // is a url, e.g. file://, or https:// + const url = new URL(path) + file = url.protocol === 'file:' ? fileURLToPath(url) : url.href + } else { + // is already a typical path + file = path + } + return file +} + +export const shell = ( + cmd: string, + { returnOutput, env, ...otherOptions }: ShellOptions = {}, +): string => + execSync(cmd, { + stdio: returnOutput ? 'pipe' : 'inherit', + env: { ...process.env, ...env }, + ...otherOptions, + })?.toString() ?? '' + +export const fromHere = (...joinWith: string[]) => + join(dirOfCaller(), ...joinWith) diff --git a/examples/tsconfig.json b/examples/tsconfig.json index 5abe1211..4df92174 100644 --- a/examples/tsconfig.json +++ b/examples/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../tsconfig.base.json", + "extends": "../dev/configs/tsconfig.base.json", "compilerOptions": { "moduleResolution": "bundler", "baseUrl": ".", diff --git a/package.json b/package.json index d00fef34..0b50a31a 100644 --- a/package.json +++ b/package.json @@ -3,27 +3,27 @@ "description": "Strict TypeScript types for Ethereum ABIs", "version": "0.8.3", "scripts": { - "bench": "vitest bench", + "bench": "vitest bench --config dev/configs/vitest.config.ts", "build": "pnpm run clean && pnpm run build:cjs && pnpm run build:esm && pnpm run build:types", - "build:cjs": "tsc --project tsconfig.build.json --module commonjs --outDir ./dist/cjs --removeComments --verbatimModuleSyntax false && echo > ./dist/cjs/package.json '{\"type\":\"commonjs\"}'", - "build:esm": "tsc --project tsconfig.build.json --module es2015 --outDir ./dist/esm --removeComments && echo > ./dist/esm/package.json '{\"type\":\"module\"}'", - "build:types": "tsc --project tsconfig.build.json --module esnext --declarationDir ./dist/types --emitDeclarationOnly --declaration --declarationMap", - "changeset": "changeset", - "changeset:release": "pnpm build && changeset publish", - "changeset:version": "changeset version && pnpm install --lockfile-only && pnpm bun scripts/updateVersion.ts", + "build:cjs": "tsc --project dev/configs/tsconfig.build.json --module commonjs --outDir ./dist/cjs --removeComments --verbatimModuleSyntax false && echo > ./dist/cjs/package.json '{\"type\":\"commonjs\"}'", + "build:esm": "tsc --project dev/configs/tsconfig.build.json --module es2015 --outDir ./dist/esm --removeComments && echo > ./dist/esm/package.json '{\"type\":\"module\"}'", + "build:types": "tsc --project dev/configs/tsconfig.build.json --module esnext --declarationDir ./dist/types --emitDeclarationOnly --declaration --declarationMap", + "changeset": "pnpm bun dev/scripts/changeset.ts", + "changeset:release": "pnpm build && pnpm bun dev/scripts/changeset.ts publish", + "changeset:version": "pnpm bun dev/scripts/changeset.ts version && pnpm install --lockfile-only && pnpm bun dev/scripts/updateVersion.ts", "clean": "rimraf dist *.tsbuildinfo", "docs:dev": "pnpm -r --filter docs dev", "docs:build": "pnpm -r --filter docs build", "docs:preview": "pnpm -r --filter docs preview", - "format": "rome format . --write", - "lint": "rome check .", + "format": "rome format . --write --config-path dev/configs/", + "lint": "rome check . --config-path dev/configs/", "lint:fix": "pnpm lint --apply", "preinstall": "npx only-allow pnpm", "prepare": "npx simple-git-hooks", - "prepublishOnly": "pnpm bun scripts/prepublishOnly.ts", - "test": "vitest", - "test:cov": "vitest run --coverage", - "test:typecheck": "vitest typecheck", + "prepublishOnly": "pnpm bun dev/scripts/prepublishOnly.ts", + "test": "vitest --config dev/configs/vitest.config.ts", + "test:cov": "vitest run --coverage --config dev/configs/vitest.config.ts", + "test:typecheck": "vitest typecheck --config dev/configs/vitest.config.ts", "test:update": "vitest --update", "trace": "tsc --noEmit --generateTrace ./trace --incremental false --project playground/tsconfig.json", "typecheck": "tsc --noEmit" diff --git a/playground/tsconfig.json b/playground/tsconfig.json index 127cf4f9..9b905c25 100644 --- a/playground/tsconfig.json +++ b/playground/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../tsconfig.base.json", + "extends": "../dev/configs/tsconfig.base.json", "compilerOptions": { "moduleResolution": "bundler", "baseUrl": ".", diff --git a/tsconfig.json b/tsconfig.json index 22b9d9d0..59d6f5d2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { // This configuration is used for local development and type checking. - "extends": "./tsconfig.base.json", + "extends": "./dev/configs/tsconfig.base.json", "include": ["src"], "exclude": [], - "references": [{ "path": "./tsconfig.node.json" }], + "references": [{ "path": "dev/configs/tsconfig.node.json" }], }