Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions context/effect-4/recipes/schema-codec-members.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,21 @@ The Date pair is another surviving-name trap: v3 `DateFromSelf` becomes v4 `Date
wire schemas become v4 `DateFromString`. Keep those rewrites separate and follow the
`schema-date` recipe for the wire contract.

## Pretty-printed JSON

V4 has no pretty-print JSON codec. `Schema.fromJsonString(S)` accepts no formatting options and
serializes with plain `JSON.stringify`. When formatted JSON is a persisted wire format, encode the
value with the schema and stringify the encoded JSON value separately:

```ts
const encoded = Schema.encodeSync(Config)(value)
const content = JSON.stringify(encoded, null, 2)
```

Use `Schema.encodeEffect` instead when the surrounding path handles schema failures as an Effect.
Preserve any existing trailing newline separately. Compare the resulting bytes against the v3
baseline; accepting file churn is not a mechanical migration.

## Affected inventory

The measured codec/member heatmap is:
Expand Down
5 changes: 2 additions & 3 deletions packages/@overeng/megarepo/bin/mr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,9 @@ const program = Effect.gen(function* () {
metricsExportInterval: 1000,
})

yield* Cli.Command.run(mrCommand, {
name: 'mr',
yield* Cli.Command.runWith(mrCommand, {
version,
})(rewriteHelpSubcommand(process.argv)).pipe(
})(rewriteHelpSubcommand(process.argv).slice(2)).pipe(
Effect.scoped,
CliVersion.enrichErrors,
Effect.provideService(CliVersion, { name: 'mr', version }),
Expand Down
37 changes: 13 additions & 24 deletions packages/@overeng/megarepo/src/cli/cli.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@

import { NodeServices } from '@effect/platform-node'
import { describe, it } from '@effect/vitest'
import { FileSystem } from 'effect/FileSystem'
import * as FileSystem from 'effect/FileSystem'
import { Effect, Exit, Option, Schema } from 'effect'
import * as Cli from 'effect/unstable/cli'
import { expect } from 'vitest'

import { EffectPath, type AbsoluteDirPath } from '@overeng/effect-path'

import { CONFIG_FILE_NAME_JSON, MegarepoConfig, validateMemberName } from '../lib/config.ts'
import { encodePrettyJson } from '../lib/json.ts'
import { makeConsoleCapture } from '../test-utils/consoleCapture.ts'
import { initGitRepo, readConfig } from '../test-utils/setup.ts'
import { mrCommand } from './mod.ts'
Expand Down Expand Up @@ -81,9 +82,7 @@ describe('mr init', () => {
'https://raw.githubusercontent.com/overengineeringstudio/megarepo/main/schema/megarepo.schema.json',
members: {},
}
const configContent = yield* Schema.encode(
Schema.fromJsonString(MegarepoConfig, { space: 2 }),
)(initialConfig)
const configContent = yield* encodePrettyJson(MegarepoConfig)(initialConfig)
yield* fs.writeFileString(configPath, configContent + '\n')

// Verify config was created
Expand Down Expand Up @@ -119,9 +118,7 @@ describe('mr init', () => {
workDir,
EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON),
)
const configContent = yield* Schema.encode(
Schema.fromJsonString(MegarepoConfig, { space: 2 }),
)(existingConfig)
const configContent = yield* encodePrettyJson(MegarepoConfig)(existingConfig)
yield* fs.writeFileString(configPath, configContent + '\n')

// Verify existing config
Expand Down Expand Up @@ -283,9 +280,7 @@ describe('mr add', () => {
EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON),
)
const initialConfig: MegarepoConfig = { members: {} }
const initialContent = yield* Schema.encode(
Schema.fromJsonString(MegarepoConfig, { space: 2 }),
)(initialConfig)
const initialContent = yield* encodePrettyJson(MegarepoConfig)(initialConfig)
yield* fs.writeFileString(configPath, initialContent + '\n')

// Add a member
Expand All @@ -298,9 +293,7 @@ describe('mr add', () => {
...config,
members: { ...config.members, [memberName]: memberSource },
}
const updatedContent = yield* Schema.encode(
Schema.fromJsonString(MegarepoConfig, { space: 2 }),
)(updatedConfig)
const updatedContent = yield* encodePrettyJson(MegarepoConfig)(updatedConfig)
yield* fs.writeFileString(configPath, updatedContent + '\n')

// Verify member was added
Expand Down Expand Up @@ -339,9 +332,7 @@ describe('mr add', () => {
...config,
members: { ...config.members, [customName]: memberSource },
}
const updatedContent = yield* Schema.encode(
Schema.fromJsonString(MegarepoConfig, { space: 2 }),
)(updatedConfig)
const updatedContent = yield* encodePrettyJson(MegarepoConfig)(updatedConfig)
yield* fs.writeFileString(configPath, updatedContent + '\n')

const finalConfig = yield* readConfig(workDir)
Expand Down Expand Up @@ -379,9 +370,7 @@ describe('mr add', () => {
const initialConfig: MegarepoConfig = {
members: { effect: 'effect-ts/effect' },
}
const initialContent = yield* Schema.encode(
Schema.fromJsonString(MegarepoConfig, { space: 2 }),
)(initialConfig)
const initialContent = yield* encodePrettyJson(MegarepoConfig)(initialConfig)
yield* fs.writeFileString(configPath, initialContent + '\n')

// Check that member already exists
Expand Down Expand Up @@ -428,7 +417,7 @@ describe('megarepo.json parsing', () => {
local: './packages/local',
},
}
const content = yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))(
const content = yield* encodePrettyJson(MegarepoConfig)(
config,
)
yield* fs.writeFileString(configPath, content + '\n')
Expand Down Expand Up @@ -465,7 +454,7 @@ describe('megarepo.json parsing', () => {
vscode: { enabled: true, exclude: ['large-repo'] },
},
}
const content = yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))(
const content = yield* encodePrettyJson(MegarepoConfig)(
config,
)
yield* fs.writeFileString(configPath, content + '\n')
Expand Down Expand Up @@ -496,7 +485,7 @@ const runRootWithCwd = ({ cwdPath }: { cwdPath: string }) =>
const { consoleLayer, getStdoutLines } = yield* makeConsoleCapture

const argv = ['node', 'mr', '--cwd', cwdPath, 'root', '--output', 'json']
const effect = Cli.Command.run(mrCommand, { name: 'mr', version: 'test' })(argv).pipe(
const effect = Cli.Command.runWith(mrCommand, { version: 'test' })(argv.slice(2)).pipe(
Effect.provide(consoleLayer),
)
const exit = yield* Effect.exit(effect)
Expand All @@ -505,7 +494,7 @@ const runRootWithCwd = ({ cwdPath }: { cwdPath: string }) =>

let state: RootState | undefined
if (stdout.trim() !== '') {
state = yield* Schema.decodeUnknown(Schema.fromJsonString(RootState))(stdout)
state = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(RootState))(stdout)
}

return {
Expand Down Expand Up @@ -575,7 +564,7 @@ describe('--cwd option', () => {

const cwdPath = '/nonexistent/path/that/does/not/exist/'
const argv = ['node', 'mr', '--cwd', cwdPath, 'root', '--output', 'json']
const effect = Cli.Command.run(mrCommand, { name: 'mr', version: 'test' })(argv).pipe(
const effect = Cli.Command.runWith(mrCommand, { version: 'test' })(argv.slice(2)).pipe(
Effect.provide(consoleLayer),
)
const exit = yield* Effect.exit(effect)
Expand Down
5 changes: 3 additions & 2 deletions packages/@overeng/megarepo/src/cli/commands/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import * as Cli from 'effect/unstable/cli'
import { EffectPath } from '@overeng/effect-path'

import { readMegarepoConfig } from '../../lib/config.ts'
import { encodePrettyJson } from '../../lib/json.ts'
import { LOCK_FILE_NAME, readLockFile } from '../../lib/lock.ts'
import { checkSourcePolicy, formatSourcePolicyViolation } from '../../lib/source-policy.ts'
import { Cwd, findMegarepoRoot, jsonOption } from '../context.ts'
import { CheckCommandError, LockFileRequiredError, NotInMegarepoError } from '../errors.ts'
import * as Observability from '../observability.ts'

/** Encodes the structured check result as pretty-printed JSON for `--json` output. */
const CheckReportJson = Schema.fromJsonString(Schema.Unknown, { space: 2 })
const CheckReportJson = Schema.Unknown

const allOption = Cli.Flag.boolean('all').pipe(
Cli.Flag.withDescription('Check member source and lock files in repos/ as well as the root'),
Expand Down Expand Up @@ -63,7 +64,7 @@ export const checkCommand = Cli.Command.make(
}

if (json === true) {
yield* Console.log(yield* Schema.encode(CheckReportJson)(result))
yield* Console.log(yield* encodePrettyJson(CheckReportJson)(result))
} else if (result.violations.length === 0) {
yield* Console.log('Megarepo checks OK')
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* Matching is done by canonical URL (org/repo), not by member name.
*/

import { FileSystem } from 'effect/FileSystem'
import * as FileSystem from 'effect/FileSystem'
import { Effect, Option } from 'effect'
import * as Cli from 'effect/unstable/cli'
import React from 'react'
Expand Down
19 changes: 10 additions & 9 deletions packages/@overeng/megarepo/src/cli/commands/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@

import { Prompt } from 'effect/unstable/cli'
import type { ChildProcessSpawner as CommandExecutor } from 'effect/unstable/process'
import type { Terminal } from 'effect/Terminal'
import type { Error as PlatformError } from 'effect'
import { FileSystem } from 'effect/FileSystem'
import { Clock, Effect, Option, type ParseResult } from 'effect'
import * as Terminal from 'effect/Terminal'
import * as PlatformError from 'effect/PlatformError'
import * as FileSystem from 'effect/FileSystem'
import { Clock, Effect, Option } from 'effect'
import * as SchemaError from 'effect/SchemaError'
import React from 'react'

import { EffectPath, type AbsoluteDirPath } from '@overeng/effect-path'
Expand Down Expand Up @@ -126,9 +127,9 @@ export const syncMegarepo = <R = never>({
| StoreHygieneError
| ConfigNotFoundError
| PlatformError.PlatformError
| ParseResult.ParseError
| SchemaError.SchemaError
| Error,
FileSystem.FileSystem | CommandExecutor.CommandExecutor | Store | StoreLock | R
FileSystem.FileSystem | CommandExecutor.ChildProcessSpawner | Store | StoreLock | R
> =>
Effect.gen(function* () {
const { mode, dryRun, force, all, only, skip, gitProtocol, createBranches } = options
Expand Down Expand Up @@ -325,7 +326,7 @@ export const syncMegarepo = <R = never>({

if (linkTarget !== null) {
if (dryRun === false) {
yield* fs.remove(entryPath).pipe(Effect.catchAll(() => Effect.void))
yield* fs.remove(entryPath).pipe(Effect.catch(() => Effect.void))
}
return {
name: entry,
Expand Down Expand Up @@ -511,7 +512,7 @@ export const syncMegarepo = <R = never>({
...(onMissingRef !== undefined ? { onMissingRef } : {}),
})
}).pipe(
Effect.catchAll((error) =>
Effect.catch((error) =>
Effect.succeed({
root: nestedRoot,
results: [
Expand Down Expand Up @@ -594,7 +595,7 @@ const createMissingRefPrompt = (
})

return yield* prompt.pipe(
Effect.catchTag('QuitException', () => Effect.succeed('abort' as const)),
Effect.catchTag('QuitError', () => Effect.succeed('abort' as const)),
)
})

Expand Down
9 changes: 5 additions & 4 deletions packages/@overeng/megarepo/src/cli/commands/ls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
* List all members in the megarepo.
*/

import type { Error as PlatformError } from 'effect'
import { FileSystem } from 'effect/FileSystem'
import { Effect, Option, type ParseResult } from 'effect'
import * as PlatformError from 'effect/PlatformError'
import * as FileSystem from 'effect/FileSystem'
import { Effect, Option } from 'effect'
import * as SchemaError from 'effect/SchemaError'
import * as Cli from 'effect/unstable/cli'
import React from 'react'

Expand Down Expand Up @@ -50,7 +51,7 @@ const scanMembersRecursive = ({
depth?: number
}): Effect.Effect<
MemberInfo[],
PlatformError.PlatformError | ParseResult.ParseError | Error,
PlatformError.PlatformError | SchemaError.SchemaError | Error,
FileSystem.FileSystem
> =>
Effect.gen(function* () {
Expand Down
13 changes: 7 additions & 6 deletions packages/@overeng/megarepo/src/cli/commands/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
*/

import type { ChildProcessSpawner as CommandExecutor } from 'effect/unstable/process'
import type { Error as PlatformError } from 'effect'
import { FileSystem } from 'effect/FileSystem'
import { Clock, Effect, Option, type ParseResult } from 'effect'
import * as PlatformError from 'effect/PlatformError'
import * as FileSystem from 'effect/FileSystem'
import { Clock, Effect, Option } from 'effect'
import * as SchemaError from 'effect/SchemaError'
import * as Cli from 'effect/unstable/cli'
import React from 'react'

Expand Down Expand Up @@ -65,8 +66,8 @@ const scanMembersRecursive = ({
depth?: number
}): Effect.Effect<
MemberStatus[],
PlatformError.PlatformError | ParseResult.ParseError | Error,
FileSystem.FileSystem | CommandExecutor.CommandExecutor | Store
PlatformError.PlatformError | SchemaError.SchemaError | Error,
FileSystem.FileSystem | CommandExecutor.ChildProcessSpawner | Store
> =>
Effect.gen(function* () {
const enterResult = yield* traversal.enterRoot({ root: megarepoRoot, depth })
Expand Down Expand Up @@ -404,7 +405,7 @@ export const statusCommand = Cli.Command.make(
})
const memberRealPath = yield* fs
.realPath(memberSymlinkPath.replace(/\/$/, ''))
.pipe(Effect.catchAll(() => Effect.void))
.pipe(Effect.catch(() => Effect.void))

if (memberRealPath !== undefined) {
const memberRealPathNorm = memberRealPath.replace(/\/$/, '')
Expand Down
2 changes: 1 addition & 1 deletion packages/@overeng/megarepo/src/cli/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import { resolve } from 'node:path'

import { FileSystem } from 'effect/FileSystem'
import * as FileSystem from 'effect/FileSystem'
import { Context, Effect, Layer, Option } from 'effect'
import * as Cli from 'effect/unstable/cli'

Expand Down
5 changes: 2 additions & 3 deletions packages/@overeng/megarepo/src/cli/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ export const mrCommand = Cli.Command.make('mr', { cwd: cwdOption }).pipe(
)

/** Exported CLI for external use */
export const cli = Cli.Command.run(mrCommand, {
name: 'mr',
export const cli = Cli.Command.runWith(mrCommand, {
version: MR_VERSION,
})(rewriteHelpSubcommand(process.argv))
})(rewriteHelpSubcommand(process.argv).slice(2))
2 changes: 1 addition & 1 deletion packages/@overeng/megarepo/src/cli/observability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ const trustOtelContract = <A, E, R>(
effect: Effect.Effect<A, E | OtelAttrEncodeError, R>,
): Effect.Effect<A, E, R> =>
effect.pipe(
Effect.catchAll((error) =>
Effect.catch((error) =>
typeof error === 'object' &&
error !== null &&
'_tag' in error &&
Expand Down
11 changes: 5 additions & 6 deletions packages/@overeng/megarepo/src/cli/pin.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import { NodeServices } from '@effect/platform-node'
import { describe, it } from '@effect/vitest'
import { FileSystem } from 'effect/FileSystem'
import * as FileSystem from 'effect/FileSystem'
import { Effect, Option, Schema } from 'effect'
import { expect } from 'vitest'

Expand All @@ -19,6 +19,7 @@ import {
MegarepoConfig,
parseSourceString,
} from '../lib/config.ts'
import { encodePrettyJson } from '../lib/json.ts'
import {
createLockedMember,
LOCK_FILE_NAME,
Expand Down Expand Up @@ -52,7 +53,7 @@ const createMinimalTestSetup = () =>
'test-repo': 'test-owner/test-repo',
},
}
const configContent = yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))(
const configContent = yield* encodePrettyJson(MegarepoConfig)(
config,
)
yield* fs.writeFileString(
Expand Down Expand Up @@ -101,9 +102,7 @@ describe('mr config pin', () => {
workspacePath,
EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON),
)
const newConfigContent = yield* Schema.encode(
Schema.fromJsonString(MegarepoConfig, { space: 2 }),
)(updatedConfig)
const newConfigContent = yield* encodePrettyJson(MegarepoConfig)(updatedConfig)
yield* fs.writeFileString(configPath, newConfigContent + '\n')

// Verify the update
Expand Down Expand Up @@ -135,7 +134,7 @@ describe('mr config pin', () => {
}
yield* fs.writeFileString(
configPath,
(yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))(config1)) +
(yield* encodePrettyJson(MegarepoConfig)(config1)) +
'\n',
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -260,10 +260,10 @@ export type StatusState = Schema.Schema.Type<typeof StatusState>
*
* Status is static output, so we only need SetState to populate the final result.
*/
export const StatusAction = Schema.Union(
export const StatusAction = Schema.Union([
/** Replace entire state */
Schema.TaggedStruct('SetState', { state: StatusState }),
)
])

/** Inferred type for status actions. */
export type StatusAction = Schema.Schema.Type<typeof StatusAction>
Expand Down
Loading
Loading