@graftland/cli 0.1.0 → 0.2.0-beta.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.
- package/README.md +7 -2
- package/README.zh-CN.md +22 -0
- package/README.zh-TW.md +22 -0
- package/dist/Manage.d.ts +1 -1
- package/dist/Manage.d.ts.map +1 -1
- package/dist/Manage.js +12 -7
- package/dist/Manage.js.map +1 -1
- package/dist/Start.d.ts +8 -2
- package/dist/Start.d.ts.map +1 -1
- package/dist/Start.js +17 -21
- package/dist/Start.js.map +1 -1
- package/dist/bin.js +9 -11
- package/dist/bin.js.map +1 -1
- package/dist/internal/configSchema.d.ts +7 -0
- package/dist/internal/configSchema.d.ts.map +1 -0
- package/dist/internal/configSchema.js +44 -0
- package/dist/internal/configSchema.js.map +1 -0
- package/dist/internal/env.js +50 -0
- package/dist/internal/env.js.map +1 -0
- package/dist/internal/intercepts.js +12 -0
- package/dist/internal/intercepts.js.map +1 -0
- package/dist/internal/watch.js +25 -0
- package/dist/internal/watch.js.map +1 -0
- package/dist/package.js +5 -0
- package/dist/package.js.map +1 -0
- package/package.json +14 -10
- package/schema.json +178 -0
- package/src/Manage.ts +28 -14
- package/src/Start.ts +36 -51
- package/src/bin.ts +13 -15
- package/src/internal/configSchema.ts +85 -0
- package/src/internal/env.ts +163 -0
- package/src/internal/intercepts.ts +23 -0
- package/src/internal/watch.ts +58 -0
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@graftland/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0-beta.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
|
-
"node": ">=22.
|
|
6
|
+
"node": ">=22.19.0"
|
|
7
7
|
},
|
|
8
8
|
"description": "The graft command: start, create, add and remove",
|
|
9
9
|
"license": "Apache-2.0",
|
|
@@ -15,15 +15,19 @@
|
|
|
15
15
|
"sideEffects": false,
|
|
16
16
|
"files": [
|
|
17
17
|
"dist",
|
|
18
|
-
"src"
|
|
18
|
+
"src",
|
|
19
|
+
"schema.json",
|
|
20
|
+
"README.zh-CN.md",
|
|
21
|
+
"README.zh-TW.md"
|
|
19
22
|
],
|
|
20
23
|
"exports": {
|
|
21
24
|
".": "./dist/index.js",
|
|
22
25
|
"./bin": "./dist/bin.js",
|
|
26
|
+
"./internal/configSchema": "./dist/internal/configSchema.js",
|
|
23
27
|
"./package.json": "./package.json"
|
|
24
28
|
},
|
|
25
29
|
"peerDependencies": {
|
|
26
|
-
"effect": "4.1
|
|
30
|
+
"effect": "4.1.*"
|
|
27
31
|
},
|
|
28
32
|
"devDependencies": {
|
|
29
33
|
"@effect/vitest": "4.1.2",
|
|
@@ -31,18 +35,18 @@
|
|
|
31
35
|
"vitest": "5.1.0"
|
|
32
36
|
},
|
|
33
37
|
"dependencies": {
|
|
34
|
-
"@graftland/graft": "0.
|
|
35
|
-
"@graftland/loader": "0.
|
|
36
|
-
"@graftland/hmr": "0.
|
|
37
|
-
"@graftland/create": "0.
|
|
38
|
-
"@graftland/package-manager": "0.
|
|
38
|
+
"@graftland/graft": "0.2.0-beta.0",
|
|
39
|
+
"@graftland/loader": "0.2.0-beta.0",
|
|
40
|
+
"@graftland/hmr": "0.2.0-beta.0",
|
|
41
|
+
"@graftland/create": "0.2.0-beta.0",
|
|
42
|
+
"@graftland/package-manager": "0.2.0-beta.0",
|
|
39
43
|
"@effect/platform-node": "4.1.2"
|
|
40
44
|
},
|
|
41
45
|
"bin": {
|
|
42
46
|
"graft": "./dist/bin.js"
|
|
43
47
|
},
|
|
44
48
|
"scripts": {
|
|
45
|
-
"build": "tsdown",
|
|
49
|
+
"build": "tsdown && node scripts/schema.mjs",
|
|
46
50
|
"test": "vitest run"
|
|
47
51
|
}
|
|
48
52
|
}
|
package/schema.json
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"anyOf": [
|
|
4
|
+
{
|
|
5
|
+
"type": "array",
|
|
6
|
+
"items": {
|
|
7
|
+
"$ref": "#/definitions/Entry"
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"type": "null"
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"title": "Graft config file",
|
|
15
|
+
"description": "The entries that graft start loads. Each entry loads one plugin, or groups other entries.",
|
|
16
|
+
"definitions": {
|
|
17
|
+
"Intercept": {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"properties": {
|
|
20
|
+
"effect/References/CurrentLogAnnotations": {
|
|
21
|
+
"type": "object",
|
|
22
|
+
"description": "Shallowly merges with outer annotations; configured keys win."
|
|
23
|
+
},
|
|
24
|
+
"effect/References/MinimumLogLevel": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"enum": [
|
|
27
|
+
"All",
|
|
28
|
+
"Fatal",
|
|
29
|
+
"Error",
|
|
30
|
+
"Warn",
|
|
31
|
+
"Info",
|
|
32
|
+
"Debug",
|
|
33
|
+
"Trace",
|
|
34
|
+
"None"
|
|
35
|
+
],
|
|
36
|
+
"description": "Replaces the outer minimum log level."
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"additionalProperties": false,
|
|
40
|
+
"description": "Patches for references, applying to this entry and its descendants."
|
|
41
|
+
},
|
|
42
|
+
"Entry": {
|
|
43
|
+
"anyOf": [
|
|
44
|
+
{
|
|
45
|
+
"type": "object",
|
|
46
|
+
"properties": {
|
|
47
|
+
"id": {
|
|
48
|
+
"type": "string",
|
|
49
|
+
"description": "Identifies the entry. Graft adds one, and writes it to the file, if an entry has none."
|
|
50
|
+
},
|
|
51
|
+
"name": {
|
|
52
|
+
"type": "string",
|
|
53
|
+
"description": "The plugin's module: a relative path, a package name, or a short name. Ignored for groups."
|
|
54
|
+
},
|
|
55
|
+
"config": {
|
|
56
|
+
"type": "array",
|
|
57
|
+
"description": "The entries of this group.",
|
|
58
|
+
"items": {
|
|
59
|
+
"$ref": "#/definitions/Entry"
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"disabled": {
|
|
63
|
+
"type": "boolean",
|
|
64
|
+
"description": "true unloads the entry but keeps it in the file."
|
|
65
|
+
},
|
|
66
|
+
"group": {
|
|
67
|
+
"type": "boolean",
|
|
68
|
+
"enum": [
|
|
69
|
+
true
|
|
70
|
+
],
|
|
71
|
+
"description": "true makes the entry a group of other entries."
|
|
72
|
+
},
|
|
73
|
+
"inject": {
|
|
74
|
+
"type": "array",
|
|
75
|
+
"description": "Service keys to wait for before loading the plugin, in addition to its own inject.",
|
|
76
|
+
"items": {
|
|
77
|
+
"type": "string"
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
"isolate": {
|
|
81
|
+
"type": "object",
|
|
82
|
+
"additionalProperties": {
|
|
83
|
+
"anyOf": [
|
|
84
|
+
{
|
|
85
|
+
"type": "boolean",
|
|
86
|
+
"enum": [
|
|
87
|
+
true
|
|
88
|
+
]
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"type": "string"
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
},
|
|
95
|
+
"description": "Service or event keys to isolate: true for a private realm, or a label for a shared one."
|
|
96
|
+
},
|
|
97
|
+
"intercept": {
|
|
98
|
+
"$ref": "#/definitions/Intercept"
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
"required": [
|
|
102
|
+
"name",
|
|
103
|
+
"group"
|
|
104
|
+
],
|
|
105
|
+
"allOf": [
|
|
106
|
+
{
|
|
107
|
+
"type": "object",
|
|
108
|
+
"additionalProperties": {}
|
|
109
|
+
}
|
|
110
|
+
]
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"type": "object",
|
|
114
|
+
"properties": {
|
|
115
|
+
"id": {
|
|
116
|
+
"type": "string",
|
|
117
|
+
"description": "Identifies the entry. Graft adds one, and writes it to the file, if an entry has none."
|
|
118
|
+
},
|
|
119
|
+
"name": {
|
|
120
|
+
"type": "string",
|
|
121
|
+
"description": "The plugin's module: a relative path, a package name, or a short name. Ignored for groups."
|
|
122
|
+
},
|
|
123
|
+
"config": {
|
|
124
|
+
"description": "The plugin's input. For a group, its list of entries."
|
|
125
|
+
},
|
|
126
|
+
"disabled": {
|
|
127
|
+
"type": "boolean",
|
|
128
|
+
"description": "true unloads the entry but keeps it in the file."
|
|
129
|
+
},
|
|
130
|
+
"group": {
|
|
131
|
+
"type": "boolean",
|
|
132
|
+
"enum": [
|
|
133
|
+
false
|
|
134
|
+
],
|
|
135
|
+
"description": "true makes the entry a group of other entries."
|
|
136
|
+
},
|
|
137
|
+
"inject": {
|
|
138
|
+
"type": "array",
|
|
139
|
+
"description": "Service keys to wait for before loading the plugin, in addition to its own inject.",
|
|
140
|
+
"items": {
|
|
141
|
+
"type": "string"
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
"isolate": {
|
|
145
|
+
"type": "object",
|
|
146
|
+
"additionalProperties": {
|
|
147
|
+
"anyOf": [
|
|
148
|
+
{
|
|
149
|
+
"type": "boolean",
|
|
150
|
+
"enum": [
|
|
151
|
+
true
|
|
152
|
+
]
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"type": "string"
|
|
156
|
+
}
|
|
157
|
+
]
|
|
158
|
+
},
|
|
159
|
+
"description": "Service or event keys to isolate: true for a private realm, or a label for a shared one."
|
|
160
|
+
},
|
|
161
|
+
"intercept": {
|
|
162
|
+
"$ref": "#/definitions/Intercept"
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
"required": [
|
|
166
|
+
"name"
|
|
167
|
+
],
|
|
168
|
+
"allOf": [
|
|
169
|
+
{
|
|
170
|
+
"type": "object",
|
|
171
|
+
"additionalProperties": {}
|
|
172
|
+
}
|
|
173
|
+
]
|
|
174
|
+
}
|
|
175
|
+
]
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
package/src/Manage.ts
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
import * as Create from "@graftland/create"
|
|
5
5
|
import * as PackageManager from "@graftland/package-manager"
|
|
6
6
|
import {
|
|
7
|
-
Array
|
|
7
|
+
Array,
|
|
8
8
|
Config,
|
|
9
9
|
Effect,
|
|
10
10
|
Option,
|
|
11
|
+
Predicate,
|
|
11
12
|
Record,
|
|
12
13
|
Schema,
|
|
13
|
-
String as Str,
|
|
14
14
|
} from "effect"
|
|
15
15
|
|
|
16
16
|
const report = (result: PackageManager.InstallResult) =>
|
|
@@ -18,16 +18,19 @@ const report = (result: PackageManager.InstallResult) =>
|
|
|
18
18
|
`${result.agent}: ${result.added.length} added, ${result.updated.length} updated, ${result.removed.length} removed`,
|
|
19
19
|
)
|
|
20
20
|
|
|
21
|
-
/**
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
)
|
|
21
|
+
/**
|
|
22
|
+
* `name@range`, split at the last `@` so a scope's `@` stays in the name. A
|
|
23
|
+
* bare name means the latest version.
|
|
24
|
+
*/
|
|
25
|
+
const Spec = Schema.Union([
|
|
26
|
+
Schema.TemplateLiteralParser([
|
|
27
|
+
PackageManager.PackageName,
|
|
28
|
+
"@",
|
|
29
|
+
Schema.String,
|
|
30
|
+
]),
|
|
31
|
+
PackageManager.PackageName,
|
|
32
|
+
])
|
|
33
|
+
const decodeSpecs = Schema.decodeEffect(Schema.Array(Spec))
|
|
31
34
|
|
|
32
35
|
const install = (
|
|
33
36
|
directory: string,
|
|
@@ -62,10 +65,21 @@ export const create = (options: {
|
|
|
62
65
|
})
|
|
63
66
|
|
|
64
67
|
export const add = (specs: ReadonlyArray<string>) =>
|
|
65
|
-
|
|
68
|
+
Effect.flatMap(decodeSpecs(specs), (specs) =>
|
|
69
|
+
install(
|
|
70
|
+
".",
|
|
71
|
+
Record.fromEntries(
|
|
72
|
+
Array.map(specs, (spec) =>
|
|
73
|
+
Predicate.isString(spec)
|
|
74
|
+
? ([spec, "latest"] as const)
|
|
75
|
+
: ([spec[0], spec[2]] as const),
|
|
76
|
+
),
|
|
77
|
+
),
|
|
78
|
+
),
|
|
79
|
+
)
|
|
66
80
|
|
|
67
81
|
export const remove = (names: ReadonlyArray<string>) =>
|
|
68
82
|
install(
|
|
69
83
|
".",
|
|
70
|
-
Record.fromEntries(
|
|
84
|
+
Record.fromEntries(Array.map(names, (name) => [name, null] as const)),
|
|
71
85
|
)
|
package/src/Start.ts
CHANGED
|
@@ -4,41 +4,25 @@
|
|
|
4
4
|
import { NodeServices } from "@effect/platform-node"
|
|
5
5
|
import { Graft, Plugin } from "@graftland/graft"
|
|
6
6
|
import * as Hmr from "@graftland/hmr"
|
|
7
|
-
import { Include, Loader, ModuleResolver } from "@graftland/loader"
|
|
8
|
-
import type { PlatformError } from "effect"
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
Layer,
|
|
14
|
-
Logger,
|
|
15
|
-
Path,
|
|
16
|
-
Queue,
|
|
17
|
-
Stream,
|
|
18
|
-
String as Str,
|
|
19
|
-
} from "effect"
|
|
7
|
+
import { Include, Loader, ModuleResolver, type Tree } from "@graftland/loader"
|
|
8
|
+
import type { PlatformError, Schema } from "effect"
|
|
9
|
+
import { Effect, FileSystem, Layer, Logger, Path } from "effect"
|
|
10
|
+
import { EnvFile, layer as env, watchEnvFile } from "./internal/env"
|
|
11
|
+
import { layer as intercepts } from "./internal/intercepts"
|
|
12
|
+
import { watchModules } from "./internal/watch"
|
|
20
13
|
|
|
21
14
|
export interface Options {
|
|
22
15
|
/** Path of the config file. */
|
|
23
16
|
readonly config: string
|
|
24
17
|
/** Reload plugin modules and the config file when they change. */
|
|
25
18
|
readonly watch: boolean
|
|
19
|
+
/**
|
|
20
|
+
* A `.env` file that `!env` reads when the process environment does not set
|
|
21
|
+
* a variable. With `watch`, changes to it are read again.
|
|
22
|
+
*/
|
|
23
|
+
readonly envFile?: string | undefined
|
|
26
24
|
}
|
|
27
25
|
|
|
28
|
-
const moduleExtensions = [".js", ".mjs", ".cjs", ".ts", ".mts", ".cts"]
|
|
29
|
-
|
|
30
|
-
/** Whether a change to this file should reload plugin modules. */
|
|
31
|
-
const isModule = (path: string) =>
|
|
32
|
-
!Str.includes("node_modules")(path) &&
|
|
33
|
-
Arr.some(moduleExtensions, (extension) => Str.endsWith(extension)(path))
|
|
34
|
-
|
|
35
|
-
const describe = (result: Hmr.ReloadResult) =>
|
|
36
|
-
result._tag === "Reloaded"
|
|
37
|
-
? Effect.logInfo(`Reloaded ${result.entries.length} plugin module(s)`)
|
|
38
|
-
: Effect.logWarning(
|
|
39
|
-
`Restart required to pick up ${Arr.join(result.modules, ", ")}`,
|
|
40
|
-
)
|
|
41
|
-
|
|
42
26
|
/**
|
|
43
27
|
* Loads the entries of the config file and runs until interrupted. Plugin
|
|
44
28
|
* modules are imported through the hot reloader, so bare specifiers resolve
|
|
@@ -46,9 +30,16 @@ const describe = (result: Hmr.ReloadResult) =>
|
|
|
46
30
|
*/
|
|
47
31
|
export const run = (
|
|
48
32
|
options: Options,
|
|
49
|
-
): Effect.Effect<
|
|
33
|
+
): Effect.Effect<
|
|
34
|
+
never,
|
|
35
|
+
| Graft.ServiceConflict
|
|
36
|
+
| PlatformError.BadArgument
|
|
37
|
+
| PlatformError.PlatformError
|
|
38
|
+
| Schema.SchemaError
|
|
39
|
+
| Loader.StoreError
|
|
40
|
+
| Tree.InvalidOperation
|
|
41
|
+
> =>
|
|
50
42
|
Effect.gen(function* () {
|
|
51
|
-
const fs = yield* FileSystem.FileSystem
|
|
52
43
|
const path = yield* Path.Path
|
|
53
44
|
const file = path.resolve(options.config)
|
|
54
45
|
const directory = path.dirname(file)
|
|
@@ -56,38 +47,30 @@ export const run = (
|
|
|
56
47
|
const hmr = yield* Hmr.make({ base: base.href })
|
|
57
48
|
const resolver = yield* ModuleResolver.make({
|
|
58
49
|
base: directory,
|
|
59
|
-
importer: hmr.
|
|
50
|
+
importer: hmr.importerScoped,
|
|
60
51
|
})
|
|
61
52
|
yield* Graft.provide(ModuleResolver.ModuleResolver, resolver)
|
|
62
|
-
yield* Graft.plugin(Include.Include, {
|
|
53
|
+
const included = yield* Graft.plugin(Include.Include, {
|
|
54
|
+
path: file,
|
|
55
|
+
watch: options.watch,
|
|
56
|
+
})
|
|
57
|
+
yield* included.await
|
|
63
58
|
if (options.watch) {
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
// Only module changes reload code; the config file is watched by the loader itself.
|
|
67
|
-
yield* fs.watch(directory, { recursive: true }).pipe(
|
|
68
|
-
Stream.filter((event) => isModule(event.path)),
|
|
69
|
-
Stream.runForEach(() => Queue.offer(changed, undefined)),
|
|
70
|
-
Effect.forkScoped,
|
|
71
|
-
)
|
|
59
|
+
const { reload } = yield* EnvFile
|
|
60
|
+
const envFile = options.envFile
|
|
72
61
|
const reloader = Plugin.make({
|
|
73
62
|
name: "reloader",
|
|
74
|
-
inject: [Loader.Loader],
|
|
63
|
+
inject: [Loader.Loader, FileSystem.FileSystem, Path.Path],
|
|
75
64
|
apply: () =>
|
|
76
65
|
Effect.gen(function* () {
|
|
77
66
|
const loader = yield* Loader.Loader
|
|
78
|
-
yield*
|
|
79
|
-
Effect.andThen(Effect.sleep("50 millis")),
|
|
80
|
-
Effect.andThen(Queue.clear(changed)),
|
|
81
|
-
Effect.andThen(hmr.reload),
|
|
82
|
-
Effect.flatMap(describe),
|
|
83
|
-
// Entries whose module failed to load get another chance with the new code.
|
|
84
|
-
Effect.andThen(loader.retry),
|
|
85
|
-
Effect.catchCause((cause) =>
|
|
86
|
-
Effect.logError("Reloading plugin modules failed", cause),
|
|
87
|
-
),
|
|
88
|
-
Effect.forever,
|
|
67
|
+
yield* watchModules(hmr, directory, loader.retry).pipe(
|
|
89
68
|
Effect.forkScoped,
|
|
90
69
|
)
|
|
70
|
+
if (envFile !== undefined)
|
|
71
|
+
yield* watchEnvFile(envFile, reload, loader.retry).pipe(
|
|
72
|
+
Effect.forkScoped,
|
|
73
|
+
)
|
|
91
74
|
}),
|
|
92
75
|
})
|
|
93
76
|
yield* Graft.plugin(reloader)
|
|
@@ -105,6 +88,8 @@ export const run = (
|
|
|
105
88
|
Graft.layer,
|
|
106
89
|
Layer.mergeAll(
|
|
107
90
|
NodeServices.layer,
|
|
91
|
+
intercepts,
|
|
92
|
+
env(options.envFile).pipe(Layer.provide(NodeServices.layer)),
|
|
108
93
|
Logger.layer([Logger.consolePretty()]),
|
|
109
94
|
),
|
|
110
95
|
),
|
package/src/bin.ts
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
* The `graft` executable. Running this module starts the program.
|
|
4
4
|
*/
|
|
5
5
|
import { NodeRuntime, NodeServices } from "@effect/platform-node"
|
|
6
|
-
import { Effect,
|
|
6
|
+
import { Effect, Option } from "effect"
|
|
7
7
|
import { Argument, Command, Flag } from "effect/cli"
|
|
8
|
+
import { version } from "../package.json" with { type: "json" }
|
|
8
9
|
import * as Manage from "./Manage"
|
|
9
10
|
import * as Start from "./Start"
|
|
10
11
|
|
|
@@ -20,8 +21,15 @@ const start = Command.make(
|
|
|
20
21
|
Flag.withDescription("Reload plugins and config when files change"),
|
|
21
22
|
Flag.withDefault(false),
|
|
22
23
|
),
|
|
24
|
+
envFile: Flag.String("env-file").pipe(
|
|
25
|
+
Flag.withDescription(
|
|
26
|
+
"Read variables for !env from a .env file; the process environment takes precedence",
|
|
27
|
+
),
|
|
28
|
+
Flag.optional,
|
|
29
|
+
),
|
|
23
30
|
},
|
|
24
|
-
|
|
31
|
+
(options) =>
|
|
32
|
+
Start.run({ ...options, envFile: Option.getOrUndefined(options.envFile) }),
|
|
25
33
|
).pipe(
|
|
26
34
|
Command.withDescription("Run the application described by a config file"),
|
|
27
35
|
)
|
|
@@ -69,17 +77,7 @@ const graft = Command.make("graft").pipe(
|
|
|
69
77
|
Command.withSubcommands([start, create, add, remove]),
|
|
70
78
|
)
|
|
71
79
|
|
|
72
|
-
|
|
73
|
-
|
|
80
|
+
Command.run(graft, { version }).pipe(
|
|
81
|
+
Effect.provide(NodeServices.layer),
|
|
82
|
+
NodeRuntime.runMain,
|
|
74
83
|
)
|
|
75
|
-
|
|
76
|
-
Effect.gen(function* () {
|
|
77
|
-
const fs = yield* FileSystem.FileSystem
|
|
78
|
-
const path = yield* Path.Path
|
|
79
|
-
const file = yield* path.fromFileUrl(
|
|
80
|
-
new URL("../package.json", import.meta.url),
|
|
81
|
-
)
|
|
82
|
-
const text = yield* fs.readFileString(file)
|
|
83
|
-
const { version } = yield* Schema.decodeEffect(Manifest)(text)
|
|
84
|
-
return yield* Command.run(graft, { version })
|
|
85
|
-
}).pipe(Effect.provide(NodeServices.layer), NodeRuntime.runMain)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The JSON Schema of the config file that graft start reads, derived from the
|
|
3
|
+
* loader's entry fields and the intercepts the CLI registers. The build writes
|
|
4
|
+
* it to the package's schema.json, so each release ships its own version.
|
|
5
|
+
*/
|
|
6
|
+
import { Entry } from "@graftland/loader"
|
|
7
|
+
import { Array, JsonSchema, Record, Schema } from "effect"
|
|
8
|
+
import { registrations } from "./intercepts"
|
|
9
|
+
|
|
10
|
+
const fields = Entry.Entry.schema.fields
|
|
11
|
+
|
|
12
|
+
const Intercept = Schema.Struct(
|
|
13
|
+
Record.fromEntries(
|
|
14
|
+
Array.map(
|
|
15
|
+
registrations,
|
|
16
|
+
({ key, schema }) => [key, Schema.optionalKey(schema)] as const,
|
|
17
|
+
),
|
|
18
|
+
),
|
|
19
|
+
).annotate({
|
|
20
|
+
identifier: "Intercept",
|
|
21
|
+
description:
|
|
22
|
+
"Patches for references, applying to this entry and its descendants.",
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const Metadata = [Schema.Record(Schema.String, Schema.Unknown)] as const
|
|
26
|
+
|
|
27
|
+
const shared = {
|
|
28
|
+
...fields,
|
|
29
|
+
id: Schema.optionalKey(
|
|
30
|
+
fields.id.annotate({
|
|
31
|
+
description:
|
|
32
|
+
"Identifies the entry. Graft adds one, and writes it to the file, if an entry has none.",
|
|
33
|
+
}),
|
|
34
|
+
),
|
|
35
|
+
intercept: Schema.optionalKey(Intercept),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Two shapes instead of one, so that only a group's config is checked as a
|
|
39
|
+
// list of entries; a plugin's config is checked by the plugin's own Schema.
|
|
40
|
+
const FileEntry: Schema.Top = Schema.Union([
|
|
41
|
+
Schema.StructWithRest(
|
|
42
|
+
Schema.Struct({
|
|
43
|
+
...shared,
|
|
44
|
+
group: Schema.Literal(true).annotate({
|
|
45
|
+
description: "true makes the entry a group of other entries.",
|
|
46
|
+
}),
|
|
47
|
+
config: Schema.optionalKey(
|
|
48
|
+
Schema.Array(Schema.suspend(() => FileEntry)).annotate({
|
|
49
|
+
description: "The entries of this group.",
|
|
50
|
+
}),
|
|
51
|
+
),
|
|
52
|
+
}),
|
|
53
|
+
Metadata,
|
|
54
|
+
),
|
|
55
|
+
Schema.StructWithRest(
|
|
56
|
+
Schema.Struct({
|
|
57
|
+
...shared,
|
|
58
|
+
group: Schema.optionalKey(
|
|
59
|
+
Schema.Literal(false).annotate({
|
|
60
|
+
description: "true makes the entry a group of other entries.",
|
|
61
|
+
}),
|
|
62
|
+
),
|
|
63
|
+
}),
|
|
64
|
+
Metadata,
|
|
65
|
+
),
|
|
66
|
+
]).annotate({ identifier: "Entry" })
|
|
67
|
+
|
|
68
|
+
const File = Schema.NullOr(Schema.Array(FileEntry)).annotate({
|
|
69
|
+
title: "Graft config file",
|
|
70
|
+
description:
|
|
71
|
+
"The entries that graft start loads. Each entry loads one plugin, or groups other entries.",
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
/** Draft-07, which YAML language servers support most widely. */
|
|
75
|
+
export const make = () => {
|
|
76
|
+
const document = JsonSchema.toDocumentDraft07(
|
|
77
|
+
// Only the intercept struct is closed: entries keep a rest record for metadata.
|
|
78
|
+
Schema.toJsonSchemaDocument(File, { onExcessProperty: "error" }),
|
|
79
|
+
)
|
|
80
|
+
return {
|
|
81
|
+
$schema: JsonSchema.META_SCHEMA_URI_DRAFT_07,
|
|
82
|
+
...document.schema,
|
|
83
|
+
definitions: document.definitions,
|
|
84
|
+
}
|
|
85
|
+
}
|