@typeonce/effect-machine-devtools 0.26.1 → 0.27.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.
@@ -8,6 +8,8 @@ import { mountMachineIndex } from "./machine-index.js"
8
8
  const root = document.querySelector<HTMLDivElement>("#app")
9
9
  if (root === null) throw new Error("Visualizer root element was not found")
10
10
 
11
+ const staticData = document.querySelector<HTMLMetaElement>("meta[name=\"effect-machine-static-data\"]")?.content
12
+
11
13
  const showConnectionFailure = (message: string): void => {
12
14
  const failure = document.createElement("div")
13
15
  failure.className = "connection-failure"
@@ -39,4 +41,25 @@ const connect = Effect.acquireRelease(
39
41
  )
40
42
  )
41
43
 
42
- Effect.scoped(connect).pipe(BrowserRuntime.runMain)
44
+ const loadStatic = (location: string) =>
45
+ Effect.tryPromise({
46
+ try: async () => {
47
+ const response = await fetch(location)
48
+ if (!response.ok) throw new Error(`Could not load ${location}: ${response.status} ${response.statusText}`)
49
+ return response.json() as Promise<unknown>
50
+ },
51
+ catch: (cause) => cause instanceof Error ? cause : new Error(String(cause))
52
+ }).pipe(
53
+ Effect.flatMap(Schema.decodeUnknownEffect(DevToolsProtocol.RegistrySnapshot)),
54
+ Effect.tap((snapshot) => Effect.sync(() => mountMachineIndex(root, snapshot))),
55
+ Effect.asVoid
56
+ )
57
+
58
+ const run = staticData === undefined ? Effect.scoped(connect) : loadStatic(staticData)
59
+
60
+ run.pipe(
61
+ Effect.catch((cause) =>
62
+ Effect.sync(() => showConnectionFailure(cause instanceof Error ? cause.message : String(cause)))
63
+ ),
64
+ BrowserRuntime.runMain
65
+ )
@@ -0,0 +1,209 @@
1
+ import * as Effect from "effect/Effect"
2
+ import * as FileSystem from "effect/FileSystem"
3
+ import * as Path from "effect/Path"
4
+ import * as Schema from "effect/Schema"
5
+ import { fileURLToPath } from "node:url"
6
+ import { build as buildVite } from "vite"
7
+ import PackageJson from "../../package.json" with { type: "json" }
8
+ import * as DevToolsProtocol from "../DevToolsProtocol.js"
9
+ import * as MachineDocument from "../MachineDocument.js"
10
+ import * as ProjectInspector from "../ProjectInspector.js"
11
+
12
+ export interface Options {
13
+ readonly root: string
14
+ readonly include?: string | undefined
15
+ readonly outputDirectory: string
16
+ }
17
+
18
+ export interface BuildResult {
19
+ readonly outputDirectory: string
20
+ readonly machineIds: ReadonlyArray<string>
21
+ }
22
+
23
+ export class StaticSiteError extends Schema.Error<StaticSiteError>(
24
+ "@typeonce/effect-machine-devtools/internal/StaticSiteError"
25
+ )({
26
+ _tag: Schema.tag("StaticSiteError"),
27
+ message: Schema.String,
28
+ cause: Schema.optional(Schema.Defect())
29
+ }) {}
30
+
31
+ const packageRoot = fileURLToPath(new URL("../..", import.meta.url))
32
+ const generatedMarker = ".effect-machine-site"
33
+ const staticDataMeta = "<meta name=\"effect-machine-static-data\" content=\"./machines.json\" />"
34
+
35
+ const prettyJson = (value: unknown): string => `${JSON.stringify(value, null, 2)}\n`
36
+
37
+ export const staticIndex = (index: string): Effect.Effect<string, StaticSiteError> => {
38
+ if (!index.includes("</head>")) {
39
+ return Effect.fail(
40
+ new StaticSiteError({
41
+ message: "The visualizer index does not contain a closing head element"
42
+ })
43
+ )
44
+ }
45
+ return Effect.succeed(index.replace("</head>", ` ${staticDataMeta}\n </head>`))
46
+ }
47
+
48
+ const formatFailures = (failures: ReadonlyArray<DevToolsProtocol.Failed>): string =>
49
+ failures.map((failure) => {
50
+ const messages = failure.diagnostics.map((diagnostic) => diagnostic.message).join("; ")
51
+ return `- ${failure.key}: ${messages}`
52
+ }).join("\n")
53
+
54
+ const ensureReplaceable = Effect.fnUntraced(
55
+ function*(
56
+ fs: FileSystem.FileSystem,
57
+ path: Path.Path,
58
+ outputDirectory: string
59
+ ) {
60
+ if (!(yield* fs.exists(outputDirectory))) return
61
+ if (yield* fs.exists(path.join(outputDirectory, generatedMarker))) return
62
+ const entries = yield* fs.readDirectory(outputDirectory)
63
+ if (entries.length === 0) return
64
+ return yield* new StaticSiteError({
65
+ message: `Refusing to replace non-generated directory: ${outputDirectory}`
66
+ })
67
+ },
68
+ (effect, _fs, _path, outputDirectory) =>
69
+ effect.pipe(
70
+ Effect.mapError((cause) =>
71
+ cause instanceof StaticSiteError
72
+ ? cause
73
+ : new StaticSiteError({
74
+ message: `Could not inspect output directory: ${outputDirectory}`,
75
+ cause
76
+ })
77
+ )
78
+ )
79
+ )
80
+
81
+ const inspect = Effect.fnUntraced(function*(options: Options) {
82
+ const inspector = yield* ProjectInspector.ProjectInspector
83
+ const results = yield* inspector.inspect({
84
+ root: options.root,
85
+ include: options.include,
86
+ revision: 1
87
+ })
88
+ const failures = results.filter((result): result is DevToolsProtocol.Failed => result._tag === "Failed")
89
+ if (failures.length > 0) {
90
+ return yield* new StaticSiteError({
91
+ message: `Static site generation failed for ${failures.length} machine candidate${
92
+ failures.length === 1 ? "" : "s"
93
+ }:\n${formatFailures(failures)}`
94
+ })
95
+ }
96
+ const ready = results
97
+ .filter((result): result is DevToolsProtocol.Ready => result._tag === "Ready")
98
+ .sort((left, right) => left.key.localeCompare(right.key))
99
+ if (ready.length === 0) {
100
+ return yield* new StaticSiteError({
101
+ message: `No Effect Machine definitions were found under ${options.root}`
102
+ })
103
+ }
104
+ const snapshot = yield* Schema.decodeUnknownEffect(DevToolsProtocol.RegistrySnapshot)({
105
+ protocolVersion: DevToolsProtocol.protocolVersion,
106
+ revision: 1,
107
+ results: ready
108
+ }).pipe(
109
+ Effect.mapError((cause) =>
110
+ new StaticSiteError({
111
+ message: "The generated machine registry is invalid",
112
+ cause
113
+ })
114
+ )
115
+ )
116
+ return { ready, snapshot }
117
+ })
118
+
119
+ export const build = (options: Options): Effect.Effect<
120
+ BuildResult,
121
+ StaticSiteError,
122
+ FileSystem.FileSystem | Path.Path | ProjectInspector.ProjectInspector
123
+ > =>
124
+ Effect.gen(function*() {
125
+ const fs = yield* FileSystem.FileSystem
126
+ const path = yield* Path.Path
127
+ const root = path.resolve(options.root)
128
+ const outputDirectory = path.resolve(options.outputDirectory)
129
+ if (outputDirectory === root || path.dirname(outputDirectory) === outputDirectory) {
130
+ return yield* new StaticSiteError({
131
+ message: `Refusing to replace project or filesystem root: ${outputDirectory}`
132
+ })
133
+ }
134
+ yield* ensureReplaceable(fs, path, outputDirectory)
135
+ const { ready, snapshot } = yield* inspect({ ...options, root })
136
+ const parent = path.dirname(outputDirectory)
137
+ yield* fs.makeDirectory(parent, { recursive: true })
138
+
139
+ return yield* Effect.acquireUseRelease(
140
+ fs.makeTempDirectory({ directory: parent, prefix: ".effect-machine-site-" }),
141
+ (stagingDirectory) =>
142
+ Effect.gen(function*() {
143
+ yield* Effect.tryPromise({
144
+ try: () =>
145
+ buildVite({
146
+ root: packageRoot,
147
+ base: "./",
148
+ configFile: false,
149
+ logLevel: "error",
150
+ build: {
151
+ outDir: stagingDirectory,
152
+ emptyOutDir: true
153
+ }
154
+ }),
155
+ catch: (cause) =>
156
+ new StaticSiteError({
157
+ message: "Could not bundle the static visualizer",
158
+ cause
159
+ })
160
+ })
161
+
162
+ const indexPath = path.join(stagingDirectory, "index.html")
163
+ const index = yield* fs.readFileString(indexPath)
164
+ yield* fs.writeFileString(indexPath, yield* staticIndex(index))
165
+ yield* fs.writeFileString(path.join(stagingDirectory, "machines.json"), prettyJson(snapshot))
166
+ yield* fs.writeFileString(
167
+ path.join(stagingDirectory, "manifest.json"),
168
+ prettyJson({
169
+ formatVersion: 1,
170
+ generator: {
171
+ name: PackageJson.name,
172
+ version: PackageJson.version
173
+ },
174
+ protocolVersion: DevToolsProtocol.protocolVersion,
175
+ machineDocumentSchemaVersion: MachineDocument.schemaVersion,
176
+ machines: ready.map((result) => ({
177
+ key: result.key,
178
+ machineId: result.document.machineId,
179
+ source: result.document.source
180
+ }))
181
+ })
182
+ )
183
+ yield* fs.writeFileString(
184
+ path.join(stagingDirectory, generatedMarker),
185
+ `${PackageJson.name}@${PackageJson.version}\n`
186
+ )
187
+
188
+ yield* ensureReplaceable(fs, path, outputDirectory)
189
+ if (yield* fs.exists(outputDirectory)) {
190
+ yield* fs.remove(outputDirectory, { recursive: true })
191
+ }
192
+ yield* fs.rename(stagingDirectory, outputDirectory)
193
+ return {
194
+ outputDirectory,
195
+ machineIds: ready.map((result) => result.document.machineId)
196
+ }
197
+ }),
198
+ (stagingDirectory) => fs.remove(stagingDirectory, { recursive: true }).pipe(Effect.ignore)
199
+ )
200
+ }).pipe(
201
+ Effect.mapError((cause) =>
202
+ cause instanceof StaticSiteError
203
+ ? cause
204
+ : new StaticSiteError({
205
+ message: "Could not build the Effect Machine static site",
206
+ cause
207
+ })
208
+ )
209
+ )