arcadible 1.0.12 → 1.0.14

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/bin/arc-deploy.js CHANGED
@@ -334,7 +334,14 @@ const start = function (argv) {
334
334
  // whoami
335
335
  current,
336
336
  // pack
337
- git ? null : pack({ input: artifact?.path, project: parsed, root }),
337
+ git
338
+ ? null
339
+ : pack({
340
+ input: artifact?.path,
341
+ onWarning: handler({ json, pb, quiet, sw, warnings }).onWarning,
342
+ project: parsed,
343
+ root,
344
+ }),
338
345
  // build: whether workflow.build ran
339
346
  Boolean(artifact),
340
347
  ]);
package/bin/arc-pack.js CHANGED
@@ -31,6 +31,7 @@ const print = output.print;
31
31
  const resolve = path.resolve;
32
32
  const SpinnerWheel = spinner.SpinnerWheel;
33
33
  const warn = output.warn;
34
+ const warning = output.warning;
34
35
 
35
36
  // [declare]
36
37
 
@@ -56,8 +57,9 @@ const start = function (argv) {
56
57
  const root = resolve(process.cwd(), argv.cwd);
57
58
  // the project, parsed once for both steps.
58
59
  let parsed = null;
59
- // what --json lists under `warnings`.
60
+ // what --json lists under `warnings`: arc validate's, then the package's.
60
61
  let warnings = [];
62
+ const packed = [];
61
63
  let sw = new SpinnerWheel("Validating… %s");
62
64
  sw.start(json || quiet);
63
65
  return checked(root, (found) => {
@@ -71,12 +73,21 @@ const start = function (argv) {
71
73
  sw.start(json || quiet);
72
74
  return pack({
73
75
  input: argv.input,
76
+ // the package's own, printed once it's packed.
77
+ onWarning({ code, message }) {
78
+ packed.push(warning(code, message));
79
+ },
74
80
  output: argv.output,
75
81
  project: parsed,
76
82
  root,
77
83
  });
78
84
  })
79
85
  .then((artifact) => {
86
+ warnings = [...warnings, ...packed];
87
+ if (!json && !quiet && packed.length) {
88
+ sw.stop(true);
89
+ packed.forEach((each) => log.pw(each.message));
90
+ }
80
91
  if (json) {
81
92
  print({ artifact, warnings });
82
93
  return;
package/bin/arc.js CHANGED
@@ -15,6 +15,7 @@ const phrase = require("../src/phrase");
15
15
  // [use]
16
16
 
17
17
  const pathExists = fs.pathExists;
18
+ const realpath = fs.realpath;
18
19
 
19
20
  // [declare]
20
21
 
@@ -47,16 +48,30 @@ const normalize = function (argv) {
47
48
 
48
49
  // [function]
49
50
 
51
+ // the script's own path, through any links: npm links only package.json's
52
+ // `bin` entries, so a command's sibling, such as `arc-default`, is found
53
+ // beside the real script, not beside the link that started it.
54
+ const resolve = function (argv) {
55
+ // left as it is when it can't be resolved, and reported as not found
56
+ return realpath(argv[1])
57
+ .catch(() => argv[1])
58
+ .then((file) => [argv[0], file, ...argv.slice(2)]);
59
+ };
60
+
61
+ // [function]
62
+
50
63
  const main = function (argv, load) {
51
- const args = normalize(dispatch(argv));
52
- pathExists(args[1], (err, exists) => {
53
- if (err || !exists) {
54
- log.hd(phrase.aborted("command"));
55
- log.pd(phrase.unableToFind(args[1]));
56
- process.exit(1);
57
- }
58
- const command = load(args[1]);
59
- command.main(args);
64
+ return resolve(argv).then((resolved) => {
65
+ const args = normalize(dispatch(resolved));
66
+ pathExists(args[1], (err, exists) => {
67
+ if (err || !exists) {
68
+ log.hd(phrase.aborted("command"));
69
+ log.pd(phrase.unableToFind(args[1]));
70
+ process.exit(1);
71
+ }
72
+ const command = load(args[1]);
73
+ command.main(args);
74
+ });
60
75
  });
61
76
  };
62
77
 
package/package.json CHANGED
@@ -65,5 +65,5 @@
65
65
  "scripts": {
66
66
  "test": "node --test \"test/**/*.test.js\""
67
67
  },
68
- "version": "1.0.12"
68
+ "version": "1.0.14"
69
69
  }
@@ -19,6 +19,7 @@ const given = require("../../parser/given");
19
19
  const limits = require("./limits");
20
20
  const phrase = require("../../phrase");
21
21
  const size = require("../../util/size");
22
+ const unsound = require("./unsound");
22
23
  const Zip = require("../../util/zip");
23
24
 
24
25
  // [use]
@@ -106,7 +107,7 @@ const verifyTotal = function (zip) {
106
107
  const pack = function (
107
108
  name,
108
109
  game,
109
- { deployDir, inputPath, mediaPath, outputPath },
110
+ { deployDir, inputPath, mediaPath, onWarning, outputPath },
110
111
  ) {
111
112
  const zip = new Zip(name);
112
113
  return pathExists(inputPath)
@@ -136,6 +137,9 @@ const pack = function (
136
137
  })
137
138
  .then(verifyTotal)
138
139
  .then((file) => {
140
+ if (onWarning) {
141
+ unsound(file.entries()).forEach(onWarning);
142
+ }
139
143
  file.write(outputPath);
140
144
  return { path: outputPath, ...file.stat() };
141
145
  });
@@ -143,7 +147,9 @@ const pack = function (
143
147
 
144
148
  // [function]
145
149
 
146
- const main = function ({ input, output, ...conf }) {
150
+ // `onWarning` gets each of the package's warnings, such as a `.env` file
151
+ // in it, as a command's own.
152
+ const main = function ({ input, onWarning, output, ...conf }) {
147
153
  const root = conf.root;
148
154
  return given(conf).then(({ game, workflow }) => {
149
155
  const name = game.handle ?? game.id;
@@ -154,6 +160,7 @@ const main = function ({ input, output, ...conf }) {
154
160
  deployDir: inputPath === deployDir,
155
161
  inputPath,
156
162
  mediaPath: resolve(root, MEDIA),
163
+ onWarning,
157
164
  outputPath: resolve(root, file),
158
165
  });
159
166
  });
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+
3
+ // [import]
4
+
5
+ const path = require("node:path");
6
+
7
+ // [import]
8
+
9
+ const cfgcode = require("../../config/code");
10
+ const phrase = require("../../phrase");
11
+
12
+ // [use]
13
+
14
+ const posixBasename = path.posix.basename;
15
+ const WARNING = cfgcode.WARNING;
16
+
17
+ // [declare]
18
+
19
+ // the files any player could download that are almost never meant to be
20
+ // public, kept short on purpose, to grow later (OUTPUT.md, Codes): a `.env`
21
+ // file other than an example of one, and a git repository.
22
+ const ENV = /^\.env(?:\..+)?$/u;
23
+ const ENV_EXAMPLE = /\.(?:example|sample|template)$/u;
24
+ const GIT = ".git";
25
+ // the folder of a project's installed packages, at the package's root.
26
+ const MODULES = "node_modules/";
27
+
28
+ // [function]
29
+
30
+ // the private files among the package's, each once: a `.env` file by its
31
+ // path, and a git repository by its folder.
32
+ const privates = function (names) {
33
+ const found = names.flatMap((name) => {
34
+ const parts = name.split("/");
35
+ const git = parts.slice(0, -1).indexOf(GIT);
36
+ if (git !== -1) {
37
+ return [`${parts.slice(0, git + 1).join("/")}/`];
38
+ }
39
+ const base = posixBasename(name);
40
+ return ENV.test(base) && !ENV_EXAMPLE.test(base) ? [name] : [];
41
+ });
42
+ return [...new Set(found)].sort();
43
+ };
44
+
45
+ // [function]
46
+
47
+ // the packed files' warnings, as a command's own (`onWarning`): what the
48
+ // package holds, never a guess at how it's served (OUTPUT.md, Codes).
49
+ const main = function (entries) {
50
+ const names = entries.map((entry) => entry.name);
51
+ return [
52
+ ...(names.some((name) => name.startsWith(MODULES))
53
+ ? [
54
+ {
55
+ code: WARNING.PACKAGE_FILE_DEV,
56
+ message: phrase.packageFileDev(MODULES),
57
+ },
58
+ ]
59
+ : []),
60
+ ...privates(names).map((file) => ({
61
+ code: WARNING.PACKAGE_FILE_PRIVATE,
62
+ message: phrase.packageFilePrivate(file),
63
+ })),
64
+ ];
65
+ };
66
+
67
+ // [export]
68
+
69
+ module.exports = main;
@@ -9,6 +9,7 @@ const readmeFile = require("../../parser/readme-file");
9
9
  const schema = require("../../util/schema");
10
10
  const sections = require("../../parser/sections");
11
11
  const unknown = require("./unknown");
12
+ const unsound = require("./unsound");
12
13
  const workflow = require("../../schema/workflow");
13
14
 
14
15
  // [use]
@@ -56,7 +57,7 @@ const main = function (conf) {
56
57
  return Promise.all([given(conf), structure(root), unknown(root)]).then(
57
58
  ([proj, readmeStructure, warnings]) => ({
58
59
  issues: validate(proj, readmeStructure, { root }),
59
- warnings: warnings.sort(schema.compare),
60
+ warnings: [...warnings, ...unsound(proj)].sort(schema.compare),
60
61
  }),
61
62
  );
62
63
  };
@@ -0,0 +1,166 @@
1
+ "use strict";
2
+
3
+ // [import]
4
+
5
+ const path = require("node:path");
6
+
7
+ // [import]
8
+
9
+ const semver = require("semver");
10
+
11
+ // [import]
12
+
13
+ const cfgart = require("../../config/art");
14
+ const cfgcode = require("../../config/code");
15
+ const cfgfile = require("../../config/file");
16
+ const output = require("../../util/output");
17
+ const phrase = require("../../phrase");
18
+
19
+ // [use]
20
+
21
+ const extname = path.extname;
22
+ const parse = path.parse;
23
+ const posixBasename = path.posix.basename;
24
+ const prerelease = semver.prerelease;
25
+ const validSemver = semver.valid;
26
+ const WARNING = cfgcode.WARNING;
27
+
28
+ // [declare]
29
+
30
+ const MEDIA = cfgfile.MEDIA;
31
+
32
+ // [declare]
33
+
34
+ // the art fields, such as `icon`, each of which may have a `_video`.
35
+ const ART = cfgart
36
+ .map((file) => parse(file).name)
37
+ .filter((name) => !name.endsWith("_video"));
38
+ // a screenshot's name, as the store reads it (ui2's `screenshots()`).
39
+ const SCREENSHOT = /^screenshot_(\d+)(?:_([^.]+))?\.[^.]+$/u;
40
+ // a name one case or separator away from a screenshot's: `Screenshot_01`,
41
+ // `screenshot-01`, `screenshot01`, with any qualifier after it.
42
+ const SCREENSHOT_NEAR = /^screenshot[-_ ]?(\d+)(?:[-_ ](.+))?$/iu;
43
+
44
+ // [function]
45
+
46
+ // a warning about a field, in an issue's shape, with its details.
47
+ const warning = function (code, field, [message, ...details]) {
48
+ return { ...output.warning(code, message, field), details };
49
+ };
50
+
51
+ // [function]
52
+
53
+ // the fields a release leaves out whose defaults it may not mean: a release
54
+ // version's `visibility` and `title`, and a public one's `inputs`. A
55
+ // prerelease changes no store page, so it's never warned about.
56
+ const unset = function (game) {
57
+ const version = game.version;
58
+ if (!validSemver(version) || prerelease(version)) {
59
+ return [];
60
+ }
61
+ const fields = [
62
+ "visibility",
63
+ "title",
64
+ ...(game.visibility === "public" ? ["inputs"] : []),
65
+ ];
66
+ return fields
67
+ .filter((field) => typeof game[field] === "undefined")
68
+ .map((field) =>
69
+ warning(WARNING.FIELD_UNSET, `game.${field}`, phrase.fieldUnset(field)),
70
+ );
71
+ };
72
+
73
+ // [function]
74
+
75
+ // each art video without its PNG, such as `icon_video` without `icon`.
76
+ const unpaired = function (game) {
77
+ return ART.filter((art) => game[`${art}_video`] && !game[art]).map((art) =>
78
+ warning(
79
+ WARNING.ART_VIDEO_UNPAIRED,
80
+ `game.${art}_video`,
81
+ phrase.artVideoUnpaired(art, `${MEDIA}/${art}.png`),
82
+ ),
83
+ );
84
+ };
85
+
86
+ // [function]
87
+
88
+ // the gallery's file names, or none when it isn't a list of paths, which
89
+ // its issues report.
90
+ const galleryNames = function (game) {
91
+ const gallery = game.gallery;
92
+ if (!Array.isArray(gallery)) {
93
+ return [];
94
+ }
95
+ return gallery
96
+ .filter((file) => typeof file === "string")
97
+ .map((file) => posixBasename(file));
98
+ };
99
+
100
+ // [function]
101
+
102
+ // screenshots with one number and qualifier, such as `screenshot_1.png` and
103
+ // `screenshot_01.png`, which the store can show only one of.
104
+ const repeated = function (names) {
105
+ const groups = {};
106
+ for (const name of names) {
107
+ const match = SCREENSHOT.exec(name);
108
+ if (match) {
109
+ const key = `${Number(match[1])}_${match[2] ?? ""}`;
110
+ groups[key] = [...(groups[key] ?? []), name];
111
+ }
112
+ }
113
+ return Object.values(groups)
114
+ .filter((group) => group.length > 1)
115
+ .map((group) =>
116
+ warning(
117
+ WARNING.SCREENSHOT_NUMBER_REPEATED,
118
+ "game.gallery",
119
+ phrase.screenshotNumberRepeated(group),
120
+ ),
121
+ );
122
+ };
123
+
124
+ // [function]
125
+
126
+ // a gallery file named almost as a screenshot, with the name it might mean.
127
+ const misnamed = function (names) {
128
+ return names.flatMap((name) => {
129
+ if (SCREENSHOT.test(name)) {
130
+ return [];
131
+ }
132
+ const ext = extname(name);
133
+ const match = SCREENSHOT_NEAR.exec(parse(name).name);
134
+ if (!match) {
135
+ return [];
136
+ }
137
+ const qualifier = match[2] ? `_${match[2]}` : "";
138
+ const near = `screenshot_${match[1]}${qualifier}${ext}`;
139
+ return [
140
+ warning(WARNING.SCREENSHOT_NAME_INVALID, "game.gallery", [
141
+ phrase.screenshotNameInvalid(name),
142
+ phrase.didYouMean(near),
143
+ ]),
144
+ ];
145
+ });
146
+ };
147
+
148
+ // [function]
149
+
150
+ // what the resolved manifest has that's sound by its rules but almost never
151
+ // meant, as warnings: facts about the project, never a guess at how the
152
+ // store will show it (OUTPUT.md, Codes).
153
+ const main = function (proj) {
154
+ const game = proj?.game ?? {};
155
+ const names = galleryNames(game);
156
+ return [
157
+ ...unset(game),
158
+ ...unpaired(game),
159
+ ...repeated(names),
160
+ ...misnamed(names),
161
+ ];
162
+ };
163
+
164
+ // [export]
165
+
166
+ module.exports = main;
@@ -103,10 +103,16 @@ const ISSUE = {
103
103
  // they follow the same rules, and share the Errors page, so no warning code
104
104
  // is also an error or issue code.
105
105
  const WARNING = {
106
+ ART_VIDEO_UNPAIRED: "art_video_unpaired",
106
107
  DEPLOY_DIR_NOT_BUILT: "deploy_dir_not_built",
107
108
  FIELD_UNKNOWN: "field_unknown",
109
+ FIELD_UNSET: "field_unset",
108
110
  GAME_HANDLE_LAPSED: "game_handle_lapsed",
109
111
  GAME_HANDLE_LIMIT_REACHED: "game_handle_limit_reached",
112
+ PACKAGE_FILE_DEV: "package_file_dev",
113
+ PACKAGE_FILE_PRIVATE: "package_file_private",
114
+ SCREENSHOT_NAME_INVALID: "screenshot_name_invalid",
115
+ SCREENSHOT_NUMBER_REPEATED: "screenshot_number_repeated",
110
116
  VERSION_UNCHECKED: "version_unchecked",
111
117
  };
112
118
  // every error code, for checking one exists.
package/src/phrase.js CHANGED
@@ -13,6 +13,15 @@ const TAIL_LINES = 20;
13
13
 
14
14
  // [function]
15
15
 
16
+ const artVideoUnpaired = function (art, png) {
17
+ return [
18
+ `Has no ${art} beside it.`,
19
+ `Add ${png} too, for places that don't show video.`,
20
+ ];
21
+ };
22
+
23
+ // [function]
24
+
16
25
  // a message's reason, then each block of details after a blank line
17
26
  // (OUTPUT.md). A block of several lines is an array.
18
27
  const blocks = function (reason, ...details) {
@@ -23,6 +32,54 @@ const blocks = function (reason, ...details) {
23
32
 
24
33
  // [function]
25
34
 
35
+ // a release's field left out, keyed by field: what leaving it out means,
36
+ // then what to set.
37
+ const fieldUnset = function (field) {
38
+ return {
39
+ inputs: [
40
+ "Not set, so this release lists no inputs.",
41
+ "Set inputs to how the game is played.",
42
+ ],
43
+ title: [
44
+ "Not set, so this release has no title.",
45
+ "Start the README with a # title, or set title.",
46
+ ],
47
+ visibility: [
48
+ "Not set, so this release is unlisted: it plays by link only.",
49
+ 'Set visibility = "public" to list it, or "unlisted" to keep it so.',
50
+ ],
51
+ }[field];
52
+ };
53
+
54
+ // [function]
55
+
56
+ const packageFileDev = function (dir) {
57
+ return `Packing ${dir}, a project's installed packages.\nSet deploy_dir to the folder your build writes.`;
58
+ };
59
+
60
+ // [function]
61
+
62
+ const packageFilePrivate = function (file) {
63
+ return `Packing ${file}, which anyone can download.\nLeave it out of the package unless it's meant to be public.`;
64
+ };
65
+
66
+ // [function]
67
+
68
+ const screenshotNameInvalid = function (name) {
69
+ return `Not a screenshot's name: ${name}.`;
70
+ };
71
+
72
+ // [function]
73
+
74
+ const screenshotNumberRepeated = function (names) {
75
+ return [
76
+ `Screenshots share a number: ${names.join(", ")}.`,
77
+ "Give each screenshot its own number.",
78
+ ];
79
+ };
80
+
81
+ // [function]
82
+
26
83
  // the last lines a tool printed, stdout's then stderr's, since some, such as
27
84
  // tsc, print their errors on stdout, or "" when it printed nothing. Lines
28
85
  // left out are marked with "…".
@@ -1077,6 +1134,7 @@ const videoWidthMin = function (width) {
1077
1134
 
1078
1135
  module.exports = {
1079
1136
  aborted,
1137
+ artVideoUnpaired,
1080
1138
  buildFailed,
1081
1139
  buildOutputMissing,
1082
1140
  buildPublishing,
@@ -1097,6 +1155,7 @@ module.exports = {
1097
1155
  docMediaType,
1098
1156
  docTagUnallowed,
1099
1157
  errorCode,
1158
+ fieldUnset,
1100
1159
  gameHandleChanged,
1101
1160
  gameHandleLapsed,
1102
1161
  gameHandleLimit,
@@ -1142,6 +1201,8 @@ module.exports = {
1142
1201
  nothingToDo,
1143
1202
  openToContinue,
1144
1203
  opened,
1204
+ packageFileDev,
1205
+ packageFilePrivate,
1145
1206
  packageFileTooLarge,
1146
1207
  packageIndexMissing,
1147
1208
  packagePathReserved,
@@ -1163,6 +1224,8 @@ module.exports = {
1163
1224
  remoteKept,
1164
1225
  remoteUnreachable,
1165
1226
  repoNotConnected,
1227
+ screenshotNameInvalid,
1228
+ screenshotNumberRepeated,
1166
1229
  serveAlone,
1167
1230
  serveNext,
1168
1231
  serveNextUrl,