arcadible 0.1.42 → 0.1.43

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 CHANGED
@@ -35,7 +35,7 @@ arc init star-drift && cd star-drift
35
35
  arc deploy
36
36
  ```
37
37
 
38
- Your repository is the store page: `arcadible.toml` holds the game's settings, `README.md` its title and description, and `.arcadible/` its art. `arc deploy` validates, builds, packages, and publishes it, then waits until the release is live.
38
+ Your repository is the store page: `arcadible.toml` holds the game's settings, `README.md` its title and description, and `.arcadible/` its art. `arc deploy` validates, builds, packages, and publishes it, then waits until the release is live. A new project starts at a prerelease version, `0.1.0-beta.1`, so its first deploy goes to staging: a link to play and share, before players see it.
39
39
 
40
40
  ## Commands
41
41
 
package/bin/arc-build.js CHANGED
@@ -35,6 +35,7 @@ const print = output.print;
35
35
  const relative = path.relative;
36
36
  const resolve = path.resolve;
37
37
  const SpinnerWheel = spinner.SpinnerWheel;
38
+ const warn = output.warn;
38
39
 
39
40
  // [declare]
40
41
 
@@ -65,7 +66,7 @@ const start = function (argv) {
65
66
  parsed = proj;
66
67
  return validate({ project: parsed, root });
67
68
  })
68
- .then((issues) => {
69
+ .then(({ issues, warnings }) => {
69
70
  const count = issues.length;
70
71
  if (count) {
71
72
  throw new ArcError(
@@ -75,6 +76,7 @@ const start = function (argv) {
75
76
  );
76
77
  }
77
78
  sw.stop(true);
79
+ warn(warnings, { json, quiet });
78
80
  sw = new SpinnerWheel("Building... %s");
79
81
  sw.start(json || quiet);
80
82
  return build({ project: parsed, root });
package/bin/arc-deploy.js CHANGED
@@ -41,6 +41,7 @@ const print = output.print;
41
41
  const ProgressBar = progress.ProgressBar;
42
42
  const resolve = path.resolve;
43
43
  const SpinnerWheel = spinner.SpinnerWheel;
44
+ const warn = output.warn;
44
45
 
45
46
  // [declare]
46
47
 
@@ -260,7 +261,7 @@ const start = function (argv) {
260
261
  parsed = proj;
261
262
  return validate({ project: parsed, root });
262
263
  })
263
- .then((issues) => {
264
+ .then(({ issues, warnings }) => {
264
265
  const count = issues.length;
265
266
  if (count) {
266
267
  throw new ArcError(
@@ -270,6 +271,7 @@ const start = function (argv) {
270
271
  );
271
272
  }
272
273
  sw.stop(true);
274
+ warn(warnings, { json, quiet });
273
275
  sw = new SpinnerWheel("Checking... %s");
274
276
  // git deploys need no login: anyone who can push to the repository can
275
277
  // release, and the Arcadible GitHub App publishes the release. Anyone
@@ -314,7 +316,23 @@ const start = function (argv) {
314
316
  .then(([current, artifact]) =>
315
317
  Promise.all([current, artifact, artifact ? project(root) : parsed]),
316
318
  )
317
- .then(([current, artifact, proj]) => {
319
+ .then(([current, artifact, proj]) =>
320
+ Promise.all([
321
+ current,
322
+ artifact,
323
+ proj,
324
+ proj === parsed ? null : validate({ project: proj, root }),
325
+ ]),
326
+ )
327
+ .then(([current, artifact, proj, validated]) => {
328
+ const count = validated?.issues.length ?? 0;
329
+ if (count) {
330
+ throw new ArcError(
331
+ cfgcode.VALIDATION_FAILED,
332
+ phrase.issuesAfterBuild(count),
333
+ { data: { issues: validated.issues } },
334
+ );
335
+ }
318
336
  parsed = proj;
319
337
  sw.stop(true);
320
338
  sw = new SpinnerWheel("Packing... %s");
package/bin/arc-pack.js CHANGED
@@ -34,6 +34,7 @@ const findRc = rc.findRc;
34
34
  const print = output.print;
35
35
  const resolve = path.resolve;
36
36
  const SpinnerWheel = spinner.SpinnerWheel;
37
+ const warn = output.warn;
37
38
 
38
39
  // [declare]
39
40
 
@@ -66,7 +67,7 @@ const start = function (argv) {
66
67
  parsed = proj;
67
68
  return validate({ project: parsed, root });
68
69
  })
69
- .then((issues) => {
70
+ .then(({ issues, warnings }) => {
70
71
  const count = issues.length;
71
72
  if (count) {
72
73
  throw new ArcError(
@@ -76,6 +77,7 @@ const start = function (argv) {
76
77
  );
77
78
  }
78
79
  sw.stop(true);
80
+ warn(warnings, { json, quiet });
79
81
  sw = new SpinnerWheel("Packing... %s");
80
82
  sw.start(json || quiet);
81
83
  return pack({
@@ -28,6 +28,7 @@ const dereference = schema.dereference;
28
28
  const findRc = rc.findRc;
29
29
  const print = output.print;
30
30
  const resolve = path.resolve;
31
+ const schemaPrint = schema.print;
31
32
 
32
33
  // [declare]
33
34
 
@@ -48,24 +49,30 @@ const conf = {
48
49
  const start = function (argv) {
49
50
  const json = argv.json;
50
51
  return validate({ root: resolve(process.cwd(), argv.cwd) })
51
- .then((issues) => {
52
+ .then(({ issues, warnings }) => {
52
53
  const count = issues.length;
53
54
  if (json) {
54
- print({ issues });
55
- } else if (count) {
55
+ print({ issues, warnings });
56
+ } else {
56
57
  const [probs, refs] = dereference(issues);
57
58
  const probcount = probs.length;
58
59
  if (probcount) {
59
60
  log.hdo(`${probcount} ${probcount === 1 ? "issue" : "issues"}:`);
60
61
  log.p(probs.map((prob) => `✗ ${prob}`));
61
62
  }
63
+ const warncount = argv.quiet ? 0 : warnings.length;
64
+ if (warncount) {
65
+ log.hwo(`${warncount} ${warncount === 1 ? "warning" : "warnings"}:`);
66
+ log.p(warnings.map((each) => `⚠ ${schemaPrint(each)}`));
67
+ }
62
68
  const refcount = refs.length;
63
69
  if (refcount) {
64
70
  log.h(`${refcount} ${refcount === 1 ? "reference" : "references"}:`);
65
71
  log.p(refs);
66
72
  }
67
- } else if (!argv.quiet) {
68
- log.hs("No issues found.");
73
+ if (!count && !argv.quiet) {
74
+ log.hs("No issues found.");
75
+ }
69
76
  }
70
77
  process.exit(count ? 1 : 0);
71
78
  })
package/package.json CHANGED
@@ -75,5 +75,5 @@
75
75
  "scripts": {
76
76
  "test": "node --test \"test/**/*.test.js\""
77
77
  },
78
- "version": "0.1.42"
78
+ "version": "0.1.43"
79
79
  }
@@ -8,6 +8,7 @@ const readme = require("../../schema/readme");
8
8
  const readmeFile = require("../../parser/readme-file");
9
9
  const schema = require("../../util/schema");
10
10
  const sections = require("../../parser/sections");
11
+ const unknown = require("./unknown");
11
12
  const workflow = require("../../schema/workflow");
12
13
 
13
14
  // [use]
@@ -49,10 +50,14 @@ const validate = function (proj, readmeStructure, { root }) {
49
50
 
50
51
  // [function]
51
52
 
53
+ // the project's issues, which fail it, and its warnings, which don't.
52
54
  const main = function (conf) {
53
55
  const root = conf.root;
54
- return Promise.all([given(conf), structure(root)]).then(
55
- ([proj, readmeStructure]) => validate(proj, readmeStructure, { root }),
56
+ return Promise.all([given(conf), structure(root), unknown(root)]).then(
57
+ ([proj, readmeStructure, warnings]) => ({
58
+ issues: validate(proj, readmeStructure, { root }),
59
+ warnings: warnings.sort(schema.compare),
60
+ }),
56
61
  );
57
62
  };
58
63
 
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+
3
+ // [import]
4
+
5
+ const cfgcode = require("../../config/code");
6
+ const game = require("../../schema/game");
7
+ const manifest = require("../../parser/manifest");
8
+ const strs = require("../../util/strs");
9
+ const workflow = require("../../schema/workflow");
10
+
11
+ // [use]
12
+
13
+ const closest = strs.closest;
14
+ const ISSUE = cfgcode.ISSUE;
15
+
16
+ // [function]
17
+
18
+ // the fields each table takes, from its schema.
19
+ const known = function (root) {
20
+ return {
21
+ game: Object.keys(game({}, { root }).shape),
22
+ workflow: Object.keys(workflow({}, { root }).shape),
23
+ };
24
+ };
25
+
26
+ // [function]
27
+
28
+ // a warning, in an issue's shape: Arcadible ignores what it doesn't know, so
29
+ // it's reported without failing, with the name it might have meant.
30
+ const warning = function (path, message, near) {
31
+ return {
32
+ code: ISSUE.FIELD_UNKNOWN,
33
+ details: near ? [`Did you mean: ${near}?`] : [],
34
+ link: null,
35
+ message,
36
+ path,
37
+ };
38
+ };
39
+
40
+ // [function]
41
+
42
+ // a value outside any table, such as a `handle` above `[game]`: the field
43
+ // it is, in the table that takes it, if any.
44
+ const untabled = function (fields, name) {
45
+ const table = Object.keys(fields).find((each) => fields[each].includes(name));
46
+ return warning(name, "Unknown field.", table ? `${table}.${name}` : null);
47
+ };
48
+
49
+ // [function]
50
+
51
+ const isTable = function (value) {
52
+ return value !== null && typeof value === "object" && !Array.isArray(value);
53
+ };
54
+
55
+ // [function]
56
+
57
+ // the tables and fields in arcadible.toml that Arcadible doesn't know, such
58
+ // as a misspelled `genre`, as warnings.
59
+ const main = function (root) {
60
+ const fields = known(root);
61
+ const tables = Object.keys(fields);
62
+ return manifest(root).then((conf) =>
63
+ Object.entries(conf).flatMap(([table, values]) => {
64
+ if (!isTable(values)) {
65
+ return Object.hasOwn(fields, table) ? [] : [untabled(fields, table)];
66
+ }
67
+ if (!Object.hasOwn(fields, table)) {
68
+ return [warning(table, "Unknown table.", closest(table, tables))];
69
+ }
70
+ return Object.keys(values)
71
+ .filter((field) => !fields[table].includes(field))
72
+ .map((field) =>
73
+ warning(
74
+ `${table}.${field}`,
75
+ "Unknown field.",
76
+ closest(field, fields[table]),
77
+ ),
78
+ );
79
+ }),
80
+ );
81
+ };
82
+
83
+ // [export]
84
+
85
+ module.exports = main;
@@ -76,6 +76,7 @@ const ISSUE = {
76
76
  DESCRIPTION_RULES_MISSING: "description_rules_missing",
77
77
  DUPLICATE: "duplicate",
78
78
  EMPTY: "empty",
79
+ FIELD_UNKNOWN: "field_unknown",
79
80
  INDEX_MISSING: "index_missing",
80
81
  INVALID: "invalid",
81
82
  MARKDOWN_INVALID: "markdown_invalid",
package/src/phrase.js CHANGED
@@ -216,7 +216,7 @@ const gitRemoteMissing = function () {
216
216
 
217
217
  const noManifest = function (file, dir, found) {
218
218
  return blocks(
219
- dir === "." ? `No ${file} in this directory.` : `No ${file} in ${dir}`,
219
+ dir === "." ? `No ${file} in this directory.` : `No ${file} in ${dir}.`,
220
220
  found
221
221
  ? `The project is in \`${found}\`: run arc there, or pass \`-C ${found}\`.`
222
222
  : "Run arc in the project's directory, or pass -C, or start a project with `arc init`.",
@@ -337,6 +337,17 @@ const timeoutInvalid = function () {
337
337
 
338
338
  // [function]
339
339
 
340
+ // issues in what the build wrote, such as its art, once arc deploy has
341
+ // parsed the project again.
342
+ const issuesAfterBuild = function (count) {
343
+ return blocks(
344
+ `${count} ${count === 1 ? "issue" : "issues"} found after the build.`,
345
+ `The build writes files the manifest reads. Run ${cmdValidate()} for details.`,
346
+ );
347
+ };
348
+
349
+ // [function]
350
+
340
351
  const issuesFound = function (count) {
341
352
  return blocks(
342
353
  `${count} ${count === 1 ? "issue" : "issues"} found.`,
@@ -786,6 +797,7 @@ module.exports = {
786
797
  imageUnsupported,
787
798
  imageWidthMax,
788
799
  imageWidthMin,
800
+ issuesAfterBuild,
789
801
  issuesFound,
790
802
  loginExpired,
791
803
  loginFailed,
@@ -177,7 +177,7 @@ const zSetBasefile = function (field, schema, { max, min }) {
177
177
  const zString = function (field, char, { max, min }) {
178
178
  return z
179
179
  .string()
180
- .min(min, atLeast(field, min, "character"))
180
+ .min(min, { ...atLeast(field, min, "character"), abort: min === 1 })
181
181
  .max(max, message(field, [`Must not exceed: ${max} characters`]))
182
182
  .superRefine((str, ctx) => {
183
183
  const invalid = invalidChars(str, char);
@@ -430,15 +430,29 @@ const zGenres = function () {
430
430
 
431
431
  // [function]
432
432
 
433
+ // each of a handle's rules on its own, so a handle lists every one it breaks:
434
+ // its length, its characters, and its hyphens. The database's
435
+ // check (db/check/game.sql) is the same rules in one regex.
433
436
  const zHandle = function () {
434
437
  const field = "handle";
435
438
  return z.optional(
436
- z
437
- .string()
438
- .min(1, { ...atLeast(field, 1), abort: true })
439
- .regex(
440
- /^[a-z0-9](?:(?![-]{2})[a-z0-9-]){1,22}[a-z0-9]$/u,
441
- message(field, ["Contains invalid characters."]),
439
+ zString(field, /[a-z0-9-]/u, { max: 24, min: 1 })
440
+ .min(3, atLeast(field, 3, "characters"))
441
+ .refine(
442
+ (str) => !str.startsWith("-") && !str.endsWith("-"),
443
+ message(
444
+ field,
445
+ ["Must not start or end with a hyphen."],
446
+ ISSUE.CHARACTERS_INVALID,
447
+ ),
448
+ )
449
+ .refine(
450
+ (str) => !str.includes("--"),
451
+ message(
452
+ field,
453
+ ["Must not contain consecutive hyphens."],
454
+ ISSUE.CHARACTERS_INVALID,
455
+ ),
442
456
  ),
443
457
  );
444
458
  };
package/src/util/log.js CHANGED
@@ -82,6 +82,14 @@ const hdo = function (message) {
82
82
 
83
83
  // [function]
84
84
 
85
+ // a warning heading on stdout, for when the warnings are part of the result,
86
+ // as with arc validate's.
87
+ const hwo = function (message) {
88
+ log(console.log, message, { bold: true, color: "yellow" });
89
+ };
90
+
91
+ // [function]
92
+
85
93
  const pw = function (message) {
86
94
  log(console.warn, message, { color: "yellow" });
87
95
  };
@@ -122,6 +130,7 @@ module.exports = {
122
130
  hd,
123
131
  hdo,
124
132
  hs,
133
+ hwo,
125
134
  nlhd,
126
135
  nlp,
127
136
  nlpd,
@@ -11,6 +11,7 @@ const error = require("./error");
11
11
  const log = require("./log");
12
12
  const objs = require("./objs");
13
13
  const phrase = require("../phrase");
14
+ const schema = require("./schema");
14
15
  const TOML = require("./toml");
15
16
 
16
17
  // [use]
@@ -106,6 +107,18 @@ const abort = function (
106
107
  process.exit(1);
107
108
  };
108
109
 
110
+ // [function]
111
+
112
+ // arc validate's warnings, such as an unknown field, on stderr in yellow,
113
+ // before a command that validates goes on. Left out with `--json` and
114
+ // `--quiet`; arc validate --json lists them.
115
+ const warn = function (warnings, { json, quiet } = {}) {
116
+ if (json || quiet) {
117
+ return;
118
+ }
119
+ warnings.forEach((each) => log.pw(`⚠ ${schema.print(each)}`));
120
+ };
121
+
109
122
  // [export]
110
123
 
111
- module.exports = { abort, print, printJson, printToml };
124
+ module.exports = { abort, print, printJson, printToml, warn };
package/src/util/strs.js CHANGED
@@ -2,6 +2,42 @@
2
2
 
3
3
  // [function]
4
4
 
5
+ // the number of single-character edits between two strings (Levenshtein).
6
+ const distance = function (a, b) {
7
+ let row = Array.from({ length: b.length + 1 }, (_, i) => i);
8
+ for (let i = 1; i <= a.length; i += 1) {
9
+ const next = [i];
10
+ for (let j = 1; j <= b.length; j += 1) {
11
+ next[j] = Math.min(
12
+ row[j] + 1,
13
+ next[j - 1] + 1,
14
+ row[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1),
15
+ );
16
+ }
17
+ row = next;
18
+ }
19
+ return row[b.length];
20
+ };
21
+
22
+ // [function]
23
+
24
+ // the candidate nearest `str`, for "Did you mean", or null when none is
25
+ // within two edits.
26
+ const closest = function (str, candidates) {
27
+ let best = null;
28
+ let min = 3;
29
+ for (const candidate of candidates) {
30
+ const d = distance(str, candidate);
31
+ if (d < min) {
32
+ best = candidate;
33
+ min = d;
34
+ }
35
+ }
36
+ return best;
37
+ };
38
+
39
+ // [function]
40
+
5
41
  const indentLines = function (strs, spaces = 2) {
6
42
  const pad = " ".repeat(spaces);
7
43
  return strs
@@ -28,4 +64,10 @@ const trimWhitespace = function (str) {
28
64
 
29
65
  // [export]
30
66
 
31
- module.exports = { indentLines, stripLeadingInvisible, trimWhitespace };
67
+ module.exports = {
68
+ closest,
69
+ distance,
70
+ indentLines,
71
+ stripLeadingInvisible,
72
+ trimWhitespace,
73
+ };