@graftland/cli 0.4.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Start.d.ts +6 -0
- package/dist/Start.d.ts.map +1 -1
- package/dist/Start.js +8 -3
- package/dist/Start.js.map +1 -1
- package/dist/bin.js +4 -2
- package/dist/bin.js.map +1 -1
- package/dist/internal/env.js +71 -26
- package/dist/internal/env.js.map +1 -1
- package/package.json +6 -6
- package/src/Start.ts +22 -3
- package/src/bin.ts +7 -0
- package/src/internal/env.ts +175 -46
package/dist/Start.d.ts
CHANGED
|
@@ -15,6 +15,12 @@ export interface Options {
|
|
|
15
15
|
* a variable. With `watch`, changes to it are read again.
|
|
16
16
|
*/
|
|
17
17
|
readonly envFile?: string | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* A directory of secret files that `!env` reads when neither the process
|
|
20
|
+
* environment nor `envFile` sets a variable. Each regular file is one
|
|
21
|
+
* variable, named after the file. With `watch`, changes are read again.
|
|
22
|
+
*/
|
|
23
|
+
readonly envDir?: string | undefined;
|
|
18
24
|
}
|
|
19
25
|
/**
|
|
20
26
|
* Loads the entries of the config file and runs until interrupted. Plugin
|
package/dist/Start.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Start.d.ts","names":[],"sources":["../src/Start.ts"],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"Start.d.ts","names":[],"sources":["../src/Start.ts"],"mappings":";;;;;;;iBA0CiB;;WAEP;;WAEA;;;;;WAKA;;;;;;WAMA;;;;;;;qBAQG,MAAG,SACN,YACP,OAAO,aAEP,mBACA,MAAM,kBACN,cAAc,cACd,cAAc,gBACd,OAAO,cACP,OAAO,aACP,KAAK"}
|
package/dist/Start.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { __exportAll } from "./_virtual/_rolldown/runtime.js";
|
|
2
2
|
import { Application, ApplicationError, platform } from "./Application.js";
|
|
3
|
-
import { EnvFile, layer, watchEnvFile } from "./internal/env.js";
|
|
3
|
+
import { EnvDir, EnvFile, layer, watchEnvDir, watchEnvFile } from "./internal/env.js";
|
|
4
4
|
import { layer as layer$1 } from "./internal/intercepts.js";
|
|
5
5
|
import { watchModules } from "./internal/watch.js";
|
|
6
6
|
import { Array, Cause, Deferred, Effect, Exit, FileSystem, Layer, Logger, Option, Path, Scope, Semaphore } from "effect";
|
|
@@ -58,7 +58,8 @@ const run = (options) => Effect.gen(function* () {
|
|
|
58
58
|
}).pipe(Effect.provideService(ModuleResolver.Watch, options.watch ? Option.some(observe) : Option.none()))).await;
|
|
59
59
|
if (options.watch) {
|
|
60
60
|
const { reload } = yield* EnvFile;
|
|
61
|
-
const
|
|
61
|
+
const { reload: reloadDir } = yield* EnvDir;
|
|
62
|
+
const { envFile, envDir } = options;
|
|
62
63
|
const reloader = Plugin.make({
|
|
63
64
|
name: "reloader",
|
|
64
65
|
inject: [
|
|
@@ -70,13 +71,17 @@ const run = (options) => Effect.gen(function* () {
|
|
|
70
71
|
const loader = yield* Loader.Loader;
|
|
71
72
|
yield* watchModules(hmr, directory, loader.retry).pipe(Effect.forkScoped);
|
|
72
73
|
if (envFile !== void 0) yield* watchEnvFile(envFile, reload, loader.retry).pipe(Effect.forkScoped);
|
|
74
|
+
if (envDir !== void 0) yield* watchEnvDir(envDir, reloadDir, loader.retry).pipe(Effect.forkScoped);
|
|
73
75
|
})
|
|
74
76
|
});
|
|
75
77
|
yield* Graft.plugin(reloader);
|
|
76
78
|
}
|
|
77
79
|
yield* Effect.logInfo(`Running ${path.relative(process.cwd(), file) || file}`);
|
|
78
80
|
return yield* Deferred.await(done);
|
|
79
|
-
}).pipe(Effect.scoped, Effect.provideService(Entry.Select, (entry) => entry["cli"] !== true), Effect.provide(Layer.provideMerge(Graft.layer, Layer.mergeAll(platform, layer$1, layer(
|
|
81
|
+
}).pipe(Effect.scoped, Effect.provideService(Entry.Select, (entry) => entry["cli"] !== true), Effect.provide(Layer.provideMerge(Graft.layer, Layer.mergeAll(platform, layer$1, layer({
|
|
82
|
+
file: options.envFile,
|
|
83
|
+
dir: options.envDir
|
|
84
|
+
}).pipe(Layer.provide(NodeServices.layer)), Logger.layer([Logger.consolePretty()])))));
|
|
80
85
|
export { Start_exports, run };
|
|
81
86
|
|
|
82
87
|
//# sourceMappingURL=Start.js.map
|
package/dist/Start.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Start.js","names":["intercepts","env"],"sources":["../src/Start.ts"],"sourcesContent":["/**\n * Runs a Graft application from its config file.\n */\nimport {\n\tArray,\n\tCause,\n\tDeferred,\n\tEffect,\n\tExit,\n\tFileSystem,\n\tLayer,\n\tLogger,\n\tOption,\n\tPath,\n\tScope,\n\tSemaphore,\n\ttype PlatformError,\n\ttype Schema,\n} from \"effect\"\nimport { NodeServices } from \"@effect/platform-node\"\n\nimport { Graft, Plugin } from \"@graftland/graft\"\nimport * as Hmr from \"@graftland/hmr\"\nimport {\n\tEntry,\n\tInclude,\n\tLoader,\n\tModuleResolver,\n\ttype Tree,\n} from \"@graftland/loader\"\n\nimport { Application, ApplicationError, platform } from \"./Application\"\nimport {
|
|
1
|
+
{"version":3,"file":"Start.js","names":["intercepts","env"],"sources":["../src/Start.ts"],"sourcesContent":["/**\n * Runs a Graft application from its config file.\n */\nimport {\n\tArray,\n\tCause,\n\tDeferred,\n\tEffect,\n\tExit,\n\tFileSystem,\n\tLayer,\n\tLogger,\n\tOption,\n\tPath,\n\tScope,\n\tSemaphore,\n\ttype PlatformError,\n\ttype Schema,\n} from \"effect\"\nimport { NodeServices } from \"@effect/platform-node\"\n\nimport { Graft, Plugin } from \"@graftland/graft\"\nimport * as Hmr from \"@graftland/hmr\"\nimport {\n\tEntry,\n\tInclude,\n\tLoader,\n\tModuleResolver,\n\ttype Tree,\n} from \"@graftland/loader\"\n\nimport { Application, ApplicationError, platform } from \"./Application\"\nimport {\n\tlayer as env,\n\tEnvDir,\n\tEnvFile,\n\twatchEnvDir,\n\twatchEnvFile,\n} from \"./internal/env\"\nimport { layer as intercepts } from \"./internal/intercepts\"\nimport { watchModules } from \"./internal/watch\"\n\nexport interface Options {\n\t/** Path of the config file. */\n\treadonly config: string\n\t/** Reload plugin modules and the config file when they change. */\n\treadonly watch: boolean\n\t/**\n\t * A `.env` file that `!env` reads when the process environment does not set\n\t * a variable. With `watch`, changes to it are read again.\n\t */\n\treadonly envFile?: string | undefined\n\t/**\n\t * A directory of secret files that `!env` reads when neither the process\n\t * environment nor `envFile` sets a variable. Each regular file is one\n\t * variable, named after the file. With `watch`, changes are read again.\n\t */\n\treadonly envDir?: string | undefined\n}\n\n/**\n * Loads the entries of the config file and runs until interrupted. Plugin\n * modules are imported through the hot reloader, so bare specifiers resolve\n * from the application directory whether or not `watch` is on.\n */\nexport const run = (\n\toptions: Options,\n): Effect.Effect<\n\tvoid,\n\t| ApplicationError\n\t| Graft.ServiceConflict\n\t| PlatformError.BadArgument\n\t| PlatformError.PlatformError\n\t| Schema.SchemaError\n\t| Loader.StoreError\n\t| Tree.InvalidOperation\n> =>\n\tEffect.gen(function* () {\n\t\tconst scope = yield* Effect.scope\n\t\tconst done = yield* Deferred.make<void, ApplicationError>()\n\t\tconst protocol = yield* Semaphore.make(1)\n\t\tyield* Graft.provide(Application, {\n\t\t\tclaimProtocol: Effect.acquireRelease(\n\t\t\t\tprotocol.takeIfAvailable(1).pipe(\n\t\t\t\t\tEffect.flatMap((acquired) =>\n\t\t\t\t\t\tacquired\n\t\t\t\t\t\t\t? Effect.void\n\t\t\t\t\t\t\t: Effect.fail(\n\t\t\t\t\t\t\t\t\tApplicationError.make({\n\t\t\t\t\t\t\t\t\t\tcause: Cause.fail(\n\t\t\t\t\t\t\t\t\t\t\t\"Only one runner may own the process stdio channel\",\n\t\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t\t() => protocol.release(1),\n\t\t\t),\n\t\t\tfinish: (exit) =>\n\t\t\t\tDeferred.done(\n\t\t\t\t\tdone,\n\t\t\t\t\tExit.isFailure(exit)\n\t\t\t\t\t\t? Exit.fail(ApplicationError.make({ cause: exit.cause }))\n\t\t\t\t\t\t: Exit.void,\n\t\t\t\t).pipe(Effect.asVoid),\n\t\t\tonExit: (release) => Scope.addFinalizer(scope, release),\n\t\t})\n\t\tconst path = yield* Path.Path\n\t\tconst file = path.resolve(options.config)\n\t\tconst directory = path.dirname(file)\n\t\tconst base = yield* path.toFileUrl(file)\n\t\tconst hmr = yield* Hmr.make({ base: base.href })\n\t\tconst resolver = yield* ModuleResolver.make({\n\t\t\tbase: directory,\n\t\t\timporter: hmr.importerScoped,\n\t\t})\n\t\tconst modules = yield* ModuleResolver.make({\n\t\t\tbase: directory,\n\t\t\timporter: options.watch ? hmr.importModuleScoped : hmr.importModule,\n\t\t})\n\t\tyield* Graft.provide(ModuleResolver.ModuleResolver, {\n\t\t\tresolve: resolver.resolve,\n\t\t\timportModule: modules.importModule,\n\t\t})\n\t\tconst fs = yield* FileSystem.FileSystem\n\t\tconst observe = hmr.observation.pipe(\n\t\t\tEffect.provideService(FileSystem.FileSystem, fs),\n\t\t\tEffect.map((source) => ({\n\t\t\t\t...source,\n\t\t\t\twatch: (files: ReadonlyArray<string>, activation: Scope.Scope) =>\n\t\t\t\t\tsource.watch(files, activation).pipe(\n\t\t\t\t\t\tEffect.mapError((cause) =>\n\t\t\t\t\t\t\tModuleResolver.ModuleInvalid.make({\n\t\t\t\t\t\t\t\tspecifier: Array.join(files, \", \"),\n\t\t\t\t\t\t\t\tcause,\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t})),\n\t\t)\n\t\tconst included = yield* Graft.plugin(Include.Include, {\n\t\t\tpath: file,\n\t\t\twatch: options.watch,\n\t\t}).pipe(\n\t\t\tEffect.provideService(\n\t\t\t\tModuleResolver.Watch,\n\t\t\t\toptions.watch ? Option.some(observe) : Option.none(),\n\t\t\t),\n\t\t)\n\t\tyield* included.await\n\t\tif (options.watch) {\n\t\t\tconst { reload } = yield* EnvFile\n\t\t\tconst { reload: reloadDir } = yield* EnvDir\n\t\t\tconst { envFile, envDir } = options\n\t\t\tconst reloader = Plugin.make({\n\t\t\t\tname: \"reloader\",\n\t\t\t\tinject: [Loader.Loader, FileSystem.FileSystem, Path.Path],\n\t\t\t\tapply: () =>\n\t\t\t\t\tEffect.gen(function* () {\n\t\t\t\t\t\tconst loader = yield* Loader.Loader\n\t\t\t\t\t\tyield* watchModules(hmr, directory, loader.retry).pipe(\n\t\t\t\t\t\t\tEffect.forkScoped,\n\t\t\t\t\t\t)\n\t\t\t\t\t\tif (envFile !== undefined)\n\t\t\t\t\t\t\tyield* watchEnvFile(envFile, reload, loader.retry).pipe(\n\t\t\t\t\t\t\t\tEffect.forkScoped,\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\tif (envDir !== undefined)\n\t\t\t\t\t\t\tyield* watchEnvDir(envDir, reloadDir, loader.retry).pipe(\n\t\t\t\t\t\t\t\tEffect.forkScoped,\n\t\t\t\t\t\t\t)\n\t\t\t\t\t}),\n\t\t\t})\n\t\t\tyield* Graft.plugin(reloader)\n\t\t}\n\n\t\tyield* Effect.logInfo(\n\t\t\t`Running ${path.relative(process.cwd(), file) || file}`,\n\t\t)\n\t\treturn yield* Deferred.await(done)\n\t}).pipe(\n\t\tEffect.scoped,\n\t\tEffect.provideService(Entry.Select, (entry) => entry[\"cli\"] !== true),\n\t\t// @effect-diagnostics-next-line strictEffectProvide:off -- this runs the whole application, so it is an entry point\n\t\tEffect.provide(\n\t\t\tLayer.provideMerge(\n\t\t\t\tGraft.layer,\n\t\t\t\tLayer.mergeAll(\n\t\t\t\t\tplatform,\n\t\t\t\t\tintercepts,\n\t\t\t\t\tenv({ file: options.envFile, dir: options.envDir }).pipe(\n\t\t\t\t\t\tLayer.provide(NodeServices.layer),\n\t\t\t\t\t),\n\t\t\t\t\tLogger.layer([Logger.consolePretty()]),\n\t\t\t\t),\n\t\t\t),\n\t\t),\n\t)\n"],"mappings":"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"}
|
package/dist/bin.js
CHANGED
|
@@ -14,10 +14,12 @@ import { command } from "@graftland/create/Command";
|
|
|
14
14
|
const start = Command.make("start", {
|
|
15
15
|
config: Argument.String("config").pipe(Argument.withDescription("Path of the config file"), Argument.withDefault("graft.yml")),
|
|
16
16
|
watch: Flag.Boolean("watch").pipe(Flag.withAlias("w"), Flag.withDescription("Reload plugins and config when files change"), Flag.withDefault(false)),
|
|
17
|
-
envFile: Flag.String("env-file").pipe(Flag.withDescription("Read variables for !env from a .env file; the process environment takes precedence"), Flag.optional)
|
|
17
|
+
envFile: Flag.String("env-file").pipe(Flag.withDescription("Read variables for !env from a .env file; the process environment takes precedence"), Flag.optional),
|
|
18
|
+
envDir: Flag.String("env-dir").pipe(Flag.withDescription("Read variables for !env from a directory with one file per variable, such as /run/secrets"), Flag.optional)
|
|
18
19
|
}, (options) => Effect.promise(() => import("./Start.js")).pipe(Effect.uninterruptible, Effect.flatMap((Start) => Start.run({
|
|
19
20
|
...options,
|
|
20
|
-
envFile: Option.getOrUndefined(options.envFile)
|
|
21
|
+
envFile: Option.getOrUndefined(options.envFile),
|
|
22
|
+
envDir: Option.getOrUndefined(options.envDir)
|
|
21
23
|
})))).pipe(Command.withDescription("Run the application described by a config file"));
|
|
22
24
|
const add = Command.make("add", {
|
|
23
25
|
packages: Argument.variadic(Argument.String("package")),
|
package/dist/bin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bin.js","names":["Host.output","Host.run","create"],"sources":["../src/bin.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * The `graft` executable. Running this module starts the program.\n */\nimport { Array, Effect, Layer, Logger, Option, Stdio } from \"effect\"\nimport { Argument, CliError, Command, Flag } from \"effect/cli\"\nimport { NodeRuntime } from \"@effect/platform-node\"\n\nimport { command as create } from \"@graftland/create/Command\"\nimport { Graft } from \"@graftland/graft\"\n\nimport { version } from \"../package.json\" with { type: \"json\" }\nimport { platform } from \"./Application\"\nimport * as Host from \"./internal/commandHost\"\n\nconst start = Command.make(\n\t\"start\",\n\t{\n\t\tconfig: Argument.String(\"config\").pipe(\n\t\t\tArgument.withDescription(\"Path of the config file\"),\n\t\t\tArgument.withDefault(\"graft.yml\"),\n\t\t),\n\t\twatch: Flag.Boolean(\"watch\").pipe(\n\t\t\tFlag.withAlias(\"w\"),\n\t\t\tFlag.withDescription(\"Reload plugins and config when files change\"),\n\t\t\tFlag.withDefault(false),\n\t\t),\n\t\tenvFile: Flag.String(\"env-file\").pipe(\n\t\t\tFlag.withDescription(\n\t\t\t\t\"Read variables for !env from a .env file; the process environment takes precedence\",\n\t\t\t),\n\t\t\tFlag.optional,\n\t\t),\n\t},\n\t(options) =>\n\t\tEffect.promise(() => import(\"./Start\")).pipe(\n\t\t\tEffect.uninterruptible,\n\t\t\tEffect.flatMap((Start) =>\n\t\t\t\tStart.run({\n\t\t\t\t\t...options,\n\t\t\t\t\tenvFile: Option.getOrUndefined(options.envFile),\n\t\t\t\t}),\n\t\t\t),\n\t\t),\n).pipe(\n\tCommand.withDescription(\"Run the application described by a config file\"),\n)\n\nconst add = Command.make(\n\t\"add\",\n\t{\n\t\tpackages: Argument.variadic(Argument.String(\"package\")),\n\t\tplugin: Flag.String(\"plugin\").pipe(Flag.optional),\n\t\tglobal: Flag.Boolean(\"global\").pipe(Flag.withDefault(false)),\n\t},\n\tEffect.fnUntraced(function* ({ packages, plugin, global }) {\n\t\tif (\n\t\t\t(global && Option.isNone(plugin)) ||\n\t\t\t(Option.isSome(plugin) && packages.length > 0)\n\t\t)\n\t\t\treturn yield* CliError.UserError.make({\n\t\t\t\tcause: \"Conflicting install options\",\n\t\t\t\tuserMessage:\n\t\t\t\t\t\"Use add --plugin <name> [--global] without package arguments\",\n\t\t\t})\n\t\tconst Manage = yield* Effect.promise(() => import(\"./Manage\")).pipe(\n\t\t\tEffect.uninterruptible,\n\t\t)\n\t\treturn yield* Option.isSome(plugin)\n\t\t\t? Manage.addPlugin(plugin.value, global)\n\t\t\t: Manage.add(packages)\n\t}),\n).pipe(\n\tCommand.withDescription(\n\t\t\"Add dependencies, restoring the project if installing fails\",\n\t),\n)\n\nconst remove = Command.make(\n\t\"remove\",\n\t{ packages: Argument.variadic(Argument.String(\"package\")) },\n\t({ packages }) =>\n\t\tEffect.promise(() => import(\"./Manage\")).pipe(\n\t\t\tEffect.uninterruptible,\n\t\t\tEffect.flatMap((Manage) => Manage.remove(packages)),\n\t\t),\n).pipe(\n\tCommand.withDescription(\n\t\t\"Remove dependencies, restoring the project if installing fails\",\n\t),\n)\n\nconst graft = Command.make(\"graft\").pipe(\n\tCommand.withDescription(\"Run and manage Graft applications\"),\n\tCommand.withSharedFlags({\n\t\tstdio: Flag.Boolean(\"stdio\").pipe(Flag.withDefault(false)),\n\t\tnoPlugins: Flag.Boolean(\"no-plugins\").pipe(Flag.withDefault(false)),\n\t}),\n)\n\nconst program = Effect.gen(function* () {\n\tconst stdio = yield* Stdio.Stdio\n\tconst args = yield* stdio.args\n\treturn yield* Host.output(\n\t\tEffect.scoped(\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst context = yield* Layer.build(Graft.layer)\n\t\t\t\treturn yield* Host.run(\n\t\t\t\t\t[start, create, add, remove],\n\t\t\t\t\tgraft,\n\t\t\t\t\tversion,\n\t\t\t\t\targs,\n\t\t\t\t).pipe(Effect.provideContext(context))\n\t\t\t}),\n\t\t),\n\t\targs,\n\t)\n})\nNodeRuntime.runMain(\n\tEffect.scoped(\n\t\tEffect.gen(function* () {\n\t\t\tconst context = yield* Layer.build(platform)\n\t\t\treturn yield* program.pipe(Effect.provideContext(context))\n\t\t}),\n\t).pipe(\n\t\t(effect) =>\n\t\t\tEffect.withFiber((main) =>\n\t\t\t\teffect.pipe(\n\t\t\t\t\tEffect.tapCause((cause) =>\n\t\t\t\t\t\tArray.every(\n\t\t\t\t\t\t\tcause.reasons,\n\t\t\t\t\t\t\t(reason) =>\n\t\t\t\t\t\t\t\treason._tag === \"Interrupt\" && reason.fiberId === main.id,\n\t\t\t\t\t\t) ||\n\t\t\t\t\t\tArray.every(\n\t\t\t\t\t\t\tcause.reasons,\n\t\t\t\t\t\t\t(reason) =>\n\t\t\t\t\t\t\t\treason._tag === \"Fail\" && CliError.isCliError(reason.error),\n\t\t\t\t\t\t)\n\t\t\t\t\t\t\t? Effect.void\n\t\t\t\t\t\t\t: Effect.logError(cause),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t),\n\t\tEffect.provideService(Logger.LogToStderr, true),\n\t),\n\t{ disableErrorReporting: true },\n)\n"],"mappings":";;;;;;;;;;;;;AAeA,MAAM,QAAQ,QAAQ,KACrB,SACA;CACC,QAAQ,SAAS,OAAO,QAAQ,CAAC,CAAC,KACjC,SAAS,gBAAgB,yBAAyB,GAClD,SAAS,YAAY,WAAW,CACjC;CACA,OAAO,KAAK,QAAQ,OAAO,CAAC,CAAC,KAC5B,KAAK,UAAU,GAAG,GAClB,KAAK,gBAAgB,6CAA6C,GAClE,KAAK,YAAY,KAAK,CACvB;CACA,SAAS,KAAK,OAAO,UAAU,CAAC,CAAC,KAChC,KAAK,gBACJ,oFACD,GACA,KAAK,QACN;AACD,IACC,YACA,OAAO,cAAc,OAAO,aAAU,CAAC,CAAC,KACvC,OAAO,iBACP,OAAO,SAAS,UACf,MAAM,IAAI;CACT,GAAG;CACH,SAAS,OAAO,eAAe,QAAQ,OAAO;
|
|
1
|
+
{"version":3,"file":"bin.js","names":["Host.output","Host.run","create"],"sources":["../src/bin.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * The `graft` executable. Running this module starts the program.\n */\nimport { Array, Effect, Layer, Logger, Option, Stdio } from \"effect\"\nimport { Argument, CliError, Command, Flag } from \"effect/cli\"\nimport { NodeRuntime } from \"@effect/platform-node\"\n\nimport { command as create } from \"@graftland/create/Command\"\nimport { Graft } from \"@graftland/graft\"\n\nimport { version } from \"../package.json\" with { type: \"json\" }\nimport { platform } from \"./Application\"\nimport * as Host from \"./internal/commandHost\"\n\nconst start = Command.make(\n\t\"start\",\n\t{\n\t\tconfig: Argument.String(\"config\").pipe(\n\t\t\tArgument.withDescription(\"Path of the config file\"),\n\t\t\tArgument.withDefault(\"graft.yml\"),\n\t\t),\n\t\twatch: Flag.Boolean(\"watch\").pipe(\n\t\t\tFlag.withAlias(\"w\"),\n\t\t\tFlag.withDescription(\"Reload plugins and config when files change\"),\n\t\t\tFlag.withDefault(false),\n\t\t),\n\t\tenvFile: Flag.String(\"env-file\").pipe(\n\t\t\tFlag.withDescription(\n\t\t\t\t\"Read variables for !env from a .env file; the process environment takes precedence\",\n\t\t\t),\n\t\t\tFlag.optional,\n\t\t),\n\t\tenvDir: Flag.String(\"env-dir\").pipe(\n\t\t\tFlag.withDescription(\n\t\t\t\t\"Read variables for !env from a directory with one file per variable, such as /run/secrets\",\n\t\t\t),\n\t\t\tFlag.optional,\n\t\t),\n\t},\n\t(options) =>\n\t\tEffect.promise(() => import(\"./Start\")).pipe(\n\t\t\tEffect.uninterruptible,\n\t\t\tEffect.flatMap((Start) =>\n\t\t\t\tStart.run({\n\t\t\t\t\t...options,\n\t\t\t\t\tenvFile: Option.getOrUndefined(options.envFile),\n\t\t\t\t\tenvDir: Option.getOrUndefined(options.envDir),\n\t\t\t\t}),\n\t\t\t),\n\t\t),\n).pipe(\n\tCommand.withDescription(\"Run the application described by a config file\"),\n)\n\nconst add = Command.make(\n\t\"add\",\n\t{\n\t\tpackages: Argument.variadic(Argument.String(\"package\")),\n\t\tplugin: Flag.String(\"plugin\").pipe(Flag.optional),\n\t\tglobal: Flag.Boolean(\"global\").pipe(Flag.withDefault(false)),\n\t},\n\tEffect.fnUntraced(function* ({ packages, plugin, global }) {\n\t\tif (\n\t\t\t(global && Option.isNone(plugin)) ||\n\t\t\t(Option.isSome(plugin) && packages.length > 0)\n\t\t)\n\t\t\treturn yield* CliError.UserError.make({\n\t\t\t\tcause: \"Conflicting install options\",\n\t\t\t\tuserMessage:\n\t\t\t\t\t\"Use add --plugin <name> [--global] without package arguments\",\n\t\t\t})\n\t\tconst Manage = yield* Effect.promise(() => import(\"./Manage\")).pipe(\n\t\t\tEffect.uninterruptible,\n\t\t)\n\t\treturn yield* Option.isSome(plugin)\n\t\t\t? Manage.addPlugin(plugin.value, global)\n\t\t\t: Manage.add(packages)\n\t}),\n).pipe(\n\tCommand.withDescription(\n\t\t\"Add dependencies, restoring the project if installing fails\",\n\t),\n)\n\nconst remove = Command.make(\n\t\"remove\",\n\t{ packages: Argument.variadic(Argument.String(\"package\")) },\n\t({ packages }) =>\n\t\tEffect.promise(() => import(\"./Manage\")).pipe(\n\t\t\tEffect.uninterruptible,\n\t\t\tEffect.flatMap((Manage) => Manage.remove(packages)),\n\t\t),\n).pipe(\n\tCommand.withDescription(\n\t\t\"Remove dependencies, restoring the project if installing fails\",\n\t),\n)\n\nconst graft = Command.make(\"graft\").pipe(\n\tCommand.withDescription(\"Run and manage Graft applications\"),\n\tCommand.withSharedFlags({\n\t\tstdio: Flag.Boolean(\"stdio\").pipe(Flag.withDefault(false)),\n\t\tnoPlugins: Flag.Boolean(\"no-plugins\").pipe(Flag.withDefault(false)),\n\t}),\n)\n\nconst program = Effect.gen(function* () {\n\tconst stdio = yield* Stdio.Stdio\n\tconst args = yield* stdio.args\n\treturn yield* Host.output(\n\t\tEffect.scoped(\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst context = yield* Layer.build(Graft.layer)\n\t\t\t\treturn yield* Host.run(\n\t\t\t\t\t[start, create, add, remove],\n\t\t\t\t\tgraft,\n\t\t\t\t\tversion,\n\t\t\t\t\targs,\n\t\t\t\t).pipe(Effect.provideContext(context))\n\t\t\t}),\n\t\t),\n\t\targs,\n\t)\n})\nNodeRuntime.runMain(\n\tEffect.scoped(\n\t\tEffect.gen(function* () {\n\t\t\tconst context = yield* Layer.build(platform)\n\t\t\treturn yield* program.pipe(Effect.provideContext(context))\n\t\t}),\n\t).pipe(\n\t\t(effect) =>\n\t\t\tEffect.withFiber((main) =>\n\t\t\t\teffect.pipe(\n\t\t\t\t\tEffect.tapCause((cause) =>\n\t\t\t\t\t\tArray.every(\n\t\t\t\t\t\t\tcause.reasons,\n\t\t\t\t\t\t\t(reason) =>\n\t\t\t\t\t\t\t\treason._tag === \"Interrupt\" && reason.fiberId === main.id,\n\t\t\t\t\t\t) ||\n\t\t\t\t\t\tArray.every(\n\t\t\t\t\t\t\tcause.reasons,\n\t\t\t\t\t\t\t(reason) =>\n\t\t\t\t\t\t\t\treason._tag === \"Fail\" && CliError.isCliError(reason.error),\n\t\t\t\t\t\t)\n\t\t\t\t\t\t\t? Effect.void\n\t\t\t\t\t\t\t: Effect.logError(cause),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t),\n\t\tEffect.provideService(Logger.LogToStderr, true),\n\t),\n\t{ disableErrorReporting: true },\n)\n"],"mappings":"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"}
|
package/dist/internal/env.js
CHANGED
|
@@ -1,34 +1,70 @@
|
|
|
1
1
|
import { observeShutdown } from "./observeShutdown.js";
|
|
2
|
-
import { Cause, ConfigProvider, Context, Deferred, Effect, Exit, FileSystem, Layer, Path, Queue, Ref, Stream } from "effect";
|
|
2
|
+
import { Array, Cause, ConfigProvider, Context, Deferred, Effect, Exit, FileSystem, Layer, Option, Path, Queue, Record, Ref, Stream, String } from "effect";
|
|
3
3
|
// src/internal/env.ts
|
|
4
4
|
/**
|
|
5
5
|
* Where `!env` values come from: the process environment, then an optional
|
|
6
|
-
* `.env` file
|
|
6
|
+
* `.env` file, then an optional directory of secret files. Watch mode reads
|
|
7
|
+
* the file and the directory again when they change.
|
|
7
8
|
*/
|
|
8
9
|
const options = { preserveEmptyStrings: true };
|
|
9
10
|
var EnvFile = class extends Context.Service()("@graftland/cli/internal/env/EnvFile") {};
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const text = yield* fs.readFileString(target);
|
|
18
|
-
return {
|
|
19
|
-
target,
|
|
20
|
-
text,
|
|
21
|
-
provider: ConfigProvider.fromDotEnvContents(text, options)
|
|
22
|
-
};
|
|
23
|
-
});
|
|
11
|
+
var EnvDir = class extends Context.Service()("@graftland/cli/internal/env/EnvDir") {};
|
|
12
|
+
const unchanged = { reload: Effect.succeed(false) };
|
|
13
|
+
/**
|
|
14
|
+
* A source that keeps the provider read from its latest snapshot. A failed
|
|
15
|
+
* read keeps the previous snapshot.
|
|
16
|
+
*/
|
|
17
|
+
const source = Effect.fnUntraced(function* (read, provider, same) {
|
|
24
18
|
const initial = yield* read;
|
|
25
|
-
const loaded = yield* Ref.make(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
19
|
+
const loaded = yield* Ref.make({
|
|
20
|
+
snapshot: initial,
|
|
21
|
+
provider: provider(initial)
|
|
22
|
+
});
|
|
23
|
+
return {
|
|
24
|
+
provider: ConfigProvider.make((path) => Effect.flatMap(Ref.get(loaded), ({ provider }) => provider.load(path))),
|
|
25
|
+
reload: Effect.gen(function* () {
|
|
26
|
+
const snapshot = yield* read;
|
|
27
|
+
return !same(snapshot, (yield* Ref.getAndSet(loaded, {
|
|
28
|
+
snapshot,
|
|
29
|
+
provider: provider(snapshot)
|
|
30
|
+
})).snapshot);
|
|
31
|
+
})
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
const readFile = Effect.fnUntraced(function* (file) {
|
|
35
|
+
const fs = yield* FileSystem.FileSystem;
|
|
36
|
+
const target = yield* fs.realPath(file);
|
|
37
|
+
return yield* fs.readFileString(target);
|
|
38
|
+
});
|
|
39
|
+
const isNotFound = (error) => error.reason._tag === "NotFound";
|
|
40
|
+
/**
|
|
41
|
+
* One variable per regular file, named after the file. Hidden entries are
|
|
42
|
+
* skipped, which also skips the bookkeeping links of Kubernetes volumes.
|
|
43
|
+
* Trailing line breaks are not part of the value.
|
|
44
|
+
*/
|
|
45
|
+
const readDir = Effect.fnUntraced(function* (directory) {
|
|
46
|
+
const fs = yield* FileSystem.FileSystem;
|
|
47
|
+
const path = yield* Path.Path;
|
|
48
|
+
const names = yield* fs.readDirectory(directory);
|
|
49
|
+
const values = yield* Effect.forEach(Array.filter(names, (name) => !String.startsWith(".")(name)), (name) => {
|
|
50
|
+
const file = path.join(directory, name);
|
|
51
|
+
return fs.stat(file).pipe(Effect.flatMap((info) => info.type === "File" ? fs.readFileString(file).pipe(Effect.map((text) => Option.some([name, String.replace(/[\r\n]+$/, "")(text)]))) : Effect.succeedNone), Effect.catchIf(isNotFound, () => Effect.succeedNone));
|
|
52
|
+
}, { concurrency: 16 });
|
|
53
|
+
return Record.fromEntries(Array.getSomes(values));
|
|
54
|
+
});
|
|
55
|
+
const sameValues = Record.makeEquivalence(String.Equivalence);
|
|
56
|
+
/**
|
|
57
|
+
* Provides `ConfigProvider`, `EnvFile` and `EnvDir`. A missing file or
|
|
58
|
+
* directory fails the layer.
|
|
59
|
+
*/
|
|
60
|
+
const layer = (sources) => Layer.unwrap(Effect.gen(function* () {
|
|
61
|
+
const fs = yield* FileSystem.FileSystem;
|
|
62
|
+
const path = yield* Path.Path;
|
|
63
|
+
const provide = (read) => read.pipe(Effect.provideService(FileSystem.FileSystem, fs), Effect.provideService(Path.Path, path));
|
|
64
|
+
const file = sources.file === void 0 ? void 0 : yield* source(provide(readFile(sources.file)), (text) => ConfigProvider.fromDotEnvContents(text, options), (a, b) => a === b);
|
|
65
|
+
const dir = sources.dir === void 0 ? void 0 : yield* source(provide(readDir(sources.dir)), (values) => ConfigProvider.fromEnvRecord(values, options), sameValues);
|
|
66
|
+
const provider = Array.reduce([file, dir], ConfigProvider.fromEnv(options), (first, next) => next === void 0 ? first : ConfigProvider.orElse(first, next.provider));
|
|
67
|
+
return Layer.mergeAll(ConfigProvider.layer(provider), Layer.succeed(EnvFile, file ?? unchanged), Layer.succeed(EnvDir, dir ?? unchanged));
|
|
32
68
|
}));
|
|
33
69
|
/**
|
|
34
70
|
* The handlers below let interruption through, and with it any failure raised
|
|
@@ -40,16 +76,25 @@ const logStopped = (file) => (exit) => Exit.isFailure(exit) && Cause.hasInterrup
|
|
|
40
76
|
* reads `!env` again. A file that cannot be read keeps its previous values.
|
|
41
77
|
*/
|
|
42
78
|
const watchEnvFile = Effect.fnUntraced(function* (file, reload, retry) {
|
|
43
|
-
const fs = yield* FileSystem.FileSystem;
|
|
44
79
|
const path = yield* Path.Path;
|
|
80
|
+
const name = path.basename(file);
|
|
81
|
+
return yield* watchSource(file, path.dirname(file), (changed) => path.basename(changed) === name, reload, retry);
|
|
82
|
+
});
|
|
83
|
+
/**
|
|
84
|
+
* Reloads the directory when anything in it changes and retries the loader's
|
|
85
|
+
* entries. A directory that cannot be read keeps its previous values.
|
|
86
|
+
*/
|
|
87
|
+
const watchEnvDir = (directory, reload, retry) => watchSource(directory, directory, () => true, reload, retry);
|
|
88
|
+
const watchSource = Effect.fnUntraced(function* (file, directory, matches, reload, retry) {
|
|
89
|
+
const fs = yield* FileSystem.FileSystem;
|
|
45
90
|
const ready = yield* Deferred.make();
|
|
46
|
-
const events = Stream.callback((queue) => fs.watch(
|
|
91
|
+
const events = Stream.callback((queue) => fs.watch(directory, { onReady: () => Deferred.doneUnsafe(ready, Effect.void) }).pipe(Stream.filter((event) => matches(event.path)), Stream.runForEachArray((events) => Queue.offerAll(queue, events)), Effect.scoped, (effect) => observeShutdown(effect, `Watching ${file} stopped with interrupted cleanup`), Effect.onExit(logStopped(file)), Effect.catchCauseIf((cause) => !Cause.hasInterrupts(cause), (cause) => Effect.logError(`Watching ${file} failed`, cause)), Effect.andThen(Effect.sleep("1 second")), Effect.forever), { bufferSize: 16 });
|
|
47
92
|
yield* Stream.mergeAll([
|
|
48
93
|
Stream.as(events, void 0),
|
|
49
94
|
Stream.fromEffect(Deferred.await(ready)),
|
|
50
95
|
Stream.tick("1 second").pipe(Stream.drop(1))
|
|
51
96
|
], { concurrency: "unbounded" }).pipe(Stream.runForEach(() => Effect.sleep("100 millis").pipe(Effect.andThen(reload), Effect.flatMap((changed) => changed ? retry : Effect.void), Effect.catchCauseIf((cause) => !Cause.hasInterrupts(cause), (cause) => Effect.logError(`Reloading ${file} failed`, cause)))), Effect.catchCauseIf((cause) => !Cause.hasInterrupts(cause), (cause) => Effect.logError(`Stopped watching ${file}`, cause)), Effect.onExit(logStopped(file)));
|
|
52
97
|
});
|
|
53
|
-
export { EnvFile, layer, watchEnvFile };
|
|
98
|
+
export { EnvDir, EnvFile, layer, watchEnvDir, watchEnvFile };
|
|
54
99
|
|
|
55
100
|
//# sourceMappingURL=env.js.map
|
package/dist/internal/env.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"env.js","names":[],"sources":["../../src/internal/env.ts"],"sourcesContent":["/**\n * Where `!env` values come from: the process environment, then an optional\n * `.env` file that watch mode reads again when it changes.\n */\nimport {\n\tCause,\n\tConfigProvider,\n\tContext,\n\tDeferred,\n\tEffect,\n\tExit,\n\tFileSystem,\n\tLayer,\n\tPath,\n\tQueue,\n\tRef,\n\tStream,\n\ttype PlatformError,\n} from \"effect\"\n\nimport { observeShutdown } from \"./observeShutdown\"\n\n// An empty variable is still set: it overrides the file, and the plugin's\n// schema decides whether it is valid.\nconst options = { preserveEmptyStrings: true } as const\n\ninterface Loaded {\n\treadonly target: string\n\treadonly text: string\n\treadonly provider: ConfigProvider.ConfigProvider\n}\n\nexport class EnvFile extends Context.Service<\n\tEnvFile,\n\t{\n\t\t/** Reads the file again; reports whether its contents changed. */\n\t\treadonly reload: Effect.Effect<boolean, PlatformError.PlatformError>\n\t}\n>()(\"@graftland/cli/internal/env/EnvFile\") {}\n\n/** Provides `ConfigProvider` and `EnvFile`; a missing file fails the layer. */\nexport const layer = (file: string | undefined) =>\n\tLayer.unwrap(\n\t\tEffect.gen(function* () {\n\t\t\tconst environment = ConfigProvider.fromEnv(options)\n\t\t\tif (file === undefined)\n\t\t\t\treturn Layer.merge(\n\t\t\t\t\tConfigProvider.layer(environment),\n\t\t\t\t\tLayer.succeed(EnvFile, { reload: Effect.succeed(false) }),\n\t\t\t\t)\n\t\t\tconst fs = yield* FileSystem.FileSystem\n\t\t\t// Resolve the request on every read, including polling. Never pin a\n\t\t\t// symlink to its initial target. The provider and its baseline are\n\t\t\t// committed together from the bytes actually read.\n\t\t\tconst read = Effect.gen(function* () {\n\t\t\t\tconst target = yield* fs.realPath(file)\n\t\t\t\tconst text = yield* fs.readFileString(target)\n\t\t\t\treturn {\n\t\t\t\t\ttarget,\n\t\t\t\t\ttext,\n\t\t\t\t\tprovider: ConfigProvider.fromDotEnvContents(text, options),\n\t\t\t\t}\n\t\t\t})\n\t\t\tconst initial: Loaded = yield* read\n\t\t\tconst loaded = yield* Ref.make(initial)\n\t\t\tconst fromFile = ConfigProvider.make((path) =>\n\t\t\t\tEffect.flatMap(Ref.get(loaded), ({ provider }) => provider.load(path)),\n\t\t\t)\n\t\t\treturn Layer.merge(\n\t\t\t\tConfigProvider.layer(ConfigProvider.orElse(environment, fromFile)),\n\t\t\t\tLayer.succeed(EnvFile, {\n\t\t\t\t\treload: Effect.gen(function* () {\n\t\t\t\t\t\tconst next = yield* read\n\t\t\t\t\t\tconst previous = yield* Ref.getAndSet(loaded, next)\n\t\t\t\t\t\treturn next.text !== previous.text\n\t\t\t\t\t}),\n\t\t\t\t}),\n\t\t\t)\n\t\t}),\n\t)\n\n/**\n * The handlers below let interruption through, and with it any failure raised\n * while stopping. Log those; failures without interruption are handled there.\n */\nconst logStopped =\n\t(file: string) =>\n\t<A, E>(exit: Exit.Exit<A, E>) =>\n\t\tExit.isFailure(exit) &&\n\t\tCause.hasInterrupts(exit.cause) &&\n\t\t(Cause.hasFails(exit.cause) || Cause.hasDies(exit.cause))\n\t\t\t? Effect.logError(`Watching ${file} stopped with a failure`, exit.cause)\n\t\t\t: Effect.void\n\n/**\n * Reloads the file when it changes and retries the loader's entries, which\n * reads `!env` again. A file that cannot be read keeps its previous values.\n */\nexport const watchEnvFile = Effect.fnUntraced(function* <E, R>(\n\tfile: string,\n\treload: Effect.Effect<boolean, PlatformError.PlatformError>,\n\tretry: Effect.Effect<void, E, R>,\n) {\n\tconst fs = yield* FileSystem.FileSystem\n\tconst path = yield* Path.Path\n\tconst ready = yield* Deferred.make<void>()\n\t// As in HMR, own the source scope inside the producer. Observe its\n\t// complete Exit after cleanup, before merge can discard it.\n\tconst events = Stream.callback<\n\t\tFileSystem.WatchEvent,\n\t\tPlatformError.PlatformError\n\t>(\n\t\t(queue) =>\n\t\t\tfs\n\t\t\t\t.watch(path.dirname(file), {\n\t\t\t\t\tonReady: () => Deferred.doneUnsafe(ready, Effect.void),\n\t\t\t\t})\n\t\t\t\t.pipe(\n\t\t\t\t\tStream.filter(\n\t\t\t\t\t\t(event) => path.basename(event.path) === path.basename(file),\n\t\t\t\t\t),\n\t\t\t\t\tStream.runForEachArray((events) => Queue.offerAll(queue, events)),\n\t\t\t\t\tEffect.scoped,\n\t\t\t\t\t(effect) =>\n\t\t\t\t\t\tobserveShutdown(\n\t\t\t\t\t\t\teffect,\n\t\t\t\t\t\t\t`Watching ${file} stopped with interrupted cleanup`,\n\t\t\t\t\t\t),\n\t\t\t\t\tEffect.onExit(logStopped(file)),\n\t\t\t\t\tEffect.catchCauseIf(\n\t\t\t\t\t\t(cause) => !Cause.hasInterrupts(cause),\n\t\t\t\t\t\t(cause) => Effect.logError(`Watching ${file} failed`, cause),\n\t\t\t\t\t),\n\t\t\t\t\t// A failed native backend must not stop content reconciliation.\n\t\t\t\t\tEffect.andThen(Effect.sleep(\"1 second\")),\n\t\t\t\t\tEffect.forever,\n\t\t\t\t),\n\t\t{ bufferSize: 16 },\n\t)\n\t// Native events accelerate updates. Poll the request too: its target\n\t// may live outside this directory, change, or lose a notification.\n\tyield* Stream.mergeAll(\n\t\t[\n\t\t\tStream.as(events, undefined),\n\t\t\tStream.fromEffect(Deferred.await(ready)),\n\t\t\tStream.tick(\"1 second\").pipe(Stream.drop(1)),\n\t\t],\n\t\t{ concurrency: \"unbounded\" },\n\t).pipe(\n\t\tStream.runForEach(() =>\n\t\t\t// Delay to let writes settle.\n\t\t\tEffect.sleep(\"100 millis\").pipe(\n\t\t\t\tEffect.andThen(reload),\n\t\t\t\tEffect.flatMap((changed) => (changed ? retry : Effect.void)),\n\t\t\t\tEffect.catchCauseIf(\n\t\t\t\t\t(cause) => !Cause.hasInterrupts(cause),\n\t\t\t\t\t(cause) => Effect.logError(`Reloading ${file} failed`, cause),\n\t\t\t\t),\n\t\t\t),\n\t\t),\n\t\tEffect.catchCauseIf(\n\t\t\t(cause) => !Cause.hasInterrupts(cause),\n\t\t\t(cause) => Effect.logError(`Stopped watching ${file}`, cause),\n\t\t),\n\t\t// Observe the whole watcher before forkScoped owns it. The steps\n\t\t// before the stream cannot fail, so this covers every failure.\n\t\tEffect.onExit(logStopped(file)),\n\t)\n})\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"env.js","names":[],"sources":["../../src/internal/env.ts"],"sourcesContent":["/**\n * Where `!env` values come from: the process environment, then an optional\n * `.env` file, then an optional directory of secret files. Watch mode reads\n * the file and the directory again when they change.\n */\nimport {\n\tArray,\n\tCause,\n\tConfigProvider,\n\tContext,\n\tDeferred,\n\tEffect,\n\tExit,\n\tFileSystem,\n\tLayer,\n\tOption,\n\tPath,\n\tQueue,\n\tRecord,\n\tRef,\n\tStream,\n\tString,\n\ttype PlatformError,\n} from \"effect\"\n\nimport { observeShutdown } from \"./observeShutdown\"\n\n// An empty variable is still set: it overrides the file, and the plugin's\n// schema decides whether it is valid.\nconst options = { preserveEmptyStrings: true } as const\n\ntype Reload = Effect.Effect<boolean, PlatformError.PlatformError>\n\nexport class EnvFile extends Context.Service<\n\tEnvFile,\n\t{\n\t\t/** Reads the file again; reports whether its contents changed. */\n\t\treadonly reload: Reload\n\t}\n>()(\"@graftland/cli/internal/env/EnvFile\") {}\n\nexport class EnvDir extends Context.Service<\n\tEnvDir,\n\t{\n\t\t/** Reads the directory again; reports whether any value changed. */\n\t\treadonly reload: Reload\n\t}\n>()(\"@graftland/cli/internal/env/EnvDir\") {}\n\nconst unchanged = { reload: Effect.succeed(false) }\n\n/**\n * A source that keeps the provider read from its latest snapshot. A failed\n * read keeps the previous snapshot.\n */\nconst source = Effect.fnUntraced(function* <A>(\n\tread: Effect.Effect<A, PlatformError.PlatformError>,\n\tprovider: (snapshot: A) => ConfigProvider.ConfigProvider,\n\tsame: (a: A, b: A) => boolean,\n) {\n\tconst initial = yield* read\n\tconst loaded = yield* Ref.make({\n\t\tsnapshot: initial,\n\t\tprovider: provider(initial),\n\t})\n\treturn {\n\t\tprovider: ConfigProvider.make((path) =>\n\t\t\tEffect.flatMap(Ref.get(loaded), ({ provider }) => provider.load(path)),\n\t\t),\n\t\treload: Effect.gen(function* () {\n\t\t\tconst snapshot = yield* read\n\t\t\tconst previous = yield* Ref.getAndSet(loaded, {\n\t\t\t\tsnapshot,\n\t\t\t\tprovider: provider(snapshot),\n\t\t\t})\n\t\t\treturn !same(snapshot, previous.snapshot)\n\t\t}),\n\t}\n})\n\nconst readFile = Effect.fnUntraced(function* (file: string) {\n\tconst fs = yield* FileSystem.FileSystem\n\t// Resolve the request on every read, including polling. Never pin a\n\t// symlink to its initial target.\n\tconst target = yield* fs.realPath(file)\n\treturn yield* fs.readFileString(target)\n})\n\nconst isNotFound = (error: PlatformError.PlatformError) =>\n\terror.reason._tag === \"NotFound\"\n\n/**\n * One variable per regular file, named after the file. Hidden entries are\n * skipped, which also skips the bookkeeping links of Kubernetes volumes.\n * Trailing line breaks are not part of the value.\n */\nconst readDir = Effect.fnUntraced(function* (directory: string) {\n\tconst fs = yield* FileSystem.FileSystem\n\tconst path = yield* Path.Path\n\tconst names = yield* fs.readDirectory(directory)\n\tconst values = yield* Effect.forEach(\n\t\tArray.filter(names, (name) => !String.startsWith(\".\")(name)),\n\t\t(name) => {\n\t\t\tconst file = path.join(directory, name)\n\t\t\treturn fs.stat(file).pipe(\n\t\t\t\tEffect.flatMap((info) =>\n\t\t\t\t\tinfo.type === \"File\"\n\t\t\t\t\t\t? fs\n\t\t\t\t\t\t\t\t.readFileString(file)\n\t\t\t\t\t\t\t\t.pipe(\n\t\t\t\t\t\t\t\t\tEffect.map((text) =>\n\t\t\t\t\t\t\t\t\t\tOption.some([\n\t\t\t\t\t\t\t\t\t\t\tname,\n\t\t\t\t\t\t\t\t\t\t\tString.replace(/[\\r\\n]+$/, \"\")(text),\n\t\t\t\t\t\t\t\t\t\t] as const),\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t: Effect.succeedNone,\n\t\t\t\t),\n\t\t\t\t// A file removed since the listing is no longer a variable.\n\t\t\t\tEffect.catchIf(isNotFound, () => Effect.succeedNone),\n\t\t\t)\n\t\t},\n\t\t{ concurrency: 16 },\n\t)\n\treturn Record.fromEntries(Array.getSomes(values))\n})\n\nconst sameValues = Record.makeEquivalence(String.Equivalence)\n\n/**\n * Provides `ConfigProvider`, `EnvFile` and `EnvDir`. A missing file or\n * directory fails the layer.\n */\nexport const layer = (sources: {\n\treadonly file?: string | undefined\n\treadonly dir?: string | undefined\n}) =>\n\tLayer.unwrap(\n\t\tEffect.gen(function* () {\n\t\t\tconst fs = yield* FileSystem.FileSystem\n\t\t\tconst path = yield* Path.Path\n\t\t\tconst provide = <A>(\n\t\t\t\tread: Effect.Effect<\n\t\t\t\t\tA,\n\t\t\t\t\tPlatformError.PlatformError,\n\t\t\t\t\tFileSystem.FileSystem | Path.Path\n\t\t\t\t>,\n\t\t\t) =>\n\t\t\t\tread.pipe(\n\t\t\t\t\tEffect.provideService(FileSystem.FileSystem, fs),\n\t\t\t\t\tEffect.provideService(Path.Path, path),\n\t\t\t\t)\n\t\t\tconst file =\n\t\t\t\tsources.file === undefined\n\t\t\t\t\t? undefined\n\t\t\t\t\t: yield* source(\n\t\t\t\t\t\t\tprovide(readFile(sources.file)),\n\t\t\t\t\t\t\t(text) => ConfigProvider.fromDotEnvContents(text, options),\n\t\t\t\t\t\t\t(a, b) => a === b,\n\t\t\t\t\t\t)\n\t\t\tconst dir =\n\t\t\t\tsources.dir === undefined\n\t\t\t\t\t? undefined\n\t\t\t\t\t: yield* source(\n\t\t\t\t\t\t\tprovide(readDir(sources.dir)),\n\t\t\t\t\t\t\t(values) => ConfigProvider.fromEnvRecord(values, options),\n\t\t\t\t\t\t\tsameValues,\n\t\t\t\t\t\t)\n\t\t\tconst provider = Array.reduce(\n\t\t\t\t[file, dir],\n\t\t\t\tConfigProvider.fromEnv(options),\n\t\t\t\t(first, next) =>\n\t\t\t\t\tnext === undefined\n\t\t\t\t\t\t? first\n\t\t\t\t\t\t: ConfigProvider.orElse(first, next.provider),\n\t\t\t)\n\t\t\treturn Layer.mergeAll(\n\t\t\t\tConfigProvider.layer(provider),\n\t\t\t\tLayer.succeed(EnvFile, file ?? unchanged),\n\t\t\t\tLayer.succeed(EnvDir, dir ?? unchanged),\n\t\t\t)\n\t\t}),\n\t)\n\n/**\n * The handlers below let interruption through, and with it any failure raised\n * while stopping. Log those; failures without interruption are handled there.\n */\nconst logStopped =\n\t(file: string) =>\n\t<A, E>(exit: Exit.Exit<A, E>) =>\n\t\tExit.isFailure(exit) &&\n\t\tCause.hasInterrupts(exit.cause) &&\n\t\t(Cause.hasFails(exit.cause) || Cause.hasDies(exit.cause))\n\t\t\t? Effect.logError(`Watching ${file} stopped with a failure`, exit.cause)\n\t\t\t: Effect.void\n\n/**\n * Reloads the file when it changes and retries the loader's entries, which\n * reads `!env` again. A file that cannot be read keeps its previous values.\n */\nexport const watchEnvFile = Effect.fnUntraced(function* <E, R>(\n\tfile: string,\n\treload: Reload,\n\tretry: Effect.Effect<void, E, R>,\n) {\n\tconst path = yield* Path.Path\n\tconst name = path.basename(file)\n\treturn yield* watchSource(\n\t\tfile,\n\t\tpath.dirname(file),\n\t\t(changed) => path.basename(changed) === name,\n\t\treload,\n\t\tretry,\n\t)\n})\n\n/**\n * Reloads the directory when anything in it changes and retries the loader's\n * entries. A directory that cannot be read keeps its previous values.\n */\nexport const watchEnvDir = <E, R>(\n\tdirectory: string,\n\treload: Reload,\n\tretry: Effect.Effect<void, E, R>,\n) => watchSource(directory, directory, () => true, reload, retry)\n\nconst watchSource = Effect.fnUntraced(function* <E, R>(\n\tfile: string,\n\tdirectory: string,\n\tmatches: (path: string) => boolean,\n\treload: Reload,\n\tretry: Effect.Effect<void, E, R>,\n) {\n\tconst fs = yield* FileSystem.FileSystem\n\tconst ready = yield* Deferred.make<void>()\n\t// As in HMR, own the source scope inside the producer. Observe its\n\t// complete Exit after cleanup, before merge can discard it.\n\tconst events = Stream.callback<\n\t\tFileSystem.WatchEvent,\n\t\tPlatformError.PlatformError\n\t>(\n\t\t(queue) =>\n\t\t\tfs\n\t\t\t\t.watch(directory, {\n\t\t\t\t\tonReady: () => Deferred.doneUnsafe(ready, Effect.void),\n\t\t\t\t})\n\t\t\t\t.pipe(\n\t\t\t\t\tStream.filter((event) => matches(event.path)),\n\t\t\t\t\tStream.runForEachArray((events) => Queue.offerAll(queue, events)),\n\t\t\t\t\tEffect.scoped,\n\t\t\t\t\t(effect) =>\n\t\t\t\t\t\tobserveShutdown(\n\t\t\t\t\t\t\teffect,\n\t\t\t\t\t\t\t`Watching ${file} stopped with interrupted cleanup`,\n\t\t\t\t\t\t),\n\t\t\t\t\tEffect.onExit(logStopped(file)),\n\t\t\t\t\tEffect.catchCauseIf(\n\t\t\t\t\t\t(cause) => !Cause.hasInterrupts(cause),\n\t\t\t\t\t\t(cause) => Effect.logError(`Watching ${file} failed`, cause),\n\t\t\t\t\t),\n\t\t\t\t\t// A failed native backend must not stop content reconciliation.\n\t\t\t\t\tEffect.andThen(Effect.sleep(\"1 second\")),\n\t\t\t\t\tEffect.forever,\n\t\t\t\t),\n\t\t{ bufferSize: 16 },\n\t)\n\t// Native events accelerate updates. Poll the request too: its target\n\t// may live outside this directory, change, or lose a notification.\n\tyield* Stream.mergeAll(\n\t\t[\n\t\t\tStream.as(events, undefined),\n\t\t\tStream.fromEffect(Deferred.await(ready)),\n\t\t\tStream.tick(\"1 second\").pipe(Stream.drop(1)),\n\t\t],\n\t\t{ concurrency: \"unbounded\" },\n\t).pipe(\n\t\tStream.runForEach(() =>\n\t\t\t// Delay to let writes settle.\n\t\t\tEffect.sleep(\"100 millis\").pipe(\n\t\t\t\tEffect.andThen(reload),\n\t\t\t\tEffect.flatMap((changed) => (changed ? retry : Effect.void)),\n\t\t\t\tEffect.catchCauseIf(\n\t\t\t\t\t(cause) => !Cause.hasInterrupts(cause),\n\t\t\t\t\t(cause) => Effect.logError(`Reloading ${file} failed`, cause),\n\t\t\t\t),\n\t\t\t),\n\t\t),\n\t\tEffect.catchCauseIf(\n\t\t\t(cause) => !Cause.hasInterrupts(cause),\n\t\t\t(cause) => Effect.logError(`Stopped watching ${file}`, cause),\n\t\t),\n\t\t// Observe the whole watcher before forkScoped owns it. The steps\n\t\t// before the stream cannot fail, so this covers every failure.\n\t\tEffect.onExit(logStopped(file)),\n\t)\n})\n"],"mappings":";;;;;;;;AA6BA,MAAM,UAAU,EAAE,sBAAsB,KAAK;AAI7C,IAAa,UAAb,cAA6B,QAAQ,QAMnC,CAAC,CAAC,qCAAqC,CAAC,CAAC,CAAC;AAE5C,IAAa,SAAb,cAA4B,QAAQ,QAMlC,CAAC,CAAC,oCAAoC,CAAC,CAAC,CAAC;AAE3C,MAAM,YAAY,EAAE,QAAQ,OAAO,QAAQ,KAAK,EAAE;;;;;AAMlD,MAAM,SAAS,OAAO,WAAW,WAChC,MACA,UACA,MACC;CACD,MAAM,UAAU,OAAO;CACvB,MAAM,SAAS,OAAO,IAAI,KAAK;EAC9B,UAAU;EACV,UAAU,SAAS,OAAO;CAC3B,CAAC;CACD,OAAO;EACN,UAAU,eAAe,MAAM,SAC9B,OAAO,QAAQ,IAAI,IAAI,MAAM,IAAI,EAAE,eAAe,SAAS,KAAK,IAAI,CAAC,CACtE;EACA,QAAQ,OAAO,IAAI,aAAa;GAC/B,MAAM,WAAW,OAAO;GAKxB,OAAO,CAAC,KAAK,WAAU,OAJC,IAAI,UAAU,QAAQ;IAC7C;IACA,UAAU,SAAS,QAAQ;GAC5B,CAAC,EAAA,CAC+B,QAAQ;EACzC,CAAC;CACF;AACD,CAAC;AAED,MAAM,WAAW,OAAO,WAAW,WAAW,MAAc;CAC3D,MAAM,KAAK,OAAO,WAAW;CAG7B,MAAM,SAAS,OAAO,GAAG,SAAS,IAAI;CACtC,OAAO,OAAO,GAAG,eAAe,MAAM;AACvC,CAAC;AAED,MAAM,cAAc,UACnB,MAAM,OAAO,SAAS;;;;;;AAOvB,MAAM,UAAU,OAAO,WAAW,WAAW,WAAmB;CAC/D,MAAM,KAAK,OAAO,WAAW;CAC7B,MAAM,OAAO,OAAO,KAAK;CACzB,MAAM,QAAQ,OAAO,GAAG,cAAc,SAAS;CAC/C,MAAM,SAAS,OAAO,OAAO,QAC5B,MAAM,OAAO,QAAQ,SAAS,CAAC,OAAO,WAAW,GAAG,CAAC,CAAC,IAAI,CAAC,IAC1D,SAAS;EACT,MAAM,OAAO,KAAK,KAAK,WAAW,IAAI;EACtC,OAAO,GAAG,KAAK,IAAI,CAAC,CAAC,KACpB,OAAO,SAAS,SACf,KAAK,SAAS,SACX,GACC,eAAe,IAAI,CAAC,CACpB,KACA,OAAO,KAAK,SACX,OAAO,KAAK,CACX,MACA,OAAO,QAAQ,YAAY,EAAE,CAAC,CAAC,IAAI,CACpC,CAAU,CACX,CACD,IACA,OAAO,WACX,GAEA,OAAO,QAAQ,kBAAkB,OAAO,WAAW,CACpD;CACD,GACA,EAAE,aAAa,GAAG,CACnB;CACA,OAAO,OAAO,YAAY,MAAM,SAAS,MAAM,CAAC;AACjD,CAAC;AAED,MAAM,aAAa,OAAO,gBAAgB,OAAO,WAAW;;;;;AAM5D,MAAa,SAAS,YAIrB,MAAM,OACL,OAAO,IAAI,aAAa;CACvB,MAAM,KAAK,OAAO,WAAW;CAC7B,MAAM,OAAO,OAAO,KAAK;CACzB,MAAM,WACL,SAMA,KAAK,KACJ,OAAO,eAAe,WAAW,YAAY,EAAE,GAC/C,OAAO,eAAe,KAAK,MAAM,IAAI,CACtC;CACD,MAAM,OACL,QAAQ,SAAS,KAAA,IACd,KAAA,IACA,OAAO,OACP,QAAQ,SAAS,QAAQ,IAAI,CAAC,IAC7B,SAAS,eAAe,mBAAmB,MAAM,OAAO,IACxD,GAAG,MAAM,MAAM,CACjB;CACH,MAAM,MACL,QAAQ,QAAQ,KAAA,IACb,KAAA,IACA,OAAO,OACP,QAAQ,QAAQ,QAAQ,GAAG,CAAC,IAC3B,WAAW,eAAe,cAAc,QAAQ,OAAO,GACxD,UACD;CACH,MAAM,WAAW,MAAM,OACtB,CAAC,MAAM,GAAG,GACV,eAAe,QAAQ,OAAO,IAC7B,OAAO,SACP,SAAS,KAAA,IACN,QACA,eAAe,OAAO,OAAO,KAAK,QAAQ,CAC/C;CACA,OAAO,MAAM,SACZ,eAAe,MAAM,QAAQ,GAC7B,MAAM,QAAQ,SAAS,QAAQ,SAAS,GACxC,MAAM,QAAQ,QAAQ,OAAO,SAAS,CACvC;AACD,CAAC,CACF;;;;;AAMD,MAAM,cACJ,UACM,SACN,KAAK,UAAU,IAAI,KACnB,MAAM,cAAc,KAAK,KAAK,MAC7B,MAAM,SAAS,KAAK,KAAK,KAAK,MAAM,QAAQ,KAAK,KAAK,KACpD,OAAO,SAAS,YAAY,KAAK,0BAA0B,KAAK,KAAK,IACrE,OAAO;;;;;AAMZ,MAAa,eAAe,OAAO,WAAW,WAC7C,MACA,QACA,OACC;CACD,MAAM,OAAO,OAAO,KAAK;CACzB,MAAM,OAAO,KAAK,SAAS,IAAI;CAC/B,OAAO,OAAO,YACb,MACA,KAAK,QAAQ,IAAI,IAChB,YAAY,KAAK,SAAS,OAAO,MAAM,MACxC,QACA,KACD;AACD,CAAC;;;;;AAMD,MAAa,eACZ,WACA,QACA,UACI,YAAY,WAAW,iBAAiB,MAAM,QAAQ,KAAK;AAEhE,MAAM,cAAc,OAAO,WAAW,WACrC,MACA,WACA,SACA,QACA,OACC;CACD,MAAM,KAAK,OAAO,WAAW;CAC7B,MAAM,QAAQ,OAAO,SAAS,KAAW;CAGzC,MAAM,SAAS,OAAO,UAIpB,UACA,GACE,MAAM,WAAW,EACjB,eAAe,SAAS,WAAW,OAAO,OAAO,IAAI,EACtD,CAAC,CAAC,CACD,KACA,OAAO,QAAQ,UAAU,QAAQ,MAAM,IAAI,CAAC,GAC5C,OAAO,iBAAiB,WAAW,MAAM,SAAS,OAAO,MAAM,CAAC,GAChE,OAAO,SACN,WACA,gBACC,QACA,YAAY,KAAK,kCAClB,GACD,OAAO,OAAO,WAAW,IAAI,CAAC,GAC9B,OAAO,cACL,UAAU,CAAC,MAAM,cAAc,KAAK,IACpC,UAAU,OAAO,SAAS,YAAY,KAAK,UAAU,KAAK,CAC5D,GAEA,OAAO,QAAQ,OAAO,MAAM,UAAU,CAAC,GACvC,OAAO,OACR,GACF,EAAE,YAAY,GAAG,CAClB;CAGA,OAAO,OAAO,SACb;EACC,OAAO,GAAG,QAAQ,KAAA,CAAS;EAC3B,OAAO,WAAW,SAAS,MAAM,KAAK,CAAC;EACvC,OAAO,KAAK,UAAU,CAAC,CAAC,KAAK,OAAO,KAAK,CAAC,CAAC;CAC5C,GACA,EAAE,aAAa,YAAY,CAC5B,CAAC,CAAC,KACD,OAAO,iBAEN,OAAO,MAAM,YAAY,CAAC,CAAC,KAC1B,OAAO,QAAQ,MAAM,GACrB,OAAO,SAAS,YAAa,UAAU,QAAQ,OAAO,IAAK,GAC3D,OAAO,cACL,UAAU,CAAC,MAAM,cAAc,KAAK,IACpC,UAAU,OAAO,SAAS,aAAa,KAAK,UAAU,KAAK,CAC7D,CACD,CACD,GACA,OAAO,cACL,UAAU,CAAC,MAAM,cAAc,KAAK,IACpC,UAAU,OAAO,SAAS,oBAAoB,QAAQ,KAAK,CAC7D,GAGA,OAAO,OAAO,WAAW,IAAI,CAAC,CAC/B;AACD,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@graftland/cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22.19.0"
|
|
@@ -37,11 +37,11 @@
|
|
|
37
37
|
"vitest": "5.1.0"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@graftland/graft": "0.4.
|
|
41
|
-
"@graftland/loader": "0.4.
|
|
42
|
-
"@graftland/hmr": "0.4.
|
|
43
|
-
"@graftland/create": "0.4.
|
|
44
|
-
"@graftland/package-manager": "0.4.
|
|
40
|
+
"@graftland/graft": "0.4.2",
|
|
41
|
+
"@graftland/loader": "0.4.2",
|
|
42
|
+
"@graftland/hmr": "0.4.2",
|
|
43
|
+
"@graftland/create": "0.4.2",
|
|
44
|
+
"@graftland/package-manager": "0.4.2",
|
|
45
45
|
"@effect/platform-node": "4.1.2",
|
|
46
46
|
"yaml": "2.9.1"
|
|
47
47
|
},
|
package/src/Start.ts
CHANGED
|
@@ -30,7 +30,13 @@ import {
|
|
|
30
30
|
} from "@graftland/loader"
|
|
31
31
|
|
|
32
32
|
import { Application, ApplicationError, platform } from "./Application"
|
|
33
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
layer as env,
|
|
35
|
+
EnvDir,
|
|
36
|
+
EnvFile,
|
|
37
|
+
watchEnvDir,
|
|
38
|
+
watchEnvFile,
|
|
39
|
+
} from "./internal/env"
|
|
34
40
|
import { layer as intercepts } from "./internal/intercepts"
|
|
35
41
|
import { watchModules } from "./internal/watch"
|
|
36
42
|
|
|
@@ -44,6 +50,12 @@ export interface Options {
|
|
|
44
50
|
* a variable. With `watch`, changes to it are read again.
|
|
45
51
|
*/
|
|
46
52
|
readonly envFile?: string | undefined
|
|
53
|
+
/**
|
|
54
|
+
* A directory of secret files that `!env` reads when neither the process
|
|
55
|
+
* environment nor `envFile` sets a variable. Each regular file is one
|
|
56
|
+
* variable, named after the file. With `watch`, changes are read again.
|
|
57
|
+
*/
|
|
58
|
+
readonly envDir?: string | undefined
|
|
47
59
|
}
|
|
48
60
|
|
|
49
61
|
/**
|
|
@@ -138,7 +150,8 @@ export const run = (
|
|
|
138
150
|
yield* included.await
|
|
139
151
|
if (options.watch) {
|
|
140
152
|
const { reload } = yield* EnvFile
|
|
141
|
-
const
|
|
153
|
+
const { reload: reloadDir } = yield* EnvDir
|
|
154
|
+
const { envFile, envDir } = options
|
|
142
155
|
const reloader = Plugin.make({
|
|
143
156
|
name: "reloader",
|
|
144
157
|
inject: [Loader.Loader, FileSystem.FileSystem, Path.Path],
|
|
@@ -152,6 +165,10 @@ export const run = (
|
|
|
152
165
|
yield* watchEnvFile(envFile, reload, loader.retry).pipe(
|
|
153
166
|
Effect.forkScoped,
|
|
154
167
|
)
|
|
168
|
+
if (envDir !== undefined)
|
|
169
|
+
yield* watchEnvDir(envDir, reloadDir, loader.retry).pipe(
|
|
170
|
+
Effect.forkScoped,
|
|
171
|
+
)
|
|
155
172
|
}),
|
|
156
173
|
})
|
|
157
174
|
yield* Graft.plugin(reloader)
|
|
@@ -171,7 +188,9 @@ export const run = (
|
|
|
171
188
|
Layer.mergeAll(
|
|
172
189
|
platform,
|
|
173
190
|
intercepts,
|
|
174
|
-
env(options.envFile).pipe(
|
|
191
|
+
env({ file: options.envFile, dir: options.envDir }).pipe(
|
|
192
|
+
Layer.provide(NodeServices.layer),
|
|
193
|
+
),
|
|
175
194
|
Logger.layer([Logger.consolePretty()]),
|
|
176
195
|
),
|
|
177
196
|
),
|
package/src/bin.ts
CHANGED
|
@@ -31,6 +31,12 @@ const start = Command.make(
|
|
|
31
31
|
),
|
|
32
32
|
Flag.optional,
|
|
33
33
|
),
|
|
34
|
+
envDir: Flag.String("env-dir").pipe(
|
|
35
|
+
Flag.withDescription(
|
|
36
|
+
"Read variables for !env from a directory with one file per variable, such as /run/secrets",
|
|
37
|
+
),
|
|
38
|
+
Flag.optional,
|
|
39
|
+
),
|
|
34
40
|
},
|
|
35
41
|
(options) =>
|
|
36
42
|
Effect.promise(() => import("./Start")).pipe(
|
|
@@ -39,6 +45,7 @@ const start = Command.make(
|
|
|
39
45
|
Start.run({
|
|
40
46
|
...options,
|
|
41
47
|
envFile: Option.getOrUndefined(options.envFile),
|
|
48
|
+
envDir: Option.getOrUndefined(options.envDir),
|
|
42
49
|
}),
|
|
43
50
|
),
|
|
44
51
|
),
|
package/src/internal/env.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Where `!env` values come from: the process environment, then an optional
|
|
3
|
-
* `.env` file
|
|
3
|
+
* `.env` file, then an optional directory of secret files. Watch mode reads
|
|
4
|
+
* the file and the directory again when they change.
|
|
4
5
|
*/
|
|
5
6
|
import {
|
|
7
|
+
Array,
|
|
6
8
|
Cause,
|
|
7
9
|
ConfigProvider,
|
|
8
10
|
Context,
|
|
@@ -11,10 +13,13 @@ import {
|
|
|
11
13
|
Exit,
|
|
12
14
|
FileSystem,
|
|
13
15
|
Layer,
|
|
16
|
+
Option,
|
|
14
17
|
Path,
|
|
15
18
|
Queue,
|
|
19
|
+
Record,
|
|
16
20
|
Ref,
|
|
17
21
|
Stream,
|
|
22
|
+
String,
|
|
18
23
|
type PlatformError,
|
|
19
24
|
} from "effect"
|
|
20
25
|
|
|
@@ -24,57 +29,156 @@ import { observeShutdown } from "./observeShutdown"
|
|
|
24
29
|
// schema decides whether it is valid.
|
|
25
30
|
const options = { preserveEmptyStrings: true } as const
|
|
26
31
|
|
|
27
|
-
|
|
28
|
-
readonly target: string
|
|
29
|
-
readonly text: string
|
|
30
|
-
readonly provider: ConfigProvider.ConfigProvider
|
|
31
|
-
}
|
|
32
|
+
type Reload = Effect.Effect<boolean, PlatformError.PlatformError>
|
|
32
33
|
|
|
33
34
|
export class EnvFile extends Context.Service<
|
|
34
35
|
EnvFile,
|
|
35
36
|
{
|
|
36
37
|
/** Reads the file again; reports whether its contents changed. */
|
|
37
|
-
readonly reload:
|
|
38
|
+
readonly reload: Reload
|
|
38
39
|
}
|
|
39
40
|
>()("@graftland/cli/internal/env/EnvFile") {}
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
export class EnvDir extends Context.Service<
|
|
43
|
+
EnvDir,
|
|
44
|
+
{
|
|
45
|
+
/** Reads the directory again; reports whether any value changed. */
|
|
46
|
+
readonly reload: Reload
|
|
47
|
+
}
|
|
48
|
+
>()("@graftland/cli/internal/env/EnvDir") {}
|
|
49
|
+
|
|
50
|
+
const unchanged = { reload: Effect.succeed(false) }
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A source that keeps the provider read from its latest snapshot. A failed
|
|
54
|
+
* read keeps the previous snapshot.
|
|
55
|
+
*/
|
|
56
|
+
const source = Effect.fnUntraced(function* <A>(
|
|
57
|
+
read: Effect.Effect<A, PlatformError.PlatformError>,
|
|
58
|
+
provider: (snapshot: A) => ConfigProvider.ConfigProvider,
|
|
59
|
+
same: (a: A, b: A) => boolean,
|
|
60
|
+
) {
|
|
61
|
+
const initial = yield* read
|
|
62
|
+
const loaded = yield* Ref.make({
|
|
63
|
+
snapshot: initial,
|
|
64
|
+
provider: provider(initial),
|
|
65
|
+
})
|
|
66
|
+
return {
|
|
67
|
+
provider: ConfigProvider.make((path) =>
|
|
68
|
+
Effect.flatMap(Ref.get(loaded), ({ provider }) => provider.load(path)),
|
|
69
|
+
),
|
|
70
|
+
reload: Effect.gen(function* () {
|
|
71
|
+
const snapshot = yield* read
|
|
72
|
+
const previous = yield* Ref.getAndSet(loaded, {
|
|
73
|
+
snapshot,
|
|
74
|
+
provider: provider(snapshot),
|
|
75
|
+
})
|
|
76
|
+
return !same(snapshot, previous.snapshot)
|
|
77
|
+
}),
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const readFile = Effect.fnUntraced(function* (file: string) {
|
|
82
|
+
const fs = yield* FileSystem.FileSystem
|
|
83
|
+
// Resolve the request on every read, including polling. Never pin a
|
|
84
|
+
// symlink to its initial target.
|
|
85
|
+
const target = yield* fs.realPath(file)
|
|
86
|
+
return yield* fs.readFileString(target)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
const isNotFound = (error: PlatformError.PlatformError) =>
|
|
90
|
+
error.reason._tag === "NotFound"
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* One variable per regular file, named after the file. Hidden entries are
|
|
94
|
+
* skipped, which also skips the bookkeeping links of Kubernetes volumes.
|
|
95
|
+
* Trailing line breaks are not part of the value.
|
|
96
|
+
*/
|
|
97
|
+
const readDir = Effect.fnUntraced(function* (directory: string) {
|
|
98
|
+
const fs = yield* FileSystem.FileSystem
|
|
99
|
+
const path = yield* Path.Path
|
|
100
|
+
const names = yield* fs.readDirectory(directory)
|
|
101
|
+
const values = yield* Effect.forEach(
|
|
102
|
+
Array.filter(names, (name) => !String.startsWith(".")(name)),
|
|
103
|
+
(name) => {
|
|
104
|
+
const file = path.join(directory, name)
|
|
105
|
+
return fs.stat(file).pipe(
|
|
106
|
+
Effect.flatMap((info) =>
|
|
107
|
+
info.type === "File"
|
|
108
|
+
? fs
|
|
109
|
+
.readFileString(file)
|
|
110
|
+
.pipe(
|
|
111
|
+
Effect.map((text) =>
|
|
112
|
+
Option.some([
|
|
113
|
+
name,
|
|
114
|
+
String.replace(/[\r\n]+$/, "")(text),
|
|
115
|
+
] as const),
|
|
116
|
+
),
|
|
117
|
+
)
|
|
118
|
+
: Effect.succeedNone,
|
|
119
|
+
),
|
|
120
|
+
// A file removed since the listing is no longer a variable.
|
|
121
|
+
Effect.catchIf(isNotFound, () => Effect.succeedNone),
|
|
122
|
+
)
|
|
123
|
+
},
|
|
124
|
+
{ concurrency: 16 },
|
|
125
|
+
)
|
|
126
|
+
return Record.fromEntries(Array.getSomes(values))
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
const sameValues = Record.makeEquivalence(String.Equivalence)
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Provides `ConfigProvider`, `EnvFile` and `EnvDir`. A missing file or
|
|
133
|
+
* directory fails the layer.
|
|
134
|
+
*/
|
|
135
|
+
export const layer = (sources: {
|
|
136
|
+
readonly file?: string | undefined
|
|
137
|
+
readonly dir?: string | undefined
|
|
138
|
+
}) =>
|
|
43
139
|
Layer.unwrap(
|
|
44
140
|
Effect.gen(function* () {
|
|
45
|
-
const environment = ConfigProvider.fromEnv(options)
|
|
46
|
-
if (file === undefined)
|
|
47
|
-
return Layer.merge(
|
|
48
|
-
ConfigProvider.layer(environment),
|
|
49
|
-
Layer.succeed(EnvFile, { reload: Effect.succeed(false) }),
|
|
50
|
-
)
|
|
51
141
|
const fs = yield* FileSystem.FileSystem
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
142
|
+
const path = yield* Path.Path
|
|
143
|
+
const provide = <A>(
|
|
144
|
+
read: Effect.Effect<
|
|
145
|
+
A,
|
|
146
|
+
PlatformError.PlatformError,
|
|
147
|
+
FileSystem.FileSystem | Path.Path
|
|
148
|
+
>,
|
|
149
|
+
) =>
|
|
150
|
+
read.pipe(
|
|
151
|
+
Effect.provideService(FileSystem.FileSystem, fs),
|
|
152
|
+
Effect.provideService(Path.Path, path),
|
|
153
|
+
)
|
|
154
|
+
const file =
|
|
155
|
+
sources.file === undefined
|
|
156
|
+
? undefined
|
|
157
|
+
: yield* source(
|
|
158
|
+
provide(readFile(sources.file)),
|
|
159
|
+
(text) => ConfigProvider.fromDotEnvContents(text, options),
|
|
160
|
+
(a, b) => a === b,
|
|
161
|
+
)
|
|
162
|
+
const dir =
|
|
163
|
+
sources.dir === undefined
|
|
164
|
+
? undefined
|
|
165
|
+
: yield* source(
|
|
166
|
+
provide(readDir(sources.dir)),
|
|
167
|
+
(values) => ConfigProvider.fromEnvRecord(values, options),
|
|
168
|
+
sameValues,
|
|
169
|
+
)
|
|
170
|
+
const provider = Array.reduce(
|
|
171
|
+
[file, dir],
|
|
172
|
+
ConfigProvider.fromEnv(options),
|
|
173
|
+
(first, next) =>
|
|
174
|
+
next === undefined
|
|
175
|
+
? first
|
|
176
|
+
: ConfigProvider.orElse(first, next.provider),
|
|
68
177
|
)
|
|
69
|
-
return Layer.
|
|
70
|
-
ConfigProvider.layer(
|
|
71
|
-
Layer.succeed(EnvFile,
|
|
72
|
-
|
|
73
|
-
const next = yield* read
|
|
74
|
-
const previous = yield* Ref.getAndSet(loaded, next)
|
|
75
|
-
return next.text !== previous.text
|
|
76
|
-
}),
|
|
77
|
-
}),
|
|
178
|
+
return Layer.mergeAll(
|
|
179
|
+
ConfigProvider.layer(provider),
|
|
180
|
+
Layer.succeed(EnvFile, file ?? unchanged),
|
|
181
|
+
Layer.succeed(EnvDir, dir ?? unchanged),
|
|
78
182
|
)
|
|
79
183
|
}),
|
|
80
184
|
)
|
|
@@ -98,11 +202,38 @@ const logStopped =
|
|
|
98
202
|
*/
|
|
99
203
|
export const watchEnvFile = Effect.fnUntraced(function* <E, R>(
|
|
100
204
|
file: string,
|
|
101
|
-
reload:
|
|
205
|
+
reload: Reload,
|
|
102
206
|
retry: Effect.Effect<void, E, R>,
|
|
103
207
|
) {
|
|
104
|
-
const fs = yield* FileSystem.FileSystem
|
|
105
208
|
const path = yield* Path.Path
|
|
209
|
+
const name = path.basename(file)
|
|
210
|
+
return yield* watchSource(
|
|
211
|
+
file,
|
|
212
|
+
path.dirname(file),
|
|
213
|
+
(changed) => path.basename(changed) === name,
|
|
214
|
+
reload,
|
|
215
|
+
retry,
|
|
216
|
+
)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Reloads the directory when anything in it changes and retries the loader's
|
|
221
|
+
* entries. A directory that cannot be read keeps its previous values.
|
|
222
|
+
*/
|
|
223
|
+
export const watchEnvDir = <E, R>(
|
|
224
|
+
directory: string,
|
|
225
|
+
reload: Reload,
|
|
226
|
+
retry: Effect.Effect<void, E, R>,
|
|
227
|
+
) => watchSource(directory, directory, () => true, reload, retry)
|
|
228
|
+
|
|
229
|
+
const watchSource = Effect.fnUntraced(function* <E, R>(
|
|
230
|
+
file: string,
|
|
231
|
+
directory: string,
|
|
232
|
+
matches: (path: string) => boolean,
|
|
233
|
+
reload: Reload,
|
|
234
|
+
retry: Effect.Effect<void, E, R>,
|
|
235
|
+
) {
|
|
236
|
+
const fs = yield* FileSystem.FileSystem
|
|
106
237
|
const ready = yield* Deferred.make<void>()
|
|
107
238
|
// As in HMR, own the source scope inside the producer. Observe its
|
|
108
239
|
// complete Exit after cleanup, before merge can discard it.
|
|
@@ -112,13 +243,11 @@ export const watchEnvFile = Effect.fnUntraced(function* <E, R>(
|
|
|
112
243
|
>(
|
|
113
244
|
(queue) =>
|
|
114
245
|
fs
|
|
115
|
-
.watch(
|
|
246
|
+
.watch(directory, {
|
|
116
247
|
onReady: () => Deferred.doneUnsafe(ready, Effect.void),
|
|
117
248
|
})
|
|
118
249
|
.pipe(
|
|
119
|
-
Stream.filter(
|
|
120
|
-
(event) => path.basename(event.path) === path.basename(file),
|
|
121
|
-
),
|
|
250
|
+
Stream.filter((event) => matches(event.path)),
|
|
122
251
|
Stream.runForEachArray((events) => Queue.offerAll(queue, events)),
|
|
123
252
|
Effect.scoped,
|
|
124
253
|
(effect) =>
|