@graftland/cli 0.4.2 → 0.5.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 +1 -1
- package/README.zh-CN.md +1 -1
- package/README.zh-TW.md +1 -1
- package/dist/Manage.d.ts +4 -3
- package/dist/Manage.d.ts.map +1 -1
- package/dist/Manage.js +27 -2
- package/dist/Manage.js.map +1 -1
- package/dist/Start.js +1 -1
- package/dist/Start.js.map +1 -1
- package/dist/bin.js +5 -2
- package/dist/bin.js.map +1 -1
- package/dist/internal/commandHost.js +1 -1
- package/dist/internal/schemaFile.js +115 -0
- package/dist/internal/schemaFile.js.map +1 -0
- package/dist/internal/schemaFileName.js +6 -0
- package/dist/internal/schemaFileName.js.map +1 -0
- package/dist/internal/watch.js +6 -1
- package/dist/internal/watch.js.map +1 -1
- package/package.json +10 -8
- package/schema.json +0 -2321
- package/src/Manage.ts +40 -2
- package/src/Start.ts +1 -1
- package/src/bin.ts +22 -1
- package/src/internal/schemaFile.ts +186 -0
- package/src/internal/schemaFileName.ts +2 -0
- package/src/internal/watch.ts +32 -7
package/src/Manage.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
Record,
|
|
13
13
|
Schema,
|
|
14
14
|
} from "effect"
|
|
15
|
+
import { CliError } from "effect/cli"
|
|
15
16
|
|
|
16
17
|
import * as Create from "@graftland/create"
|
|
17
18
|
import * as PackageManager from "@graftland/package-manager"
|
|
@@ -20,6 +21,7 @@ import { parseDocument, type Document } from "yaml"
|
|
|
20
21
|
|
|
21
22
|
import { CommandConfigError } from "./Commands"
|
|
22
23
|
import { home } from "./internal/commandConfig"
|
|
24
|
+
import { schemaFileName } from "./internal/schemaFileName"
|
|
23
25
|
|
|
24
26
|
const report = (result: PackageManager.InstallResult) =>
|
|
25
27
|
Effect.logInfo(
|
|
@@ -52,6 +54,29 @@ const Spec = Schema.Union([
|
|
|
52
54
|
])
|
|
53
55
|
const decodeSpecs = Schema.decodeEffect(Schema.Array(Spec))
|
|
54
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Regenerates the project's editor schema, but only when it has one, so that
|
|
59
|
+
* the plugins just added or removed are checked. Imports no plugin.
|
|
60
|
+
*/
|
|
61
|
+
const refreshSchema = Effect.fnUntraced(function* (directory: string) {
|
|
62
|
+
const fs = yield* FileSystem.FileSystem
|
|
63
|
+
const path = yield* Path.Path
|
|
64
|
+
const root = path.resolve(directory)
|
|
65
|
+
const file = path.join(root, schemaFileName)
|
|
66
|
+
if (!(yield* fs.exists(file))) return
|
|
67
|
+
const EditorSchema = yield* Effect.promise(
|
|
68
|
+
() => import("./internal/schemaFile"),
|
|
69
|
+
).pipe(Effect.uninterruptible)
|
|
70
|
+
yield* EditorSchema.write(root, file).pipe(
|
|
71
|
+
Effect.mapError((cause) =>
|
|
72
|
+
CliError.UserError.make({
|
|
73
|
+
cause,
|
|
74
|
+
userMessage: `Dependencies changed, but ${file} was not regenerated: ${cause.message}`,
|
|
75
|
+
}),
|
|
76
|
+
),
|
|
77
|
+
)
|
|
78
|
+
})
|
|
79
|
+
|
|
55
80
|
const install = Effect.fn("Manage.install")(function* (
|
|
56
81
|
directory: string,
|
|
57
82
|
changes: Readonly<Record<string, string | null>>,
|
|
@@ -64,6 +89,7 @@ const install = Effect.fn("Manage.install")(function* (
|
|
|
64
89
|
const manager = yield* PackageManager.make({ directory, userAgent })
|
|
65
90
|
const result = yield* manager.install(decoded)
|
|
66
91
|
yield* report(result)
|
|
92
|
+
yield* refreshSchema(directory)
|
|
67
93
|
})
|
|
68
94
|
|
|
69
95
|
export const add = Effect.fn("Manage.add")(function* (
|
|
@@ -144,11 +170,23 @@ export const addPlugin = Effect.fn("Manage.addPlugin")(function* (
|
|
|
144
170
|
const path = yield* Path.Path
|
|
145
171
|
const fs = yield* FileSystem.FileSystem
|
|
146
172
|
const directory = global ? yield* home : path.resolve(".")
|
|
147
|
-
|
|
173
|
+
if (global) yield* prepareHome(directory)
|
|
174
|
+
const userAgent = yield* Config.String("npm_config_user_agent").pipe(
|
|
175
|
+
Config.withDefault(""),
|
|
176
|
+
)
|
|
177
|
+
const packageName = yield* Create.pluginPackage(name, {
|
|
178
|
+
directory,
|
|
179
|
+
userAgent,
|
|
180
|
+
}).pipe(
|
|
181
|
+
Effect.mapError((error) =>
|
|
182
|
+
error._tag === "PluginNotFound" || error._tag === "AmbiguousPluginName"
|
|
183
|
+
? CliError.UserError.make({ cause: error, userMessage: error.message })
|
|
184
|
+
: error,
|
|
185
|
+
),
|
|
186
|
+
)
|
|
148
187
|
const module = `${packageName}/cli`
|
|
149
188
|
const file = path.join(directory, "graft.yml")
|
|
150
189
|
const text = yield* enabledConfig(file, module)
|
|
151
|
-
if (global) yield* prepareHome(directory)
|
|
152
190
|
yield* install(directory, Record.singleton(packageName, "latest"))
|
|
153
191
|
// Once accepted, the native write must finish before command cleanup can proceed.
|
|
154
192
|
yield* fs.writeFileString(file, text).pipe(Effect.uninterruptible)
|
package/src/Start.ts
CHANGED
|
@@ -153,7 +153,7 @@ export const run = (
|
|
|
153
153
|
const { reload: reloadDir } = yield* EnvDir
|
|
154
154
|
const { envFile, envDir } = options
|
|
155
155
|
const reloader = Plugin.make({
|
|
156
|
-
name: "reloader",
|
|
156
|
+
name: "@graftland/cli/Start/reloader",
|
|
157
157
|
inject: [Loader.Loader, FileSystem.FileSystem, Path.Path],
|
|
158
158
|
apply: () =>
|
|
159
159
|
Effect.gen(function* () {
|
package/src/bin.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { Graft } from "@graftland/graft"
|
|
|
12
12
|
import { version } from "../package.json" with { type: "json" }
|
|
13
13
|
import { platform } from "./Application"
|
|
14
14
|
import * as Host from "./internal/commandHost"
|
|
15
|
+
import { schemaFileName } from "./internal/schemaFileName"
|
|
15
16
|
|
|
16
17
|
const start = Command.make(
|
|
17
18
|
"start",
|
|
@@ -97,6 +98,26 @@ const remove = Command.make(
|
|
|
97
98
|
),
|
|
98
99
|
)
|
|
99
100
|
|
|
101
|
+
const schema = Command.make(
|
|
102
|
+
"schema",
|
|
103
|
+
{
|
|
104
|
+
output: Flag.String("output").pipe(
|
|
105
|
+
Flag.withAlias("o"),
|
|
106
|
+
Flag.withDescription("Path of the schema file"),
|
|
107
|
+
Flag.withDefault(schemaFileName),
|
|
108
|
+
),
|
|
109
|
+
},
|
|
110
|
+
({ output }) =>
|
|
111
|
+
Effect.promise(() => import("./internal/schemaFile")).pipe(
|
|
112
|
+
Effect.uninterruptible,
|
|
113
|
+
Effect.flatMap((EditorSchema) => EditorSchema.write(".", output)),
|
|
114
|
+
),
|
|
115
|
+
).pipe(
|
|
116
|
+
Command.withDescription(
|
|
117
|
+
"Write the JSON Schema of graft.yml, with the config of installed plugins",
|
|
118
|
+
),
|
|
119
|
+
)
|
|
120
|
+
|
|
100
121
|
const graft = Command.make("graft").pipe(
|
|
101
122
|
Command.withDescription("Run and manage Graft applications"),
|
|
102
123
|
Command.withSharedFlags({
|
|
@@ -113,7 +134,7 @@ const program = Effect.gen(function* () {
|
|
|
113
134
|
Effect.gen(function* () {
|
|
114
135
|
const context = yield* Layer.build(Graft.layer)
|
|
115
136
|
return yield* Host.run(
|
|
116
|
-
[start, create, add, remove],
|
|
137
|
+
[start, create, add, remove, schema],
|
|
117
138
|
graft,
|
|
118
139
|
version,
|
|
119
140
|
args,
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/** Composes the editor schema of a project's config file from its installed plugins. */
|
|
2
|
+
import {
|
|
3
|
+
Array,
|
|
4
|
+
Effect,
|
|
5
|
+
FileSystem,
|
|
6
|
+
Option,
|
|
7
|
+
Path,
|
|
8
|
+
Predicate,
|
|
9
|
+
Record,
|
|
10
|
+
Schema,
|
|
11
|
+
type PlatformError,
|
|
12
|
+
} from "effect"
|
|
13
|
+
|
|
14
|
+
import { make } from "./configSchema"
|
|
15
|
+
import * as Importer from "./importer"
|
|
16
|
+
|
|
17
|
+
/** A dependency's `./schema` export could not be read. */
|
|
18
|
+
export class SchemaRegistrationError extends Schema.TaggedError<
|
|
19
|
+
SchemaRegistrationError,
|
|
20
|
+
{ readonly brand: unique symbol }
|
|
21
|
+
>()("SchemaRegistrationError", {
|
|
22
|
+
packageName: Schema.String,
|
|
23
|
+
detail: Schema.String,
|
|
24
|
+
cause: Schema.optionalKey(Schema.Unknown),
|
|
25
|
+
}) {
|
|
26
|
+
override get message() {
|
|
27
|
+
return `${this.packageName}/schema: ${this.detail}`
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const Dependencies = Schema.optionalKey(
|
|
32
|
+
Schema.Record(Schema.String, Schema.String),
|
|
33
|
+
)
|
|
34
|
+
const ProjectManifest = Schema.fromJsonString(
|
|
35
|
+
Schema.Struct({
|
|
36
|
+
dependencies: Dependencies,
|
|
37
|
+
devDependencies: Dependencies,
|
|
38
|
+
optionalDependencies: Dependencies,
|
|
39
|
+
}),
|
|
40
|
+
)
|
|
41
|
+
const InstalledManifest = Schema.fromJsonString(
|
|
42
|
+
Schema.Struct({
|
|
43
|
+
exports: Schema.optionalKey(Schema.Unknown),
|
|
44
|
+
dependencies: Dependencies,
|
|
45
|
+
peerDependencies: Dependencies,
|
|
46
|
+
}),
|
|
47
|
+
)
|
|
48
|
+
const Registrations = Schema.Array(
|
|
49
|
+
Schema.Struct({
|
|
50
|
+
names: Schema.Array(Schema.String),
|
|
51
|
+
schema: Schema.Record(Schema.String, Schema.Unknown),
|
|
52
|
+
required: Schema.optionalKey(Schema.Boolean),
|
|
53
|
+
}),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The manifest of an installed package, looked up as Node does: in
|
|
58
|
+
* `node_modules` of the directory and then of each parent, so hoisted
|
|
59
|
+
* workspace installs are found.
|
|
60
|
+
*/
|
|
61
|
+
const installedManifest = (
|
|
62
|
+
directory: string,
|
|
63
|
+
packageName: string,
|
|
64
|
+
): Effect.Effect<
|
|
65
|
+
Option.Option<string>,
|
|
66
|
+
PlatformError.PlatformError,
|
|
67
|
+
FileSystem.FileSystem | Path.Path
|
|
68
|
+
> =>
|
|
69
|
+
Effect.gen(function* () {
|
|
70
|
+
const fs = yield* FileSystem.FileSystem
|
|
71
|
+
const path = yield* Path.Path
|
|
72
|
+
const file = path.join(
|
|
73
|
+
directory,
|
|
74
|
+
"node_modules",
|
|
75
|
+
packageName,
|
|
76
|
+
"package.json",
|
|
77
|
+
)
|
|
78
|
+
if (yield* fs.exists(file)) return Option.some(file)
|
|
79
|
+
const parent = path.dirname(directory)
|
|
80
|
+
return parent === directory
|
|
81
|
+
? Option.none()
|
|
82
|
+
: yield* installedManifest(parent, packageName)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Whether an installed dependency is a plugin package that exports `./schema`,
|
|
87
|
+
* read without importing it. Other packages, such as `effect`, may export a
|
|
88
|
+
* `./schema` of their own.
|
|
89
|
+
*/
|
|
90
|
+
const exportsSchema = Effect.fnUntraced(function* (
|
|
91
|
+
directory: string,
|
|
92
|
+
packageName: string,
|
|
93
|
+
) {
|
|
94
|
+
const fs = yield* FileSystem.FileSystem
|
|
95
|
+
const file = yield* installedManifest(directory, packageName)
|
|
96
|
+
if (Option.isNone(file)) {
|
|
97
|
+
yield* Effect.logWarning(
|
|
98
|
+
`${packageName} is not installed; its config is left open`,
|
|
99
|
+
)
|
|
100
|
+
return false
|
|
101
|
+
}
|
|
102
|
+
const text = yield* fs.readFileString(file.value)
|
|
103
|
+
const manifest = yield* Schema.decodeEffect(InstalledManifest)(text)
|
|
104
|
+
return (
|
|
105
|
+
Predicate.hasProperty(manifest.exports, "./schema") &&
|
|
106
|
+
(Record.has(manifest.dependencies ?? {}, "@graftland/graft") ||
|
|
107
|
+
Record.has(manifest.peerDependencies ?? {}, "@graftland/graft"))
|
|
108
|
+
)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
const load = Effect.fnUntraced(function* (
|
|
112
|
+
importer: (specifier: string) => Promise<unknown>,
|
|
113
|
+
packageName: string,
|
|
114
|
+
) {
|
|
115
|
+
const failed = (detail: string) => (cause: unknown) =>
|
|
116
|
+
SchemaRegistrationError.make({ packageName, detail, cause })
|
|
117
|
+
const module = yield* Effect.tryPromise({
|
|
118
|
+
try: () => importer(`${packageName}/schema`),
|
|
119
|
+
catch: failed("cannot be imported"),
|
|
120
|
+
})
|
|
121
|
+
if (
|
|
122
|
+
!Predicate.hasProperty(module, "entries") ||
|
|
123
|
+
!Predicate.isFunction(module.entries)
|
|
124
|
+
)
|
|
125
|
+
return yield* SchemaRegistrationError.make({
|
|
126
|
+
packageName,
|
|
127
|
+
detail: "does not export an entries function",
|
|
128
|
+
})
|
|
129
|
+
const entries = module.entries
|
|
130
|
+
const value = yield* Effect.try({
|
|
131
|
+
try: () => entries(),
|
|
132
|
+
catch: failed("entries() threw"),
|
|
133
|
+
})
|
|
134
|
+
return yield* Schema.decodeUnknownEffect(Registrations)(value).pipe(
|
|
135
|
+
Effect.mapError(failed("entries() returned invalid registrations")),
|
|
136
|
+
)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The registrations exported as `./schema` by the dependencies in the
|
|
141
|
+
* project's package.json. Only these modules are imported, never the plugins.
|
|
142
|
+
*/
|
|
143
|
+
export const registrations = Effect.fn("EditorSchema.registrations")(function* (
|
|
144
|
+
directory: string,
|
|
145
|
+
) {
|
|
146
|
+
const fs = yield* FileSystem.FileSystem
|
|
147
|
+
const path = yield* Path.Path
|
|
148
|
+
const text = yield* fs.readFileString(path.join(directory, "package.json"))
|
|
149
|
+
const manifest = yield* Schema.decodeEffect(ProjectManifest)(text)
|
|
150
|
+
const names = Array.dedupe(
|
|
151
|
+
Array.flatMap(
|
|
152
|
+
[
|
|
153
|
+
manifest.dependencies,
|
|
154
|
+
manifest.devDependencies,
|
|
155
|
+
manifest.optionalDependencies,
|
|
156
|
+
],
|
|
157
|
+
(dependencies) => Record.keys(dependencies ?? {}),
|
|
158
|
+
),
|
|
159
|
+
)
|
|
160
|
+
const plugins = yield* Effect.filter(names, (name) =>
|
|
161
|
+
exportsSchema(directory, name),
|
|
162
|
+
)
|
|
163
|
+
const importer = yield* Importer.make(directory)
|
|
164
|
+
const loaded = yield* Effect.forEach(plugins, (name) => load(importer, name))
|
|
165
|
+
return { plugins, entries: Array.flatten(loaded) }
|
|
166
|
+
}, Effect.scoped)
|
|
167
|
+
|
|
168
|
+
const encode = Schema.encodeEffect(
|
|
169
|
+
Schema.fromJsonString(Schema.Unknown, { space: "\t" }),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
/** Writes the editor schema for the project in a directory. */
|
|
173
|
+
export const write = Effect.fn("EditorSchema.write")(function* (
|
|
174
|
+
directory: string,
|
|
175
|
+
output: string,
|
|
176
|
+
) {
|
|
177
|
+
const fs = yield* FileSystem.FileSystem
|
|
178
|
+
const { plugins, entries } = yield* registrations(directory)
|
|
179
|
+
const text = yield* encode(make(entries))
|
|
180
|
+
yield* fs.writeFileString(output, text + "\n")
|
|
181
|
+
yield* Effect.logInfo(
|
|
182
|
+
plugins.length === 0
|
|
183
|
+
? `Wrote ${output}; no dependency exports ./schema`
|
|
184
|
+
: `Wrote ${output} with ${Array.join(plugins, ", ")}`,
|
|
185
|
+
)
|
|
186
|
+
})
|
package/src/internal/watch.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Array, Cause, Effect, Exit, Stream, String } from "effect"
|
|
2
2
|
|
|
3
|
+
import type { Graft } from "@graftland/graft"
|
|
3
4
|
import type * as Hmr from "@graftland/hmr"
|
|
4
5
|
|
|
5
6
|
import { observeShutdown } from "./observeShutdown"
|
|
@@ -11,16 +12,40 @@ export const isModule = (path: string) =>
|
|
|
11
12
|
!Array.contains(String.split(/[/\\]/)(path), "node_modules") &&
|
|
12
13
|
Array.some(moduleExtensions, (extension) => String.endsWith(extension)(path))
|
|
13
14
|
|
|
15
|
+
/** One warning per failed or timed-out plugin; failures retain their full cause. */
|
|
16
|
+
const rolledBack = (failures: ReadonlyArray<Graft.ReplacementFailure>) =>
|
|
17
|
+
Effect.forEach(
|
|
18
|
+
failures,
|
|
19
|
+
(failure) =>
|
|
20
|
+
(failure._tag === "Failed"
|
|
21
|
+
? Effect.logWarning(
|
|
22
|
+
`Kept the previous code: plugin "${failure.plugin}" failed to start`,
|
|
23
|
+
failure.cause,
|
|
24
|
+
)
|
|
25
|
+
: Effect.logWarning(
|
|
26
|
+
`Kept the previous code: plugin "${failure.plugin}" did not start in time`,
|
|
27
|
+
)
|
|
28
|
+
).pipe(
|
|
29
|
+
Effect.annotateLogs({
|
|
30
|
+
"graft.fork": failure.fork,
|
|
31
|
+
"graft.plugin": failure.plugin,
|
|
32
|
+
}),
|
|
33
|
+
),
|
|
34
|
+
{ discard: true },
|
|
35
|
+
)
|
|
36
|
+
|
|
14
37
|
const describe = (result: Hmr.ReloadResult) =>
|
|
15
38
|
result._tag === "Reloaded"
|
|
16
39
|
? Effect.logInfo(`Reloaded ${result.entries.length} plugin module(s)`)
|
|
17
|
-
:
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
? `Restart required: plugin
|
|
22
|
-
:
|
|
23
|
-
|
|
40
|
+
: result._tag === "RolledBack"
|
|
41
|
+
? rolledBack(result.failures)
|
|
42
|
+
: Effect.logWarning(
|
|
43
|
+
result.reason === "ambiguous-exports"
|
|
44
|
+
? `Restart required: shared plugin exports diverged in ${Array.join(result.modules, ", ")}`
|
|
45
|
+
: result.reason === "identity-conflict"
|
|
46
|
+
? `Restart required: plugin replacement identities conflict in ${Array.join(result.modules, ", ")}`
|
|
47
|
+
: `Restart required to pick up ${Array.join(result.modules, ", ")}`,
|
|
48
|
+
)
|
|
24
49
|
|
|
25
50
|
/** Owns the HMR stream and retries failed loader entries after each reload. */
|
|
26
51
|
export const watchModules = <E, R>(
|