SourceTanglingBuild settings for a file

Part V, Chapter 4

Part V, Chapter 4: Build settings for a file

Package: packages/loom-lang/src/PackageConfig.ts

Language: TypeScript

Raw source

Build settings for a file

PackageConfig reads a .loom file's configuration and gives the compiler the settings it needs to build that file: the anchor delimiters its sections cut with, the primary language a section without a specifier inherits, and the root its sinks tangle under. The formatter reads the same settings, for the width the file's lines keep to. It reads through @athrio/loom-config, which resolves the file's configuration from the one workspace manifest — a project has no per-package configuration. This service turns that resolved configuration into the compiler's own terms.

What the compiler reads

BuildSettings is the answer the compiler gets for one file. It holds a full anchor delimiter pair, the file's primary language, the workspace variables a warp interpolates, the width its lines keep to, the root the file's package tangles under, the workspace root a book's higher-order sink resolves its directories against, the corpus directory the compiler enumerates to discover the file's sibling looms, and the patterns of files the project ignores.

The anchor delimiters are always a complete pair. A configuration may set one side, both, or neither, so anchorDelimsOf fills each missing side from defaultAnchorDelims, the built-in ::[ and ]. A package overrides a delimiter only where the default would collide with its own product language.

The primary language is the language a specifier-less section inherits. It comes from the workspace configuration, and is absent when the configuration names none. The product pass reads it as the default beneath the document's own lang Warp, so a project can name its language once and let every document drop the Warp.

The variables are the workspace's named strings, the values a warp interpolates with ${name}. They come from the configuration and are empty when it declares none, so a document that names no variable composes untouched.

The width is the column the file's lines keep to, which the formatter fits a code section to. The configuration supplies it, a hundred columns when it names none.

The package root is the absolute directory the file's package tangles into — the directory holding the file's corpus/. @athrio/loom-config derives it from that convention, and it is absent when the file sits in no corpus directory.

The ignore patterns are the ones the configuration lists under ignore, absent when it lists none. Together with the workspace root they make up the file's project, which decides the looms the compiler gathers from the corpus directory.

TypeScript
export interface BuildSettings {
  readonly delims: AnchorDelims
  readonly primaryLanguage: string | undefined
  readonly variables: Record<string, string>
  readonly width: number
  readonly packageRoot: string | undefined
  readonly workspaceRoot: string | undefined
  readonly corpusDir: string | undefined
  readonly ignore?: ReadonlyArray<string>
}

const anchorDelimsOf = (
  anchor: { readonly open?: string; readonly close?: string } | undefined,
): AnchorDelims => ({
  open: anchor?.open ?? defaultAnchorDelims.open,
  close: anchor?.close ?? defaultAnchorDelims.close,
})

The service

PackageConfig is the service the compiler depends on, so it reaches build settings through the same dependency injection it uses for every other pass. It depends on LoomConfig and reads through it, holding no file logic of its own. Its resolve takes a file's path, asks LoomConfig for that file's package configuration, and maps the answer to a BuildSettings.

TypeScript
export class PackageConfig extends Context.Service<PackageConfig>()('PackageConfig', {
  make: Effect.gen(function* () {
    const config = yield* LoomConfig
    return {
      resolve: (path: Path): Effect.Effect<BuildSettings> =>
        config.resolve(path).pipe(
          Effect.map((c) => ({
            delims: anchorDelimsOf(c.anchor),
            primaryLanguage: c.primary,
            variables: c.variables,
            width: c.width,
            packageRoot: c.packageRoot,
            workspaceRoot: c.workspaceRoot,
            corpusDir: c.corpusDir,
            ignore: c.ignore,
          })),
        ),
    }
  }),
}) {
  static readonly layer = Layer.effect(this, this.make).pipe(Layer.provide(LoomConfig.layer))
}

What this module draws on

Effect supplies the service and the effect type. LoomConfig, from @athrio/loom-config, finds and reads a file's configuration. AnchorDelims and defaultAnchorDelims come from the token layer, where the built-in delimiters live, and Path types a file path.

TypeScript
import { Context, Effect, Layer } from 'effect'
import { LoomConfig } from '@athrio/loom-config/LoomConfig'
import { type AnchorDelims, defaultAnchorDelims } from '@athrio/loom-ast/LoomTokens'
import { type Path } from '@athrio/loom-ast/LoomCorpusAst'
Loom