Merge commit '36c66dd290d3ce6eb1ccd310d0c658d4a32bb8eb' as 'repos/effect'

This commit is contained in:
-Puter
2026-07-19 03:25:10 +05:30
parent dd1071cc10
commit 2daf979036
2214 changed files with 673090 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
{
"$schema": "../../../node_modules/@effect/docgen/schema.json",
"exclude": ["**/*.ts"]
}

View File

@@ -0,0 +1,58 @@
{
"name": "@effect/utils",
"version": "0.0.0",
"type": "module",
"private": true,
"license": "MIT",
"description": "Build tools for the repositories following the Effect standard setup",
"homepage": "https://effect.website",
"repository": {
"type": "git",
"url": "https://github.com/Effect-TS/effect.git",
"directory": "packages/tools/utils"
},
"sideEffects": [],
"bin": {
"effect-utils": "./src/bin.ts"
},
"exports": {
"./package.json": "./package.json",
"./*": "./src/*.ts",
"./commands/*": null,
"./bin": null
},
"files": [
"src/**/*.ts",
"dist/**/*.js",
"dist/**/*.js.map",
"dist/**/*.d.ts",
"dist/**/*.d.ts.map"
],
"publishConfig": {
"provenance": true,
"bin": {
"effect-utils": "./dist/bin.js"
},
"exports": {
"./package.json": "./package.json",
"./*": "./dist/*.js",
"./commands/*": null,
"./bin": null
}
},
"scripts": {
"build": "tsc -b tsconfig.json && pnpm babel",
"babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps",
"check": "tsc -b tsconfig.json",
"test": "vitest",
"coverage": "vitest --coverage"
},
"dependencies": {
"@effect/platform-node": "workspace:^",
"effect": "workspace:^",
"glob": "^13.0.6"
},
"devDependencies": {
"@types/node": "^26.1.1"
}
}

View File

@@ -0,0 +1,214 @@
/**
* Barrel file discovery and export generation for Effect development tools.
*
* This module provides the `BarrelGenerator` service used by the codegen CLI to
* find files annotated with `@barrel` comments and rewrite the generated export
* section beneath each annotation. The generator resolves matching modules
* relative to each annotated barrel file, copies each module's top-level
* `@since` tag into a minimal JSDoc block, and normalizes export paths so the
* produced TypeScript is stable across platforms.
*
* @since 4.0.0
*/
import * as Context from "effect/Context"
import * as Data from "effect/Data"
import * as Effect from "effect/Effect"
import * as FileSystem from "effect/FileSystem"
import * as Layer from "effect/Layer"
import * as Path from "effect/Path"
import type { PlatformError } from "effect/PlatformError"
import * as Glob from "./Glob.ts"
/**
* Error raised when barrel export generation cannot read required module metadata.
*
* @category errors
* @since 4.0.0
*/
export class BarrelCodegenError extends Data.TaggedError("BarrelCodegenError")<{
readonly path: string
readonly reason: string
}> {}
const findAnnotation = (content: string): { pattern: string; offset: number } | undefined => {
const lines = content.split("\n")
// Find the line containing the annotation
let annotationLine = -1
let pattern: string | undefined
for (let i = 0; i < lines.length; i++) {
pattern = parseAnnotation(lines[i])
if (pattern !== undefined) {
annotationLine = i
break
}
}
if (annotationLine === -1 || pattern === undefined) {
return undefined
}
// Walk forwards to find the end of the comment block
let commentEnd = annotationLine
while (commentEnd < lines.length - 1 && lines[commentEnd + 1].trimStart().startsWith("//")) {
commentEnd++
}
return { pattern, offset: commentEnd + 1 }
}
const parseAnnotation = (line: string): string | undefined => {
const match = line.match(/^\/\/\s*@barrel(?:\((.+?)\))?/)
if (!match) {
return undefined
}
return match[1] ?? "*.ts"
}
const extractModuleSince = (file: string, content: string): Effect.Effect<string, BarrelCodegenError> => {
const block = content.match(/^\s*(\/\*\*[\s\S]*?\*\/)/)?.[1]
if (block === undefined) {
return Effect.fail(
new BarrelCodegenError({
path: file,
reason: "missing top-level module JSDoc"
})
)
}
const matches = Array.from(block.matchAll(/^\s*\*\s*@since(?:\s+(.*))?$/gm))
if (matches.length !== 1) {
return Effect.fail(
new BarrelCodegenError({
path: file,
reason: matches.length === 0
? "missing top-level module @since tag"
: "multiple top-level module @since tags"
})
)
}
const since = matches[0]?.[1]?.trim() ?? ""
if (since.length === 0) {
return Effect.fail(
new BarrelCodegenError({
path: file,
reason: "empty top-level module @since tag"
})
)
}
return Effect.succeed(since)
}
const renderExportJSDoc = (since: string): string =>
`/**
* @since ${since}
*/`
/**
* Metadata for a barrel file discovered from a `@barrel` annotation, including the file path, glob pattern, and insertion offset for generated exports.
*
* @category models
* @since 4.0.0
*/
export interface BarrelFile {
readonly path: string
readonly pattern: string
readonly offset: number
}
/**
* Service interface for discovering annotated barrel files and regenerating their export contents.
*
* @category models
* @since 4.0.0
*/
export interface BarrelGenerator {
readonly discoverFiles: (
pattern: string,
cwd: string
) => Effect.Effect<Array<BarrelFile>, PlatformError | Glob.GlobError>
readonly processFile: (file: BarrelFile) => Effect.Effect<void, PlatformError | Glob.GlobError | BarrelCodegenError>
}
/**
* Service tag for barrel file generation.
*
* @category services
* @since 4.0.0
*/
export const BarrelGenerator: Context.Service<BarrelGenerator, BarrelGenerator> = Context.Service(
"@effect/utils/BarrelGenerator"
)
/**
* Builds the `BarrelGenerator` service, discovering files with `@barrel` annotations and rewriting their generated export sections from matching modules.
*
* @category layers
* @since 4.0.0
*/
export const layer: Layer.Layer<BarrelGenerator, never, FileSystem.FileSystem | Path.Path | Glob.Glob> = Effect.gen(
function*() {
const fs = yield* FileSystem.FileSystem
const path = yield* Path.Path
const glob = yield* Glob.Glob
// Convert native path separators to POSIX (forward slashes) for import/export statements
const toPosix = path.sep === "/" ? (file: string) => file : (file: string) => file.split(path.sep).join("/")
const fileToModuleName = (posix: string): string => {
const withoutExt = posix.slice(0, -path.extname(posix).length)
return withoutExt.replace(/\//g, "_")
}
const processModule = Effect.fn("processModule")(function*(directory: string, file: string) {
const fullPath = path.join(directory, file)
const posixPath = toPosix(file)
const content = yield* fs.readFileString(fullPath)
const since = yield* extractModuleSince(fullPath, content)
const moduleName = fileToModuleName(posixPath)
return `${renderExportJSDoc(since)}\nexport * as ${moduleName} from "./${posixPath}"`
})
const discoverFile = Effect.fn("discoverFile")(function*(file: string) {
const content = yield* fs.readFileString(file)
const parsed = findAnnotation(content)
return parsed ? { path: file, ...parsed } : undefined
})
const discoverFiles = Effect.fn("discoverFiles")(function*(pattern: string, cwd: string) {
const indexFiles = yield* glob.glob(pattern, {
cwd,
dot: false,
follow: false,
nodir: true
})
const results = yield* Effect.forEach(indexFiles, (file) => discoverFile(path.join(cwd, file)))
return results.filter((file) => file !== undefined)
})
const processFile = Effect.fn("processFile")(function*(file: BarrelFile) {
const { offset, pattern } = file
const directory = path.dirname(file.path)
const self = path.basename(file.path)
// Find all matching files relative to the current directory excluding the barrel file itself
const matchedFiles = yield* glob.glob(pattern, {
cwd: directory,
dot: false,
follow: false,
nodir: true,
ignore: [self]
}).pipe(Effect.map((files) => files.sort((a, b) => a.localeCompare(b))))
const moduleContents = yield* Effect.forEach(matchedFiles, (file) => processModule(directory, file))
const content = yield* fs.readFileString(file.path)
const lines = content.split("\n")
const header = lines.slice(0, offset).join("\n")
const generated = moduleContents.join("\n\n")
yield* fs.writeFileString(file.path, `${header}\n\n${generated}\n`)
})
return { discoverFiles, processFile }
}
).pipe(Layer.effect(BarrelGenerator), Layer.provide(Glob.layer))

View File

@@ -0,0 +1,61 @@
/**
* Provides the glob service used by tooling commands to discover files from
* glob patterns while keeping filesystem matching inside Effect.
*
* The service wraps the `glob` package and converts matching failures into
* `GlobError` values so command implementations can compose file discovery
* with other typed Effect workflows.
*
* @since 4.0.0
*/
import * as Context from "effect/Context"
import * as Data from "effect/Data"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import * as GlobLib from "glob"
/**
* Error raised when glob pattern matching fails.
*
* @category errors
* @since 4.0.0
*/
export class GlobError extends Data.TaggedError("GlobError")<{
readonly pattern: string | ReadonlyArray<string>
readonly cause: unknown
}> {}
/**
* Service interface for matching filesystem paths with glob patterns.
*
* @category models
* @since 4.0.0
*/
export interface Glob {
readonly glob: (
pattern: string | ReadonlyArray<string>,
options?: GlobLib.GlobOptions
) => Effect.Effect<Array<string>, GlobError>
}
/**
* Service tag for filesystem glob pattern matching.
*
* @category services
* @since 4.0.0
*/
export const Glob: Context.Service<Glob, Glob> = Context.Service("@effect/utils/Glob")
/**
* Layer that provides the `Glob` service using the `glob` package and maps matching failures to `GlobError`.
*
* @category layers
* @since 4.0.0
*/
export const layer: Layer.Layer<Glob> = Layer.succeed(Glob, {
glob: (pattern, options) =>
Effect.tryPromise({
try: () => GlobLib.glob(pattern as string | Array<string>, options ?? {}) as Promise<Array<string>>,
catch: (cause) => new GlobError({ pattern, cause })
})
})

View File

@@ -0,0 +1,16 @@
#!/usr/bin/env node
import * as NodeServices from "@effect/platform-node/NodeServices"
import * as Effect from "effect/Effect"
import * as Command from "effect/unstable/cli/Command"
import { codegen } from "./commands/codegen.ts"
const cli = Command.make("effect-utils").pipe(
Command.withSubcommands([codegen])
)
const main = Command.run(cli, { version: "0.0.0" }).pipe(
Effect.provide(NodeServices.layer)
)
Effect.runPromise(main)

View File

@@ -0,0 +1,44 @@
/**
* Defines the `codegen` CLI command for regenerating annotated barrel files in
* the utilities package. The command wires together the code generation and
* globbing services, resolves the requested working directory, discovers
* `@barrel` annotations, and rewrites matching barrel exports.
*
* @since 4.0.0
*/
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import * as Path from "effect/Path"
import * as Command from "effect/unstable/cli/Command"
import * as Flag from "effect/unstable/cli/Flag"
import * as Codegen from "../Codegen.ts"
import * as Glob from "../Glob.ts"
const CodegenLayer = Layer.provideMerge(Codegen.layer, Glob.layer)
/**
* CLI command that regenerates annotated barrel files.
*
* **Details**
*
* The command scans `cwd` for files matching `pattern`, finds `@barrel`
* annotations, and rewrites each annotated barrel with generated exports from
* matching modules.
*
* @category commands
* @since 4.0.0
*/
export const codegen = Command.make("codegen", {
cwd: Flag.directory("cwd", { mustExist: true }).pipe(Flag.withDefault(".")),
pattern: Flag.string("pattern").pipe(Flag.withDefault("src/**/index.ts"))
}, (config) =>
Effect.gen(function*() {
const path = yield* Path.Path
const generator = yield* Codegen.BarrelGenerator
const files = yield* generator.discoverFiles(config.pattern, path.resolve(config.cwd))
yield* Effect.forEach(files, (file) => generator.processFile(file), {
concurrency: "inherit",
discard: true
})
})).pipe(Command.provide(CodegenLayer))

View File

@@ -0,0 +1,9 @@
{
"$schema": "http://json.schemastore.org/tsconfig",
"extends": "../../../tsconfig.base.json",
"include": ["src"],
"references": [
{ "path": "../../effect" },
{ "path": "../../platform-node" }
]
}