@graftland/cli 0.2.0-beta.1 → 0.3.0

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.
@@ -1 +1 @@
1
- {"version":3,"file":"Manage.d.ts","names":[],"sources":["../src/Manage.ts"],"mappings":";;;;qBAiDa,SAAM;WACT;WACA,MAAM,OAAO;WACb;MACT,OAAA;qBAaY,MAAG,OAAW,0BAAqB,OAAA;qBAcnC,SAAM,OAAW,0BAAqB,OAAA"}
1
+ {"version":3,"file":"Manage.d.ts","names":[],"sources":["../src/Manage.ts"],"mappings":";;;;qBA6Da,SAAM;WACT;WACA,MAAM,OAAO;WACb;MACT,OAAA;qBAaY,MAAG,OAAW,0BAAqB,OAAA;qBAcnC,SAAM,OAAW,0BAAqB,OAAA"}
package/dist/Manage.js CHANGED
@@ -11,7 +11,10 @@ var Manage_exports = /* @__PURE__ */ __exportAll({
11
11
  create: () => create,
12
12
  remove: () => remove
13
13
  });
14
- const report = (result) => Effect.logInfo(`${result.agent}: ${result.added.length} added, ${result.updated.length} updated, ${result.removed.length} removed`);
14
+ 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, {
15
+ onNone: () => Effect.void,
16
+ onSome: ({ cause, snapshotDirectory }) => Effect.logWarning("Installation committed; snapshot cleanup failed. Retry removing the retained snapshot.", cause, { snapshotDirectory })
17
+ })));
15
18
  /**
16
19
  * `name@range`, split at the last `@` so a scope's `@` stays in the name. A
17
20
  * bare name means the latest version.
@@ -1 +1 @@
1
- {"version":3,"file":"Manage.js","names":[],"sources":["../src/Manage.ts"],"sourcesContent":["/**\n * Creating projects and changing their dependencies.\n */\nimport * as Create from \"@graftland/create\"\nimport * as PackageManager from \"@graftland/package-manager\"\nimport {\n\tArray,\n\tConfig,\n\tEffect,\n\tOption,\n\tPredicate,\n\tRecord,\n\tSchema,\n} from \"effect\"\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)\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 = (\n\tdirectory: string,\n\tchanges: Readonly<Record<string, string | null>>,\n) =>\n\tEffect.gen(function* () {\n\t\tconst decoded = yield* Schema.decodeEffect(PackageManager.Changes)(changes)\n\t\t// Set by npm, pnpm and Yarn when they run graft, such as through `pnpm dlx`.\n\t\tconst userAgent = yield* Config.String(\"npm_config_user_agent\").pipe(\n\t\t\tConfig.withDefault(\"\"),\n\t\t)\n\t\tconst manager = yield* PackageManager.make({ directory, userAgent })\n\t\tconst result = yield* manager.install(decoded)\n\t\tyield* report(result)\n\t})\n\nexport const create = (options: {\n\treadonly directory: string\n\treadonly name: Option.Option<string>\n\treadonly install: boolean\n}) =>\n\tEffect.gen(function* () {\n\t\tconst project = yield* Create.create({\n\t\t\tdirectory: options.directory,\n\t\t\t...Option.match(options.name, {\n\t\t\t\tonNone: () => ({}),\n\t\t\t\tonSome: (name) => ({ name }),\n\t\t\t}),\n\t\t})\n\t\tyield* Effect.logInfo(`Created ${project.name} in ${project.directory}`)\n\t\tif (options.install) yield* install(project.directory, {})\n\t})\n\nexport const add = (specs: ReadonlyArray<string>) =>\n\tEffect.flatMap(decodeSpecs(specs), (specs) =>\n\t\tinstall(\n\t\t\t\".\",\n\t\t\tRecord.fromEntries(\n\t\t\t\tArray.map(specs, (spec) =>\n\t\t\t\t\tPredicate.isString(spec)\n\t\t\t\t\t\t? ([spec, \"latest\"] as const)\n\t\t\t\t\t\t: ([spec[0], spec[2]] as const),\n\t\t\t\t),\n\t\t\t),\n\t\t),\n\t)\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"],"mappings":";;;;;;;;;;;;;AAeA,MAAM,UAAU,WACf,OAAO,QACN,GAAG,OAAO,MAAM,IAAI,OAAO,MAAM,OAAO,UAAU,OAAO,QAAQ,OAAO,YAAY,OAAO,QAAQ,OAAO,SAC3G;;;;;AAMD,MAAM,OAAO,OAAO,MAAM,CACzB,OAAO,sBAAsB;CAC5B,eAAe;CACf;CACA,OAAO;AACR,CAAC,GACD,eAAe,WAChB,CAAC;AACD,MAAM,cAAc,OAAO,aAAa,OAAO,MAAM,IAAI,CAAC;AAE1D,MAAM,WACL,WACA,YAEA,OAAO,IAAI,aAAa;CACvB,MAAM,UAAU,OAAO,OAAO,aAAa,eAAe,OAAO,CAAC,CAAC,OAAO;CAE1E,MAAM,YAAY,OAAO,OAAO,OAAO,uBAAuB,CAAC,CAAC,KAC/D,OAAO,YAAY,EAAE,CACtB;CAEA,MAAM,SAAS,QAAO,OADC,eAAe,KAAK;EAAE;EAAW;CAAU,CAAC,EAAA,CACrC,QAAQ,OAAO;CAC7C,OAAO,OAAO,MAAM;AACrB,CAAC;AAEF,MAAa,UAAU,YAKtB,OAAO,IAAI,aAAa;CACvB,MAAM,UAAU,OAAO,OAAO,OAAO;EACpC,WAAW,QAAQ;EACnB,GAAG,OAAO,MAAM,QAAQ,MAAM;GAC7B,eAAe,CAAC;GAChB,SAAS,UAAU,EAAE,KAAK;EAC3B,CAAC;CACF,CAAC;CACD,OAAO,OAAO,QAAQ,WAAW,QAAQ,KAAK,MAAM,QAAQ,WAAW;CACvE,IAAI,QAAQ,SAAS,OAAO,QAAQ,QAAQ,WAAW,CAAC,CAAC;AAC1D,CAAC;AAEF,MAAa,OAAO,UACnB,OAAO,QAAQ,YAAY,KAAK,IAAI,UACnC,QACC,KACA,OAAO,YACN,MAAM,IAAI,QAAQ,SACjB,UAAU,SAAS,IAAI,IACnB,CAAC,MAAM,QAAQ,IACf,CAAC,KAAK,IAAI,KAAK,EAAE,CACtB,CACD,CACD,CACD;AAED,MAAa,UAAU,UACtB,QACC,KACA,OAAO,YAAY,MAAM,IAAI,QAAQ,SAAS,CAAC,MAAM,IAAI,CAAU,CAAC,CACrE"}
1
+ {"version":3,"file":"Manage.js","names":[],"sources":["../src/Manage.ts"],"sourcesContent":["/**\n * Creating projects and changing their dependencies.\n */\nimport * as Create from \"@graftland/create\"\nimport * as PackageManager from \"@graftland/package-manager\"\nimport {\n\tArray,\n\tConfig,\n\tEffect,\n\tOption,\n\tPredicate,\n\tRecord,\n\tSchema,\n} from \"effect\"\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 = (\n\tdirectory: string,\n\tchanges: Readonly<Record<string, string | null>>,\n) =>\n\tEffect.gen(function* () {\n\t\tconst decoded = yield* Schema.decodeEffect(PackageManager.Changes)(changes)\n\t\t// Set by npm, pnpm and Yarn when they run graft, such as through `pnpm dlx`.\n\t\tconst userAgent = yield* Config.String(\"npm_config_user_agent\").pipe(\n\t\t\tConfig.withDefault(\"\"),\n\t\t)\n\t\tconst manager = yield* PackageManager.make({ directory, userAgent })\n\t\tconst result = yield* manager.install(decoded)\n\t\tyield* report(result)\n\t})\n\nexport const create = (options: {\n\treadonly directory: string\n\treadonly name: Option.Option<string>\n\treadonly install: boolean\n}) =>\n\tEffect.gen(function* () {\n\t\tconst project = yield* Create.create({\n\t\t\tdirectory: options.directory,\n\t\t\t...Option.match(options.name, {\n\t\t\t\tonNone: () => ({}),\n\t\t\t\tonSome: (name) => ({ name }),\n\t\t\t}),\n\t\t})\n\t\tyield* Effect.logInfo(`Created ${project.name} in ${project.directory}`)\n\t\tif (options.install) yield* install(project.directory, {})\n\t})\n\nexport const add = (specs: ReadonlyArray<string>) =>\n\tEffect.flatMap(decodeSpecs(specs), (specs) =>\n\t\tinstall(\n\t\t\t\".\",\n\t\t\tRecord.fromEntries(\n\t\t\t\tArray.map(specs, (spec) =>\n\t\t\t\t\tPredicate.isString(spec)\n\t\t\t\t\t\t? ([spec, \"latest\"] as const)\n\t\t\t\t\t\t: ([spec[0], spec[2]] as const),\n\t\t\t\t),\n\t\t\t),\n\t\t),\n\t)\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"],"mappings":";;;;;;;;;;;;;AAeA,MAAM,UAAU,WACf,OAAO,QACN,GAAG,OAAO,MAAM,IAAI,OAAO,MAAM,OAAO,UAAU,OAAO,QAAQ,OAAO,YAAY,OAAO,QAAQ,OAAO,SAC3G,CAAC,CAAC,KACD,OAAO,QACN,OAAO,MAAM,OAAO,wBAAwB;CAC3C,cAAc,OAAO;CACrB,SAAS,EAAE,OAAO,wBACjB,OAAO,WACN,0FACA,OACA,EAAE,kBAAkB,CACrB;AACF,CAAC,CACF,CACD;;;;;AAMD,MAAM,OAAO,OAAO,MAAM,CACzB,OAAO,sBAAsB;CAC5B,eAAe;CACf;CACA,OAAO;AACR,CAAC,GACD,eAAe,WAChB,CAAC;AACD,MAAM,cAAc,OAAO,aAAa,OAAO,MAAM,IAAI,CAAC;AAE1D,MAAM,WACL,WACA,YAEA,OAAO,IAAI,aAAa;CACvB,MAAM,UAAU,OAAO,OAAO,aAAa,eAAe,OAAO,CAAC,CAAC,OAAO;CAE1E,MAAM,YAAY,OAAO,OAAO,OAAO,uBAAuB,CAAC,CAAC,KAC/D,OAAO,YAAY,EAAE,CACtB;CAEA,MAAM,SAAS,QAAO,OADC,eAAe,KAAK;EAAE;EAAW;CAAU,CAAC,EAAA,CACrC,QAAQ,OAAO;CAC7C,OAAO,OAAO,MAAM;AACrB,CAAC;AAEF,MAAa,UAAU,YAKtB,OAAO,IAAI,aAAa;CACvB,MAAM,UAAU,OAAO,OAAO,OAAO;EACpC,WAAW,QAAQ;EACnB,GAAG,OAAO,MAAM,QAAQ,MAAM;GAC7B,eAAe,CAAC;GAChB,SAAS,UAAU,EAAE,KAAK;EAC3B,CAAC;CACF,CAAC;CACD,OAAO,OAAO,QAAQ,WAAW,QAAQ,KAAK,MAAM,QAAQ,WAAW;CACvE,IAAI,QAAQ,SAAS,OAAO,QAAQ,QAAQ,WAAW,CAAC,CAAC;AAC1D,CAAC;AAEF,MAAa,OAAO,UACnB,OAAO,QAAQ,YAAY,KAAK,IAAI,UACnC,QACC,KACA,OAAO,YACN,MAAM,IAAI,QAAQ,SACjB,UAAU,SAAS,IAAI,IACnB,CAAC,MAAM,QAAQ,IACf,CAAC,KAAK,IAAI,KAAK,EAAE,CACtB,CACD,CACD,CACD;AAED,MAAa,UAAU,UACtB,QACC,KACA,OAAO,YAAY,MAAM,IAAI,QAAQ,SAAS,CAAC,MAAM,IAAI,CAAU,CAAC,CACrE"}
@@ -1 +1 @@
1
- {"version":3,"file":"Start.d.ts","names":[],"sources":["../src/Start.ts"],"mappings":";;;;;;iBAaiB;;WAEP;;WAEA;;;;;WAKA;;;;;;;qBAQG,MAAG,SACN,YACP,OAAO,cAEP,MAAM,kBACN,cAAc,cACd,cAAc,gBACd,OAAO,cACP,OAAO,aACP,KAAK"}
1
+ {"version":3,"file":"Start.d.ts","names":[],"sources":["../src/Start.ts"],"mappings":";;;;;;iBAciB;;WAEP;;WAEA;;;;;WAKA;;;;;;;qBAQG,MAAG,SACN,YACP,OAAO,cAEP,MAAM,kBACN,cAAc,cACd,cAAc,gBACd,OAAO,cACP,OAAO,aACP,KAAK"}
package/dist/Start.js CHANGED
@@ -4,6 +4,7 @@ import { layer as layer$1 } from "./internal/intercepts.js";
4
4
  import { watchModules } from "./internal/watch.js";
5
5
  import { Effect, FileSystem, Layer, Logger, Path } from "effect";
6
6
  import { NodeServices } from "@effect/platform-node";
7
+ import { SecureRandom } from "effect/crypto";
7
8
  import { Graft, Plugin } from "@graftland/graft";
8
9
  import * as Hmr from "@graftland/hmr";
9
10
  import { Include, Loader, ModuleResolver } from "@graftland/loader";
@@ -27,7 +28,14 @@ const run = (options) => Effect.gen(function* () {
27
28
  base: directory,
28
29
  importer: hmr.importerScoped
29
30
  });
30
- yield* Graft.provide(ModuleResolver.ModuleResolver, resolver);
31
+ const modules = yield* ModuleResolver.make({
32
+ base: directory,
33
+ importer: hmr.importModule
34
+ });
35
+ yield* Graft.provide(ModuleResolver.ModuleResolver, {
36
+ resolve: resolver.resolve,
37
+ importModule: modules.importModule
38
+ });
31
39
  yield* (yield* Graft.plugin(Include.Include, {
32
40
  path: file,
33
41
  watch: options.watch
@@ -52,7 +60,7 @@ const run = (options) => Effect.gen(function* () {
52
60
  }
53
61
  yield* Effect.logInfo(`Running ${path.relative(process.cwd(), file) || file}`);
54
62
  return yield* Effect.never;
55
- }).pipe(Effect.scoped, Effect.provide(Layer.provideMerge(Graft.layer, Layer.mergeAll(NodeServices.layer, layer$1, layer(options.envFile).pipe(Layer.provide(NodeServices.layer)), Logger.layer([Logger.consolePretty()])))));
63
+ }).pipe(Effect.scoped, Effect.provide(Layer.provideMerge(Graft.layer, Layer.mergeAll(SecureRandom.layerWebCrypto, NodeServices.layer, layer$1, layer(options.envFile).pipe(Layer.provide(NodeServices.layer)), Logger.layer([Logger.consolePretty()])))));
56
64
  export { Start_exports, run };
57
65
 
58
66
  //# 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 { NodeServices } from \"@effect/platform-node\"\nimport { Graft, Plugin } from \"@graftland/graft\"\nimport * as Hmr from \"@graftland/hmr\"\nimport { Include, Loader, ModuleResolver, type Tree } from \"@graftland/loader\"\nimport type { PlatformError, Schema } from \"effect\"\nimport { Effect, FileSystem, Layer, Logger, Path } from \"effect\"\nimport { EnvFile, layer as env, watchEnvFile } 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}\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\tnever,\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 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\tyield* Graft.provide(ModuleResolver.ModuleResolver, resolver)\n\t\tconst included = yield* Graft.plugin(Include.Include, {\n\t\t\tpath: file,\n\t\t\twatch: options.watch,\n\t\t})\n\t\tyield* included.await\n\t\tif (options.watch) {\n\t\t\tconst { reload } = yield* EnvFile\n\t\t\tconst envFile = options.envFile\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}),\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* Effect.never\n\t}).pipe(\n\t\tEffect.scoped,\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\tNodeServices.layer,\n\t\t\t\t\tintercepts,\n\t\t\t\t\tenv(options.envFile).pipe(Layer.provide(NodeServices.layer)),\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":";;;;;;;;;;;;;;;;;;;AA8BA,MAAa,OACZ,YAUA,OAAO,IAAI,aAAa;CACvB,MAAM,OAAO,OAAO,KAAK;CACzB,MAAM,OAAO,KAAK,QAAQ,QAAQ,MAAM;CACxC,MAAM,YAAY,KAAK,QAAQ,IAAI;CACnC,MAAM,OAAO,OAAO,KAAK,UAAU,IAAI;CACvC,MAAM,MAAM,OAAO,IAAI,KAAK,EAAE,MAAM,KAAK,KAAK,CAAC;CAC/C,MAAM,WAAW,OAAO,eAAe,KAAK;EAC3C,MAAM;EACN,UAAU,IAAI;CACf,CAAC;CACD,OAAO,MAAM,QAAQ,eAAe,gBAAgB,QAAQ;CAK5D,QAAO,OAJiB,MAAM,OAAO,QAAQ,SAAS;EACrD,MAAM;EACN,OAAO,QAAQ;CAChB,CAAC,EAAA,CACe;CAChB,IAAI,QAAQ,OAAO;EAClB,MAAM,EAAE,WAAW,OAAO;EAC1B,MAAM,UAAU,QAAQ;EACxB,MAAM,WAAW,OAAO,KAAK;GAC5B,MAAM;GACN,QAAQ;IAAC,OAAO;IAAQ,WAAW;IAAY,KAAK;GAAI;GACxD,aACC,OAAO,IAAI,aAAa;IACvB,MAAM,SAAS,OAAO,OAAO;IAC7B,OAAO,aAAa,KAAK,WAAW,OAAO,KAAK,CAAC,CAAC,KACjD,OAAO,UACR;IACA,IAAI,YAAY,KAAA,GACf,OAAO,aAAa,SAAS,QAAQ,OAAO,KAAK,CAAC,CAAC,KAClD,OAAO,UACR;GACF,CAAC;EACH,CAAC;EACD,OAAO,MAAM,OAAO,QAAQ;CAC7B;CAEA,OAAO,OAAO,QACb,WAAW,KAAK,SAAS,QAAQ,IAAI,GAAG,IAAI,KAAK,MAClD;CACA,OAAO,OAAO,OAAO;AACtB,CAAC,CAAC,CAAC,KACF,OAAO,QAEP,OAAO,QACN,MAAM,aACL,MAAM,OACN,MAAM,SACL,aAAa,OACbA,SACAC,MAAI,QAAQ,OAAO,CAAC,CAAC,KAAK,MAAM,QAAQ,aAAa,KAAK,CAAC,GAC3D,OAAO,MAAM,CAAC,OAAO,cAAc,CAAC,CAAC,CACtC,CACD,CACD,CACD"}
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 { NodeServices } from \"@effect/platform-node\"\nimport { SecureRandom } from \"effect/crypto\"\nimport { Graft, Plugin } from \"@graftland/graft\"\nimport * as Hmr from \"@graftland/hmr\"\nimport { Include, Loader, ModuleResolver, type Tree } from \"@graftland/loader\"\nimport type { PlatformError, Schema } from \"effect\"\nimport { Effect, FileSystem, Layer, Logger, Path } from \"effect\"\nimport { EnvFile, layer as env, watchEnvFile } 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}\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\tnever,\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 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: 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 included = yield* Graft.plugin(Include.Include, {\n\t\t\tpath: file,\n\t\t\twatch: options.watch,\n\t\t})\n\t\tyield* included.await\n\t\tif (options.watch) {\n\t\t\tconst { reload } = yield* EnvFile\n\t\t\tconst envFile = options.envFile\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}),\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* Effect.never\n\t}).pipe(\n\t\tEffect.scoped,\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\tSecureRandom.layerWebCrypto,\n\t\t\t\t\tNodeServices.layer,\n\t\t\t\t\tintercepts,\n\t\t\t\t\tenv(options.envFile).pipe(Layer.provide(NodeServices.layer)),\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":";;;;;;;;;;;;;;;;;;;;AA+BA,MAAa,OACZ,YAUA,OAAO,IAAI,aAAa;CACvB,MAAM,OAAO,OAAO,KAAK;CACzB,MAAM,OAAO,KAAK,QAAQ,QAAQ,MAAM;CACxC,MAAM,YAAY,KAAK,QAAQ,IAAI;CACnC,MAAM,OAAO,OAAO,KAAK,UAAU,IAAI;CACvC,MAAM,MAAM,OAAO,IAAI,KAAK,EAAE,MAAM,KAAK,KAAK,CAAC;CAC/C,MAAM,WAAW,OAAO,eAAe,KAAK;EAC3C,MAAM;EACN,UAAU,IAAI;CACf,CAAC;CACD,MAAM,UAAU,OAAO,eAAe,KAAK;EAC1C,MAAM;EACN,UAAU,IAAI;CACf,CAAC;CACD,OAAO,MAAM,QAAQ,eAAe,gBAAgB;EACnD,SAAS,SAAS;EAClB,cAAc,QAAQ;CACvB,CAAC;CAKD,QAAO,OAJiB,MAAM,OAAO,QAAQ,SAAS;EACrD,MAAM;EACN,OAAO,QAAQ;CAChB,CAAC,EAAA,CACe;CAChB,IAAI,QAAQ,OAAO;EAClB,MAAM,EAAE,WAAW,OAAO;EAC1B,MAAM,UAAU,QAAQ;EACxB,MAAM,WAAW,OAAO,KAAK;GAC5B,MAAM;GACN,QAAQ;IAAC,OAAO;IAAQ,WAAW;IAAY,KAAK;GAAI;GACxD,aACC,OAAO,IAAI,aAAa;IACvB,MAAM,SAAS,OAAO,OAAO;IAC7B,OAAO,aAAa,KAAK,WAAW,OAAO,KAAK,CAAC,CAAC,KACjD,OAAO,UACR;IACA,IAAI,YAAY,KAAA,GACf,OAAO,aAAa,SAAS,QAAQ,OAAO,KAAK,CAAC,CAAC,KAClD,OAAO,UACR;GACF,CAAC;EACH,CAAC;EACD,OAAO,MAAM,OAAO,QAAQ;CAC7B;CAEA,OAAO,OAAO,QACb,WAAW,KAAK,SAAS,QAAQ,IAAI,GAAG,IAAI,KAAK,MAClD;CACA,OAAO,OAAO,OAAO;AACtB,CAAC,CAAC,CAAC,KACF,OAAO,QAEP,OAAO,QACN,MAAM,aACL,MAAM,OACN,MAAM,SACL,aAAa,gBACb,aAAa,OACbA,SACAC,MAAI,QAAQ,OAAO,CAAC,CAAC,KAAK,MAAM,QAAQ,aAAa,KAAK,CAAC,GAC3D,OAAO,MAAM,CAAC,OAAO,cAAc,CAAC,CAAC,CACtC,CACD,CACD,CACD"}
package/dist/bin.js CHANGED
@@ -11,7 +11,7 @@ const start = Command.make("start", {
11
11
  config: Argument.String("config").pipe(Argument.withDescription("Path of the config file"), Argument.withDefault("graft.yml")),
12
12
  watch: Flag.Boolean("watch").pipe(Flag.withAlias("w"), Flag.withDescription("Reload plugins and config when files change"), Flag.withDefault(false)),
13
13
  envFile: Flag.String("env-file").pipe(Flag.withDescription("Read variables for !env from a .env file; the process environment takes precedence"), Flag.optional)
14
- }, (options) => Effect.promise(() => import("./Start.js")).pipe(Effect.flatMap((Start) => Start.run({
14
+ }, (options) => Effect.promise(() => import("./Start.js")).pipe(Effect.uninterruptible, Effect.flatMap((Start) => Start.run({
15
15
  ...options,
16
16
  envFile: Option.getOrUndefined(options.envFile)
17
17
  })))).pipe(Command.withDescription("Run the application described by a config file"));
@@ -19,9 +19,9 @@ const create = Command.make("create", {
19
19
  directory: Argument.String("directory").pipe(Argument.withDescription("Where to create the project")),
20
20
  name: Flag.String("name").pipe(Flag.withDescription("Package name; defaults to the directory name"), Flag.optional),
21
21
  install: Flag.Boolean("install").pipe(Flag.withDescription("Install dependencies afterwards"), Flag.withDefault(false))
22
- }, (options) => Effect.promise(() => import("./Manage.js")).pipe(Effect.flatMap((Manage) => Manage.create(options)))).pipe(Command.withDescription("Create a new Graft project"));
23
- const add = Command.make("add", { packages: Argument.variadic(Argument.String("package")) }, ({ packages }) => Effect.promise(() => import("./Manage.js")).pipe(Effect.flatMap((Manage) => Manage.add(packages)))).pipe(Command.withDescription("Add dependencies, restoring the project if installing fails"));
24
- const remove = Command.make("remove", { packages: Argument.variadic(Argument.String("package")) }, ({ packages }) => Effect.promise(() => import("./Manage.js")).pipe(Effect.flatMap((Manage) => Manage.remove(packages)))).pipe(Command.withDescription("Remove dependencies, restoring the project if installing fails"));
22
+ }, (options) => Effect.promise(() => import("./Manage.js")).pipe(Effect.uninterruptible, Effect.flatMap((Manage) => Manage.create(options)))).pipe(Command.withDescription("Create a new Graft project"));
23
+ const add = Command.make("add", { packages: Argument.variadic(Argument.String("package")) }, ({ packages }) => Effect.promise(() => import("./Manage.js")).pipe(Effect.uninterruptible, Effect.flatMap((Manage) => Manage.add(packages)))).pipe(Command.withDescription("Add dependencies, restoring the project if installing fails"));
24
+ const remove = Command.make("remove", { packages: Argument.variadic(Argument.String("package")) }, ({ packages }) => Effect.promise(() => import("./Manage.js")).pipe(Effect.uninterruptible, Effect.flatMap((Manage) => Manage.remove(packages)))).pipe(Command.withDescription("Remove dependencies, restoring the project if installing fails"));
25
25
  const graft = Command.make("graft").pipe(Command.withDescription("Run and manage Graft applications"), Command.withSubcommands([
26
26
  start,
27
27
  create,
package/dist/bin.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"bin.js","names":[],"sources":["../src/bin.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * The `graft` executable. Running this module starts the program.\n */\nimport { NodeRuntime, NodeServices } from \"@effect/platform-node\"\nimport { Effect, Option } from \"effect\"\nimport { Argument, Command, Flag } from \"effect/cli\"\nimport { version } from \"../package.json\" with { type: \"json\" }\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.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 create = Command.make(\n\t\"create\",\n\t{\n\t\tdirectory: Argument.String(\"directory\").pipe(\n\t\t\tArgument.withDescription(\"Where to create the project\"),\n\t\t),\n\t\tname: Flag.String(\"name\").pipe(\n\t\t\tFlag.withDescription(\"Package name; defaults to the directory name\"),\n\t\t\tFlag.optional,\n\t\t),\n\t\tinstall: Flag.Boolean(\"install\").pipe(\n\t\t\tFlag.withDescription(\"Install dependencies afterwards\"),\n\t\t\tFlag.withDefault(false),\n\t\t),\n\t},\n\t(options) =>\n\t\tEffect.promise(() => import(\"./Manage\")).pipe(\n\t\t\tEffect.flatMap((Manage) => Manage.create(options)),\n\t\t),\n).pipe(Command.withDescription(\"Create a new Graft project\"))\n\nconst add = Command.make(\n\t\"add\",\n\t{ packages: Argument.variadic(Argument.String(\"package\")) },\n\t({ packages }) =>\n\t\tEffect.promise(() => import(\"./Manage\")).pipe(\n\t\t\tEffect.flatMap((Manage) => Manage.add(packages)),\n\t\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.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.withSubcommands([start, create, add, remove]),\n)\n\nCommand.run(graft, { version }).pipe(\n\tEffect.provide(NodeServices.layer),\n\tNodeRuntime.runMain,\n)\n"],"mappings":";;;;;;;;;AASA,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,SAAS,UACf,MAAM,IAAI;CACT,GAAG;CACH,SAAS,OAAO,eAAe,QAAQ,OAAO;AAC/C,CAAC,CACF,CACD,CACF,CAAC,CAAC,KACD,QAAQ,gBAAgB,gDAAgD,CACzE;AAEA,MAAM,SAAS,QAAQ,KACtB,UACA;CACC,WAAW,SAAS,OAAO,WAAW,CAAC,CAAC,KACvC,SAAS,gBAAgB,6BAA6B,CACvD;CACA,MAAM,KAAK,OAAO,MAAM,CAAC,CAAC,KACzB,KAAK,gBAAgB,8CAA8C,GACnE,KAAK,QACN;CACA,SAAS,KAAK,QAAQ,SAAS,CAAC,CAAC,KAChC,KAAK,gBAAgB,iCAAiC,GACtD,KAAK,YAAY,KAAK,CACvB;AACD,IACC,YACA,OAAO,cAAc,OAAO,cAAW,CAAC,CAAC,KACxC,OAAO,SAAS,WAAW,OAAO,OAAO,OAAO,CAAC,CAClD,CACF,CAAC,CAAC,KAAK,QAAQ,gBAAgB,4BAA4B,CAAC;AAE5D,MAAM,MAAM,QAAQ,KACnB,OACA,EAAE,UAAU,SAAS,SAAS,SAAS,OAAO,SAAS,CAAC,EAAE,IACzD,EAAE,eACF,OAAO,cAAc,OAAO,cAAW,CAAC,CAAC,KACxC,OAAO,SAAS,WAAW,OAAO,IAAI,QAAQ,CAAC,CAChD,CACF,CAAC,CAAC,KACD,QAAQ,gBACP,6DACD,CACD;AAEA,MAAM,SAAS,QAAQ,KACtB,UACA,EAAE,UAAU,SAAS,SAAS,SAAS,OAAO,SAAS,CAAC,EAAE,IACzD,EAAE,eACF,OAAO,cAAc,OAAO,cAAW,CAAC,CAAC,KACxC,OAAO,SAAS,WAAW,OAAO,OAAO,QAAQ,CAAC,CACnD,CACF,CAAC,CAAC,KACD,QAAQ,gBACP,gEACD,CACD;AAEA,MAAM,QAAQ,QAAQ,KAAK,OAAO,CAAC,CAAC,KACnC,QAAQ,gBAAgB,mCAAmC,GAC3D,QAAQ,gBAAgB;CAAC;CAAO;CAAQ;CAAK;AAAM,CAAC,CACrD;AAEA,QAAQ,IAAI,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,KAC/B,OAAO,QAAQ,aAAa,KAAK,GACjC,YAAY,OACb"}
1
+ {"version":3,"file":"bin.js","names":[],"sources":["../src/bin.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * The `graft` executable. Running this module starts the program.\n */\nimport { NodeRuntime, NodeServices } from \"@effect/platform-node\"\nimport { Effect, Option } from \"effect\"\nimport { Argument, Command, Flag } from \"effect/cli\"\nimport { version } from \"../package.json\" with { type: \"json\" }\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 create = Command.make(\n\t\"create\",\n\t{\n\t\tdirectory: Argument.String(\"directory\").pipe(\n\t\t\tArgument.withDescription(\"Where to create the project\"),\n\t\t),\n\t\tname: Flag.String(\"name\").pipe(\n\t\t\tFlag.withDescription(\"Package name; defaults to the directory name\"),\n\t\t\tFlag.optional,\n\t\t),\n\t\tinstall: Flag.Boolean(\"install\").pipe(\n\t\t\tFlag.withDescription(\"Install dependencies afterwards\"),\n\t\t\tFlag.withDefault(false),\n\t\t),\n\t},\n\t(options) =>\n\t\tEffect.promise(() => import(\"./Manage\")).pipe(\n\t\t\tEffect.uninterruptible,\n\t\t\tEffect.flatMap((Manage) => Manage.create(options)),\n\t\t),\n).pipe(Command.withDescription(\"Create a new Graft project\"))\n\nconst add = Command.make(\n\t\"add\",\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.add(packages)),\n\t\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.withSubcommands([start, create, add, remove]),\n)\n\nCommand.run(graft, { version }).pipe(\n\tEffect.provide(NodeServices.layer),\n\tNodeRuntime.runMain,\n)\n"],"mappings":";;;;;;;;;AASA,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;AAC/C,CAAC,CACF,CACD,CACF,CAAC,CAAC,KACD,QAAQ,gBAAgB,gDAAgD,CACzE;AAEA,MAAM,SAAS,QAAQ,KACtB,UACA;CACC,WAAW,SAAS,OAAO,WAAW,CAAC,CAAC,KACvC,SAAS,gBAAgB,6BAA6B,CACvD;CACA,MAAM,KAAK,OAAO,MAAM,CAAC,CAAC,KACzB,KAAK,gBAAgB,8CAA8C,GACnE,KAAK,QACN;CACA,SAAS,KAAK,QAAQ,SAAS,CAAC,CAAC,KAChC,KAAK,gBAAgB,iCAAiC,GACtD,KAAK,YAAY,KAAK,CACvB;AACD,IACC,YACA,OAAO,cAAc,OAAO,cAAW,CAAC,CAAC,KACxC,OAAO,iBACP,OAAO,SAAS,WAAW,OAAO,OAAO,OAAO,CAAC,CAClD,CACF,CAAC,CAAC,KAAK,QAAQ,gBAAgB,4BAA4B,CAAC;AAE5D,MAAM,MAAM,QAAQ,KACnB,OACA,EAAE,UAAU,SAAS,SAAS,SAAS,OAAO,SAAS,CAAC,EAAE,IACzD,EAAE,eACF,OAAO,cAAc,OAAO,cAAW,CAAC,CAAC,KACxC,OAAO,iBACP,OAAO,SAAS,WAAW,OAAO,IAAI,QAAQ,CAAC,CAChD,CACF,CAAC,CAAC,KACD,QAAQ,gBACP,6DACD,CACD;AAEA,MAAM,SAAS,QAAQ,KACtB,UACA,EAAE,UAAU,SAAS,SAAS,SAAS,OAAO,SAAS,CAAC,EAAE,IACzD,EAAE,eACF,OAAO,cAAc,OAAO,cAAW,CAAC,CAAC,KACxC,OAAO,iBACP,OAAO,SAAS,WAAW,OAAO,OAAO,QAAQ,CAAC,CACnD,CACF,CAAC,CAAC,KACD,QAAQ,gBACP,gEACD,CACD;AAEA,MAAM,QAAQ,QAAQ,KAAK,OAAO,CAAC,CAAC,KACnC,QAAQ,gBAAgB,mCAAmC,GAC3D,QAAQ,gBAAgB;CAAC;CAAO;CAAQ;CAAK;AAAM,CAAC,CACrD;AAEA,QAAQ,IAAI,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,KAC/B,OAAO,QAAQ,aAAa,KAAK,GACjC,YAAY,OACb"}
@@ -1,5 +1,10 @@
1
+ import { observeShutdown } from "./observeShutdown.js";
1
2
  import { Cause, ConfigProvider, Context, Deferred, Effect, Exit, FileSystem, Layer, Path, Queue, Ref, Stream } from "effect";
2
3
  // src/internal/env.ts
4
+ /**
5
+ * Where `!env` values come from: the process environment, then an optional
6
+ * `.env` file that watch mode reads again when it changes.
7
+ */
3
8
  const options = { preserveEmptyStrings: true };
4
9
  var EnvFile = class extends Context.Service()("@graftland/cli/internal/env/EnvFile") {};
5
10
  /** Provides `ConfigProvider` and `EnvFile`; a missing file fails the layer. */
@@ -38,7 +43,7 @@ const watchEnvFile = (file, reload, retry) => Effect.gen(function* () {
38
43
  const fs = yield* FileSystem.FileSystem;
39
44
  const path = yield* Path.Path;
40
45
  const ready = yield* Deferred.make();
41
- const events = Stream.callback((queue) => fs.watch(path.dirname(file), { onReady: () => Deferred.doneUnsafe(ready, Effect.void) }).pipe(Stream.filter((event) => path.basename(event.path) === path.basename(file)), Stream.runForEachArray((events) => Queue.offerAll(queue, events)), Effect.scoped, 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 });
46
+ const events = Stream.callback((queue) => fs.watch(path.dirname(file), { onReady: () => Deferred.doneUnsafe(ready, Effect.void) }).pipe(Stream.filter((event) => path.basename(event.path) === path.basename(file)), 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 });
42
47
  yield* Stream.mergeAll([
43
48
  Stream.as(events, void 0),
44
49
  Stream.fromEffect(Deferred.await(ready)),
@@ -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 type { PlatformError } from \"effect\"\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} from \"effect\"\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 = <E, R>(\n\tfile: string,\n\treload: Effect.Effect<boolean, PlatformError.PlatformError>,\n\tretry: Effect.Effect<void, E, R>,\n) =>\n\tEffect.gen(function* () {\n\t\tconst fs = yield* FileSystem.FileSystem\n\t\tconst path = yield* Path.Path\n\t\tconst ready = yield* Deferred.make<void>()\n\t\t// As in HMR, own the source scope inside the producer. Observe its\n\t\t// complete Exit after cleanup, before merge can discard it.\n\t\tconst events = Stream.callback<\n\t\t\tFileSystem.WatchEvent,\n\t\t\tPlatformError.PlatformError\n\t\t>(\n\t\t\t(queue) =>\n\t\t\t\tfs\n\t\t\t\t\t.watch(path.dirname(file), {\n\t\t\t\t\t\tonReady: () => Deferred.doneUnsafe(ready, Effect.void),\n\t\t\t\t\t})\n\t\t\t\t\t.pipe(\n\t\t\t\t\t\tStream.filter(\n\t\t\t\t\t\t\t(event) => path.basename(event.path) === path.basename(file),\n\t\t\t\t\t\t),\n\t\t\t\t\t\tStream.runForEachArray((events) => Queue.offerAll(queue, events)),\n\t\t\t\t\t\tEffect.scoped,\n\t\t\t\t\t\tEffect.onExit(logStopped(file)),\n\t\t\t\t\t\tEffect.catchCauseIf(\n\t\t\t\t\t\t\t(cause) => !Cause.hasInterrupts(cause),\n\t\t\t\t\t\t\t(cause) => Effect.logError(`Watching ${file} failed`, cause),\n\t\t\t\t\t\t),\n\t\t\t\t\t\t// A failed native backend must not stop content reconciliation.\n\t\t\t\t\t\tEffect.andThen(Effect.sleep(\"1 second\")),\n\t\t\t\t\t\tEffect.forever,\n\t\t\t\t\t),\n\t\t\t{ bufferSize: 16 },\n\t\t)\n\t\t// Native events accelerate updates. Poll the request too: its target\n\t\t// may live outside this directory, change, or lose a notification.\n\t\tyield* Stream.mergeAll(\n\t\t\t[\n\t\t\t\tStream.as(events, undefined),\n\t\t\t\tStream.fromEffect(Deferred.await(ready)),\n\t\t\t\tStream.tick(\"1 second\").pipe(Stream.drop(1)),\n\t\t\t],\n\t\t\t{ concurrency: \"unbounded\" },\n\t\t).pipe(\n\t\t\tStream.runForEach(() =>\n\t\t\t\t// Delay to let writes settle.\n\t\t\t\tEffect.sleep(\"100 millis\").pipe(\n\t\t\t\t\tEffect.andThen(reload),\n\t\t\t\t\tEffect.flatMap((changed) => (changed ? retry : Effect.void)),\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(`Reloading ${file} failed`, cause),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t),\n\t\t\tEffect.catchCauseIf(\n\t\t\t\t(cause) => !Cause.hasInterrupts(cause),\n\t\t\t\t(cause) => Effect.logError(`Stopped watching ${file}`, cause),\n\t\t\t),\n\t\t)\n\t}).pipe(\n\t\t// Observe the whole watcher before forkScoped owns it.\n\t\tEffect.onExit(logStopped(file)),\n\t)\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 that watch mode reads again when it changes.\n */\nimport { observeShutdown } from \"./observeShutdown\"\nimport type { PlatformError } from \"effect\"\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} from \"effect\"\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 = <E, R>(\n\tfile: string,\n\treload: Effect.Effect<boolean, PlatformError.PlatformError>,\n\tretry: Effect.Effect<void, E, R>,\n) =>\n\tEffect.gen(function* () {\n\t\tconst fs = yield* FileSystem.FileSystem\n\t\tconst path = yield* Path.Path\n\t\tconst ready = yield* Deferred.make<void>()\n\t\t// As in HMR, own the source scope inside the producer. Observe its\n\t\t// complete Exit after cleanup, before merge can discard it.\n\t\tconst events = Stream.callback<\n\t\t\tFileSystem.WatchEvent,\n\t\t\tPlatformError.PlatformError\n\t\t>(\n\t\t\t(queue) =>\n\t\t\t\tfs\n\t\t\t\t\t.watch(path.dirname(file), {\n\t\t\t\t\t\tonReady: () => Deferred.doneUnsafe(ready, Effect.void),\n\t\t\t\t\t})\n\t\t\t\t\t.pipe(\n\t\t\t\t\t\tStream.filter(\n\t\t\t\t\t\t\t(event) => path.basename(event.path) === path.basename(file),\n\t\t\t\t\t\t),\n\t\t\t\t\t\tStream.runForEachArray((events) => Queue.offerAll(queue, events)),\n\t\t\t\t\t\tEffect.scoped,\n\t\t\t\t\t\t(effect) =>\n\t\t\t\t\t\t\tobserveShutdown(\n\t\t\t\t\t\t\t\teffect,\n\t\t\t\t\t\t\t\t`Watching ${file} stopped with interrupted cleanup`,\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\tEffect.onExit(logStopped(file)),\n\t\t\t\t\t\tEffect.catchCauseIf(\n\t\t\t\t\t\t\t(cause) => !Cause.hasInterrupts(cause),\n\t\t\t\t\t\t\t(cause) => Effect.logError(`Watching ${file} failed`, cause),\n\t\t\t\t\t\t),\n\t\t\t\t\t\t// A failed native backend must not stop content reconciliation.\n\t\t\t\t\t\tEffect.andThen(Effect.sleep(\"1 second\")),\n\t\t\t\t\t\tEffect.forever,\n\t\t\t\t\t),\n\t\t\t{ bufferSize: 16 },\n\t\t)\n\t\t// Native events accelerate updates. Poll the request too: its target\n\t\t// may live outside this directory, change, or lose a notification.\n\t\tyield* Stream.mergeAll(\n\t\t\t[\n\t\t\t\tStream.as(events, undefined),\n\t\t\t\tStream.fromEffect(Deferred.await(ready)),\n\t\t\t\tStream.tick(\"1 second\").pipe(Stream.drop(1)),\n\t\t\t],\n\t\t\t{ concurrency: \"unbounded\" },\n\t\t).pipe(\n\t\t\tStream.runForEach(() =>\n\t\t\t\t// Delay to let writes settle.\n\t\t\t\tEffect.sleep(\"100 millis\").pipe(\n\t\t\t\t\tEffect.andThen(reload),\n\t\t\t\t\tEffect.flatMap((changed) => (changed ? retry : Effect.void)),\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(`Reloading ${file} failed`, cause),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t),\n\t\t\tEffect.catchCauseIf(\n\t\t\t\t(cause) => !Cause.hasInterrupts(cause),\n\t\t\t\t(cause) => Effect.logError(`Stopped watching ${file}`, cause),\n\t\t\t),\n\t\t)\n\t}).pipe(\n\t\t// Observe the whole watcher before forkScoped owns it.\n\t\tEffect.onExit(logStopped(file)),\n\t)\n"],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import { Array, Cause, Effect, Exit, Fiber } from "effect";
2
+ // src/internal/observeShutdown.ts
3
+ /** Observe self-interrupted cleanup before a stream or scoped fork discards its Exit. */
4
+ const observeShutdown = (effect, message) => Effect.scoped(Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
5
+ const owner = yield* Effect.withFiber((self) => Effect.succeed(self.id));
6
+ const worker = yield* Effect.forkScoped(restore(effect));
7
+ const waiting = yield* Effect.exit(restore(Fiber.await(worker)));
8
+ if (Exit.isFailure(waiting)) yield* Fiber.interrupt(worker);
9
+ const exit = yield* Fiber.await(worker);
10
+ if (Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause) && !(Exit.isFailure(waiting) && Array.every(exit.cause.reasons, (reason) => reason._tag === "Interrupt" && reason.fiberId === owner))) yield* Effect.logError(message, exit.cause);
11
+ return yield* exit;
12
+ })));
13
+ export { observeShutdown };
14
+
15
+ //# sourceMappingURL=observeShutdown.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observeShutdown.js","names":[],"sources":["../../src/internal/observeShutdown.ts"],"sourcesContent":["import { Array, Cause, Effect, Exit, Fiber } from \"effect\"\n\n/** Observe self-interrupted cleanup before a stream or scoped fork discards its Exit. */\nexport const observeShutdown = <A, E, R>(\n\teffect: Effect.Effect<A, E, R>,\n\tmessage: string,\n) =>\n\tEffect.scoped(\n\t\tEffect.uninterruptibleMask((restore) =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst owner = yield* Effect.withFiber((self) => Effect.succeed(self.id))\n\t\t\t\tconst worker = yield* Effect.forkScoped(restore(effect))\n\t\t\t\tconst waiting = yield* Effect.exit(restore(Fiber.await(worker)))\n\t\t\t\tif (Exit.isFailure(waiting)) yield* Fiber.interrupt(worker)\n\t\t\t\tconst exit = yield* Fiber.await(worker)\n\t\t\t\t// Ordinary failures already have their own logger; only unexpected pure interruption needs this outlet.\n\t\t\t\tif (\n\t\t\t\t\tExit.isFailure(exit) &&\n\t\t\t\t\tCause.hasInterruptsOnly(exit.cause) &&\n\t\t\t\t\t!(\n\t\t\t\t\t\tExit.isFailure(waiting) &&\n\t\t\t\t\t\tArray.every(\n\t\t\t\t\t\t\texit.cause.reasons,\n\t\t\t\t\t\t\t(reason) =>\n\t\t\t\t\t\t\t\treason._tag === \"Interrupt\" && reason.fiberId === owner,\n\t\t\t\t\t\t)\n\t\t\t\t\t)\n\t\t\t\t)\n\t\t\t\t\tyield* Effect.logError(message, exit.cause)\n\t\t\t\treturn yield* exit\n\t\t\t}),\n\t\t),\n\t)\n"],"mappings":";;;AAGA,MAAa,mBACZ,QACA,YAEA,OAAO,OACN,OAAO,qBAAqB,YAC3B,OAAO,IAAI,aAAa;CACvB,MAAM,QAAQ,OAAO,OAAO,WAAW,SAAS,OAAO,QAAQ,KAAK,EAAE,CAAC;CACvE,MAAM,SAAS,OAAO,OAAO,WAAW,QAAQ,MAAM,CAAC;CACvD,MAAM,UAAU,OAAO,OAAO,KAAK,QAAQ,MAAM,MAAM,MAAM,CAAC,CAAC;CAC/D,IAAI,KAAK,UAAU,OAAO,GAAG,OAAO,MAAM,UAAU,MAAM;CAC1D,MAAM,OAAO,OAAO,MAAM,MAAM,MAAM;CAEtC,IACC,KAAK,UAAU,IAAI,KACnB,MAAM,kBAAkB,KAAK,KAAK,KAClC,EACC,KAAK,UAAU,OAAO,KACtB,MAAM,MACL,KAAK,MAAM,UACV,WACA,OAAO,SAAS,eAAe,OAAO,YAAY,KACpD,IAGD,OAAO,OAAO,SAAS,SAAS,KAAK,KAAK;CAC3C,OAAO,OAAO;AACf,CAAC,CACF,CACD"}
@@ -1,3 +1,4 @@
1
+ import { observeShutdown } from "./observeShutdown.js";
1
2
  import { Array, Cause, Effect, Exit, Stream, String } from "effect";
2
3
  // src/internal/watch.ts
3
4
  const moduleExtensions = [
@@ -19,7 +20,7 @@ const watchModules = (hmr, directory, retry) => hmr.watch([directory], {
19
20
  if (result._tag === "Failed") return yield* Effect.failCause(result.cause);
20
21
  yield* describe(result);
21
22
  yield* retry;
22
- }).pipe(Effect.catchCauseIf((cause) => !Cause.hasInterrupts(cause), (cause) => Effect.logError("Reloading plugin modules failed", cause)))), Effect.catchCauseIf((cause) => !Cause.hasInterrupts(cause), (cause) => Effect.logError("Watching plugin modules failed", cause)), Effect.andThen(Effect.sleep("1 second")), Effect.forever, Effect.onExit((exit) => Exit.isFailure(exit) && (Cause.hasFails(exit.cause) || Cause.hasDies(exit.cause)) ? Effect.logError("Module watcher exited with a failure", exit.cause) : Effect.void));
23
+ }).pipe(Effect.catchCauseIf((cause) => !Cause.hasInterrupts(cause), (cause) => Effect.logError("Reloading plugin modules failed", cause)))), Effect.catchCauseIf((cause) => !Cause.hasInterrupts(cause), (cause) => Effect.logError("Watching plugin modules failed", cause)), Effect.andThen(Effect.sleep("1 second")), Effect.forever, (effect) => observeShutdown(effect, "Module watcher stopped with interrupted cleanup"), Effect.onExit((exit) => Exit.isFailure(exit) && (Cause.hasFails(exit.cause) || Cause.hasDies(exit.cause)) ? Effect.logError("Module watcher exited with a failure", exit.cause) : Effect.void));
23
24
  export { isModule, watchModules };
24
25
 
25
26
  //# sourceMappingURL=watch.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"watch.js","names":[],"sources":["../../src/internal/watch.ts"],"sourcesContent":["import type * as Hmr from \"@graftland/hmr\"\nimport { Array, Cause, Effect, Exit, Stream, String } from \"effect\"\n\nconst moduleExtensions = [\".js\", \".mjs\", \".cjs\", \".ts\", \".mts\", \".cts\"]\n\n/** Whether a change to this file should reload plugin modules. */\nexport const isModule = (path: string) =>\n\t!Array.contains(String.split(/[/\\\\]/)(path), \"node_modules\") &&\n\tArray.some(moduleExtensions, (extension) => String.endsWith(extension)(path))\n\nconst describe = (result: Hmr.ReloadResult) =>\n\tresult._tag === \"Reloaded\"\n\t\t? Effect.logInfo(`Reloaded ${result.entries.length} plugin module(s)`)\n\t\t: Effect.logWarning(\n\t\t\t\tresult.reason === \"ambiguous-exports\"\n\t\t\t\t\t? `Restart required: shared plugin exports diverged in ${Array.join(result.modules, \", \")}`\n\t\t\t\t\t: result.reason === \"identity-conflict\"\n\t\t\t\t\t\t? `Restart required: plugin replacement identities conflict in ${Array.join(result.modules, \", \")}`\n\t\t\t\t\t\t: `Restart required to pick up ${Array.join(result.modules, \", \")}`,\n\t\t\t)\n\n/** Owns the HMR stream and retries failed loader entries after each reload. */\nexport const watchModules = <E, R>(\n\thmr: Pick<Hmr.Hmr, \"watch\">,\n\tdirectory: string,\n\tretry: Effect.Effect<void, E, R>,\n) =>\n\thmr.watch([directory], { recursive: true, filter: isModule }).pipe(\n\t\tStream.runForEach((result) =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\tif (result._tag === \"Failed\")\n\t\t\t\t\treturn yield* Effect.failCause(result.cause)\n\t\t\t\tyield* describe(result)\n\t\t\t\tyield* retry\n\t\t\t}).pipe(\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 plugin modules 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(\"Watching plugin modules failed\", cause),\n\t\t),\n\t\t// A backend failure ends the stream. Reinstall it after a delay rather than\n\t\t// leaving the application running without a watcher.\n\t\tEffect.andThen(Effect.sleep(\"1 second\")),\n\t\tEffect.forever,\n\t\t// onExit runs uninterruptibly, after stream cleanup. Observe the entire\n\t\t// worker before forkScoped owns it, including mixed interruption causes.\n\t\tEffect.onExit((exit) =>\n\t\t\tExit.isFailure(exit) &&\n\t\t\t(Cause.hasFails(exit.cause) || Cause.hasDies(exit.cause))\n\t\t\t\t? Effect.logError(\"Module watcher exited with a failure\", exit.cause)\n\t\t\t\t: Effect.void,\n\t\t),\n\t)\n"],"mappings":";;AAGA,MAAM,mBAAmB;CAAC;CAAO;CAAQ;CAAQ;CAAO;CAAQ;AAAM;;AAGtE,MAAa,YAAY,SACxB,CAAC,MAAM,SAAS,OAAO,MAAM,OAAO,CAAC,CAAC,IAAI,GAAG,cAAc,KAC3D,MAAM,KAAK,mBAAmB,cAAc,OAAO,SAAS,SAAS,CAAC,CAAC,IAAI,CAAC;AAE7E,MAAM,YAAY,WACjB,OAAO,SAAS,aACb,OAAO,QAAQ,YAAY,OAAO,QAAQ,OAAO,kBAAkB,IACnE,OAAO,WACP,OAAO,WAAW,sBACf,uDAAuD,MAAM,KAAK,OAAO,SAAS,IAAI,MACtF,OAAO,WAAW,sBACjB,+DAA+D,MAAM,KAAK,OAAO,SAAS,IAAI,MAC9F,+BAA+B,MAAM,KAAK,OAAO,SAAS,IAAI,GACnE;;AAGH,MAAa,gBACZ,KACA,WACA,UAEA,IAAI,MAAM,CAAC,SAAS,GAAG;CAAE,WAAW;CAAM,QAAQ;AAAS,CAAC,CAAC,CAAC,KAC7D,OAAO,YAAY,WAClB,OAAO,IAAI,aAAa;CACvB,IAAI,OAAO,SAAS,UACnB,OAAO,OAAO,OAAO,UAAU,OAAO,KAAK;CAC5C,OAAO,SAAS,MAAM;CACtB,OAAO;AACR,CAAC,CAAC,CAAC,KACF,OAAO,cACL,UAAU,CAAC,MAAM,cAAc,KAAK,IACpC,UAAU,OAAO,SAAS,mCAAmC,KAAK,CACpE,CACD,CACD,GACA,OAAO,cACL,UAAU,CAAC,MAAM,cAAc,KAAK,IACpC,UAAU,OAAO,SAAS,kCAAkC,KAAK,CACnE,GAGA,OAAO,QAAQ,OAAO,MAAM,UAAU,CAAC,GACvC,OAAO,SAGP,OAAO,QAAQ,SACd,KAAK,UAAU,IAAI,MAClB,MAAM,SAAS,KAAK,KAAK,KAAK,MAAM,QAAQ,KAAK,KAAK,KACpD,OAAO,SAAS,wCAAwC,KAAK,KAAK,IAClE,OAAO,IACX,CACD"}
1
+ {"version":3,"file":"watch.js","names":[],"sources":["../../src/internal/watch.ts"],"sourcesContent":["import { observeShutdown } from \"./observeShutdown\"\nimport type * as Hmr from \"@graftland/hmr\"\nimport { Array, Cause, Effect, Exit, Stream, String } from \"effect\"\n\nconst moduleExtensions = [\".js\", \".mjs\", \".cjs\", \".ts\", \".mts\", \".cts\"]\n\n/** Whether a change to this file should reload plugin modules. */\nexport const isModule = (path: string) =>\n\t!Array.contains(String.split(/[/\\\\]/)(path), \"node_modules\") &&\n\tArray.some(moduleExtensions, (extension) => String.endsWith(extension)(path))\n\nconst describe = (result: Hmr.ReloadResult) =>\n\tresult._tag === \"Reloaded\"\n\t\t? Effect.logInfo(`Reloaded ${result.entries.length} plugin module(s)`)\n\t\t: Effect.logWarning(\n\t\t\t\tresult.reason === \"ambiguous-exports\"\n\t\t\t\t\t? `Restart required: shared plugin exports diverged in ${Array.join(result.modules, \", \")}`\n\t\t\t\t\t: result.reason === \"identity-conflict\"\n\t\t\t\t\t\t? `Restart required: plugin replacement identities conflict in ${Array.join(result.modules, \", \")}`\n\t\t\t\t\t\t: `Restart required to pick up ${Array.join(result.modules, \", \")}`,\n\t\t\t)\n\n/** Owns the HMR stream and retries failed loader entries after each reload. */\nexport const watchModules = <E, R>(\n\thmr: Pick<Hmr.Hmr, \"watch\">,\n\tdirectory: string,\n\tretry: Effect.Effect<void, E, R>,\n) =>\n\thmr.watch([directory], { recursive: true, filter: isModule }).pipe(\n\t\tStream.runForEach((result) =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\tif (result._tag === \"Failed\")\n\t\t\t\t\treturn yield* Effect.failCause(result.cause)\n\t\t\t\tyield* describe(result)\n\t\t\t\tyield* retry\n\t\t\t}).pipe(\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 plugin modules 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(\"Watching plugin modules failed\", cause),\n\t\t),\n\t\t// A backend failure ends the stream. Reinstall it after a delay rather than\n\t\t// leaving the application running without a watcher.\n\t\tEffect.andThen(Effect.sleep(\"1 second\")),\n\t\tEffect.forever,\n\t\t(effect) =>\n\t\t\tobserveShutdown(\n\t\t\t\teffect,\n\t\t\t\t\"Module watcher stopped with interrupted cleanup\",\n\t\t\t),\n\t\t// onExit runs uninterruptibly, after stream cleanup. Observe the entire\n\t\t// worker before forkScoped owns it, including mixed interruption causes.\n\t\tEffect.onExit((exit) =>\n\t\t\tExit.isFailure(exit) &&\n\t\t\t(Cause.hasFails(exit.cause) || Cause.hasDies(exit.cause))\n\t\t\t\t? Effect.logError(\"Module watcher exited with a failure\", exit.cause)\n\t\t\t\t: Effect.void,\n\t\t),\n\t)\n"],"mappings":";;;AAIA,MAAM,mBAAmB;CAAC;CAAO;CAAQ;CAAQ;CAAO;CAAQ;AAAM;;AAGtE,MAAa,YAAY,SACxB,CAAC,MAAM,SAAS,OAAO,MAAM,OAAO,CAAC,CAAC,IAAI,GAAG,cAAc,KAC3D,MAAM,KAAK,mBAAmB,cAAc,OAAO,SAAS,SAAS,CAAC,CAAC,IAAI,CAAC;AAE7E,MAAM,YAAY,WACjB,OAAO,SAAS,aACb,OAAO,QAAQ,YAAY,OAAO,QAAQ,OAAO,kBAAkB,IACnE,OAAO,WACP,OAAO,WAAW,sBACf,uDAAuD,MAAM,KAAK,OAAO,SAAS,IAAI,MACtF,OAAO,WAAW,sBACjB,+DAA+D,MAAM,KAAK,OAAO,SAAS,IAAI,MAC9F,+BAA+B,MAAM,KAAK,OAAO,SAAS,IAAI,GACnE;;AAGH,MAAa,gBACZ,KACA,WACA,UAEA,IAAI,MAAM,CAAC,SAAS,GAAG;CAAE,WAAW;CAAM,QAAQ;AAAS,CAAC,CAAC,CAAC,KAC7D,OAAO,YAAY,WAClB,OAAO,IAAI,aAAa;CACvB,IAAI,OAAO,SAAS,UACnB,OAAO,OAAO,OAAO,UAAU,OAAO,KAAK;CAC5C,OAAO,SAAS,MAAM;CACtB,OAAO;AACR,CAAC,CAAC,CAAC,KACF,OAAO,cACL,UAAU,CAAC,MAAM,cAAc,KAAK,IACpC,UAAU,OAAO,SAAS,mCAAmC,KAAK,CACpE,CACD,CACD,GACA,OAAO,cACL,UAAU,CAAC,MAAM,cAAc,KAAK,IACpC,UAAU,OAAO,SAAS,kCAAkC,KAAK,CACnE,GAGA,OAAO,QAAQ,OAAO,MAAM,UAAU,CAAC,GACvC,OAAO,UACN,WACA,gBACC,QACA,iDACD,GAGD,OAAO,QAAQ,SACd,KAAK,UAAU,IAAI,MAClB,MAAM,SAAS,KAAK,KAAK,KAAK,MAAM,QAAQ,KAAK,KAAK,KACpD,OAAO,SAAS,wCAAwC,KAAK,KAAK,IAClE,OAAO,IACX,CACD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@graftland/cli",
3
- "version": "0.2.0-beta.1",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22.19.0"
@@ -35,11 +35,11 @@
35
35
  "vitest": "5.1.0"
36
36
  },
37
37
  "dependencies": {
38
- "@graftland/graft": "0.2.0-beta.1",
39
- "@graftland/loader": "0.2.0-beta.1",
40
- "@graftland/hmr": "0.2.0-beta.1",
41
- "@graftland/create": "0.2.0-beta.1",
42
- "@graftland/package-manager": "0.2.0-beta.1",
38
+ "@graftland/graft": "0.3.0",
39
+ "@graftland/loader": "0.3.0",
40
+ "@graftland/hmr": "0.3.0",
41
+ "@graftland/create": "0.3.0",
42
+ "@graftland/package-manager": "0.3.0",
43
43
  "@effect/platform-node": "4.1.2"
44
44
  },
45
45
  "bin": {
package/src/Manage.ts CHANGED
@@ -16,6 +16,18 @@ import {
16
16
  const report = (result: PackageManager.InstallResult) =>
17
17
  Effect.logInfo(
18
18
  `${result.agent}: ${result.added.length} added, ${result.updated.length} updated, ${result.removed.length} removed`,
19
+ ).pipe(
20
+ Effect.andThen(
21
+ Option.match(result.snapshotCleanupFailure, {
22
+ onNone: () => Effect.void,
23
+ onSome: ({ cause, snapshotDirectory }) =>
24
+ Effect.logWarning(
25
+ "Installation committed; snapshot cleanup failed. Retry removing the retained snapshot.",
26
+ cause,
27
+ { snapshotDirectory },
28
+ ),
29
+ }),
30
+ ),
19
31
  )
20
32
 
21
33
  /**
package/src/Start.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  * Runs a Graft application from its config file.
3
3
  */
4
4
  import { NodeServices } from "@effect/platform-node"
5
+ import { SecureRandom } from "effect/crypto"
5
6
  import { Graft, Plugin } from "@graftland/graft"
6
7
  import * as Hmr from "@graftland/hmr"
7
8
  import { Include, Loader, ModuleResolver, type Tree } from "@graftland/loader"
@@ -49,7 +50,14 @@ export const run = (
49
50
  base: directory,
50
51
  importer: hmr.importerScoped,
51
52
  })
52
- yield* Graft.provide(ModuleResolver.ModuleResolver, resolver)
53
+ const modules = yield* ModuleResolver.make({
54
+ base: directory,
55
+ importer: hmr.importModule,
56
+ })
57
+ yield* Graft.provide(ModuleResolver.ModuleResolver, {
58
+ resolve: resolver.resolve,
59
+ importModule: modules.importModule,
60
+ })
53
61
  const included = yield* Graft.plugin(Include.Include, {
54
62
  path: file,
55
63
  watch: options.watch,
@@ -87,6 +95,7 @@ export const run = (
87
95
  Layer.provideMerge(
88
96
  Graft.layer,
89
97
  Layer.mergeAll(
98
+ SecureRandom.layerWebCrypto,
90
99
  NodeServices.layer,
91
100
  intercepts,
92
101
  env(options.envFile).pipe(Layer.provide(NodeServices.layer)),
package/src/bin.ts CHANGED
@@ -28,6 +28,7 @@ const start = Command.make(
28
28
  },
29
29
  (options) =>
30
30
  Effect.promise(() => import("./Start")).pipe(
31
+ Effect.uninterruptible,
31
32
  Effect.flatMap((Start) =>
32
33
  Start.run({
33
34
  ...options,
@@ -56,6 +57,7 @@ const create = Command.make(
56
57
  },
57
58
  (options) =>
58
59
  Effect.promise(() => import("./Manage")).pipe(
60
+ Effect.uninterruptible,
59
61
  Effect.flatMap((Manage) => Manage.create(options)),
60
62
  ),
61
63
  ).pipe(Command.withDescription("Create a new Graft project"))
@@ -65,6 +67,7 @@ const add = Command.make(
65
67
  { packages: Argument.variadic(Argument.String("package")) },
66
68
  ({ packages }) =>
67
69
  Effect.promise(() => import("./Manage")).pipe(
70
+ Effect.uninterruptible,
68
71
  Effect.flatMap((Manage) => Manage.add(packages)),
69
72
  ),
70
73
  ).pipe(
@@ -78,6 +81,7 @@ const remove = Command.make(
78
81
  { packages: Argument.variadic(Argument.String("package")) },
79
82
  ({ packages }) =>
80
83
  Effect.promise(() => import("./Manage")).pipe(
84
+ Effect.uninterruptible,
81
85
  Effect.flatMap((Manage) => Manage.remove(packages)),
82
86
  ),
83
87
  ).pipe(
@@ -2,6 +2,7 @@
2
2
  * Where `!env` values come from: the process environment, then an optional
3
3
  * `.env` file that watch mode reads again when it changes.
4
4
  */
5
+ import { observeShutdown } from "./observeShutdown"
5
6
  import type { PlatformError } from "effect"
6
7
  import {
7
8
  Cause,
@@ -120,6 +121,11 @@ export const watchEnvFile = <E, R>(
120
121
  ),
121
122
  Stream.runForEachArray((events) => Queue.offerAll(queue, events)),
122
123
  Effect.scoped,
124
+ (effect) =>
125
+ observeShutdown(
126
+ effect,
127
+ `Watching ${file} stopped with interrupted cleanup`,
128
+ ),
123
129
  Effect.onExit(logStopped(file)),
124
130
  Effect.catchCauseIf(
125
131
  (cause) => !Cause.hasInterrupts(cause),
@@ -0,0 +1,33 @@
1
+ import { Array, Cause, Effect, Exit, Fiber } from "effect"
2
+
3
+ /** Observe self-interrupted cleanup before a stream or scoped fork discards its Exit. */
4
+ export const observeShutdown = <A, E, R>(
5
+ effect: Effect.Effect<A, E, R>,
6
+ message: string,
7
+ ) =>
8
+ Effect.scoped(
9
+ Effect.uninterruptibleMask((restore) =>
10
+ Effect.gen(function* () {
11
+ const owner = yield* Effect.withFiber((self) => Effect.succeed(self.id))
12
+ const worker = yield* Effect.forkScoped(restore(effect))
13
+ const waiting = yield* Effect.exit(restore(Fiber.await(worker)))
14
+ if (Exit.isFailure(waiting)) yield* Fiber.interrupt(worker)
15
+ const exit = yield* Fiber.await(worker)
16
+ // Ordinary failures already have their own logger; only unexpected pure interruption needs this outlet.
17
+ if (
18
+ Exit.isFailure(exit) &&
19
+ Cause.hasInterruptsOnly(exit.cause) &&
20
+ !(
21
+ Exit.isFailure(waiting) &&
22
+ Array.every(
23
+ exit.cause.reasons,
24
+ (reason) =>
25
+ reason._tag === "Interrupt" && reason.fiberId === owner,
26
+ )
27
+ )
28
+ )
29
+ yield* Effect.logError(message, exit.cause)
30
+ return yield* exit
31
+ }),
32
+ ),
33
+ )
@@ -1,3 +1,4 @@
1
+ import { observeShutdown } from "./observeShutdown"
1
2
  import type * as Hmr from "@graftland/hmr"
2
3
  import { Array, Cause, Effect, Exit, Stream, String } from "effect"
3
4
 
@@ -47,6 +48,11 @@ export const watchModules = <E, R>(
47
48
  // leaving the application running without a watcher.
48
49
  Effect.andThen(Effect.sleep("1 second")),
49
50
  Effect.forever,
51
+ (effect) =>
52
+ observeShutdown(
53
+ effect,
54
+ "Module watcher stopped with interrupted cleanup",
55
+ ),
50
56
  // onExit runs uninterruptibly, after stream cleanup. Observe the entire
51
57
  // worker before forkScoped owns it, including mixed interruption causes.
52
58
  Effect.onExit((exit) =>