diff --git a/.changeset/bright-tools-report.md b/.changeset/bright-tools-report.md new file mode 100644 index 00000000..88288bd3 --- /dev/null +++ b/.changeset/bright-tools-report.md @@ -0,0 +1,5 @@ +--- +"@effect/tsgo": minor +--- + +Add an `effect-tsgo diagnostics` command for reporting Effect diagnostics from a file or TypeScript project. The command supports pretty, text, JSON, and GitHub Actions output, severity filtering, progress reporting, strict warning handling, and inline language-service configuration. diff --git a/.changeset/fix-layer-magic-output.md b/.changeset/fix-layer-magic-output.md new file mode 100644 index 00000000..1a9e26cd --- /dev/null +++ b/.changeset/fix-layer-magic-output.md @@ -0,0 +1,5 @@ +--- +"@effect/tsgo": patch +--- + +Start layer magic compositions from `Layer.empty` when the first layer does not provide a requested output service. diff --git a/.changeset/setup-vscode-settings.md b/.changeset/setup-vscode-settings.md new file mode 100644 index 00000000..2b06913e --- /dev/null +++ b/.changeset/setup-vscode-settings.md @@ -0,0 +1,5 @@ +--- +"@effect/tsgo": patch +--- + +Update `effect-tsgo setup` to configure the current TypeScript Go VS Code settings, including the workspace TypeScript SDK path and additional SDK locations. diff --git a/.changeset/update-typescript-go.md b/.changeset/update-typescript-go.md index 0bf1c9bb..d43a9d4c 100644 --- a/.changeset/update-typescript-go.md +++ b/.changeset/update-typescript-go.md @@ -2,4 +2,4 @@ "@effect/tsgo": patch --- -Update to [`typescript@next`](https://www.npmjs.com/package/typescript/v/7.1.0-dev.20260712.1), which ships [`typescript-go`](https://github.com/microsoft/typescript-go/commit/168e7015edf98244febc8f4ae450b673b5d195d7) commit `168e7015edf98244febc8f4ae450b673b5d195d7`. +Update to [`typescript@next`](https://www.npmjs.com/package/typescript/v/7.1.0-dev.20260713.1), which ships [`typescript-go`](https://github.com/microsoft/typescript-go/commit/168e7015edf98244febc8f4ae450b673b5d195d7) commit `168e7015edf98244febc8f4ae450b673b5d195d7`. diff --git a/_packages/tsgo/src/cli.ts b/_packages/tsgo/src/cli.ts index 91f40d86..c62c0507 100644 --- a/_packages/tsgo/src/cli.ts +++ b/_packages/tsgo/src/cli.ts @@ -13,6 +13,11 @@ import * as Schema from "effect/Schema" import * as Command from "effect/unstable/cli/Command" import * as Flag from "effect/unstable/cli/Flag" import { configCommand } from "./config.js" +import { + type DiagnosticsOutputFormat, + propagateDiagnosticsExit, + runDiagnosticsBinary +} from "./diagnostics.js" import { setupCommand } from "./setup/index.js" import { defaultTypescriptPackageNames, isNativeTypescriptVersion } from "./setup/consts.js" import * as pkgJson from "../package.json" with { type: "json" } @@ -558,8 +563,62 @@ const getExePathCommand = Command.make("get-exe-path").pipe( ) ) +const diagnosticsCommand = Command.make("diagnostics", { + file: Flag.file("file").pipe( + Flag.optional, + Flag.withDescription("The full path of the file to check for diagnostics") + ), + project: Flag.file("project").pipe( + Flag.optional, + Flag.withDescription("The full path of the project tsconfig.json file to check for diagnostics") + ), + format: Flag.choice( + "format", + ["json", "pretty", "text", "github-actions"] as ReadonlyArray + ).pipe( + Flag.withDefault("pretty" as const), + Flag.withDescription( + "Output format: json (machine-readable), pretty (colored with context), text (plain text), github-actions (workflow commands)" + ) + ), + strict: Flag.boolean("strict").pipe( + Flag.withDefault(false), + Flag.withDescription("Treat warnings as errors (affects exit code)") + ), + severity: Flag.string("severity").pipe( + Flag.optional, + Flag.withDescription("Filter by severity levels (comma-separated: error,warning,message)") + ), + progress: Flag.boolean("progress").pipe( + Flag.withDefault(false), + Flag.withDescription("Show progress as files are checked (outputs to stderr)") + ), + lspconfig: Flag.string("lspconfig").pipe( + Flag.optional, + Flag.withDescription("An optional inline JSON lsp config that replaces the current project lsp config") + ) +}).pipe( + Command.withDescription("Gets the Effect language service diagnostics on the given files or project"), + Command.withHandler(({ file, format, lspconfig, progress, project, severity, strict }) => + Effect.gen(function*() { + const installedTypeScript = yield* resolveInstalledTypeScriptBinary(defaultTypescriptPackageNames) + const binaryPath = yield* getPackagedBinaryPath(installedTypeScript, false) + const result = runDiagnosticsBinary(binaryPath, { + cwd: process.cwd(), + file: Option.getOrUndefined(file), + project: Option.getOrUndefined(project), + format, + strict, + severity: Option.getOrUndefined(severity), + progress, + lspconfig: Option.getOrUndefined(lspconfig) + }) + propagateDiagnosticsExit(result) + })) +) + const rootCommand = Command.make("tsgo").pipe( - Command.withSubcommands([patchCommand, unpatchCommand, getExePathCommand, setupCommand, configCommand]) + Command.withSubcommands([patchCommand, unpatchCommand, getExePathCommand, diagnosticsCommand, setupCommand, configCommand]) ) diff --git a/_packages/tsgo/src/diagnostics.ts b/_packages/tsgo/src/diagnostics.ts new file mode 100644 index 00000000..48a90693 --- /dev/null +++ b/_packages/tsgo/src/diagnostics.ts @@ -0,0 +1,55 @@ +import * as childProcess from "node:child_process" + +export type DiagnosticsOutputFormat = "json" | "pretty" | "text" | "github-actions" + +export interface DiagnosticsRequest { + readonly cwd: string + readonly file?: string + readonly project?: string + readonly format: DiagnosticsOutputFormat + readonly strict: boolean + readonly severity?: string + readonly progress: boolean + readonly lspconfig?: string +} + +export interface DiagnosticsProcessResult { + readonly status: number | null + readonly signal: NodeJS.Signals | null + readonly error?: Error +} + +export type SpawnDiagnosticsProcess = ( + binaryPath: string, + argv: ReadonlyArray, + options: { readonly stdio: "inherit" } +) => DiagnosticsProcessResult + +export interface DiagnosticsParentProcess { + readonly pid: number + exitCode: number | string | null | undefined + kill(pid: number, signal: NodeJS.Signals): boolean +} + +export const runDiagnosticsBinary = ( + binaryPath: string, + request: DiagnosticsRequest, + spawn: SpawnDiagnosticsProcess = childProcess.spawnSync +): DiagnosticsProcessResult => { + const result = spawn(binaryPath, ["--effect-cli-diagnostics", JSON.stringify(request)], { stdio: "inherit" }) + if (result.error !== undefined) { + throw result.error + } + return result +} + +export const propagateDiagnosticsExit = ( + result: DiagnosticsProcessResult, + parent: DiagnosticsParentProcess = process +): void => { + if (result.signal !== null) { + parent.kill(parent.pid, result.signal) + return + } + parent.exitCode = result.status ?? 1 +} diff --git a/_packages/tsgo/src/setup/changes.ts b/_packages/tsgo/src/setup/changes.ts index 8ddf4ad3..2e5d7901 100644 --- a/_packages/tsgo/src/setup/changes.ts +++ b/_packages/tsgo/src/setup/changes.ts @@ -742,9 +742,18 @@ const computeVSCodeSettingsChanges = ( return emptyFileChangesResult() } - const ctx = createTrackerContext() - - const fileChanges = tsInternal.textChanges.ChangeTracker.with( + const ctx = createTrackerContext() + + const createSettingValue = (value: unknown): ts.Expression => + typeof value === "string" + ? ts.factory.createStringLiteral(value) + : typeof value === "boolean" + ? value ? ts.factory.createTrue() : ts.factory.createFalse() + : Array.isArray(value) && value.every((item): item is string => typeof item === "string") + ? ts.factory.createArrayLiteralExpression(value.map((item) => ts.factory.createStringLiteral(item))) + : ts.factory.createNull() + + const fileChanges = tsInternal.textChanges.ChangeTracker.with( ctx, (tracker: any) => { if (rootObj.properties.length === 0) { @@ -754,14 +763,10 @@ const computeVSCodeSettingsChanges = ( for (const [key, value] of Object.entries(target.settings)) { descriptions.push(`Add ${key} setting`) newProperties.push( - ts.factory.createPropertyAssignment( - ts.factory.createStringLiteral(key), - typeof value === "string" - ? ts.factory.createStringLiteral(value) - : typeof value === "boolean" - ? value ? ts.factory.createTrue() : ts.factory.createFalse() - : ts.factory.createNull() - ) + ts.factory.createPropertyAssignment( + ts.factory.createStringLiteral(key), + createSettingValue(value) + ) ) } @@ -775,14 +780,10 @@ const computeVSCodeSettingsChanges = ( if (!existingProp) { descriptions.push(`Add ${key} setting`) - const newProp = ts.factory.createPropertyAssignment( - ts.factory.createStringLiteral(key), - typeof value === "string" - ? ts.factory.createStringLiteral(value) - : typeof value === "boolean" - ? value ? ts.factory.createTrue() : ts.factory.createFalse() - : ts.factory.createNull() - ) + const newProp = ts.factory.createPropertyAssignment( + ts.factory.createStringLiteral(key), + createSettingValue(value) + ) insertNodeAtEndOfList(tracker, current.sourceFile, rootObj.properties, newProp) } } diff --git a/_packages/tsgo/src/setup/consts.ts b/_packages/tsgo/src/setup/consts.ts index 2cf8d8de..6c452061 100644 --- a/_packages/tsgo/src/setup/consts.ts +++ b/_packages/tsgo/src/setup/consts.ts @@ -15,9 +15,3 @@ export const isNativeTypescriptVersion = (version: string): boolean => { const match = /\d+/.exec(version.trim()) return match !== null && Number(match[0]) >= 7 } - -/** - * Resolve the VS Code TypeScript 7 tsdk folder. - */ -export const nativeBackendTsdkPath = (packageName: string): string => - "node_modules/" + packageName diff --git a/_packages/tsgo/src/setup/target-prompt.ts b/_packages/tsgo/src/setup/target-prompt.ts index aee11598..1e9a1781 100644 --- a/_packages/tsgo/src/setup/target-prompt.ts +++ b/_packages/tsgo/src/setup/target-prompt.ts @@ -3,7 +3,7 @@ import * as Option from "effect/Option" import type * as Terminal from "effect/Terminal" import * as Prompt from "effect/unstable/cli/Prompt" import { applyPresetDiagnosticSeverities, type DiagnosticPresetName, isPresetEnabled } from "../presets.js" -import { defaultTypescriptPackageNames, nativeBackendTsdkPath } from "./consts.js" +import { defaultTypescriptPackageNames } from "./consts.js" import type { Assessment } from "./types.js" import type { Editor, Target } from "./target.js" import { getAllPresets, getAllRules } from "./rule-info.js" @@ -134,14 +134,14 @@ export const gatherTargetState = ( }) // Build target state - // Point the TypeScript 7 extension at the project TypeScript package. const defaultTypescriptPackageName = defaultTypescriptPackageNames[0] const vscodeSettings: Option.Option = editors.includes("vscode") ? Option.some({ settings: { - "typescript.native-preview.tsdk": nativeBackendTsdkPath(defaultTypescriptPackageName), - "typescript.experimental.useTsgo": true, - "js/ts.experimental.useTsgo": true + "js/ts.experimental.useTsgo": true, + "js/ts.tsdk.path": "./node_modules/typescript/bin", + "js/ts.tsdk.promptToUseWorkspaceVersion": true, + "js/ts.tsdk.additionalLocations": ["./node_modules/typescript/bin"] } }) : Option.none() diff --git a/_packages/tsgo/test/setup/__snapshots__/setup-cli.test.ts.snap b/_packages/tsgo/test/setup/__snapshots__/setup-cli.test.ts.snap index c8f00420..2ec4d02a 100644 --- a/_packages/tsgo/test/setup/__snapshots__/setup-cli.test.ts.snap +++ b/_packages/tsgo/test/setup/__snapshots__/setup-cli.test.ts.snap @@ -48,9 +48,12 @@ exports[`Setup CLI > should add LSP plugin alongside existing plugins > tsconfig exports[`Setup CLI > should add LSP with VS Code editor selected and create new settings file > .vscode/settings.json 1`] = ` "{ - "typescript.native-preview.tsdk": "node_modules/typescript", - "typescript.experimental.useTsgo": true, - "js/ts.experimental.useTsgo": true + "js/ts.experimental.useTsgo": true, + "js/ts.tsdk.path": "./node_modules/typescript/bin", + "js/ts.tsdk.promptToUseWorkspaceVersion": true, + "js/ts.tsdk.additionalLocations": [ + "./node_modules/typescript/bin" + ] } " `; @@ -511,9 +514,10 @@ exports[`Setup CLI > should preserve all existing VS Code settings from a real r "editor.wordBasedSuggestions": "matchingDocuments", "editor.parameterHints.enabled": true, "files.insertFinalNewline": true, -"js/ts.experimental.useTsgo": true, -"typescript.experimental.useTsgo": true, -"typescript.native-preview.tsdk": "node_modules/typescript" +"js/ts.tsdk.additionalLocations": ["./node_modules/typescript/bin"], +"js/ts.tsdk.promptToUseWorkspaceVersion": true, +"js/ts.tsdk.path": "./node_modules/typescript/bin", +"js/ts.experimental.useTsgo": true }" `; @@ -528,7 +532,7 @@ exports[`Setup CLI > should preserve all existing VS Code settings from a real r "file": "tsconfig.json", }, { - "description": "Add typescript.native-preview.tsdk setting; Add typescript.experimental.useTsgo setting; Add js/ts.experimental.useTsgo setting", + "description": "Add js/ts.experimental.useTsgo setting; Add js/ts.tsdk.path setting; Add js/ts.tsdk.promptToUseWorkspaceVersion setting; Add js/ts.tsdk.additionalLocations setting", "file": ".vscode/settings.json", }, ] @@ -575,9 +579,10 @@ exports[`Setup CLI > should preserve existing VS Code settings when adding LSP-s "editor.formatOnSave": true, "editor.tabSize": 2, "files.autoSave": "onFocusChange", -"js/ts.experimental.useTsgo": true, -"typescript.experimental.useTsgo": true, -"typescript.native-preview.tsdk": "node_modules/typescript" +"js/ts.tsdk.additionalLocations": ["./node_modules/typescript/bin"], +"js/ts.tsdk.promptToUseWorkspaceVersion": true, +"js/ts.tsdk.path": "./node_modules/typescript/bin", +"js/ts.experimental.useTsgo": true }" `; @@ -592,7 +597,7 @@ exports[`Setup CLI > should preserve existing VS Code settings when adding LSP-s "file": "tsconfig.json", }, { - "description": "Add typescript.native-preview.tsdk setting; Add typescript.experimental.useTsgo setting; Add js/ts.experimental.useTsgo setting", + "description": "Add js/ts.experimental.useTsgo setting; Add js/ts.tsdk.path setting; Add js/ts.tsdk.promptToUseWorkspaceVersion setting; Add js/ts.tsdk.additionalLocations setting", "file": ".vscode/settings.json", }, ] diff --git a/_packages/tsgo/test/setup/changes.test.ts b/_packages/tsgo/test/setup/changes.test.ts index 5eba9593..c40e1a3d 100644 --- a/_packages/tsgo/test/setup/changes.test.ts +++ b/_packages/tsgo/test/setup/changes.test.ts @@ -361,6 +361,21 @@ describe("computeChanges", () => { } } }) + + it("should serialize array settings when modifying existing vscode settings", () => { + const result = runComputeChanges({ + vscodeSettingsText: JSON.stringify({ "editor.formatOnSave": true }, null, 2), + vscodeTargetSettings: { + "js/ts.tsdk.additionalLocations": ["./node_modules/typescript/bin"] + } + }) + + const vscodeChange = result.codeActions + .flatMap((action) => action.changes) + .find((change) => change.fileName.includes("settings.json")) + + expect(vscodeChange?.textChanges[0]?.newText).toContain('["./node_modules/typescript/bin"]') + }) }) describe("new-file code action for .vscode/settings.json", () => { diff --git a/_packages/tsgo/test/setup/consts.test.ts b/_packages/tsgo/test/setup/consts.test.ts index 8177531b..43ed21b9 100644 --- a/_packages/tsgo/test/setup/consts.test.ts +++ b/_packages/tsgo/test/setup/consts.test.ts @@ -1,8 +1,7 @@ import { describe, it, expect } from "vitest" import { defaultTypescriptPackageNames, - isNativeTypescriptVersion, - nativeBackendTsdkPath + isNativeTypescriptVersion } from "../../src/setup/consts.js" describe("isNativeTypescriptVersion", () => { @@ -29,12 +28,6 @@ describe("isNativeTypescriptVersion", () => { }) }) -describe("nativeBackendTsdkPath", () => { - it("returns the node_modules folder for TypeScript", () => { - expect(nativeBackendTsdkPath(defaultTypescriptPackageNames[0])).toBe("node_modules/typescript") - }) -}) - describe("defaultTypescriptPackageNames", () => { it("tries typescript before the @typescript/native alias", () => { expect(defaultTypescriptPackageNames).toEqual(["typescript", "@typescript/native"]) diff --git a/_packages/tsgo/test/setup/setup-cli.test.ts b/_packages/tsgo/test/setup/setup-cli.test.ts index 286b7355..b393c1b8 100644 --- a/_packages/tsgo/test/setup/setup-cli.test.ts +++ b/_packages/tsgo/test/setup/setup-cli.test.ts @@ -138,9 +138,10 @@ function expectSetupChanges( const VSCODE_SETTINGS: Target.VSCodeSettings = { settings: { - "typescript.native-preview.tsdk": "node_modules/typescript", - "typescript.experimental.useTsgo": true, - "js/ts.experimental.useTsgo": true + "js/ts.experimental.useTsgo": true, + "js/ts.tsdk.path": "./node_modules/typescript/bin", + "js/ts.tsdk.promptToUseWorkspaceVersion": true, + "js/ts.tsdk.additionalLocations": ["./node_modules/typescript/bin"] } } diff --git a/_patches/001-cmd-tsgo-main.patch b/_patches/001-cmd-tsgo-main.patch index f0579f1e..749ec33a 100644 --- a/_patches/001-cmd-tsgo-main.patch +++ b/_patches/001-cmd-tsgo-main.patch @@ -1,16 +1,22 @@ diff --git a/cmd/tsgo/main.go b/cmd/tsgo/main.go -index 8d6816fa3d..49c6cbecba 100644 +index 8d6816fa3d..c24db44dfa 100644 --- a/cmd/tsgo/main.go +++ b/cmd/tsgo/main.go -@@ -6,6 +6,11 @@ import ( - "os/signal" - "syscall" +@@ -8,2 +8,8 @@ import ( + // Import Effect hooks to register via init() -+ _ "github.com/effect-ts/tsgo/etsexecutehooks" // exit-code filtering hooks + _ "github.com/effect-ts/tsgo/etscheckerhooks" // checker diagnostics hooks ++ "github.com/effect-ts/tsgo/etsdiagnostics" ++ _ "github.com/effect-ts/tsgo/etsexecutehooks" // exit-code filtering hooks + _ "github.com/effect-ts/tsgo/etslshooks" // LS codefix hooks + "github.com/microsoft/typescript-go/internal/core" - "github.com/microsoft/typescript-go/internal/execute" - ) +@@ -22,6 +28,8 @@ func runMain() int { + case "--lsp": + return runLSP(args[1:]) + case "--api": + return runAPI(args[1:]) ++ case "--effect-cli-diagnostics": ++ return etsdiagnostics.Run(context.Background(), args[1:], os.Stdout, os.Stderr) + } + } diff --git a/etsdiagnostics/diagnostics.go b/etsdiagnostics/diagnostics.go new file mode 100644 index 00000000..2f305487 --- /dev/null +++ b/etsdiagnostics/diagnostics.go @@ -0,0 +1,406 @@ +// Package etsdiagnostics implements the native Effect diagnostics CLI mode. +package etsdiagnostics + +import ( + "context" + "encoding/json" + "fmt" + "io" + "maps" + "strings" + + "github.com/effect-ts/tsgo/etscore" + "github.com/effect-ts/tsgo/internal/rule" + "github.com/effect-ts/tsgo/internal/rules" + "github.com/microsoft/typescript-go/shim/ast" + "github.com/microsoft/typescript-go/shim/bundled" + "github.com/microsoft/typescript-go/shim/collections" + "github.com/microsoft/typescript-go/shim/core" + tsdiag "github.com/microsoft/typescript-go/shim/diagnostics" + "github.com/microsoft/typescript-go/shim/ls/lsconv" + "github.com/microsoft/typescript-go/shim/lsp/lsproto" + "github.com/microsoft/typescript-go/shim/project" + "github.com/microsoft/typescript-go/shim/scanner" + "github.com/microsoft/typescript-go/shim/tspath" + "github.com/microsoft/typescript-go/shim/vfs/osvfs" +) + +const noFilesMessage = "No files to check. Please provide an existing .ts file or a project tsconfig.json" + +type request struct { + CWD string `json:"cwd"` + File string `json:"file,omitempty"` + Project string `json:"project,omitempty"` + Format string `json:"format"` + Strict bool `json:"strict"` + Severity string `json:"severity,omitempty"` + Progress bool `json:"progress"` + LSPConfig *string `json:"lspconfig,omitempty"` +} + +type severity string + +const ( + severityError severity = "error" + severityWarning severity = "warning" + severityMessage severity = "message" +) + +type formattedDiagnostic struct { + File string `json:"file"` + Start int `json:"start"` + Length int `json:"length"` + Line int `json:"line"` + Column int `json:"column"` + EndLine int `json:"endLine"` + EndColumn int `json:"endColumn"` + Severity severity `json:"severity"` + Code int32 `json:"code"` + Name string `json:"name"` + Message string `json:"message"` + source string +} + +type summary struct { + FilesChecked int `json:"filesChecked"` + TotalFiles int `json:"totalFiles"` + Errors int `json:"errors"` + Warnings int `json:"warnings"` + Messages int `json:"messages"` +} + +type jsonOutput struct { + Diagnostics []formattedDiagnostic `json:"diagnostics"` + Summary summary `json:"summary"` +} + +// Run decodes and executes one JSON diagnostics request. +func Run(ctx context.Context, args []string, stdout io.Writer, stderr io.Writer) int { + if len(args) != 1 { + fmt.Fprintln(stderr, "expected one JSON request after --effect-cli-diagnostics") + return 2 + } + + var req request + if err := json.Unmarshal([]byte(args[0]), &req); err != nil { + fmt.Fprintf(stderr, "invalid diagnostics request: %v\n", err) + return 2 + } + if req.CWD == "" { + fmt.Fprintln(stderr, "diagnostics request is missing cwd") + return 2 + } + if req.Format == "" { + req.Format = "pretty" + } + if req.Format != "json" && req.Format != "pretty" && req.Format != "text" && req.Format != "github-actions" { + fmt.Fprintf(stderr, "unsupported diagnostics format %q\n", req.Format) + return 2 + } + + override, overrideProvided, err := parseLSPConfig(req.LSPConfig) + if err != nil { + fmt.Fprintf(stderr, "Invalid JSON lsp config: %s\n", *req.LSPConfig) + return 1 + } + + diagnostics, resultSummary, err := collect(ctx, req, override, overrideProvided, stderr) + if err != nil { + fmt.Fprintln(stderr, err) + return 1 + } + if err := writeOutput(stdout, req.Format, diagnostics, resultSummary); err != nil { + fmt.Fprintf(stderr, "unable to write diagnostics: %v\n", err) + return 2 + } + if resultSummary.Errors > 0 || (req.Strict && resultSummary.Warnings > 0) { + return 1 + } + return 0 +} + +func parseLSPConfig(value *string) (*etscore.EffectPluginOptions, bool, error) { + if value == nil { + return nil, false, nil + } + var decoded any + if err := json.Unmarshal([]byte(*value), &decoded); err != nil { + return nil, true, err + } + config := make(map[string]any) + if object, ok := decoded.(map[string]any); ok { + maps.Copy(config, object) + } + config["name"] = etscore.EffectPluginName + return etscore.ParseFromPlugins([]any{config}), true, nil +} + +func collect(ctx context.Context, req request, override *etscore.EffectPluginOptions, overrideProvided bool, stderr io.Writer) ([]formattedDiagnostic, summary, error) { + fs := bundled.WrapFS(osvfs.FS()) + session := project.NewSession(&project.SessionInit{ + BackgroundCtx: ctx, + FS: fs, + Options: &project.SessionOptions{ + CurrentDirectory: req.CWD, + DefaultLibraryPath: bundled.LibPath(), + PositionEncoding: lsproto.PositionEncodingKindUTF8, + }, + }) + defer session.Close() + + targets := make([]string, 0) + seenTargets := make(map[tspath.Path]struct{}) + addTarget := func(fileName string) { + path := tspath.ToPath(fileName, req.CWD, fs.UseCaseSensitiveFileNames()) + if _, seen := seenTargets[path]; seen { + return + } + seenTargets[path] = struct{}{} + targets = append(targets, fileName) + } + + if req.Project != "" { + projectName := tspath.GetNormalizedAbsolutePath(req.Project, req.CWD) + if fs.DirectoryExists(projectName) { + projectName = tspath.CombinePaths(projectName, "tsconfig.json") + } + openProjects := &collections.Set[string]{} + openProjects.Add(projectName) + if err := updateSession(ctx, session, &project.APISnapshotRequest{OpenProjects: openProjects}); err != nil { + return nil, summary{}, err + } + + session.WithSnapshotLoadingProjectTree(ctx, nil, func(snapshot *project.Snapshot) { + for _, configuredProject := range snapshot.ProjectCollection.ConfiguredProjects() { + if configuredProject.CommandLine == nil { + continue + } + for _, fileName := range configuredProject.CommandLine.FileNames() { + addTarget(fileName) + } + } + }) + } + + if req.File != "" { + fileName := tspath.GetNormalizedAbsolutePath(req.File, req.CWD) + uri := lsconv.FileNameToDocumentURI(fileName) + openFiles := &collections.Set[lsproto.DocumentUri]{} + openFiles.Add(uri) + if err := updateSession(ctx, session, &project.APISnapshotRequest{OpenFiles: openFiles}); err != nil { + return nil, summary{}, err + } + addTarget(fileName) + } + + resultSummary := summary{TotalFiles: len(targets)} + if len(targets) == 0 { + return nil, resultSummary, fmt.Errorf("%s", noFilesMessage) + } + + severityFilter := parseSeverityFilter(req.Severity) + results := make([]formattedDiagnostic, 0) + ctx = core.WithCheckerLifetime(ctx, core.CheckerLifetimeDiagnostics) + if req.Progress { + fmt.Fprintf(stderr, "Starting diagnostics for %d files...\n", len(targets)) + } + + var collectErr error + session.WithSnapshotLoadingProjectTree(ctx, nil, func(snapshot *project.Snapshot) { + for index, fileName := range targets { + if req.Progress { + fmt.Fprintf(stderr, "[%d/%d] %60s\r", index+1, len(targets), truncateLeft(fileName, 60)) + } + uri := lsconv.FileNameToDocumentURI(fileName) + configuredProject := snapshot.GetDefaultProject(uri) + if configuredProject == nil || configuredProject.GetProgram() == nil { + continue + } + program := configuredProject.GetProgram() + sourceFile := program.GetSourceFile(fileName) + if sourceFile == nil { + continue + } + if overrideProvided { + program.Options().Effect = override + } + if program.Options().Effect == nil { + continue + } + + for _, diagnostic := range program.GetSemanticDiagnostics(ctx, sourceFile) { + if !rule.IsEffectCode(diagnostic.Code()) { + continue + } + formatted := formatDiagnostic(diagnostic) + if severityFilter != nil { + if _, included := severityFilter[formatted.Severity]; !included { + continue + } + } + results = append(results, formatted) + switch formatted.Severity { + case severityError: + resultSummary.Errors++ + case severityWarning: + resultSummary.Warnings++ + default: + resultSummary.Messages++ + } + } + resultSummary.FilesChecked++ + if err := ctx.Err(); err != nil { + collectErr = err + return + } + } + }) + if req.Progress { + fmt.Fprintln(stderr) + } + if collectErr != nil { + return nil, resultSummary, collectErr + } + return results, resultSummary, nil +} + +func updateSession(ctx context.Context, session *project.Session, request *project.APISnapshotRequest) error { + snapshot, err := session.APIUpdate(ctx, project.FileChangeSummary{}, request) + if snapshot != nil { + snapshot.Deref(session) + } + return err +} + +func parseSeverityFilter(value string) map[severity]struct{} { + if value == "" { + return nil + } + result := make(map[severity]struct{}) + for item := range strings.SplitSeq(value, ",") { + level := severity(strings.ToLower(strings.TrimSpace(item))) + if level == severityError || level == severityWarning || level == severityMessage { + result[level] = struct{}{} + } + } + if len(result) == 0 { + return nil + } + return result +} + +func formatDiagnostic(diagnostic *ast.Diagnostic) formattedDiagnostic { + file := diagnostic.File() + start := file.GetPositionMap().UTF8ToUTF16(diagnostic.Pos()) + end := file.GetPositionMap().UTF8ToUTF16(diagnostic.End()) + line, column := scanner.GetECMALineAndUTF16CharacterOfPosition(file, diagnostic.Pos()) + endLine, endColumn := scanner.GetECMALineAndUTF16CharacterOfPosition(file, diagnostic.End()) + name := rule.CodeToRuleName(rules.All, diagnostic.Code()) + if name == "" { + name = fmt.Sprintf("effect(%d)", diagnostic.Code()) + } + message := flattenMessage(diagnostic, 0) + message = strings.TrimSuffix(message, " effect("+name+")") + return formattedDiagnostic{ + File: file.FileName(), + Start: start, + Length: end - start, + Line: line + 1, + Column: int(column) + 1, + EndLine: endLine + 1, + EndColumn: int(endColumn) + 1, + Severity: categoryToSeverity(diagnostic.Category()), + Code: diagnostic.Code(), + Name: name, + Message: message, + source: file.Text(), + } +} + +func flattenMessage(diagnostic *ast.Diagnostic, level int) string { + var output strings.Builder + output.WriteString(diagnostic.String()) + for _, child := range diagnostic.MessageChain() { + output.WriteByte('\n') + output.WriteString(strings.Repeat(" ", level+1)) + output.WriteString(flattenMessage(child, level+1)) + } + return output.String() +} + +func categoryToSeverity(category tsdiag.Category) severity { + switch category { + case tsdiag.CategoryError: + return severityError + case tsdiag.CategoryWarning: + return severityWarning + default: + return severityMessage + } +} + +func writeOutput(output io.Writer, format string, diagnostics []formattedDiagnostic, resultSummary summary) error { + switch format { + case "json": + encoder := json.NewEncoder(output) + encoder.SetIndent("", " ") + return encoder.Encode(jsonOutput{Diagnostics: diagnostics, Summary: resultSummary}) + case "github-actions": + for _, diagnostic := range diagnostics { + command := string(diagnostic.Severity) + if diagnostic.Severity == severityMessage { + command = "notice" + } + message := strings.NewReplacer("%", "%25", "\r", "%0D", "\n", "%0A").Replace(diagnostic.Message) + fmt.Fprintf(output, "::%s file=%s,line=%d,col=%d,endLine=%d,endColumn=%d,title=%s::%s\n", + command, diagnostic.File, diagnostic.Line, diagnostic.Column, diagnostic.EndLine, diagnostic.EndColumn, diagnostic.Name, message) + } + case "text": + for _, diagnostic := range diagnostics { + fmt.Fprintf(output, "%s(%d,%d): %s %s: %s\n", + diagnostic.File, diagnostic.Line, diagnostic.Column, diagnostic.Severity, diagnosticLabel(diagnostic.Name), diagnostic.Message) + } + case "pretty": + for _, diagnostic := range diagnostics { + writePrettyDiagnostic(output, diagnostic) + } + } + _, err := fmt.Fprintf(output, "Checked %d files out of %d files. \n%d errors, %d warnings and %d messages.\n", + resultSummary.FilesChecked, resultSummary.TotalFiles, resultSummary.Errors, resultSummary.Warnings, resultSummary.Messages) + return err +} + +func writePrettyDiagnostic(output io.Writer, diagnostic formattedDiagnostic) { + var color string + switch diagnostic.Severity { + case severityError: + color = "\x1b[91m" + case severityWarning: + color = "\x1b[93m" + default: + color = "\x1b[96m" + } + reset := "\x1b[0m" + fmt.Fprintf(output, "%s%s:%d:%d - %s %s:%s %s\n", + color, diagnostic.File, diagnostic.Line, diagnostic.Column, diagnostic.Severity, diagnosticLabel(diagnostic.Name), reset, diagnostic.Message) + lines := strings.Split(diagnostic.source, "\n") + if diagnostic.Line > 0 && diagnostic.Line <= len(lines) { + line := strings.TrimSuffix(lines[diagnostic.Line-1], "\r") + fmt.Fprintf(output, "\n%d %s\n %s%s%s\n\n", diagnostic.Line, line, color, + strings.Repeat(" ", max(diagnostic.Column-1, 0))+strings.Repeat("~", max(diagnostic.EndColumn-diagnostic.Column, 1)), reset) + } +} + +func diagnosticLabel(name string) string { + if strings.HasPrefix(name, "effect(") { + return name + } + return "effect(" + name + ")" +} + +func truncateLeft(value string, width int) string { + if len(value) <= width { + return value + } + return value[len(value)-width:] +} diff --git a/etsdiagnostics/diagnostics_test.go b/etsdiagnostics/diagnostics_test.go new file mode 100644 index 00000000..26ac2557 --- /dev/null +++ b/etsdiagnostics/diagnostics_test.go @@ -0,0 +1,122 @@ +package etsdiagnostics + +import ( + "bytes" + "context" + "encoding/json" + "path/filepath" + "strings" + "testing" + + _ "github.com/effect-ts/tsgo/etscheckerhooks" +) + +func TestParseSeverityFilter(t *testing.T) { + t.Parallel() + + filter := parseSeverityFilter(" ERROR, warning,invalid ") + if _, ok := filter[severityError]; !ok { + t.Fatal("expected error severity") + } + if _, ok := filter[severityWarning]; !ok { + t.Fatal("expected warning severity") + } + if _, ok := filter[severityMessage]; ok { + t.Fatal("did not expect message severity") + } + if filter := parseSeverityFilter("invalid"); filter != nil { + t.Fatal("an entirely invalid filter should disable filtering") + } +} + +func TestParseLSPConfigPresence(t *testing.T) { + t.Parallel() + + if config, provided, err := parseLSPConfig(nil); err != nil || provided || config != nil { + t.Fatalf("unexpected omitted config result: config=%#v provided=%t err=%v", config, provided, err) + } + empty := "" + if _, provided, err := parseLSPConfig(&empty); err == nil || !provided { + t.Fatalf("expected an explicitly empty config to fail: provided=%t err=%v", provided, err) + } + null := "null" + if config, provided, err := parseLSPConfig(&null); err != nil || !provided || config == nil { + t.Fatalf("unexpected null config result: config=%#v provided=%t err=%v", config, provided, err) + } +} + +func TestWriteJSONOutput(t *testing.T) { + t.Parallel() + + diagnostics := []formattedDiagnostic{{ + File: "/workspace/main.ts", Start: 2, Length: 4, Line: 1, Column: 3, + EndLine: 1, EndColumn: 7, Severity: severityError, Code: 377001, + Name: "floatingEffect", Message: "Effect must be handled", + }} + wantSummary := summary{FilesChecked: 1, TotalFiles: 1, Errors: 1} + var output bytes.Buffer + if err := writeOutput(&output, "json", diagnostics, wantSummary); err != nil { + t.Fatal(err) + } + for _, expected := range []string{ + `"file": "/workspace/main.ts"`, + `"severity": "error"`, + `"name": "floatingEffect"`, + `"filesChecked": 1`, + } { + if !strings.Contains(output.String(), expected) { + t.Fatalf("output did not contain %q:\n%s", expected, output.String()) + } + } +} + +func TestWriteGitHubActionsOutput(t *testing.T) { + t.Parallel() + + diagnostics := []formattedDiagnostic{{ + File: "/workspace/main.ts", Line: 2, Column: 3, EndLine: 2, EndColumn: 8, + Severity: severityMessage, Name: "floatingEffect", Message: "first%\nsecond", + }} + var output bytes.Buffer + if err := writeOutput(&output, "github-actions", diagnostics, summary{FilesChecked: 1, TotalFiles: 1, Messages: 1}); err != nil { + t.Fatal(err) + } + want := "::notice file=/workspace/main.ts,line=2,col=3,endLine=2,endColumn=8,title=floatingEffect::first%25%0Asecond\n" + if !strings.HasPrefix(output.String(), want) { + t.Fatalf("unexpected output:\n%s", output.String()) + } +} + +func TestRunProjectJSON(t *testing.T) { + t.Parallel() + + cwd, err := filepath.Abs("testdata/native-diagnostics") + if err != nil { + t.Fatal(err) + } + request, err := json.Marshal(request{ + CWD: cwd, Project: "tsconfig.json", Format: "json", + }) + if err != nil { + t.Fatal(err) + } + var stdout bytes.Buffer + var stderr bytes.Buffer + if status := Run(context.Background(), []string{string(request)}, &stdout, &stderr); status != 1 { + t.Fatalf("unexpected status %d; stderr:\n%s", status, stderr.String()) + } + var output jsonOutput + if err := json.Unmarshal(stdout.Bytes(), &output); err != nil { + t.Fatalf("invalid JSON output: %v\n%s", err, stdout.String()) + } + if len(output.Diagnostics) != 1 { + t.Fatalf("expected one diagnostic, got %d", len(output.Diagnostics)) + } + diagnostic := output.Diagnostics[0] + if diagnostic.Name != "asyncFunction" || diagnostic.Code != 377081 || diagnostic.Severity != severityError { + t.Fatalf("unexpected diagnostic: %#v", diagnostic) + } + if strings.Contains(diagnostic.Message, "effect(asyncFunction)") { + t.Fatalf("message includes redundant rule name: %q", diagnostic.Message) + } +} diff --git a/etsdiagnostics/testdata/native-diagnostics/main.ts b/etsdiagnostics/testdata/native-diagnostics/main.ts new file mode 100644 index 00000000..680fd3fc --- /dev/null +++ b/etsdiagnostics/testdata/native-diagnostics/main.ts @@ -0,0 +1,3 @@ +async function run() { + return 1 +} diff --git a/etsdiagnostics/testdata/native-diagnostics/tsconfig.json b/etsdiagnostics/testdata/native-diagnostics/tsconfig.json new file mode 100644 index 00000000..93145358 --- /dev/null +++ b/etsdiagnostics/testdata/native-diagnostics/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "skipLibCheck": true, + "plugins": [ + { + "name": "@effect/language-service", + "diagnosticSeverity": { + "asyncFunction": "error" + } + } + ] + }, + "files": ["main.ts"] +} diff --git a/internal/refactors/layer_magic.go b/internal/refactors/layer_magic.go index 69e3857c..00e74917 100644 --- a/internal/refactors/layer_magic.go +++ b/internal/refactors/layer_magic.go @@ -150,19 +150,27 @@ func tryBuildRefactor(ctx *refactor.Context, tp *typeparser.TypeParser, c *check return []ls.CodeAction{*action} } -// buildLayerMagicBuild generates: firstLayer.pipe(Layer.provideMerge(...), Layer.provide(...), ...) +// buildLayerMagicBuild generates a pipe from the first requested output layer, or Layer.empty +// when the first layer does not provide a requested output. func buildLayerMagicBuild(tracker *rewriter.Tracker, ctx *refactor.Context, c *checker.Checker, oldNode *ast.Node, magicResult *layergraph.LayerMagicResult, layerIdentifier string) { nodes := magicResult.Nodes if len(nodes) == 0 { return } - // Clone the first node's expression - firstExpr := tracker.DeepCloneNode(nodes[0].Node) + var receiver *ast.Node + firstPipeArg := 0 + if nodes[0].Merges { + receiver = tracker.DeepCloneNode(nodes[0].Node) + firstPipeArg = 1 + } else { + receiver = tracker.NewPropertyAccessExpression( + tracker.NewIdentifier(layerIdentifier), nil, tracker.NewIdentifier("empty"), ast.NodeFlagsNone, + ) + } - // Build pipe arguments for remaining nodes var pipeArgs []*ast.Node - for _, mn := range nodes[1:] { + for _, mn := range nodes[firstPipeArg:] { // Determine combinator name var combinatorName string switch { @@ -188,9 +196,8 @@ func buildLayerMagicBuild(tracker *rewriter.Tracker, ctx *refactor.Context, c *c pipeArgs = append(pipeArgs, call) } - // firstExpr.pipe(args...) pipeAccess := tracker.NewPropertyAccessExpression( - firstExpr, nil, tracker.NewIdentifier("pipe"), ast.NodeFlagsNone, + receiver, nil, tracker.NewIdentifier("pipe"), ast.NodeFlagsNone, ) newDeclaration := tracker.NewCallExpression( pipeAccess, nil, nil, diff --git a/testdata/baselines/reference/effect-v3/layerMagic_build.refactors.txt b/testdata/baselines/reference/effect-v3/layerMagic_build.refactors.txt index e81315ba..a4202104 100644 --- a/testdata/baselines/reference/effect-v3/layerMagic_build.refactors.txt +++ b/testdata/baselines/reference/effect-v3/layerMagic_build.refactors.txt @@ -212,7 +212,7 @@ class UserRepository extends Effect.Service()("UserRepository", effect: Effect.as(Effect.zipRight(DbConnection, Cache), {}) }) {} -export const expect_Cache_provideMergeFileSystem = Cache.Default.pipe(Layer.provideMerge(FileSystem.Default)); +export const expect_Cache_provideMergeFileSystem = Layer.empty.pipe(Layer.provide(Cache.Default), Layer.provideMerge(FileSystem.Default)); export const prepareSomewhatComplex = [ DbConnection.Default, @@ -1172,5 +1172,5 @@ export const provideRequireSame = [FileSystem.bothInAndOut, FileSystem.Default, export const tooLessOutput = [Cache.Default] as any as Layer.Layer -export const missingImplementations = UserRepository.Default.pipe(Layer.provide(FileSystem.Default)) /* Unable to find Cache in the provided layers. */; +export const missingImplementations = Layer.empty.pipe(Layer.provide(UserRepository.Default), Layer.provide(FileSystem.Default)) /* Unable to find Cache in the provided layers. */; diff --git a/testdata/baselines/reference/effect-v4/layerMagic_build.refactors.txt b/testdata/baselines/reference/effect-v4/layerMagic_build.refactors.txt index 68a1a155..7ca33509 100644 --- a/testdata/baselines/reference/effect-v4/layerMagic_build.refactors.txt +++ b/testdata/baselines/reference/effect-v4/layerMagic_build.refactors.txt @@ -35,7 +35,13 @@ Refactor 2: "Toggle lazy const" (refactor.rewrite.effect.toggleLazyConst) Refactor 3: "Compose layers automatically with target output services" (refactor.rewrite.effect.layerMagicBuild) -[R7] selection: "empty (0:0-0:0)" +[R7] selection: "57:14-57:41" + Refactor 0: "Wrap with pipe" (refactor.rewrite.effect.wrapWithPipe) + Refactor 1: "Toggle type annotation" (refactor.rewrite.effect.toggleTypeAnnotation) + Refactor 2: "Toggle lazy const" (refactor.rewrite.effect.toggleLazyConst) + Refactor 3: "Compose layers automatically with target output services" (refactor.rewrite.effect.layerMagicBuild) + +[R8] selection: "empty (0:0-0:0)" (no refactors) === Refactor Application Results === @@ -43,7 +49,7 @@ === [R1] Refactor 0: "Wrap with pipe" === --- file:///.src/layerMagic_build.ts --- -// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36 +// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36, 57:14-57:41 import { Effect, Layer, Context } from "effect" class DbConnection extends Context.Service()("DbConnection", { @@ -95,11 +101,21 @@ export const tooLessOutput = [Cache.Default] as any as Layer.Layer export const missingImplementations = [UserRepository.Default, FileSystem.Default] as any as Layer.Layer +const RequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const AlsoRequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const FileSystemWithExtrasLayer = Layer.mergeAll(FileSystem.Default, DbConnection.Default, Layer.succeed(Cache, {})) + +export const onlyFileSystemFromLastLayer = [ + RequiresFileSystemLayer, + AlsoRequiresFileSystemLayer, + FileSystemWithExtrasLayer +] as any as Layer.Layer + === [R1] Refactor 1: "Toggle type annotation" === --- file:///.src/layerMagic_build.ts --- -// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36 +// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36, 57:14-57:41 import { Effect, Layer, Context } from "effect" class DbConnection extends Context.Service()("DbConnection", { @@ -151,11 +167,21 @@ export const tooLessOutput = [Cache.Default] as any as Layer.Layer export const missingImplementations = [UserRepository.Default, FileSystem.Default] as any as Layer.Layer +const RequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const AlsoRequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const FileSystemWithExtrasLayer = Layer.mergeAll(FileSystem.Default, DbConnection.Default, Layer.succeed(Cache, {})) + +export const onlyFileSystemFromLastLayer = [ + RequiresFileSystemLayer, + AlsoRequiresFileSystemLayer, + FileSystemWithExtrasLayer +] as any as Layer.Layer + === [R1] Refactor 2: "Toggle lazy const" === --- file:///.src/layerMagic_build.ts --- -// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36 +// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36, 57:14-57:41 import { Effect, Layer, Context } from "effect" class DbConnection extends Context.Service()("DbConnection", { @@ -207,11 +233,21 @@ export const tooLessOutput = [Cache.Default] as any as Layer.Layer export const missingImplementations = [UserRepository.Default, FileSystem.Default] as any as Layer.Layer +const RequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const AlsoRequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const FileSystemWithExtrasLayer = Layer.mergeAll(FileSystem.Default, DbConnection.Default, Layer.succeed(Cache, {})) + +export const onlyFileSystemFromLastLayer = [ + RequiresFileSystemLayer, + AlsoRequiresFileSystemLayer, + FileSystemWithExtrasLayer +] as any as Layer.Layer + === [R1] Refactor 3: "Compose layers automatically with target output services" === --- file:///.src/layerMagic_build.ts --- -// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36 +// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36, 57:14-57:41 import { Effect, Layer, Context } from "effect" class DbConnection extends Context.Service()("DbConnection", { @@ -236,7 +272,7 @@ class UserRepository extends Context.Service()("UserRepository", static Default = Layer.effect(this, this.make) } -export const expect_Cache_provideMergeFileSystem = Cache.Default.pipe(Layer.provideMerge(FileSystem.Default)); +export const expect_Cache_provideMergeFileSystem = Layer.empty.pipe(Layer.provide(Cache.Default), Layer.provideMerge(FileSystem.Default)); export const prepareSomewhatComplex = [ DbConnection.Default, @@ -260,11 +296,21 @@ export const tooLessOutput = [Cache.Default] as any as Layer.Layer export const missingImplementations = [UserRepository.Default, FileSystem.Default] as any as Layer.Layer +const RequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const AlsoRequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const FileSystemWithExtrasLayer = Layer.mergeAll(FileSystem.Default, DbConnection.Default, Layer.succeed(Cache, {})) + +export const onlyFileSystemFromLastLayer = [ + RequiresFileSystemLayer, + AlsoRequiresFileSystemLayer, + FileSystemWithExtrasLayer +] as any as Layer.Layer + === [R2] Refactor 0: "Wrap with pipe" === --- file:///.src/layerMagic_build.ts --- -// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36 +// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36, 57:14-57:41 import { Effect, Layer, Context } from "effect" class DbConnection extends Context.Service()("DbConnection", { @@ -316,11 +362,21 @@ export const tooLessOutput = [Cache.Default] as any as Layer.Layer export const missingImplementations = [UserRepository.Default, FileSystem.Default] as any as Layer.Layer +const RequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const AlsoRequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const FileSystemWithExtrasLayer = Layer.mergeAll(FileSystem.Default, DbConnection.Default, Layer.succeed(Cache, {})) + +export const onlyFileSystemFromLastLayer = [ + RequiresFileSystemLayer, + AlsoRequiresFileSystemLayer, + FileSystemWithExtrasLayer +] as any as Layer.Layer + === [R2] Refactor 1: "Toggle type annotation" === --- file:///.src/layerMagic_build.ts --- -// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36 +// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36, 57:14-57:41 import { Effect, Layer, Context } from "effect" class DbConnection extends Context.Service()("DbConnection", { @@ -372,11 +428,21 @@ export const tooLessOutput = [Cache.Default] as any as Layer.Layer export const missingImplementations = [UserRepository.Default, FileSystem.Default] as any as Layer.Layer +const RequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const AlsoRequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const FileSystemWithExtrasLayer = Layer.mergeAll(FileSystem.Default, DbConnection.Default, Layer.succeed(Cache, {})) + +export const onlyFileSystemFromLastLayer = [ + RequiresFileSystemLayer, + AlsoRequiresFileSystemLayer, + FileSystemWithExtrasLayer +] as any as Layer.Layer + === [R2] Refactor 2: "Toggle lazy const" === --- file:///.src/layerMagic_build.ts --- -// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36 +// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36, 57:14-57:41 import { Effect, Layer, Context } from "effect" class DbConnection extends Context.Service()("DbConnection", { @@ -428,11 +494,21 @@ export const tooLessOutput = [Cache.Default] as any as Layer.Layer export const missingImplementations = [UserRepository.Default, FileSystem.Default] as any as Layer.Layer +const RequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const AlsoRequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const FileSystemWithExtrasLayer = Layer.mergeAll(FileSystem.Default, DbConnection.Default, Layer.succeed(Cache, {})) + +export const onlyFileSystemFromLastLayer = [ + RequiresFileSystemLayer, + AlsoRequiresFileSystemLayer, + FileSystemWithExtrasLayer +] as any as Layer.Layer + === [R2] Refactor 3: "Compose layers automatically with target output services" === --- file:///.src/layerMagic_build.ts --- -// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36 +// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36, 57:14-57:41 import { Effect, Layer, Context } from "effect" class DbConnection extends Context.Service()("DbConnection", { @@ -479,11 +555,21 @@ export const tooLessOutput = [Cache.Default] as any as Layer.Layer export const missingImplementations = [UserRepository.Default, FileSystem.Default] as any as Layer.Layer +const RequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const AlsoRequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const FileSystemWithExtrasLayer = Layer.mergeAll(FileSystem.Default, DbConnection.Default, Layer.succeed(Cache, {})) + +export const onlyFileSystemFromLastLayer = [ + RequiresFileSystemLayer, + AlsoRequiresFileSystemLayer, + FileSystemWithExtrasLayer +] as any as Layer.Layer + === [R3] Refactor 0: "Wrap with pipe" === --- file:///.src/layerMagic_build.ts --- -// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36 +// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36, 57:14-57:41 import { Effect, Layer, Context } from "effect" class DbConnection extends Context.Service()("DbConnection", { @@ -535,11 +621,21 @@ export const tooLessOutput = [Cache.Default] as any as Layer.Layer export const missingImplementations = [UserRepository.Default, FileSystem.Default] as any as Layer.Layer +const RequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const AlsoRequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const FileSystemWithExtrasLayer = Layer.mergeAll(FileSystem.Default, DbConnection.Default, Layer.succeed(Cache, {})) + +export const onlyFileSystemFromLastLayer = [ + RequiresFileSystemLayer, + AlsoRequiresFileSystemLayer, + FileSystemWithExtrasLayer +] as any as Layer.Layer + === [R3] Refactor 1: "Toggle type annotation" === --- file:///.src/layerMagic_build.ts --- -// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36 +// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36, 57:14-57:41 import { Effect, Layer, Context } from "effect" class DbConnection extends Context.Service()("DbConnection", { @@ -591,11 +687,21 @@ export const tooLessOutput = [Cache.Default] as any as Layer.Layer export const missingImplementations = [UserRepository.Default, FileSystem.Default] as any as Layer.Layer +const RequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const AlsoRequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const FileSystemWithExtrasLayer = Layer.mergeAll(FileSystem.Default, DbConnection.Default, Layer.succeed(Cache, {})) + +export const onlyFileSystemFromLastLayer = [ + RequiresFileSystemLayer, + AlsoRequiresFileSystemLayer, + FileSystemWithExtrasLayer +] as any as Layer.Layer + === [R3] Refactor 2: "Toggle lazy const" === --- file:///.src/layerMagic_build.ts --- -// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36 +// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36, 57:14-57:41 import { Effect, Layer, Context } from "effect" class DbConnection extends Context.Service()("DbConnection", { @@ -647,11 +753,21 @@ export const tooLessOutput = [Cache.Default] as any as Layer.Layer export const missingImplementations = [UserRepository.Default, FileSystem.Default] as any as Layer.Layer +const RequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const AlsoRequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const FileSystemWithExtrasLayer = Layer.mergeAll(FileSystem.Default, DbConnection.Default, Layer.succeed(Cache, {})) + +export const onlyFileSystemFromLastLayer = [ + RequiresFileSystemLayer, + AlsoRequiresFileSystemLayer, + FileSystemWithExtrasLayer +] as any as Layer.Layer + === [R3] Refactor 3: "Compose layers automatically with target output services" === --- file:///.src/layerMagic_build.ts --- -// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36 +// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36, 57:14-57:41 import { Effect, Layer, Context } from "effect" class DbConnection extends Context.Service()("DbConnection", { @@ -698,11 +814,21 @@ export const tooLessOutput = [Cache.Default] as any as Layer.Layer export const missingImplementations = [UserRepository.Default, FileSystem.Default] as any as Layer.Layer +const RequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const AlsoRequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const FileSystemWithExtrasLayer = Layer.mergeAll(FileSystem.Default, DbConnection.Default, Layer.succeed(Cache, {})) + +export const onlyFileSystemFromLastLayer = [ + RequiresFileSystemLayer, + AlsoRequiresFileSystemLayer, + FileSystemWithExtrasLayer +] as any as Layer.Layer + === [R4] Refactor 0: "Wrap with pipe" === --- file:///.src/layerMagic_build.ts --- -// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36 +// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36, 57:14-57:41 import { Effect, Layer, Context } from "effect" class DbConnection extends Context.Service()("DbConnection", { @@ -754,11 +880,21 @@ export const tooLessOutput = [Cache.Default] as any as Layer.Layer export const missingImplementations = [UserRepository.Default, FileSystem.Default] as any as Layer.Layer +const RequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const AlsoRequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const FileSystemWithExtrasLayer = Layer.mergeAll(FileSystem.Default, DbConnection.Default, Layer.succeed(Cache, {})) + +export const onlyFileSystemFromLastLayer = [ + RequiresFileSystemLayer, + AlsoRequiresFileSystemLayer, + FileSystemWithExtrasLayer +] as any as Layer.Layer + === [R4] Refactor 1: "Toggle type annotation" === --- file:///.src/layerMagic_build.ts --- -// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36 +// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36, 57:14-57:41 import { Effect, Layer, Context } from "effect" class DbConnection extends Context.Service()("DbConnection", { @@ -810,11 +946,21 @@ export const tooLessOutput = [Cache.Default] as any as Layer.Layer export const missingImplementations = [UserRepository.Default, FileSystem.Default] as any as Layer.Layer +const RequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const AlsoRequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const FileSystemWithExtrasLayer = Layer.mergeAll(FileSystem.Default, DbConnection.Default, Layer.succeed(Cache, {})) + +export const onlyFileSystemFromLastLayer = [ + RequiresFileSystemLayer, + AlsoRequiresFileSystemLayer, + FileSystemWithExtrasLayer +] as any as Layer.Layer + === [R4] Refactor 2: "Toggle lazy const" === --- file:///.src/layerMagic_build.ts --- -// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36 +// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36, 57:14-57:41 import { Effect, Layer, Context } from "effect" class DbConnection extends Context.Service()("DbConnection", { @@ -866,11 +1012,21 @@ export const tooLessOutput = [Cache.Default] as any as Layer.Layer export const missingImplementations = [UserRepository.Default, FileSystem.Default] as any as Layer.Layer +const RequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const AlsoRequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const FileSystemWithExtrasLayer = Layer.mergeAll(FileSystem.Default, DbConnection.Default, Layer.succeed(Cache, {})) + +export const onlyFileSystemFromLastLayer = [ + RequiresFileSystemLayer, + AlsoRequiresFileSystemLayer, + FileSystemWithExtrasLayer +] as any as Layer.Layer + === [R4] Refactor 3: "Compose layers automatically with target output services" === --- file:///.src/layerMagic_build.ts --- -// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36 +// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36, 57:14-57:41 import { Effect, Layer, Context } from "effect" class DbConnection extends Context.Service()("DbConnection", { @@ -920,11 +1076,21 @@ export const tooLessOutput = [Cache.Default] as any as Layer.Layer export const missingImplementations = [UserRepository.Default, FileSystem.Default] as any as Layer.Layer +const RequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const AlsoRequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const FileSystemWithExtrasLayer = Layer.mergeAll(FileSystem.Default, DbConnection.Default, Layer.succeed(Cache, {})) + +export const onlyFileSystemFromLastLayer = [ + RequiresFileSystemLayer, + AlsoRequiresFileSystemLayer, + FileSystemWithExtrasLayer +] as any as Layer.Layer + === [R5] Refactor 0: "Wrap with pipe" === --- file:///.src/layerMagic_build.ts --- -// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36 +// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36, 57:14-57:41 import { Effect, Layer, Context } from "effect" class DbConnection extends Context.Service()("DbConnection", { @@ -976,11 +1142,21 @@ export const pipe(tooLessOutput) = [Cache.Default] as any as Layer.Layer export const missingImplementations = [UserRepository.Default, FileSystem.Default] as any as Layer.Layer +const RequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const AlsoRequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const FileSystemWithExtrasLayer = Layer.mergeAll(FileSystem.Default, DbConnection.Default, Layer.succeed(Cache, {})) + +export const onlyFileSystemFromLastLayer = [ + RequiresFileSystemLayer, + AlsoRequiresFileSystemLayer, + FileSystemWithExtrasLayer +] as any as Layer.Layer + === [R5] Refactor 1: "Toggle type annotation" === --- file:///.src/layerMagic_build.ts --- -// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36 +// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36, 57:14-57:41 import { Effect, Layer, Context } from "effect" class DbConnection extends Context.Service()("DbConnection", { @@ -1032,11 +1208,21 @@ export const tooLessOutput: Layer.Layer = [Cache.Default] a export const missingImplementations = [UserRepository.Default, FileSystem.Default] as any as Layer.Layer +const RequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const AlsoRequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const FileSystemWithExtrasLayer = Layer.mergeAll(FileSystem.Default, DbConnection.Default, Layer.succeed(Cache, {})) + +export const onlyFileSystemFromLastLayer = [ + RequiresFileSystemLayer, + AlsoRequiresFileSystemLayer, + FileSystemWithExtrasLayer +] as any as Layer.Layer + === [R5] Refactor 2: "Toggle lazy const" === --- file:///.src/layerMagic_build.ts --- -// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36 +// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36, 57:14-57:41 import { Effect, Layer, Context } from "effect" class DbConnection extends Context.Service()("DbConnection", { @@ -1088,11 +1274,21 @@ export const tooLessOutput = () => [Cache.Default] as any as Layer.Layer export const missingImplementations = [UserRepository.Default, FileSystem.Default] as any as Layer.Layer +const RequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const AlsoRequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const FileSystemWithExtrasLayer = Layer.mergeAll(FileSystem.Default, DbConnection.Default, Layer.succeed(Cache, {})) + +export const onlyFileSystemFromLastLayer = [ + RequiresFileSystemLayer, + AlsoRequiresFileSystemLayer, + FileSystemWithExtrasLayer +] as any as Layer.Layer + === [R6] Refactor 0: "Wrap with pipe" === --- file:///.src/layerMagic_build.ts --- -// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36 +// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36, 57:14-57:41 import { Effect, Layer, Context } from "effect" class DbConnection extends Context.Service()("DbConnection", { @@ -1144,11 +1340,21 @@ export const tooLessOutput = [Cache.Default] as any as Layer.Layer export const pipe(missingImplementations) = [UserRepository.Default, FileSystem.Default] as any as Layer.Layer +const RequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const AlsoRequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const FileSystemWithExtrasLayer = Layer.mergeAll(FileSystem.Default, DbConnection.Default, Layer.succeed(Cache, {})) + +export const onlyFileSystemFromLastLayer = [ + RequiresFileSystemLayer, + AlsoRequiresFileSystemLayer, + FileSystemWithExtrasLayer +] as any as Layer.Layer + === [R6] Refactor 1: "Toggle type annotation" === --- file:///.src/layerMagic_build.ts --- -// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36 +// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36, 57:14-57:41 import { Effect, Layer, Context } from "effect" class DbConnection extends Context.Service()("DbConnection", { @@ -1200,11 +1406,21 @@ export const tooLessOutput = [Cache.Default] as any as Layer.Layer export const missingImplementations: Layer.Layer = [UserRepository.Default, FileSystem.Default] as any as Layer.Layer +const RequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const AlsoRequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const FileSystemWithExtrasLayer = Layer.mergeAll(FileSystem.Default, DbConnection.Default, Layer.succeed(Cache, {})) + +export const onlyFileSystemFromLastLayer = [ + RequiresFileSystemLayer, + AlsoRequiresFileSystemLayer, + FileSystemWithExtrasLayer +] as any as Layer.Layer + === [R6] Refactor 2: "Toggle lazy const" === --- file:///.src/layerMagic_build.ts --- -// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36 +// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36, 57:14-57:41 import { Effect, Layer, Context } from "effect" class DbConnection extends Context.Service()("DbConnection", { @@ -1256,11 +1472,21 @@ export const tooLessOutput = [Cache.Default] as any as Layer.Layer export const missingImplementations = () => [UserRepository.Default, FileSystem.Default] as any as Layer.Layer +const RequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const AlsoRequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const FileSystemWithExtrasLayer = Layer.mergeAll(FileSystem.Default, DbConnection.Default, Layer.succeed(Cache, {})) + +export const onlyFileSystemFromLastLayer = [ + RequiresFileSystemLayer, + AlsoRequiresFileSystemLayer, + FileSystemWithExtrasLayer +] as any as Layer.Layer + === [R6] Refactor 3: "Compose layers automatically with target output services" === --- file:///.src/layerMagic_build.ts --- -// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36 +// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36, 57:14-57:41 import { Effect, Layer, Context } from "effect" class DbConnection extends Context.Service()("DbConnection", { @@ -1310,5 +1536,275 @@ export const provideRequireSame = [FileSystem.bothInAndOut, FileSystem.Default, export const tooLessOutput = [Cache.Default] as any as Layer.Layer -export const missingImplementations = UserRepository.Default.pipe(Layer.provide(FileSystem.Default)) /* Unable to find Cache in the provided layers. */; +export const missingImplementations = Layer.empty.pipe(Layer.provide(UserRepository.Default), Layer.provide(FileSystem.Default)) /* Unable to find Cache in the provided layers. */; + +const RequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const AlsoRequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const FileSystemWithExtrasLayer = Layer.mergeAll(FileSystem.Default, DbConnection.Default, Layer.succeed(Cache, {})) + +export const onlyFileSystemFromLastLayer = [ + RequiresFileSystemLayer, + AlsoRequiresFileSystemLayer, + FileSystemWithExtrasLayer +] as any as Layer.Layer + + +=== [R7] Refactor 0: "Wrap with pipe" === + +--- file:///.src/layerMagic_build.ts --- +// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36, 57:14-57:41 +import { Effect, Layer, Context } from "effect" + +class DbConnection extends Context.Service()("DbConnection", { + make: Effect.succeed({}) +}) { + static Default = Layer.effect(this, this.make) +} +class FileSystem extends Context.Service()("FileSystem", { + make: Effect.succeed({}) +}) { + static Default = Layer.effect(this, this.make) + static bothInAndOut = Layer.effect(FileSystem, FileSystem) +} +class Cache extends Context.Service()("Cache", { + make: Effect.as(FileSystem, {}) +}) { + static Default = Layer.effect(this, this.make) +} +class UserRepository extends Context.Service()("UserRepository", { + make: Effect.as(Effect.andThen(DbConnection, Cache), {}) +}) { + static Default = Layer.effect(this, this.make) +} + +export const expect_Cache_provideMergeFileSystem = [ + FileSystem.Default, + Cache.Default +] as any as Layer.Layer + +export const prepareSomewhatComplex = [ + DbConnection.Default, + Cache.Default, + UserRepository.Default, + FileSystem.Default +] as any as Layer.Layer + +export const prepareSomewhatComplex2 = [ + DbConnection.Default, + Cache.Default, + UserRepository.Default, + FileSystem.Default +] as any as Layer.Layer + +export const provideRequireSame = [FileSystem.bothInAndOut, FileSystem.Default, Cache.Default] as any as Layer.Layer< + Cache | FileSystem +> + +export const tooLessOutput = [Cache.Default] as any as Layer.Layer + +export const missingImplementations = [UserRepository.Default, FileSystem.Default] as any as Layer.Layer + +const RequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const AlsoRequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const FileSystemWithExtrasLayer = Layer.mergeAll(FileSystem.Default, DbConnection.Default, Layer.succeed(Cache, {})) + +export const pipe(onlyFileSystemFromLastLayer) = [ + RequiresFileSystemLayer, + AlsoRequiresFileSystemLayer, + FileSystemWithExtrasLayer +] as any as Layer.Layer + + +=== [R7] Refactor 1: "Toggle type annotation" === + +--- file:///.src/layerMagic_build.ts --- +// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36, 57:14-57:41 +import { Effect, Layer, Context } from "effect" + +class DbConnection extends Context.Service()("DbConnection", { + make: Effect.succeed({}) +}) { + static Default = Layer.effect(this, this.make) +} +class FileSystem extends Context.Service()("FileSystem", { + make: Effect.succeed({}) +}) { + static Default = Layer.effect(this, this.make) + static bothInAndOut = Layer.effect(FileSystem, FileSystem) +} +class Cache extends Context.Service()("Cache", { + make: Effect.as(FileSystem, {}) +}) { + static Default = Layer.effect(this, this.make) +} +class UserRepository extends Context.Service()("UserRepository", { + make: Effect.as(Effect.andThen(DbConnection, Cache), {}) +}) { + static Default = Layer.effect(this, this.make) +} + +export const expect_Cache_provideMergeFileSystem = [ + FileSystem.Default, + Cache.Default +] as any as Layer.Layer + +export const prepareSomewhatComplex = [ + DbConnection.Default, + Cache.Default, + UserRepository.Default, + FileSystem.Default +] as any as Layer.Layer + +export const prepareSomewhatComplex2 = [ + DbConnection.Default, + Cache.Default, + UserRepository.Default, + FileSystem.Default +] as any as Layer.Layer + +export const provideRequireSame = [FileSystem.bothInAndOut, FileSystem.Default, Cache.Default] as any as Layer.Layer< + Cache | FileSystem +> + +export const tooLessOutput = [Cache.Default] as any as Layer.Layer + +export const missingImplementations = [UserRepository.Default, FileSystem.Default] as any as Layer.Layer + +const RequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const AlsoRequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const FileSystemWithExtrasLayer = Layer.mergeAll(FileSystem.Default, DbConnection.Default, Layer.succeed(Cache, {})) + +export const onlyFileSystemFromLastLayer: Layer.Layer = [ + RequiresFileSystemLayer, + AlsoRequiresFileSystemLayer, + FileSystemWithExtrasLayer +] as any as Layer.Layer + + +=== [R7] Refactor 2: "Toggle lazy const" === + +--- file:///.src/layerMagic_build.ts --- +// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36, 57:14-57:41 +import { Effect, Layer, Context } from "effect" + +class DbConnection extends Context.Service()("DbConnection", { + make: Effect.succeed({}) +}) { + static Default = Layer.effect(this, this.make) +} +class FileSystem extends Context.Service()("FileSystem", { + make: Effect.succeed({}) +}) { + static Default = Layer.effect(this, this.make) + static bothInAndOut = Layer.effect(FileSystem, FileSystem) +} +class Cache extends Context.Service()("Cache", { + make: Effect.as(FileSystem, {}) +}) { + static Default = Layer.effect(this, this.make) +} +class UserRepository extends Context.Service()("UserRepository", { + make: Effect.as(Effect.andThen(DbConnection, Cache), {}) +}) { + static Default = Layer.effect(this, this.make) +} + +export const expect_Cache_provideMergeFileSystem = [ + FileSystem.Default, + Cache.Default +] as any as Layer.Layer + +export const prepareSomewhatComplex = [ + DbConnection.Default, + Cache.Default, + UserRepository.Default, + FileSystem.Default +] as any as Layer.Layer + +export const prepareSomewhatComplex2 = [ + DbConnection.Default, + Cache.Default, + UserRepository.Default, + FileSystem.Default +] as any as Layer.Layer + +export const provideRequireSame = [FileSystem.bothInAndOut, FileSystem.Default, Cache.Default] as any as Layer.Layer< + Cache | FileSystem +> + +export const tooLessOutput = [Cache.Default] as any as Layer.Layer + +export const missingImplementations = [UserRepository.Default, FileSystem.Default] as any as Layer.Layer + +const RequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const AlsoRequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const FileSystemWithExtrasLayer = Layer.mergeAll(FileSystem.Default, DbConnection.Default, Layer.succeed(Cache, {})) + +export const onlyFileSystemFromLastLayer = () => [ + RequiresFileSystemLayer, + AlsoRequiresFileSystemLayer, + FileSystemWithExtrasLayer +] as any as Layer.Layer + + +=== [R7] Refactor 3: "Compose layers automatically with target output services" === + +--- file:///.src/layerMagic_build.ts --- +// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36, 57:14-57:41 +import { Effect, Layer, Context } from "effect" + +class DbConnection extends Context.Service()("DbConnection", { + make: Effect.succeed({}) +}) { + static Default = Layer.effect(this, this.make) +} +class FileSystem extends Context.Service()("FileSystem", { + make: Effect.succeed({}) +}) { + static Default = Layer.effect(this, this.make) + static bothInAndOut = Layer.effect(FileSystem, FileSystem) +} +class Cache extends Context.Service()("Cache", { + make: Effect.as(FileSystem, {}) +}) { + static Default = Layer.effect(this, this.make) +} +class UserRepository extends Context.Service()("UserRepository", { + make: Effect.as(Effect.andThen(DbConnection, Cache), {}) +}) { + static Default = Layer.effect(this, this.make) +} + +export const expect_Cache_provideMergeFileSystem = [ + FileSystem.Default, + Cache.Default +] as any as Layer.Layer + +export const prepareSomewhatComplex = [ + DbConnection.Default, + Cache.Default, + UserRepository.Default, + FileSystem.Default +] as any as Layer.Layer + +export const prepareSomewhatComplex2 = [ + DbConnection.Default, + Cache.Default, + UserRepository.Default, + FileSystem.Default +] as any as Layer.Layer + +export const provideRequireSame = [FileSystem.bothInAndOut, FileSystem.Default, Cache.Default] as any as Layer.Layer< + Cache | FileSystem +> + +export const tooLessOutput = [Cache.Default] as any as Layer.Layer + +export const missingImplementations = [UserRepository.Default, FileSystem.Default] as any as Layer.Layer + +const RequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const AlsoRequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const FileSystemWithExtrasLayer = Layer.mergeAll(FileSystem.Default, DbConnection.Default, Layer.succeed(Cache, {})) + +export const onlyFileSystemFromLastLayer = Layer.empty.pipe(Layer.provide(AlsoRequiresFileSystemLayer), Layer.provide(RequiresFileSystemLayer), Layer.provideMerge(FileSystemWithExtrasLayer)); diff --git a/testdata/tests/effect-v4-refactors/layerMagic_build.ts b/testdata/tests/effect-v4-refactors/layerMagic_build.ts index 834c0a19..c66a5b70 100644 --- a/testdata/tests/effect-v4-refactors/layerMagic_build.ts +++ b/testdata/tests/effect-v4-refactors/layerMagic_build.ts @@ -1,4 +1,4 @@ -// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36 +// refactor: 26:14-26:49, 31:14-31:36, 38:14-38:37, 45:14-45:32, 49:14-49:27, 51:14-51:36, 57:14-57:41 import { Effect, Layer, Context } from "effect" class DbConnection extends Context.Service()("DbConnection", { @@ -49,3 +49,13 @@ export const provideRequireSame = [FileSystem.bothInAndOut, FileSystem.Default, export const tooLessOutput = [Cache.Default] as any as Layer.Layer export const missingImplementations = [UserRepository.Default, FileSystem.Default] as any as Layer.Layer + +const RequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const AlsoRequiresFileSystemLayer = Layer.effectDiscard(FileSystem) +const FileSystemWithExtrasLayer = Layer.mergeAll(FileSystem.Default, DbConnection.Default, Layer.succeed(Cache, {})) + +export const onlyFileSystemFromLastLayer = [ + RequiresFileSystemLayer, + AlsoRequiresFileSystemLayer, + FileSystemWithExtrasLayer +] as any as Layer.Layer