@reventlessdev/reventless-spec 3.0.0-alpha.143 → 3.0.0-alpha.145

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.
@@ -6,6 +6,8 @@ import * as Nodeurl from "node:url";
6
6
  import * as Nodemodule from "node:module";
7
7
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
8
8
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
9
+ import * as Stdlib_Result from "@rescript/runtime/lib/es6/Stdlib_Result.js";
10
+ import * as CliArgs$Reventless from "../CliArgs.res.mjs";
9
11
  import * as Util_Sury$Reventless from "../util/Util_Sury.res.mjs";
10
12
  import * as PackageVersion$Reventless from "../PackageVersion.res.mjs";
11
13
  import * as TraitCertificate$Reventless from "../components/TraitCertificate.res.mjs";
@@ -60,86 +62,99 @@ function readJson(path) {
60
62
  }
61
63
  }
62
64
 
63
- async function main() {
64
- let argv = process.argv.slice(2, process.argv.length);
65
- let flag = key => {
66
- let i = argv.indexOf("--" + key);
67
- if (i !== -1) {
68
- return argv[i + 1 | 0];
69
- }
70
- };
71
- let traitPackage = argv[0];
72
- if (traitPackage !== undefined) {
73
- switch (traitPackage) {
74
- case "" :
75
- case "--help" :
76
- case "-h" :
77
- break;
78
- default:
79
- let match = flag("host");
80
- let match$1 = flag("report");
81
- let match$2 = flag("out");
82
- if (match === undefined) {
83
- return fail("--host, --report and --out are all required.\n\n" + usage);
84
- }
85
- if (match$1 === undefined) {
86
- return fail("--host, --report and --out are all required.\n\n" + usage);
87
- }
88
- if (match$2 === undefined) {
89
- return fail("--host, --report and --out are all required.\n\n" + usage);
90
- }
91
- let conformanceModule = Stdlib_Option.getOr(Stdlib_Array.last(traitPackage.split("/")), "").replace("trait-", "").split("-").map(part => part.charAt(0).toUpperCase() + part.slice(1, part.length)).join("") + "_Conformance";
92
- let specifier = traitPackage + `/src/` + conformanceModule + `.res.mjs`;
93
- let modulePath;
94
- try {
95
- modulePath = Nodemodule.createRequire(process.cwd() + "/index.js").resolve(specifier);
96
- } catch (exn) {
97
- modulePath = specifier;
98
- }
99
- let conformance;
100
- try {
101
- conformance = await import(Nodeurl.pathToFileURL(modulePath).href);
102
- } catch (exn$1) {
103
- fail(traitPackage + ` ships no conformance suite (looked for ` + specifier + `).\n A trait without one cannot be certified — there is nothing to prove.`);
104
- conformance = undefined;
105
- }
106
- let suite = conformance.suiteName(match);
107
- let parsed;
108
- try {
109
- parsed = Util_Sury$Reventless.fromJson(readJson(match$1), reportSchema);
110
- } catch (exn$2) {
111
- fail(match$1 + ` is not a Jest JSON report (expected \`testResults\`).`);
112
- parsed = undefined;
113
- }
114
- let results = assertionsFor(parsed, suite);
115
- if (results.length === 0) {
116
- return fail(`the report contains no suite titled "` + suite + `".\n` + (` Either the conformance binding was never registered, or ` + match + ` is not the `) + `name its Spec declares.`);
117
- }
118
- let resolveVersion = specifier => {
119
- try {
120
- let entry = Nodemodule.createRequire(process.cwd() + "/index.js").resolve(specifier);
121
- return PackageVersion$Reventless.fromModuleUrl(Nodeurl.pathToFileURL(entry).href);
122
- } catch (exn) {
123
- return "0.0.0";
65
+ function parseArgs(argv) {
66
+ return Stdlib_Result.flatMap(Stdlib_Result.flatMap(CliArgs$Reventless.parse([
67
+ "host",
68
+ "report",
69
+ "out"
70
+ ], undefined, undefined, undefined, argv), a => CliArgs$Reventless.atMost(a, 1)), a => {
71
+ let match = CliArgs$Reventless.positionals(a)[0];
72
+ let match$1 = CliArgs$Reventless.string(a, "host");
73
+ let match$2 = CliArgs$Reventless.string(a, "report");
74
+ let match$3 = CliArgs$Reventless.string(a, "out");
75
+ if (match !== undefined && match !== "") {
76
+ if (match$1 !== undefined && match$2 !== undefined && match$3 !== undefined) {
77
+ return {
78
+ TAG: "Ok",
79
+ _0: {
80
+ traitPackage: match,
81
+ host: match$1,
82
+ report: match$2,
83
+ out: match$3
124
84
  }
125
85
  };
126
- let certificate = TraitCertificate$Reventless.fromReport(traitPackage, resolveVersion(specifier), resolveVersion("@reventlessdev/reventless-spec/package.json"), match, suite, results);
127
- Nodefs.writeFileSync(match$2, TraitCertificate$Reventless.render(certificate), "utf8");
128
- console.log(`certify-trait: ` + TraitCertificate$Reventless.summarize(certificate));
129
- console.log(`Wrote: ` + match$2);
130
- if (!TraitCertificate$Reventless.verified(certificate)) {
131
- process.exit(1);
132
- return;
133
- } else {
134
- return;
135
- }
86
+ } else {
87
+ return {
88
+ TAG: "Error",
89
+ _0: "--host, --report and --out are all required."
90
+ };
91
+ }
92
+ } else {
93
+ return {
94
+ TAG: "Error",
95
+ _0: "<trait-package> is required."
96
+ };
136
97
  }
137
- }
138
- console.log(usage);
139
- process.exit(argv.length === 0 ? 1 : 0);
98
+ });
140
99
  }
141
100
 
142
- main();
101
+ let cli = {
102
+ bin: "certify-trait",
103
+ usage: usage,
104
+ parse: parseArgs
105
+ };
106
+
107
+ function main() {
108
+ return CliArgs$Reventless.run(cli, undefined, undefined, async param => {
109
+ let out = param.out;
110
+ let reportPath = param.report;
111
+ let host = param.host;
112
+ let traitPackage = param.traitPackage;
113
+ let conformanceModule = Stdlib_Option.getOr(Stdlib_Array.last(traitPackage.split("/")), "").replace("trait-", "").split("-").map(part => part.charAt(0).toUpperCase() + part.slice(1, part.length)).join("") + "_Conformance";
114
+ let specifier = traitPackage + `/src/` + conformanceModule + `.res.mjs`;
115
+ let modulePath;
116
+ try {
117
+ modulePath = Nodemodule.createRequire(process.cwd() + "/index.js").resolve(specifier);
118
+ } catch (exn) {
119
+ modulePath = specifier;
120
+ }
121
+ let conformance;
122
+ try {
123
+ conformance = await import(Nodeurl.pathToFileURL(modulePath).href);
124
+ } catch (exn$1) {
125
+ fail(traitPackage + ` ships no conformance suite (looked for ` + specifier + `).\n A trait without one cannot be certified — there is nothing to prove.`);
126
+ conformance = undefined;
127
+ }
128
+ let suite = conformance.suiteName(host);
129
+ let parsed;
130
+ try {
131
+ parsed = Util_Sury$Reventless.fromJson(readJson(reportPath), reportSchema);
132
+ } catch (exn$2) {
133
+ fail(reportPath + ` is not a Jest JSON report (expected \`testResults\`).`);
134
+ parsed = undefined;
135
+ }
136
+ let results = assertionsFor(parsed, suite);
137
+ if (results.length === 0) {
138
+ return fail(`the report contains no suite titled "` + suite + `".\n` + (` Either the conformance binding was never registered, or ` + host + ` is not the `) + `name its Spec declares.`);
139
+ }
140
+ let resolveVersion = specifier => {
141
+ try {
142
+ let entry = Nodemodule.createRequire(process.cwd() + "/index.js").resolve(specifier);
143
+ return PackageVersion$Reventless.fromModuleUrl(Nodeurl.pathToFileURL(entry).href);
144
+ } catch (exn) {
145
+ return "0.0.0";
146
+ }
147
+ };
148
+ let certificate = TraitCertificate$Reventless.fromReport(traitPackage, resolveVersion(specifier), resolveVersion("@reventlessdev/reventless-spec/package.json"), host, suite, results);
149
+ Nodefs.writeFileSync(out, TraitCertificate$Reventless.render(certificate), "utf8");
150
+ console.log(`certify-trait: ` + TraitCertificate$Reventless.summarize(certificate));
151
+ console.log(`Wrote: ` + out);
152
+ if (!TraitCertificate$Reventless.verified(certificate)) {
153
+ process.exit(1);
154
+ return;
155
+ }
156
+ });
157
+ }
143
158
 
144
159
  export {
145
160
  fail,
@@ -149,6 +164,8 @@ export {
149
164
  reportSchema,
150
165
  assertionsFor,
151
166
  readJson,
167
+ parseArgs,
168
+ cli,
152
169
  main,
153
170
  }
154
171
  /* reportAssertionSchema Not a pure module */
@@ -46,30 +46,6 @@ let usage = `Usage: graft-trait <trait-package> --into <srcDir> --tests <testsDi
46
46
 
47
47
  // ── Arguments ────────────────────────────────────────────────────────────────
48
48
 
49
- /** `--key value` pairs, in order, with the leading positional taken off first.
50
- A flag with no value (`--dry-run`) reads as `true`, so the config schema can
51
- take it as a boolean without the CLI knowing which keys are flags. */
52
- let parseFlags = (args: array<string>): dict<JSON.t> => {
53
- let out = Dict.make()
54
- let rec go = i =>
55
- switch args->Array.get(i) {
56
- | None => ()
57
- | Some(arg) if arg->String.startsWith("--") =>
58
- let key = arg->String.slice(~start=2, ~end=arg->String.length)
59
- switch args->Array.get(i + 1) {
60
- | Some(value) if !(value->String.startsWith("--")) =>
61
- out->Dict.set(key, JSON.Encode.string(value))
62
- go(i + 2)
63
- | _ =>
64
- out->Dict.set(key, JSON.Encode.bool(true))
65
- go(i + 1)
66
- }
67
- | Some(_) => go(i + 1)
68
- }
69
- go(0)
70
- out
71
- }
72
-
73
49
  /** A comma-separated value becomes an array. Applied only to keys the trait's
74
50
  schema declares as arrays, so a name that happens to contain a comma is not
75
51
  split behind the author's back. */
@@ -106,134 +82,157 @@ let arrayFieldNames = (schema: S.t<unknown>): array<string> =>
106
82
 
107
83
  // ── Entry point ──────────────────────────────────────────────────────────────
108
84
 
109
- let main = async () => {
110
- let argv = NodeProcess.argv->Array.slice(~start=2, ~end=NodeProcess.argv->Array.length)
111
- switch argv->Array.get(0) {
112
- | None | Some("") | Some("--help") | Some("-h") => {
113
- Console.log(usage)
114
- NodeProcess.exit(argv->Array.length == 0 ? 1 : 0)
85
+ /** The trait, where its graft goes, and every other `--key value` as a field of
86
+ the trait's config — a string, or `true` for a key given no value. */
87
+ type args = {
88
+ traitPackage: string,
89
+ into: string,
90
+ tests: string,
91
+ dryRun: bool,
92
+ fields: dict<JSON.t>,
93
+ }
94
+
95
+ let parseArgs = (argv: array<string>): result<args, string> =>
96
+ CliArgs.parse(~strings=["into", "tests"], ~bools=["dry-run"], ~pairs=true, argv)
97
+ ->Result.flatMap(a => a->CliArgs.atMost(1))
98
+ ->Result.flatMap(a =>
99
+ switch (
100
+ a->CliArgs.positionals->Array.get(0),
101
+ a->CliArgs.string("into"),
102
+ a->CliArgs.string("tests"),
103
+ ) {
104
+ | (None | Some(""), _, _) => Error("<trait-package> is required.")
105
+ | (Some(traitPackage), Some(into), Some(tests)) =>
106
+ // A key given no value reads as `true`, so the config schema can take it
107
+ // as a boolean without the tool knowing which keys are flags.
108
+ let fields = Dict.make()
109
+ a
110
+ ->CliArgs.pairs
111
+ ->Array.forEach(((key, value)) =>
112
+ fields->Dict.set(
113
+ key,
114
+ switch value {
115
+ | Text(text) => JSON.Encode.string(text)
116
+ | Present => JSON.Encode.bool(true)
117
+ },
118
+ )
119
+ )
120
+ Ok({traitPackage, into, tests, dryRun: a->CliArgs.bool("dry-run"), fields})
121
+ | (Some(_), _, _) => Error("--into and --tests are both required.")
122
+ }
123
+ )
124
+
125
+ let cli: CliArgs.cli<args> = {bin: "graft-trait", usage, parse: parseArgs}
126
+
127
+ let main = () =>
128
+ CliArgs.run(cli, async ({traitPackage, into, tests, dryRun, fields}) => {
129
+ // Resolved by name, from the consumer's own `node_modules`. A trait
130
+ // that ships no scaffold fails here, saying so, rather than half-way
131
+ // through writing files.
132
+ // `@scope/trait-attachments` → `Attachments_Scaffold`;
133
+ // `@scope/trait-address-geocoding` → `AddressGeocoding_Scaffold`.
134
+ let scaffoldModule =
135
+ traitPackage
136
+ ->String.split("/")
137
+ ->Array.last
138
+ ->Option.getOr("")
139
+ ->String.replace("trait-", "")
140
+ ->String.split("-")
141
+ ->Array.map(part =>
142
+ part->String.charAt(0)->String.toUpperCase ++
143
+ part->String.slice(~start=1, ~end=part->String.length)
144
+ )
145
+ ->Array.join("") ++ "_Scaffold"
146
+ let specifier = `${traitPackage}/src/${scaffoldModule}.res.mjs`
147
+ // Resolved from the **caller's** directory, not this file's. The trait
148
+ // is a dependency of the plugin being grafted, and deliberately not one
149
+ // of this package — a bare `import()` here would look in
150
+ // `reventless-spec`'s own tree and never find it. Same reason
151
+ // `HostShellDist` reaches for the host shell this way.
152
+ let modulePath = try NodeModule.createRequire(
153
+ NodeProcess.cwd() ++ "/index.js",
154
+ )->NodeModule.requireResolve(specifier) catch {
155
+ | _ => specifier
156
+ }
157
+ let scaffold: scaffoldExports = try await dynImport(
158
+ NodeUrl.pathToFileURL(modulePath)["href"],
159
+ ) catch {
160
+ | _ =>
161
+ fail(
162
+ `${traitPackage} ships no scaffold (looked for ${specifier}).\n` ++
163
+ ` Not every trait has one — a trait whose graft is all patches has nothing to write.\n` ++ ` Write the graft by hand instead; its README says what the host must declare.`,
164
+ )
165
+ %raw(`undefined`)
115
166
  }
116
- | Some(traitPackage) => {
117
- let flags = parseFlags(argv->Array.slice(~start=1, ~end=argv->Array.length))
118
- let stringFlag = key => flags->Dict.get(key)->Option.flatMap(JSON.Decode.string)
119
- let into = stringFlag("into")
120
- let tests = stringFlag("tests")
121
- let dryRun = flags->Dict.get("dry-run")->Option.isSome
122
-
123
- switch (into, tests) {
124
- | (None, _) | (_, None) => fail("--into and --tests are both required.\n\n" ++ usage)
125
- | (Some(into), Some(tests)) => {
126
- // Resolved by name, from the consumer's own `node_modules`. A trait
127
- // that ships no scaffold fails here, saying so, rather than half-way
128
- // through writing files.
129
- // `@scope/trait-attachments` → `Attachments_Scaffold`;
130
- // `@scope/trait-address-geocoding` → `AddressGeocoding_Scaffold`.
131
- let scaffoldModule =
132
- traitPackage
133
- ->String.split("/")
134
- ->Array.last
135
- ->Option.getOr("")
136
- ->String.replace("trait-", "")
137
- ->String.split("-")
138
- ->Array.map(part =>
139
- part->String.charAt(0)->String.toUpperCase ++
140
- part->String.slice(~start=1, ~end=part->String.length)
141
- )
142
- ->Array.join("") ++ "_Scaffold"
143
- let specifier = `${traitPackage}/src/${scaffoldModule}.res.mjs`
144
- // Resolved from the **caller's** directory, not this file's. The trait
145
- // is a dependency of the plugin being grafted, and deliberately not one
146
- // of this package — a bare `import()` here would look in
147
- // `reventless-spec`'s own tree and never find it. Same reason
148
- // `HostShellDist` reaches for the host shell this way.
149
- let modulePath = try NodeModule.createRequire(
150
- NodeProcess.cwd() ++ "/index.js",
151
- )->NodeModule.requireResolve(specifier) catch {
152
- | _ => specifier
153
- }
154
- let scaffold: scaffoldExports = try await dynImport(
155
- NodeUrl.pathToFileURL(modulePath)["href"],
156
- ) catch {
157
- | _ =>
158
- fail(
159
- `${traitPackage} ships no scaffold (looked for ${specifier}).\n` ++
160
- ` Not every trait has one — a trait whose graft is all patches has nothing to write.\n` ++ ` Write the graft by hand instead; its README says what the host must declare.`,
161
- )
162
- %raw(`undefined`)
163
- }
164
167
 
165
- // The trait validates its own config, so the CLI never learns a
166
- // trait's vocabulary — and a misspelled key is named here.
167
- let listFields = arrayFieldNames(scaffold.configSchema)
168
- let raw = Dict.make()
169
- flags
170
- ->Dict.toArray
171
- ->Array.forEach(((key, value)) =>
172
- switch (listFields->Array.includes(key), value->JSON.Decode.string) {
173
- | (true, Some(text)) => raw->Dict.set(key, splitList(text))
174
- | _ => raw->Dict.set(key, value)
175
- }
176
- )
177
- ["into", "tests", "dry-run"]->Array.forEach(k => raw->Dict.delete(k))
178
-
179
- let config = try raw
180
- ->JSON.Encode.object
181
- ->Util_Sury.fromJson(scaffold.configSchema) catch {
182
- | exn => {
183
- fail(
184
- `${traitPackage} refused this config: ${Util_Sury.exnMessage(
185
- exn,
186
- )}\n` ++ ` Every --key is a field of the trait's own config; it decides what it needs.`,
187
- )
188
- %raw(`undefined`)
189
- }
190
- }
168
+ // The trait validates its own config, so the CLI never learns a
169
+ // trait's vocabulary — and a misspelled key is named here.
170
+ let listFields = arrayFieldNames(scaffold.configSchema)
171
+ let raw = Dict.make()
172
+ fields
173
+ ->Dict.toArray
174
+ ->Array.forEach(((key, value)) =>
175
+ switch (listFields->Array.includes(key), value->JSON.Decode.string) {
176
+ | (true, Some(text)) => raw->Dict.set(key, splitList(text))
177
+ | _ => raw->Dict.set(key, value)
178
+ }
179
+ )
191
180
 
192
- let {files, patches} = scaffold.emit(~config, ~into, ~tests)
193
-
194
- files->Array.forEach(({path, contents}) =>
195
- if dryRun {
196
- Console.log(
197
- `Would write: ${path} (${contents
198
- ->String.split("\n")
199
- ->Array.length
200
- ->Int.toString} lines)`,
201
- )
202
- } else if NodeFs.existsSync(path) {
203
- // Never overwrite. From the moment a graft lands it is the host's
204
- // source, edited with the policy the trait deliberately does not
205
- // write — silently replacing it would delete exactly the part no
206
- // generator can reproduce.
207
- Console.log(`Skipped (exists): ${path}`)
208
- } else {
209
- NodeFs.mkdirSync(NodePath.dirname(path), {recursive: true})
210
- NodeFs.writeFileSync(path, contents)
211
- Console.log(`Wrote: ${path}`)
212
- }
213
- )
214
-
215
- if patches->Array.length > 0 {
216
- Console.log("")
217
- Console.log(
218
- "── Paste these; they go into files you already own ──────────────",
219
- )
220
- patches->Array.forEach(({into, at, contents}) => {
221
- Console.log("")
222
- Console.log(`# ${into} — ${at}`)
223
- Console.log(contents)
224
- })
225
- Console.log("")
226
- Console.log(
227
- "Printed rather than written: placing an arm in an existing ordered `switch`\n" ++
228
- "is an AST operation, and a text splice into the wrong arm is a bug the\n" ++ "compiler cannot see.",
229
- )
230
- }
181
+ let config = try raw
182
+ ->JSON.Encode.object
183
+ ->Util_Sury.fromJson(scaffold.configSchema) catch {
184
+ | exn => {
185
+ fail(
186
+ `${traitPackage} refused this config: ${Util_Sury.exnMessage(
187
+ exn,
188
+ )}\n` ++ ` Every --key is a field of the trait's own config; it decides what it needs.`,
189
+ )
190
+ %raw(`undefined`)
191
+ }
192
+ }
193
+
194
+ let {files, patches} = scaffold.emit(~config, ~into, ~tests)
231
195
 
232
- NodeProcess.exit(0)
233
- }
196
+ files->Array.forEach(({path, contents}) =>
197
+ if dryRun {
198
+ Console.log(
199
+ `Would write: ${path} (${contents
200
+ ->String.split("\n")
201
+ ->Array.length
202
+ ->Int.toString} lines)`,
203
+ )
204
+ } else if NodeFs.existsSync(path) {
205
+ // Never overwrite. From the moment a graft lands it is the host's
206
+ // source, edited with the policy the trait deliberately does not
207
+ // write — silently replacing it would delete exactly the part no
208
+ // generator can reproduce.
209
+ Console.log(`Skipped (exists): ${path}`)
210
+ } else {
211
+ NodeFs.mkdirSync(NodePath.dirname(path), {recursive: true})
212
+ NodeFs.writeFileSync(path, contents)
213
+ Console.log(`Wrote: ${path}`)
234
214
  }
215
+ )
216
+
217
+ if patches->Array.length > 0 {
218
+ Console.log("")
219
+ Console.log(
220
+ "── Paste these; they go into files you already own ──────────────",
221
+ )
222
+ patches->Array.forEach(({into, at, contents}) => {
223
+ Console.log("")
224
+ Console.log(`# ${into} — ${at}`)
225
+ Console.log(contents)
226
+ })
227
+ Console.log("")
228
+ Console.log(
229
+ "Printed rather than written: placing an arm in an existing ordered `switch`\n" ++
230
+ "is an AST operation, and a text splice into the wrong arm is a bug the\n" ++ "compiler cannot see.",
231
+ )
235
232
  }
236
- }
237
- }
238
233
 
239
- await main()
234
+ NodeProcess.exit(0)
235
+ })
236
+
237
+ // No top-level call: `../../run-graft-trait.mjs` invokes [main], so a test can
238
+ // import this module.