@graftland/cli 0.4.0 → 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/README.md +3 -2
- package/README.zh-CN.md +2 -2
- package/README.zh-TW.md +2 -2
- package/dist/Manage.d.ts +2 -8
- package/dist/Manage.d.ts.map +1 -1
- package/dist/Manage.js +4 -16
- package/dist/Manage.js.map +1 -1
- 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 +6 -8
- 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/Manage.ts +2 -22
- package/src/Start.ts +22 -3
- package/src/bin.ts +8 -22
- package/src/internal/env.ts +175 -46
package/README.md
CHANGED
|
@@ -11,8 +11,9 @@ The `graft` command: `start`, `create`, `add` and `remove`.
|
|
|
11
11
|
npm install @graftland/cli effect
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
Create a project with `
|
|
15
|
-
with `
|
|
14
|
+
Create a project with `npm create @graftland@latest my-app`, which runs the same
|
|
15
|
+
`create` command. Then run it with `npm run dev`. You need Node.js 22.19 or
|
|
16
|
+
later.
|
|
16
17
|
|
|
17
18
|
## Documentation
|
|
18
19
|
|
package/README.zh-CN.md
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
npm install @graftland/cli effect
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
用 `
|
|
14
|
-
`
|
|
13
|
+
用 `npm create @graftland@latest my-app` 创建项目,它运行的就是同一个 `create`
|
|
14
|
+
命令。然后用 `npm run dev` 运行它。需要 Node.js 22.19 或更高版本。
|
|
15
15
|
|
|
16
16
|
## 文档
|
|
17
17
|
|
package/README.zh-TW.md
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
npm install @graftland/cli effect
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
用 `
|
|
14
|
-
`
|
|
13
|
+
用 `npm create @graftland@latest my-app` 建立專案,它執行的是同一個 `create`
|
|
14
|
+
命令。然後用 `npm run dev` 執行它。需要 Node.js 22.19 或更新版本。
|
|
15
15
|
|
|
16
16
|
## 文件
|
|
17
17
|
|
package/dist/Manage.d.ts
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
import { CommandConfigError } from "./Commands.js";
|
|
2
|
-
import { Config, Effect, FileSystem,
|
|
3
|
-
import * as Create from "@graftland/create";
|
|
2
|
+
import { Config, Effect, FileSystem, Path } from "effect";
|
|
4
3
|
import * as PackageManager from "@graftland/package-manager";
|
|
5
4
|
declare namespace Manage_d_exports {
|
|
6
|
-
export { add, addPlugin,
|
|
5
|
+
export { add, addPlugin, remove };
|
|
7
6
|
}
|
|
8
|
-
export declare const create: (options: {
|
|
9
|
-
readonly directory: string;
|
|
10
|
-
readonly name: Option.Option<string>;
|
|
11
|
-
readonly install: boolean;
|
|
12
|
-
}) => Effect.Effect<void, Config.ConfigError | Create.CreateError | Create.InvalidName | Create.InvalidPackageManager | PackageManager.Error, import("effect/process/ChildProcessSpawner").ChildProcessSpawner | FileSystem.FileSystem | Path.Path>;
|
|
13
7
|
export declare const add: (specs: readonly string[]) => Effect.Effect<void, Config.ConfigError | PackageManager.Error, import("effect/process/ChildProcessSpawner").ChildProcessSpawner | FileSystem.FileSystem | Path.Path>;
|
|
14
8
|
export declare const remove: (names: ReadonlyArray<string>) => Effect.Effect<void, Config.ConfigError | PackageManager.Error, import("effect/process/ChildProcessSpawner").ChildProcessSpawner | FileSystem.FileSystem | Path.Path>;
|
|
15
9
|
/** Install a command plugin and explicitly enable its /cli entry. */
|
package/dist/Manage.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Manage.d.ts","names":[],"sources":["../src/Manage.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"Manage.d.ts","names":[],"sources":["../src/Manage.ts"],"mappings":";;;;;;qBAoEa,MAAG,6BAAA,OAAA,aAAA,OAAA,cAAA,eAAA,oDAAA,sBAAA,WAAA,aAAA,KAAA;qBAgBH,SAAM,OAAW,0BAAqB,OAAA,aAAA,OAAA,cAAA,eAAA,oDAAA,sBAAA,WAAA,aAAA,KAAA;;qBAuDtC,YAAS,cAAA,iCAAA,OAAA,aAAA,qBAAA,OAAA,cAAA,eAAA,oDAAA,sBAAA,WAAA,aAAA,KAAA"}
|
package/dist/Manage.js
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import { __exportAll } from "./_virtual/_rolldown/runtime.js";
|
|
2
2
|
import { CommandConfigError } from "./Commands.js";
|
|
3
3
|
import { home } from "./internal/commandConfig.js";
|
|
4
|
-
import { Array, Config, Effect, FileSystem, Option, Path, Predicate, Record, Schema
|
|
4
|
+
import { Array, Config, Effect, FileSystem, Option, Path, Predicate, Record, Schema } from "effect";
|
|
5
5
|
import * as Create from "@graftland/create";
|
|
6
6
|
import * as PackageManager from "@graftland/package-manager";
|
|
7
7
|
import { parseDocument } from "yaml";
|
|
8
8
|
// src/Manage.ts
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
10
|
+
* Changing a project's dependencies.
|
|
11
11
|
*/
|
|
12
12
|
var Manage_exports = /* @__PURE__ */ __exportAll({
|
|
13
13
|
add: () => add,
|
|
14
14
|
addPlugin: () => addPlugin,
|
|
15
|
-
create: () => create,
|
|
16
15
|
remove: () => remove
|
|
17
16
|
});
|
|
18
17
|
const report = (result) => Effect.logInfo(`${result.agent}: ${result.added.length} added, ${result.updated.length} updated, ${result.removed.length} removed`).pipe(Effect.andThen(Option.match(result.snapshotCleanupFailure, {
|
|
@@ -38,17 +37,6 @@ const install = Effect.fn("Manage.install")(function* (directory, changes) {
|
|
|
38
37
|
})).install(decoded);
|
|
39
38
|
yield* report(result);
|
|
40
39
|
});
|
|
41
|
-
const create = Effect.fn("Manage.create")(function* (options) {
|
|
42
|
-
const project = yield* Create.create({
|
|
43
|
-
directory: options.directory,
|
|
44
|
-
...Option.match(options.name, {
|
|
45
|
-
onNone: () => ({}),
|
|
46
|
-
onSome: (name) => ({ name })
|
|
47
|
-
})
|
|
48
|
-
});
|
|
49
|
-
yield* Effect.logInfo(`Created ${project.name} in ${project.directory}`);
|
|
50
|
-
if (options.install) yield* install(project.directory, {});
|
|
51
|
-
});
|
|
52
40
|
const add = Effect.fn("Manage.add")(function* (specs) {
|
|
53
41
|
const decoded = yield* decodeSpecs(specs);
|
|
54
42
|
yield* install(".", Record.fromEntries(Array.map(decoded, (spec) => Predicate.isString(spec) ? [spec, "latest"] : [spec[0], spec[2]])));
|
|
@@ -99,7 +87,7 @@ const addPlugin = Effect.fn("Manage.addPlugin")(function* (name, global = false)
|
|
|
99
87
|
const path = yield* Path.Path;
|
|
100
88
|
const fs = yield* FileSystem.FileSystem;
|
|
101
89
|
const directory = global ? yield* home : path.resolve(".");
|
|
102
|
-
const packageName =
|
|
90
|
+
const packageName = Create.pluginPackage(name);
|
|
103
91
|
const module = `${packageName}/cli`;
|
|
104
92
|
const file = path.join(directory, "graft.yml");
|
|
105
93
|
const text = yield* enabledConfig(file, module);
|
|
@@ -108,6 +96,6 @@ const addPlugin = Effect.fn("Manage.addPlugin")(function* (name, global = false)
|
|
|
108
96
|
yield* fs.writeFileString(file, text).pipe(Effect.uninterruptible);
|
|
109
97
|
yield* Effect.logInfo(`Enabled ${module} in ${file}`);
|
|
110
98
|
});
|
|
111
|
-
export { Manage_exports, add, addPlugin,
|
|
99
|
+
export { Manage_exports, add, addPlugin, remove };
|
|
112
100
|
|
|
113
101
|
//# sourceMappingURL=Manage.js.map
|
package/dist/Manage.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Manage.js","names":[],"sources":["../src/Manage.ts"],"sourcesContent":["/**\n *
|
|
1
|
+
{"version":3,"file":"Manage.js","names":[],"sources":["../src/Manage.ts"],"sourcesContent":["/**\n * Changing a project's dependencies.\n */\nimport {\n\tArray,\n\tConfig,\n\tEffect,\n\tFileSystem,\n\tOption,\n\tPath,\n\tPredicate,\n\tRecord,\n\tSchema,\n} from \"effect\"\n\nimport * as Create from \"@graftland/create\"\nimport * as PackageManager from \"@graftland/package-manager\"\n\nimport { parseDocument, type Document } from \"yaml\"\n\nimport { CommandConfigError } from \"./Commands\"\nimport { home } from \"./internal/commandConfig\"\n\nconst report = (result: PackageManager.InstallResult) =>\n\tEffect.logInfo(\n\t\t`${result.agent}: ${result.added.length} added, ${result.updated.length} updated, ${result.removed.length} removed`,\n\t).pipe(\n\t\tEffect.andThen(\n\t\t\tOption.match(result.snapshotCleanupFailure, {\n\t\t\t\tonNone: () => Effect.void,\n\t\t\t\tonSome: ({ cause, snapshotDirectory }) =>\n\t\t\t\t\tEffect.logWarning(\n\t\t\t\t\t\t\"Installation committed; snapshot cleanup failed. Retry removing the retained snapshot.\",\n\t\t\t\t\t\tcause,\n\t\t\t\t\t\t{ snapshotDirectory },\n\t\t\t\t\t),\n\t\t\t}),\n\t\t),\n\t)\n\n/**\n * `name@range`, split at the last `@` so a scope's `@` stays in the name. A\n * bare name means the latest version.\n */\nconst Spec = Schema.Union([\n\tSchema.TemplateLiteralParser([\n\t\tPackageManager.PackageName,\n\t\t\"@\",\n\t\tSchema.String,\n\t]),\n\tPackageManager.PackageName,\n])\nconst decodeSpecs = Schema.decodeEffect(Schema.Array(Spec))\n\nconst install = Effect.fn(\"Manage.install\")(function* (\n\tdirectory: string,\n\tchanges: Readonly<Record<string, string | null>>,\n) {\n\tconst decoded = yield* Schema.decodeEffect(PackageManager.Changes)(changes)\n\t// Set by npm, pnpm and Yarn when they run graft, such as through `pnpm dlx`.\n\tconst userAgent = yield* Config.String(\"npm_config_user_agent\").pipe(\n\t\tConfig.withDefault(\"\"),\n\t)\n\tconst manager = yield* PackageManager.make({ directory, userAgent })\n\tconst result = yield* manager.install(decoded)\n\tyield* report(result)\n})\n\nexport const add = Effect.fn(\"Manage.add\")(function* (\n\tspecs: ReadonlyArray<string>,\n) {\n\tconst decoded = yield* decodeSpecs(specs)\n\tyield* install(\n\t\t\".\",\n\t\tRecord.fromEntries(\n\t\t\tArray.map(decoded, (spec) =>\n\t\t\t\tPredicate.isString(spec)\n\t\t\t\t\t? ([spec, \"latest\"] as const)\n\t\t\t\t\t: ([spec[0], spec[2]] as const),\n\t\t\t),\n\t\t),\n\t)\n})\n\nexport const remove = (names: ReadonlyArray<string>) =>\n\tinstall(\n\t\t\".\",\n\t\tRecord.fromEntries(Array.map(names, (name) => [name, null] as const)),\n\t)\n\nconst enabledConfig = Effect.fnUntraced(function* (\n\tfile: string,\n\tmodule: string,\n) {\n\tconst fs = yield* FileSystem.FileSystem\n\tconst text = (yield* fs.exists(file))\n\t\t? yield* fs.readFileString(file)\n\t\t: \"[]\\n\"\n\tconst document = yield* Effect.try({\n\t\ttry: () => {\n\t\t\tconst document: Document = parseDocument(text)\n\t\t\tconst error = document.errors[0]\n\t\t\tif (error !== undefined) throw error\n\t\t\tif (document.toJS() === null) document.contents = document.createNode([])\n\t\t\treturn document\n\t\t},\n\t\tcatch: (cause) => CommandConfigError.make({ file, cause }),\n\t})\n\tconst entries = yield* Schema.decodeUnknownEffect(\n\t\tSchema.Array(Schema.Unknown),\n\t)(document.toJS())\n\tconst existing = Array.findFirstIndex(\n\t\tentries,\n\t\t(entry) =>\n\t\t\tPredicate.hasProperty(entry, \"name\") &&\n\t\t\tentry.name === module &&\n\t\t\tPredicate.hasProperty(entry, \"cli\") &&\n\t\t\tentry.cli === true,\n\t)\n\treturn yield* Effect.try({\n\t\ttry: () => {\n\t\t\tif (Option.isSome(existing))\n\t\t\t\tdocument.setIn([existing.value, \"disabled\"], false)\n\t\t\telse document.add({ name: module, cli: true })\n\t\t\treturn document.toString()\n\t\t},\n\t\tcatch: (cause) => CommandConfigError.make({ file, cause }),\n\t})\n})\nconst prepareHome = Effect.fnUntraced(function* (directory: string) {\n\tconst fs = yield* FileSystem.FileSystem\n\tconst path = yield* Path.Path\n\tyield* fs.makeDirectory(directory, { recursive: true })\n\tconst manifest = path.join(directory, \"package.json\")\n\tif (!(yield* fs.exists(manifest)))\n\t\tyield* fs.writeFileString(manifest, '{\"private\":true,\"type\":\"module\"}\\n')\n}, Effect.uninterruptible)\n\n/** Install a command plugin and explicitly enable its /cli entry. */\nexport const addPlugin = Effect.fn(\"Manage.addPlugin\")(function* (\n\tname: string,\n\tglobal: boolean = false,\n) {\n\tconst path = yield* Path.Path\n\tconst fs = yield* FileSystem.FileSystem\n\tconst directory = global ? yield* home : path.resolve(\".\")\n\tconst packageName = Create.pluginPackage(name)\n\tconst module = `${packageName}/cli`\n\tconst file = path.join(directory, \"graft.yml\")\n\tconst text = yield* enabledConfig(file, module)\n\tif (global) yield* prepareHome(directory)\n\tyield* install(directory, Record.singleton(packageName, \"latest\"))\n\t// Once accepted, the native write must finish before command cleanup can proceed.\n\tyield* fs.writeFileString(file, text).pipe(Effect.uninterruptible)\n\tyield* Effect.logInfo(`Enabled ${module} in ${file}`)\n})\n"],"mappings":"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"}
|
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
|
@@ -6,6 +6,7 @@ import { Array, Effect, Layer, Logger, Option, Stdio } from "effect";
|
|
|
6
6
|
import { NodeRuntime } from "@effect/platform-node";
|
|
7
7
|
import { Graft } from "@graftland/graft";
|
|
8
8
|
import { Argument, CliError, Command, Flag } from "effect/cli";
|
|
9
|
+
import { command } from "@graftland/create/Command";
|
|
9
10
|
// src/bin.ts
|
|
10
11
|
/**
|
|
11
12
|
* The `graft` executable. Running this module starts the program.
|
|
@@ -13,16 +14,13 @@ import { Argument, CliError, Command, Flag } from "effect/cli";
|
|
|
13
14
|
const start = Command.make("start", {
|
|
14
15
|
config: Argument.String("config").pipe(Argument.withDescription("Path of the config file"), Argument.withDefault("graft.yml")),
|
|
15
16
|
watch: Flag.Boolean("watch").pipe(Flag.withAlias("w"), Flag.withDescription("Reload plugins and config when files change"), Flag.withDefault(false)),
|
|
16
|
-
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)
|
|
17
19
|
}, (options) => Effect.promise(() => import("./Start.js")).pipe(Effect.uninterruptible, Effect.flatMap((Start) => Start.run({
|
|
18
20
|
...options,
|
|
19
|
-
envFile: Option.getOrUndefined(options.envFile)
|
|
21
|
+
envFile: Option.getOrUndefined(options.envFile),
|
|
22
|
+
envDir: Option.getOrUndefined(options.envDir)
|
|
20
23
|
})))).pipe(Command.withDescription("Run the application described by a config file"));
|
|
21
|
-
const create = Command.make("create", {
|
|
22
|
-
directory: Argument.String("directory").pipe(Argument.withDescription("Where to create the project")),
|
|
23
|
-
name: Flag.String("name").pipe(Flag.withDescription("Package name; defaults to the directory name"), Flag.optional),
|
|
24
|
-
install: Flag.Boolean("install").pipe(Flag.withDescription("Install dependencies afterwards"), Flag.withDefault(false))
|
|
25
|
-
}, (options) => Effect.promise(() => import("./Manage.js")).pipe(Effect.uninterruptible, Effect.flatMap((Manage) => Manage.create(options)))).pipe(Command.withDescription("Create a new Graft project"));
|
|
26
24
|
const add = Command.make("add", {
|
|
27
25
|
packages: Argument.variadic(Argument.String("package")),
|
|
28
26
|
plugin: Flag.String("plugin").pipe(Flag.optional),
|
|
@@ -46,7 +44,7 @@ const program = Effect.gen(function* () {
|
|
|
46
44
|
const context = yield* Layer.build(Graft.layer);
|
|
47
45
|
return yield* run([
|
|
48
46
|
start,
|
|
49
|
-
|
|
47
|
+
command,
|
|
50
48
|
add,
|
|
51
49
|
remove
|
|
52
50
|
], graft, version, args).pipe(Effect.provideContext(context));
|
package/dist/bin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bin.js","names":["Host.output","Host.run"],"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 { 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
|
|
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":"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"}
|
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/Manage.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Changing a project's dependencies.
|
|
3
3
|
*/
|
|
4
4
|
import {
|
|
5
5
|
Array,
|
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
Predicate,
|
|
12
12
|
Record,
|
|
13
13
|
Schema,
|
|
14
|
-
String,
|
|
15
14
|
} from "effect"
|
|
16
15
|
|
|
17
16
|
import * as Create from "@graftland/create"
|
|
@@ -67,22 +66,6 @@ const install = Effect.fn("Manage.install")(function* (
|
|
|
67
66
|
yield* report(result)
|
|
68
67
|
})
|
|
69
68
|
|
|
70
|
-
export const create = Effect.fn("Manage.create")(function* (options: {
|
|
71
|
-
readonly directory: string
|
|
72
|
-
readonly name: Option.Option<string>
|
|
73
|
-
readonly install: boolean
|
|
74
|
-
}) {
|
|
75
|
-
const project = yield* Create.create({
|
|
76
|
-
directory: options.directory,
|
|
77
|
-
...Option.match(options.name, {
|
|
78
|
-
onNone: () => ({}),
|
|
79
|
-
onSome: (name) => ({ name }),
|
|
80
|
-
}),
|
|
81
|
-
})
|
|
82
|
-
yield* Effect.logInfo(`Created ${project.name} in ${project.directory}`)
|
|
83
|
-
if (options.install) yield* install(project.directory, {})
|
|
84
|
-
})
|
|
85
|
-
|
|
86
69
|
export const add = Effect.fn("Manage.add")(function* (
|
|
87
70
|
specs: ReadonlyArray<string>,
|
|
88
71
|
) {
|
|
@@ -161,10 +144,7 @@ export const addPlugin = Effect.fn("Manage.addPlugin")(function* (
|
|
|
161
144
|
const path = yield* Path.Path
|
|
162
145
|
const fs = yield* FileSystem.FileSystem
|
|
163
146
|
const directory = global ? yield* home : path.resolve(".")
|
|
164
|
-
const packageName =
|
|
165
|
-
String.startsWith("@")(name) || String.startsWith("graft-plugin-")(name)
|
|
166
|
-
? name
|
|
167
|
-
: `@graftland/plugin-${name}`
|
|
147
|
+
const packageName = Create.pluginPackage(name)
|
|
168
148
|
const module = `${packageName}/cli`
|
|
169
149
|
const file = path.join(directory, "graft.yml")
|
|
170
150
|
const text = yield* enabledConfig(file, module)
|
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
|
@@ -6,6 +6,7 @@ import { Array, Effect, Layer, Logger, Option, Stdio } from "effect"
|
|
|
6
6
|
import { Argument, CliError, Command, Flag } from "effect/cli"
|
|
7
7
|
import { NodeRuntime } from "@effect/platform-node"
|
|
8
8
|
|
|
9
|
+
import { command as create } from "@graftland/create/Command"
|
|
9
10
|
import { Graft } from "@graftland/graft"
|
|
10
11
|
|
|
11
12
|
import { version } from "../package.json" with { type: "json" }
|
|
@@ -30,6 +31,12 @@ const start = Command.make(
|
|
|
30
31
|
),
|
|
31
32
|
Flag.optional,
|
|
32
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
|
+
),
|
|
33
40
|
},
|
|
34
41
|
(options) =>
|
|
35
42
|
Effect.promise(() => import("./Start")).pipe(
|
|
@@ -38,6 +45,7 @@ const start = Command.make(
|
|
|
38
45
|
Start.run({
|
|
39
46
|
...options,
|
|
40
47
|
envFile: Option.getOrUndefined(options.envFile),
|
|
48
|
+
envDir: Option.getOrUndefined(options.envDir),
|
|
41
49
|
}),
|
|
42
50
|
),
|
|
43
51
|
),
|
|
@@ -45,28 +53,6 @@ const start = Command.make(
|
|
|
45
53
|
Command.withDescription("Run the application described by a config file"),
|
|
46
54
|
)
|
|
47
55
|
|
|
48
|
-
const create = Command.make(
|
|
49
|
-
"create",
|
|
50
|
-
{
|
|
51
|
-
directory: Argument.String("directory").pipe(
|
|
52
|
-
Argument.withDescription("Where to create the project"),
|
|
53
|
-
),
|
|
54
|
-
name: Flag.String("name").pipe(
|
|
55
|
-
Flag.withDescription("Package name; defaults to the directory name"),
|
|
56
|
-
Flag.optional,
|
|
57
|
-
),
|
|
58
|
-
install: Flag.Boolean("install").pipe(
|
|
59
|
-
Flag.withDescription("Install dependencies afterwards"),
|
|
60
|
-
Flag.withDefault(false),
|
|
61
|
-
),
|
|
62
|
-
},
|
|
63
|
-
(options) =>
|
|
64
|
-
Effect.promise(() => import("./Manage")).pipe(
|
|
65
|
-
Effect.uninterruptible,
|
|
66
|
-
Effect.flatMap((Manage) => Manage.create(options)),
|
|
67
|
-
),
|
|
68
|
-
).pipe(Command.withDescription("Create a new Graft project"))
|
|
69
|
-
|
|
70
56
|
const add = Command.make(
|
|
71
57
|
"add",
|
|
72
58
|
{
|
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) =>
|