@pmelab/gtd 63.0.0 → 64.0.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/.claude-plugin/plugin.json +1 -1
- package/README.md +4 -3
- package/dist/chunks/{flows-DENt0uWV.mjs → flows-B7ZRCe-B.mjs} +1 -1
- package/dist/chunks/{rolldown-runtime-8H4AJuhK.mjs → rolldown-runtime-D7D4PA-g.mjs} +0 -1
- package/dist/chunks/{steering-DF_BMadH.mjs → steering-DkA51qDa.mjs} +1 -1
- package/dist/chunks/{testkit-DwypA7cH.mjs → testkit-zGjNeQNL.mjs} +1011 -313
- package/dist/flows.mjs +1 -1
- package/dist/gtd.bundle.mjs +3890 -1294
- package/dist/gtd.core.mjs +119 -118
- package/dist/steering.mjs +1 -1
- package/dist/testkit/gherkin.mjs +2 -2
- package/dist/testkit.mjs +2 -2
- package/dist/workflow-kit.mjs +2 -2
- package/package.json +1 -1
- package/src/cli/Cli.test.ts +5 -8
- package/src/cli/Cli.ts +9 -27
- package/src/cli/Output.ts +2 -1
- package/src/cli/Program.test.ts +55 -53
- package/src/cli/Program.ts +11 -30
- package/src/cli/ToolCommands.ts +3 -4
- package/src/edge/Edge.test.ts +38 -12
- package/src/edge/Edge.ts +30 -10
- package/src/emit/SteeringMode.test.ts +4 -2
- package/src/emit/SteeringMode.ts +10 -9
- package/src/gherkin/messages.test.ts +1 -1
- package/src/gherkin/module-config.feature +1 -1
- package/src/gherkin/testkit.feature +2 -2
- package/src/install/Version.ts +14 -13
- package/src/platform/Host.ts +75 -22
- package/src/platform/NodePinFile.ts +44 -0
- package/src/platform/NodeRuntime.ts +19 -0
- package/src/platform/NodeSync.ts +64 -0
- package/src/platform/PinFile.ts +0 -50
- package/src/platform/Sha.ts +121 -0
- package/src/platform/SyncIo.ts +35 -0
- package/src/platform/Workspace.test.ts +8 -3
- package/src/platform/Workspace.ts +82 -123
- package/src/platform/WorktreeState.test.ts +8 -4
- package/src/platform/WorktreeState.ts +21 -19
- package/src/platform/index.ts +18 -5
- package/src/replay/Format.test.ts +16 -13
- package/src/replay/Format.ts +4 -2
- package/src/replay/Replay.test.ts +25 -5
- package/src/replay/Replay.ts +10 -6
- package/src/replay/Trailers.test.ts +10 -10
- package/src/replay/Trailers.ts +23 -1
- package/src/replay/index.ts +1 -0
- package/src/step/planStart.test.ts +2 -2
- package/src/step/planStep.test.ts +4 -4
- package/src/testkit/TestRepo.test.ts +1 -1
- package/src/testkit/TestRepo.ts +4 -18
- package/src/twins/Host.test.ts +151 -0
- package/src/twins/Layers.test.ts +2 -2
- package/src/twins/Layers.ts +44 -18
- package/src/twins/Path.test.ts +52 -0
- package/src/twins/SyncIo.test.ts +153 -0
- package/src/twins/SyncIo.ts +159 -0
- package/src/twins/cliIo.ts +8 -1
- package/src/twins/index.ts +5 -0
- package/src/workflow/discovery.test.ts +6 -5
- package/src/workflow/discovery.ts +43 -32
- package/src/workflow/fingerprint.test.ts +23 -3
- package/src/workflow/fingerprint.ts +9 -11
- package/src/workflow/load.test.ts +16 -6
- package/src/workflow/load.ts +107 -69
- package/src/workflow/pin.test.ts +3 -2
- package/src/workflow/pin.ts +17 -11
package/src/workflow/load.ts
CHANGED
|
@@ -1,12 +1,6 @@
|
|
|
1
|
-
import { existsSync, readFileSync, realpathSync } from "node:fs"
|
|
2
|
-
import { createRequire, isBuiltin } from "node:module"
|
|
3
|
-
import { dirname, isAbsolute, join, relative, resolve as resolvePath, sep } from "node:path"
|
|
4
|
-
import { fileURLToPath, pathToFileURL } from "node:url"
|
|
5
|
-
import { stripVTControlCharacters } from "node:util"
|
|
6
|
-
import { runInThisContext } from "node:vm"
|
|
7
1
|
import type { Plugin as RolldownPlugin } from "rolldown"
|
|
8
|
-
import { Context, Effect, Layer } from "effect"
|
|
9
|
-
import { GtdError, Narrator } from "../platform/index.js"
|
|
2
|
+
import { Context, Effect, Layer, Path } from "effect"
|
|
3
|
+
import { GtdError, Narrator, NodeRuntime, nodeSyncIo } from "../platform/index.js"
|
|
10
4
|
import { accessShapeFault, replay, type TreeView } from "../replay/index.js"
|
|
11
5
|
// The published facade: what a config's `@pmelab/gtd/flows` import is served.
|
|
12
6
|
import * as flows from "../flows/index.js"
|
|
@@ -129,22 +123,23 @@ const configModule: Effect.Effect<WorkflowModule, Error, Host | ConfigDiscovery>
|
|
|
129
123
|
export const load: Effect.Effect<
|
|
130
124
|
ConfigOperations,
|
|
131
125
|
Error,
|
|
132
|
-
Narrator | Workspace | Host | ConfigDiscovery
|
|
126
|
+
Narrator | Workspace | Host | Path.Path | ConfigDiscovery
|
|
133
127
|
> = Effect.gen(function* () {
|
|
134
128
|
const host = yield* Host
|
|
129
|
+
const paths = yield* Path.Path
|
|
135
130
|
yield* refuseLegacyRc
|
|
136
131
|
const pinned = pinnedWorkflow()
|
|
137
132
|
const module: WorkflowModule =
|
|
138
133
|
pinned === undefined
|
|
139
134
|
? yield* configModule
|
|
140
|
-
: { filepath:
|
|
135
|
+
: { filepath: paths.resolve(host.root, pinned.origin), source: "" }
|
|
141
136
|
yield* (yield* Narrator).narrate(
|
|
142
137
|
`config: ${pinned === undefined ? "module" : "pinned module"} ${module.filepath}`,
|
|
143
138
|
)
|
|
144
139
|
const shipped =
|
|
145
140
|
pinned === undefined
|
|
146
141
|
? yield* Effect.tryPromise({
|
|
147
|
-
try: shippedModules,
|
|
142
|
+
try: () => shippedModules(paths),
|
|
148
143
|
catch: (e) => new GtdError(`gtd: ${e instanceof Error ? e.message : String(e)}`),
|
|
149
144
|
})
|
|
150
145
|
: undefined
|
|
@@ -153,8 +148,8 @@ export const load: Effect.Effect<
|
|
|
153
148
|
loadWorkflows(
|
|
154
149
|
module.filepath,
|
|
155
150
|
shipped === undefined
|
|
156
|
-
? runPinned(pinned!, module.filepath, host.root)
|
|
157
|
-
: await evaluate(module, shipped),
|
|
151
|
+
? runPinned(paths, pinned!, module.filepath, host.root)
|
|
152
|
+
: await evaluate(paths, module, shipped),
|
|
158
153
|
),
|
|
159
154
|
catch: (e) =>
|
|
160
155
|
new GtdError(
|
|
@@ -205,14 +200,22 @@ export const load: Effect.Effect<
|
|
|
205
200
|
warnings: diagnostics.filter((d) => d.severity === "warning"),
|
|
206
201
|
fingerprint:
|
|
207
202
|
pinned?.fingerprint ??
|
|
208
|
-
configFingerprint(
|
|
203
|
+
configFingerprint(
|
|
204
|
+
nodeSyncIo,
|
|
205
|
+
paths,
|
|
206
|
+
loaded.file.dependencyFiles,
|
|
207
|
+
host.root,
|
|
208
|
+
module.read ?? readFromDisk,
|
|
209
|
+
),
|
|
209
210
|
drift:
|
|
210
211
|
pinned === undefined
|
|
211
212
|
? []
|
|
212
213
|
: changedConfigFiles(
|
|
213
214
|
pinned.fingerprint,
|
|
214
215
|
configFingerprint(
|
|
215
|
-
|
|
216
|
+
nodeSyncIo,
|
|
217
|
+
paths,
|
|
218
|
+
Object.keys(pinned.fingerprint).map((file) => paths.resolve(host.root, file)),
|
|
216
219
|
host.root,
|
|
217
220
|
readFromDisk,
|
|
218
221
|
),
|
|
@@ -220,9 +223,11 @@ export const load: Effect.Effect<
|
|
|
220
223
|
}
|
|
221
224
|
})
|
|
222
225
|
|
|
226
|
+
// Loading reads the real disk in every tier, as rolldown does: gtd's own
|
|
227
|
+
// package and the config's node_modules exist nowhere else.
|
|
223
228
|
const readFromDisk = (path: string): string | undefined => {
|
|
224
229
|
try {
|
|
225
|
-
return
|
|
230
|
+
return nodeSyncIo.readText(path)
|
|
226
231
|
} catch {
|
|
227
232
|
return undefined
|
|
228
233
|
}
|
|
@@ -275,15 +280,17 @@ interface PackageJson {
|
|
|
275
280
|
|
|
276
281
|
// Only gtd's own package.json: whatever other one sits nearest (a vendored
|
|
277
282
|
// bundle inside a project) must never get its exports executed.
|
|
278
|
-
export const ownPackage = (
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
283
|
+
export const ownPackage = (
|
|
284
|
+
paths: Path.Path,
|
|
285
|
+
): { readonly root: string; readonly pkg: PackageJson } => {
|
|
286
|
+
const start = paths.dirname(Effect.runSync(paths.fromFileUrl(new URL(import.meta.url))))
|
|
287
|
+
for (let dir = start; ; dir = paths.dirname(dir)) {
|
|
288
|
+
const source = readFromDisk(paths.join(dir, "package.json"))
|
|
289
|
+
if (source !== undefined) {
|
|
290
|
+
const pkg = JSON.parse(source) as PackageJson
|
|
284
291
|
if (pkg.name === PACKAGE) return { root: dir, pkg }
|
|
285
292
|
}
|
|
286
|
-
if (dirname(dir) === dir) throw new Error(`no ${PACKAGE} package.json above ${start}`)
|
|
293
|
+
if (paths.dirname(dir) === dir) throw new Error(`no ${PACKAGE} package.json above ${start}`)
|
|
287
294
|
}
|
|
288
295
|
}
|
|
289
296
|
|
|
@@ -302,10 +309,10 @@ interface ShippedModules {
|
|
|
302
309
|
// config — or a workflow as it runs — requires it, so a command loads only the
|
|
303
310
|
// subpaths its config uses; elsewhere, and from source, all of them up front.
|
|
304
311
|
let shippedMemo: Promise<ShippedModules> | undefined
|
|
305
|
-
const shippedModules = (): Promise<ShippedModules> =>
|
|
312
|
+
const shippedModules = (paths: Path.Path): Promise<ShippedModules> =>
|
|
306
313
|
(shippedMemo ??= (async () => {
|
|
307
314
|
const fromSource = import.meta.url.endsWith(".ts")
|
|
308
|
-
const files = subpathFiles(fromSource ? "source" : "default")
|
|
315
|
+
const files = subpathFiles(paths, fromSource ? "source" : "default")
|
|
309
316
|
const modules: Record<string, unknown> = { [FLOWS]: flows }
|
|
310
317
|
const missing: Record<string, string> = {}
|
|
311
318
|
if (process.features.require_module && !fromSource) {
|
|
@@ -313,7 +320,7 @@ const shippedModules = (): Promise<ShippedModules> =>
|
|
|
313
320
|
}
|
|
314
321
|
for (const [id, path] of files) {
|
|
315
322
|
try {
|
|
316
|
-
modules[id] = await import(
|
|
323
|
+
modules[id] = await import(fileUrl(paths, path))
|
|
317
324
|
} catch {
|
|
318
325
|
missing[id] = path
|
|
319
326
|
}
|
|
@@ -322,14 +329,14 @@ const shippedModules = (): Promise<ShippedModules> =>
|
|
|
322
329
|
})())
|
|
323
330
|
|
|
324
331
|
/** The file each subpath a config may import serves: all but `flows`, the engine's own, and the testkit's. */
|
|
325
|
-
const subpathFiles = (condition: string): ReadonlyMap<string, string> => {
|
|
326
|
-
const { root, pkg } = ownPackage()
|
|
332
|
+
const subpathFiles = (paths: Path.Path, condition: string): ReadonlyMap<string, string> => {
|
|
333
|
+
const { root, pkg } = ownPackage(paths)
|
|
327
334
|
const files = new Map<string, string>()
|
|
328
335
|
for (const [subpath, target] of Object.entries(pkg.exports)) {
|
|
329
336
|
const file = typeof target === "string" ? target : target[condition]
|
|
330
337
|
// A config runs no tests, and the gherkin steps import the consumer's test runner.
|
|
331
338
|
if (subpath === "./flows" || subpath.startsWith("./testkit") || file === undefined) continue
|
|
332
|
-
files.set(`${PACKAGE}/${subpath.replace(/^\.\//, "")}`, join(root, file))
|
|
339
|
+
files.set(`${PACKAGE}/${subpath.replace(/^\.\//, "")}`, paths.join(root, file))
|
|
333
340
|
}
|
|
334
341
|
return files
|
|
335
342
|
}
|
|
@@ -338,7 +345,7 @@ const requiredOnUse = (
|
|
|
338
345
|
modules: Record<string, unknown>,
|
|
339
346
|
files: ReadonlyMap<string, string>,
|
|
340
347
|
): Record<string, unknown> => {
|
|
341
|
-
const require = createRequire(import.meta.url)
|
|
348
|
+
const require = NodeRuntime.createRequire(import.meta.url)
|
|
342
349
|
return new Proxy(modules, {
|
|
343
350
|
has: (loaded, id) => id in loaded || (typeof id === "string" && files.has(id)),
|
|
344
351
|
get: (loaded, id) =>
|
|
@@ -619,12 +626,12 @@ const shadowingDoors = (
|
|
|
619
626
|
|
|
620
627
|
// How a TypeScript module is found behind a relative specifier: as written,
|
|
621
628
|
// its `.js` spelling as `.ts`, with `.ts` appended, or as a directory index.
|
|
622
|
-
const moduleCandidates = (path: string): readonly string[] => [
|
|
629
|
+
const moduleCandidates = (paths: Path.Path, path: string): readonly string[] => [
|
|
623
630
|
path,
|
|
624
631
|
path.replace(/\.js$/, ".ts"),
|
|
625
632
|
path.replace(/\.mjs$/, ".mts"),
|
|
626
633
|
`${path}.ts`,
|
|
627
|
-
join(path, "index.ts"),
|
|
634
|
+
paths.join(path, "index.ts"),
|
|
628
635
|
]
|
|
629
636
|
|
|
630
637
|
interface Evaluated {
|
|
@@ -641,9 +648,14 @@ type Chunk = (
|
|
|
641
648
|
__dirname: string,
|
|
642
649
|
) => void
|
|
643
650
|
|
|
644
|
-
const execute = (
|
|
651
|
+
const execute = (
|
|
652
|
+
paths: Path.Path,
|
|
653
|
+
chunk: Chunk,
|
|
654
|
+
filepath: string,
|
|
655
|
+
require: (id: string) => unknown,
|
|
656
|
+
): unknown => {
|
|
645
657
|
const cjs = { exports: {} as unknown }
|
|
646
|
-
chunk(cjs.exports, require, cjs, filepath, dirname(filepath))
|
|
658
|
+
chunk(cjs.exports, require, cjs, filepath, paths.dirname(filepath))
|
|
647
659
|
return cjs.exports
|
|
648
660
|
}
|
|
649
661
|
|
|
@@ -656,9 +668,13 @@ const execute = (chunk: Chunk, filepath: string, require: (id: string) => unknow
|
|
|
656
668
|
* it, as Node would. `@pmelab/gtd/*` is the running gtd's own copy, and
|
|
657
669
|
* `files` leaves out every package file.
|
|
658
670
|
*/
|
|
659
|
-
const evaluate = async (
|
|
660
|
-
|
|
661
|
-
|
|
671
|
+
const evaluate = async (
|
|
672
|
+
paths: Path.Path,
|
|
673
|
+
module: WorkflowModule,
|
|
674
|
+
shipped: ShippedModules,
|
|
675
|
+
): Promise<Evaluated> => {
|
|
676
|
+
const { code, files } = await bundleConfig(paths, module, { kind: "evaluate", shipped })
|
|
677
|
+
const fromConfig = NodeRuntime.createRequire(module.filepath)
|
|
662
678
|
const require = Object.assign(
|
|
663
679
|
(id: string): unknown =>
|
|
664
680
|
id in shipped.modules
|
|
@@ -666,11 +682,11 @@ const evaluate = async (module: WorkflowModule, shipped: ShippedModules): Promis
|
|
|
666
682
|
: fromConfig(id in shipped.missing ? shipped.missing[id]! : id),
|
|
667
683
|
{ resolve: fromConfig.resolve },
|
|
668
684
|
)
|
|
669
|
-
const chunk =
|
|
685
|
+
const chunk = NodeRuntime.compileScript(
|
|
670
686
|
`(function (exports, require, module, __filename, __dirname) {${code}\n})`,
|
|
671
|
-
|
|
687
|
+
module.filepath,
|
|
672
688
|
) as Chunk
|
|
673
|
-
return { exports: execute(chunk, module.filepath, require), files }
|
|
689
|
+
return { exports: execute(paths, chunk, module.filepath, require), files }
|
|
674
690
|
}
|
|
675
691
|
|
|
676
692
|
/**
|
|
@@ -688,12 +704,17 @@ interface PinnedWorkflow {
|
|
|
688
704
|
const pinnedWorkflow = (): PinnedWorkflow | undefined =>
|
|
689
705
|
(globalThis as { [PIN_KEY]?: PinnedWorkflow })[PIN_KEY]
|
|
690
706
|
|
|
691
|
-
const runPinned = (
|
|
707
|
+
const runPinned = (
|
|
708
|
+
paths: Path.Path,
|
|
709
|
+
pinned: PinnedWorkflow,
|
|
710
|
+
filepath: string,
|
|
711
|
+
root: string,
|
|
712
|
+
): Evaluated => ({
|
|
692
713
|
// Everything but the engine's own flows and Node's builtins is in the bundle.
|
|
693
|
-
exports: execute(pinned.run, filepath, (id) =>
|
|
694
|
-
id === FLOWS ? flows : createRequire(import.meta.url)(id),
|
|
714
|
+
exports: execute(paths, pinned.run, filepath, (id) =>
|
|
715
|
+
id === FLOWS ? flows : NodeRuntime.createRequire(import.meta.url)(id),
|
|
695
716
|
),
|
|
696
|
-
files: Object.keys(pinned.fingerprint).map((file) =>
|
|
717
|
+
files: Object.keys(pinned.fingerprint).map((file) => paths.resolve(root, file)),
|
|
697
718
|
})
|
|
698
719
|
|
|
699
720
|
/**
|
|
@@ -702,22 +723,25 @@ const runPinned = (pinned: PinnedWorkflow, filepath: string, root: string): Eval
|
|
|
702
723
|
* one function, so the engine runs it without a config, a package or rolldown.
|
|
703
724
|
*/
|
|
704
725
|
export const pinnedWorkflowSource = async (
|
|
726
|
+
paths: Path.Path,
|
|
705
727
|
module: WorkflowModule,
|
|
706
728
|
root: string,
|
|
707
729
|
version: string,
|
|
708
730
|
): Promise<string> => {
|
|
709
|
-
const gtd =
|
|
710
|
-
const { code, files } = await bundleConfig(module, {
|
|
731
|
+
const gtd = nodeSyncIo.realPath(ownPackage(paths).root) + paths.sep
|
|
732
|
+
const { code, files } = await bundleConfig(paths, module, {
|
|
711
733
|
kind: "pin",
|
|
712
|
-
shipped: subpathFiles(import.meta.url.endsWith(".ts") ? "source" : "default"),
|
|
734
|
+
shipped: subpathFiles(paths, import.meta.url.endsWith(".ts") ? "source" : "default"),
|
|
713
735
|
})
|
|
714
736
|
// gtd's own subpaths are bundled from its package, which only a config
|
|
715
737
|
// living in gtd's own repository may count among its files.
|
|
716
738
|
const own = (file: string) => file.startsWith(gtd) && !module.filepath.startsWith(gtd)
|
|
717
739
|
const registered = {
|
|
718
740
|
version,
|
|
719
|
-
origin: relative(root, module.filepath),
|
|
741
|
+
origin: paths.relative(root, module.filepath),
|
|
720
742
|
fingerprint: configFingerprint(
|
|
743
|
+
nodeSyncIo,
|
|
744
|
+
paths,
|
|
721
745
|
files.filter((file) => !own(file)),
|
|
722
746
|
root,
|
|
723
747
|
module.read ?? readFromDisk,
|
|
@@ -743,13 +767,14 @@ const shippedImport = (
|
|
|
743
767
|
}
|
|
744
768
|
|
|
745
769
|
const bundleConfig = async (
|
|
770
|
+
paths: Path.Path,
|
|
746
771
|
module: WorkflowModule,
|
|
747
772
|
mode: BundleMode,
|
|
748
773
|
): Promise<{ readonly code: string; readonly files: readonly string[] }> => {
|
|
749
774
|
const { read } = module
|
|
750
775
|
const served = new Set<string>()
|
|
751
776
|
const fromRead = (path: string): string | null => {
|
|
752
|
-
const hit = moduleCandidates(path).find((candidate) => read?.(candidate) !== undefined)
|
|
777
|
+
const hit = moduleCandidates(paths, path).find((candidate) => read?.(candidate) !== undefined)
|
|
753
778
|
if (hit === undefined) return null
|
|
754
779
|
served.add(hit)
|
|
755
780
|
return hit
|
|
@@ -760,13 +785,15 @@ const bundleConfig = async (
|
|
|
760
785
|
if (importer === undefined) return source === module.filepath ? source : null
|
|
761
786
|
const shipped = shippedImport(source, mode)
|
|
762
787
|
if (shipped !== undefined) return shipped
|
|
763
|
-
if (/^\.\.?(\/|$)/.test(source)) return fromRead(join(dirname(importer), source))
|
|
764
|
-
if (!isPackage(source)) return null
|
|
788
|
+
if (/^\.\.?(\/|$)/.test(source)) return fromRead(paths.join(paths.dirname(importer), source))
|
|
789
|
+
if (!isPackage(paths, source)) return null
|
|
765
790
|
const resolved = await this.resolve(source, importer, { skipSelf: true })
|
|
766
|
-
return mode.kind === "pin" ? resolved : packageImport(source, resolved)
|
|
791
|
+
return mode.kind === "pin" ? resolved : packageImport(paths, source, resolved)
|
|
767
792
|
},
|
|
768
793
|
load(id) {
|
|
769
|
-
if (mode.kind === "pin" && isNativeAddon(id))
|
|
794
|
+
if (mode.kind === "pin" && isNativeAddon(paths, id)) {
|
|
795
|
+
throw new Error(nativeAddon(paths, id))
|
|
796
|
+
}
|
|
770
797
|
if (id === module.filepath) return module.source
|
|
771
798
|
return served.has(id) ? read!(id) : null
|
|
772
799
|
},
|
|
@@ -774,7 +801,7 @@ const bundleConfig = async (
|
|
|
774
801
|
transform(code, id) {
|
|
775
802
|
return code.replace(/\bimport\.meta\.(url|filename|dirname)\b/g, (_, key: string) =>
|
|
776
803
|
JSON.stringify(
|
|
777
|
-
key === "url" ?
|
|
804
|
+
key === "url" ? fileUrl(paths, id) : key === "filename" ? id : paths.dirname(id),
|
|
778
805
|
),
|
|
779
806
|
)
|
|
780
807
|
},
|
|
@@ -783,7 +810,7 @@ const bundleConfig = async (
|
|
|
783
810
|
input: module.filepath,
|
|
784
811
|
platform: "node",
|
|
785
812
|
plugins: [plugin],
|
|
786
|
-
external: (id) => isBuiltin(id),
|
|
813
|
+
external: (id) => NodeRuntime.isBuiltin(id),
|
|
787
814
|
resolve: { extensionAlias: { ".js": [".js", ".ts"], ".mjs": [".mjs", ".mts"] } },
|
|
788
815
|
// Every statement runs, as it would unbundled.
|
|
789
816
|
treeshake: false,
|
|
@@ -814,17 +841,23 @@ const bundleConfig = async (
|
|
|
814
841
|
|
|
815
842
|
// A pin is plain JavaScript: a compiled addon stays a file on one machine.
|
|
816
843
|
// A package node-gyp builds loads its addon by a path no bundler sees.
|
|
817
|
-
const isNativeAddon = (id: string): boolean => {
|
|
844
|
+
const isNativeAddon = (paths: Path.Path, id: string): boolean => {
|
|
818
845
|
if (id.endsWith(".node")) return true
|
|
846
|
+
const { sep } = paths
|
|
819
847
|
const at = id.lastIndexOf(`node_modules${sep}`)
|
|
820
848
|
if (at < 0) return false
|
|
821
849
|
const rest = id.slice(at + `node_modules${sep}`.length).split(sep)
|
|
822
850
|
const root =
|
|
823
851
|
id.slice(0, at) + ["node_modules", ...rest.slice(0, rest[0]!.startsWith("@") ? 2 : 1)].join(sep)
|
|
824
|
-
|
|
852
|
+
try {
|
|
853
|
+
return nodeSyncIo.stat(paths.join(root, "binding.gyp")) !== undefined
|
|
854
|
+
} catch {
|
|
855
|
+
return false
|
|
856
|
+
}
|
|
825
857
|
}
|
|
826
858
|
|
|
827
|
-
const nativeAddon = (path: string): string => {
|
|
859
|
+
const nativeAddon = (paths: Path.Path, path: string): string => {
|
|
860
|
+
const { sep } = paths
|
|
828
861
|
const at = path.lastIndexOf(`node_modules${sep}`)
|
|
829
862
|
const rest = at < 0 ? [] : path.slice(at + `node_modules${sep}`.length).split(sep)
|
|
830
863
|
const name = rest[0]?.startsWith("@") ? rest.slice(0, 2).join("/") : rest[0]
|
|
@@ -834,28 +867,32 @@ const nativeAddon = (path: string): string => {
|
|
|
834
867
|
// Bundled, an ES module gets the shipped `@pmelab/gtd/*` like the config
|
|
835
868
|
// does. CommonJS stays Node's: it may read `__dirname`, which a bundle loses.
|
|
836
869
|
const packageImport = (
|
|
870
|
+
paths: Path.Path,
|
|
837
871
|
source: string,
|
|
838
872
|
resolved: { readonly id: string } | null,
|
|
839
873
|
): string | { readonly id: string; readonly external: true } =>
|
|
840
|
-
resolved !== null && isEsModule(resolved.id)
|
|
874
|
+
resolved !== null && isEsModule(paths, resolved.id)
|
|
841
875
|
? resolved.id
|
|
842
876
|
: { id: resolved?.id ?? source, external: true }
|
|
843
877
|
|
|
844
|
-
const isEsModule = (path: string): boolean => {
|
|
878
|
+
const isEsModule = (paths: Path.Path, path: string): boolean => {
|
|
845
879
|
if (/\.(mjs|[cm]?tsx?)$/.test(path)) return true
|
|
846
880
|
if (!/\.jsx?$/.test(path)) return false
|
|
847
|
-
for (let dir = dirname(path); dirname(dir) !== dir; dir = dirname(dir)) {
|
|
848
|
-
const
|
|
849
|
-
if (
|
|
850
|
-
return (JSON.parse(readFileSync(file, "utf8")) as { type?: string }).type === "module"
|
|
851
|
-
}
|
|
881
|
+
for (let dir = paths.dirname(path); paths.dirname(dir) !== dir; dir = paths.dirname(dir)) {
|
|
882
|
+
const source = readFromDisk(paths.join(dir, "package.json"))
|
|
883
|
+
if (source !== undefined) return (JSON.parse(source) as { type?: string }).type === "module"
|
|
852
884
|
}
|
|
853
885
|
return false
|
|
854
886
|
}
|
|
855
887
|
|
|
856
888
|
const inPackage = (path: string): boolean => /[\\/]node_modules[\\/]/.test(path)
|
|
857
889
|
|
|
858
|
-
const isPackage = (specifier: string): boolean =>
|
|
890
|
+
const isPackage = (paths: Path.Path, specifier: string): boolean =>
|
|
891
|
+
!paths.isAbsolute(specifier) && !NodeRuntime.isBuiltin(specifier)
|
|
892
|
+
|
|
893
|
+
// `toFileUrl` fails only where `pathToFileURL` throws.
|
|
894
|
+
const fileUrl = (paths: Path.Path, path: string): string =>
|
|
895
|
+
Effect.runSync(paths.toFileUrl(path)).href
|
|
859
896
|
|
|
860
897
|
// rolldown reports each error with a colored code frame; a diagnostic takes
|
|
861
898
|
// its headline and location.
|
|
@@ -863,7 +900,7 @@ const bundleError = (e: unknown): unknown => {
|
|
|
863
900
|
const errors = (e as { errors?: readonly { message: string }[] } | null)?.errors
|
|
864
901
|
if (errors === undefined) return e
|
|
865
902
|
const lines = errors.map(({ message }) => {
|
|
866
|
-
const plain = stripVTControlCharacters(message)
|
|
903
|
+
const plain = NodeRuntime.stripVTControlCharacters(message)
|
|
867
904
|
const at = /\[ (.+:\d+:\d+) \]/.exec(plain)?.[1]
|
|
868
905
|
const headline = plain.split("\n")[0]!
|
|
869
906
|
return at === undefined ? headline : `${headline} (${at})`
|
|
@@ -874,7 +911,8 @@ const bundleError = (e: unknown): unknown => {
|
|
|
874
911
|
type Rolldown = typeof import("rolldown")
|
|
875
912
|
let rolldownModule: Rolldown | undefined
|
|
876
913
|
// Required at run time, never bundled: rolldown ships a native binding.
|
|
877
|
-
const requireRolldown = (): Rolldown =>
|
|
914
|
+
const requireRolldown = (): Rolldown =>
|
|
915
|
+
NodeRuntime.createRequire(import.meta.url)("rolldown") as Rolldown
|
|
878
916
|
|
|
879
917
|
const isWorkflow = (value: unknown): boolean =>
|
|
880
918
|
markedOptions(value) !== undefined || isFactoryWorkflow(value)
|
|
@@ -1119,7 +1157,7 @@ interface ConfigServiceOperations {
|
|
|
1119
1157
|
readonly load: Effect.Effect<
|
|
1120
1158
|
ConfigOperations,
|
|
1121
1159
|
Error,
|
|
1122
|
-
Narrator | Workspace | Host | ConfigDiscovery
|
|
1160
|
+
Narrator | Workspace | Host | Path.Path | ConfigDiscovery
|
|
1123
1161
|
>
|
|
1124
1162
|
}
|
|
1125
1163
|
|
package/src/workflow/pin.test.ts
CHANGED
|
@@ -2,10 +2,11 @@ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "nod
|
|
|
2
2
|
import { isBuiltin } from "node:module"
|
|
3
3
|
import { tmpdir } from "node:os"
|
|
4
4
|
import { join } from "node:path"
|
|
5
|
-
import { Effect, Exit } from "effect"
|
|
5
|
+
import { Effect, Exit, Layer } from "effect"
|
|
6
6
|
import { parseAst } from "rolldown/parseAst"
|
|
7
7
|
import { afterEach, describe, expect, it } from "vitest"
|
|
8
8
|
import { PIN_KEY } from "../install/index.js"
|
|
9
|
+
import { NodeServices } from "../platform/index.js"
|
|
9
10
|
import { InMemRepo, testLayers } from "../twins/index.js"
|
|
10
11
|
import { ConfigDiscovery, ConfigService } from "./index.js"
|
|
11
12
|
import { EngineCore, pin, readPin } from "./pin.js"
|
|
@@ -16,7 +17,7 @@ afterEach(() => {
|
|
|
16
17
|
|
|
17
18
|
// A pin must run with nothing installed: the engine imports Node and nothing else.
|
|
18
19
|
describe.each([
|
|
19
|
-
["live", EngineCore.Live],
|
|
20
|
+
["live", EngineCore.Live.pipe(Layer.provide(NodeServices.layer))],
|
|
20
21
|
["in memory", testLayers(new InMemRepo())],
|
|
21
22
|
])("EngineCore (%s)", (_, layer) => {
|
|
22
23
|
const source = () =>
|
package/src/workflow/pin.ts
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { gunzipSync, gzipSync } from "node:zlib"
|
|
4
|
-
import { Context, Effect, Layer } from "effect"
|
|
5
|
-
import { GtdError } from "../platform/index.js"
|
|
1
|
+
import { Context, Effect, Layer, Path } from "effect"
|
|
2
|
+
import { GtdError, NodeRuntime, nodeSyncIo } from "../platform/index.js"
|
|
6
3
|
import { ownPackage, pinnedWorkflowSource } from "./load.js"
|
|
7
4
|
import type { WorkflowModule } from "./discovery.js"
|
|
8
5
|
|
|
@@ -22,12 +19,20 @@ export class EngineCore extends Context.Service<
|
|
|
22
19
|
source: Effect.suspend(() => {
|
|
23
20
|
const file = path()
|
|
24
21
|
return Effect.try({
|
|
25
|
-
try: () =>
|
|
22
|
+
try: () => {
|
|
23
|
+
const source = nodeSyncIo.readText(file)
|
|
24
|
+
if (source === undefined) throw new Error("absent")
|
|
25
|
+
return source
|
|
26
|
+
},
|
|
26
27
|
catch: () => new GtdError(`gtd bundle: no engine at ${file} — build gtd first`),
|
|
27
28
|
})
|
|
28
29
|
}),
|
|
29
30
|
})
|
|
30
|
-
static Live =
|
|
31
|
+
static Live = Layer.unwrap(
|
|
32
|
+
Effect.map(Path.Path, (path) =>
|
|
33
|
+
EngineCore.layer(() => path.join(ownPackage(path).root, "dist", "gtd.core.mjs")),
|
|
34
|
+
),
|
|
35
|
+
)
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
/**
|
|
@@ -39,11 +44,12 @@ export const pin = (
|
|
|
39
44
|
module: WorkflowModule,
|
|
40
45
|
root: string,
|
|
41
46
|
version: string,
|
|
42
|
-
): Effect.Effect<Uint8Array, Error, EngineCore> =>
|
|
47
|
+
): Effect.Effect<Uint8Array, Error, EngineCore | Path.Path> =>
|
|
43
48
|
Effect.gen(function* () {
|
|
44
49
|
const core = yield* (yield* EngineCore).source
|
|
50
|
+
const path = yield* Path.Path
|
|
45
51
|
const workflow = yield* Effect.tryPromise({
|
|
46
|
-
try: () => pinnedWorkflowSource(module, root, version),
|
|
52
|
+
try: () => pinnedWorkflowSource(path, module, root, version),
|
|
47
53
|
catch: (e) =>
|
|
48
54
|
new GtdError(
|
|
49
55
|
`gtd bundle: ${module.filepath}: ${e instanceof Error ? e.message : String(e)}`,
|
|
@@ -53,7 +59,7 @@ export const pin = (
|
|
|
53
59
|
const source = workflow + core.replace(/^#!.*\n/, "")
|
|
54
60
|
return Buffer.concat([
|
|
55
61
|
Buffer.from(`gtdpin ${PIN_FORMAT} gtd ${version}\n`),
|
|
56
|
-
|
|
62
|
+
NodeRuntime.gzip(source),
|
|
57
63
|
])
|
|
58
64
|
})
|
|
59
65
|
|
|
@@ -75,6 +81,6 @@ export const readPin = (
|
|
|
75
81
|
if (header === undefined) throw new Error("not a gtd pin")
|
|
76
82
|
return {
|
|
77
83
|
...header,
|
|
78
|
-
source:
|
|
84
|
+
source: NodeRuntime.gunzip(bytes.subarray(bytes.indexOf(0x0a) + 1)),
|
|
79
85
|
}
|
|
80
86
|
}
|