drop2run 0.2.1 → 0.4.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/README.md CHANGED
@@ -16,6 +16,7 @@ drop2run login [--device] Sign in and store a token
16
16
  drop2run logout Remove the stored token
17
17
  drop2run init [dir] [--site <subdomain>] Tie this folder to a site
18
18
  drop2run deploy [dir] [--site <subdomain>] Publish a folder
19
+ drop2run init|deploy --subdomain <name> Create a new site under a name you pick
19
20
  drop2run ls List your sites
20
21
  drop2run open [site] Open a site in a browser
21
22
  drop2run rollback <deployId> [--site X] Put an earlier version back live
@@ -30,14 +31,26 @@ drop2run --version Print the version
30
31
 
31
32
  ## Which site a command acts on
32
33
 
33
- `--site`, then `drop2run.json`, then a new one. `init` writes that file, so this is the whole of a
34
- normal project:
34
+ `--subdomain` creates one under the name you give; otherwise `--site`, then `drop2run.json`, then a new
35
+ site with a generated name. `init` writes that file, so this is the whole of a normal project:
35
36
 
36
37
  ```bash
37
38
  drop2run init dist # creates a site, writes drop2run.json
38
39
  drop2run deploy # publishes dist to it, no arguments
39
40
  ```
40
41
 
42
+ ```bash
43
+ drop2run init dist --subdomain my-docs # same, but you choose the address
44
+ ```
45
+
46
+ Lowercase letters, digits and hyphens, not starting or ending with one. A name that is taken, reserved,
47
+ or past the number of self-picked names your plan allows is refused with the reason. It cannot be changed
48
+ later — a site is reached by the address it was created with — so a wrong name means deleting the site,
49
+ which is not undoable.
50
+
51
+ `--site` never creates anything: it finds a site you already have, and fails when nothing matches. That
52
+ is what keeps a subdomain typed one letter wrong from quietly becoming a second site.
53
+
41
54
  `deploy` never creates a second site while a `drop2run.json` sits next to it — that would leave the real
42
55
  site untouched and you looking at a URL you did not expect.
43
56
 
package/dist/index.js CHANGED
@@ -55,11 +55,17 @@ async function listSites(credentials) {
55
55
  const body = await call(credentials, "sites");
56
56
  return body.sites;
57
57
  }
58
- async function createSite(credentials) {
58
+ async function createSite(credentials, subdomain) {
59
59
  const created = await call(
60
60
  credentials,
61
61
  "sites",
62
- { method: "POST", body: JSON.stringify({}) }
62
+ {
63
+ method: "POST",
64
+ // An absent field rather than a null one, because the API reads null, empty and missing the
65
+ // same way and a body carrying only what was asked for is the one that reads correctly in a
66
+ // log.
67
+ body: JSON.stringify(subdomain === void 0 ? {} : { subdomain })
68
+ }
63
69
  );
64
70
  return { ...created, name: null };
65
71
  }
@@ -369,6 +375,7 @@ const ClientErrorCode = {
369
375
  /** An upload kept failing after every retry. */
370
376
  UploadFailed: "upload_failed"
371
377
  };
378
+ const ACCOUNT_HEADER = "X-Drop2Run-Account";
372
379
  const CLAIM_TOKEN_HEADER = "X-Claim-Token";
373
380
  async function prepareDeploy(siteId, files, options, signal) {
374
381
  const manifest = files.map((file) => ({
@@ -405,6 +412,7 @@ async function request(path, body, options, signal) {
405
412
  if (body !== void 0) headers["Content-Type"] = "application/json";
406
413
  if (options.claimToken) headers[CLAIM_TOKEN_HEADER] = options.claimToken;
407
414
  if (options.token) headers.Authorization = `Bearer ${options.token}`;
415
+ if (options.accountId) headers[ACCOUNT_HEADER] = options.accountId;
408
416
  const response = await doFetch(`${base}/${path}`, {
409
417
  method: "POST",
410
418
  headers,
@@ -517,7 +525,7 @@ function renameOfLoneHtmlPage(paths) {
517
525
  const lower = only.toLowerCase();
518
526
  return lower.endsWith(".html") || lower.endsWith(".htm") ? only : null;
519
527
  }
520
- function formatBytes(bytes) {
528
+ function formatBytes$1(bytes) {
521
529
  if (bytes < 1024) return `${bytes} B`;
522
530
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
523
531
  if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
@@ -548,7 +556,7 @@ function checkLimits(files, limits) {
548
556
  if (oversized) {
549
557
  throw new DeployError(
550
558
  ClientErrorCode.FileTooLarge,
551
- `${oversized.path} is ${formatBytes(oversized.bytes.length)}, and this plan allows ${formatBytes(limits.maxFileBytes)} per file.`,
559
+ `${oversized.path} is ${formatBytes$1(oversized.bytes.length)}, and this plan allows ${formatBytes$1(limits.maxFileBytes)} per file.`,
552
560
  {
553
561
  path: oversized.path,
554
562
  limit: limits.maxFileBytes,
@@ -560,7 +568,7 @@ function checkLimits(files, limits) {
560
568
  if (limits.maxSiteBytes !== null && total > limits.maxSiteBytes) {
561
569
  throw new DeployError(
562
570
  ClientErrorCode.TooLarge,
563
- `That drop is ${formatBytes(total)}, and this plan allows ${formatBytes(limits.maxSiteBytes)}.`,
571
+ `That drop is ${formatBytes$1(total)}, and this plan allows ${formatBytes$1(limits.maxSiteBytes)}.`,
564
572
  { limit: limits.maxSiteBytes, actual: total }
565
573
  );
566
574
  }
@@ -698,7 +706,7 @@ async function uploadAll(targets, uploadUrl, files, onProgress, signal, deps = {
698
706
  });
699
707
  done += 1;
700
708
  uploadedBytes += file.bytes.length;
701
- onProgress?.(done, targets.length, uploadedBytes);
709
+ onProgress?.(done, targets.length, uploadedBytes, file.path);
702
710
  }
703
711
  };
704
712
  const workers = Array.from(
@@ -793,12 +801,13 @@ async function deploy(source, siteId, onProgress, signal, options = {}) {
793
801
  prepared.upload,
794
802
  prepared.uploadUrl,
795
803
  hashed,
796
- (done, _total, bytes) => onProgress({
804
+ (done, _total, bytes, path) => onProgress({
797
805
  type: "uploading",
798
806
  done: prepared.reused + done,
799
807
  total: prepared.total,
800
808
  bytes,
801
- reused: prepared.reused
809
+ reused: prepared.reused,
810
+ path
802
811
  }),
803
812
  signal,
804
813
  options.upload
@@ -864,7 +873,15 @@ function escapesRoot(root, candidate) {
864
873
  const inside = relative(root, candidate);
865
874
  return inside === "" || inside.startsWith("..");
866
875
  }
867
- async function resolveSite(credentials, site) {
876
+ async function resolveSite(credentials, site, subdomain) {
877
+ if (subdomain !== void 0) {
878
+ if (site !== void 0) {
879
+ throw new Error(
880
+ "Give either an existing site or a subdomain for a new one, not both: publishing over a site and creating one are different requests."
881
+ );
882
+ }
883
+ return await createSite(credentials, subdomain);
884
+ }
868
885
  if (site === void 0) return await createSite(credentials);
869
886
  const wanted = site.trim().toLowerCase();
870
887
  const found = (await listSites(credentials)).find(
@@ -872,13 +889,13 @@ async function resolveSite(credentials, site) {
872
889
  );
873
890
  if (found === void 0) {
874
891
  throw new Error(
875
- `No site of yours is called "${site}". Leave the site out to publish to a new one, or use list_sites to see what exists.`
892
+ `No site of yours is called "${site}". Leave the site out to publish to a new one, or list your sites to see what exists.`
876
893
  );
877
894
  }
878
895
  return found;
879
896
  }
880
- async function publish(credentials, source, site) {
881
- const target = await resolveSite(credentials, site);
897
+ async function publish(credentials, source, site, onProgress, subdomain) {
898
+ const target = await resolveSite(credentials, site, subdomain);
882
899
  let files = 0;
883
900
  let url = target.url;
884
901
  let unchanged = false;
@@ -895,6 +912,7 @@ async function publish(credentials, source, site) {
895
912
  failure2 = event.message;
896
913
  failureDetail = event.detail;
897
914
  }
915
+ onProgress?.(event);
898
916
  };
899
917
  await deploy(source, target.siteId, record, void 0, {
900
918
  baseUrl: credentials.apiBaseUrl,
@@ -908,8 +926,75 @@ async function publish(credentials, source, site) {
908
926
  });
909
927
  return { url, siteId: target.siteId, subdomain: target.subdomain, files, unchanged };
910
928
  }
911
- function publishDirectory(credentials, directory, site) {
912
- return publish(credentials, directorySource(directory), site);
929
+ function publishDirectory(credentials, directory, site, onProgress, subdomain) {
930
+ return publish(credentials, directorySource(directory), site, onProgress, subdomain);
931
+ }
932
+ const silentProgress = Object.assign(() => {
933
+ }, { done: () => {
934
+ } });
935
+ const STEP = 1024;
936
+ const UNITS = ["B", "KB", "MB", "GB"];
937
+ function formatBytes(bytes) {
938
+ let value = bytes;
939
+ let unit = 0;
940
+ while (value >= STEP && unit < UNITS.length - 1) {
941
+ value /= STEP;
942
+ unit += 1;
943
+ }
944
+ return `${unit === 0 ? value : value.toFixed(1)} ${UNITS[unit]}`;
945
+ }
946
+ function shortenPath(path, width) {
947
+ if (path.length <= width) return path;
948
+ return `…${path.slice(path.length - width + 1)}`;
949
+ }
950
+ function describe(event, width = 80) {
951
+ switch (event.type) {
952
+ case "collecting":
953
+ return `Collected ${event.files.toLocaleString()} ${event.files === 1 ? "file" : "files"}`;
954
+ case "hashing":
955
+ return `Hashing ${event.done.toLocaleString()}/${event.total.toLocaleString()}`;
956
+ case "preparing":
957
+ return "Asking the server which files it already has";
958
+ case "uploading": {
959
+ const counted = `Uploading ${event.done.toLocaleString()}/${event.total.toLocaleString()}`;
960
+ const reused = event.reused === 0 ? "" : ` (${event.reused.toLocaleString()} already there)`;
961
+ const head = `${counted}${reused} · ${formatBytes(event.bytes)}`;
962
+ const room = width - head.length - 3;
963
+ return event.path === void 0 || room < 12 ? head : `${head} · ${shortenPath(event.path, room)}`;
964
+ }
965
+ case "completing":
966
+ return "Publishing";
967
+ // Both end the deploy, and the command prints the URL from the result it returns. Saying it here
968
+ // as well would print it twice, once on stderr and once on stdout.
969
+ case "done":
970
+ case "unchanged":
971
+ case "error":
972
+ return null;
973
+ }
974
+ }
975
+ function progressWriter(write, isTerminal, width = 80) {
976
+ let painted = 0;
977
+ let lastStage = null;
978
+ const writer = ((event) => {
979
+ const line = describe(event, width - 1);
980
+ if (line === null) return;
981
+ if (!isTerminal) {
982
+ if (event.type === lastStage) return;
983
+ lastStage = event.type;
984
+ write(`${line}
985
+ `);
986
+ return;
987
+ }
988
+ lastStage = event.type;
989
+ write(`\r${line}${" ".repeat(Math.max(0, painted - line.length))}`);
990
+ painted = line.length;
991
+ });
992
+ writer.done = () => {
993
+ if (!isTerminal || painted === 0) return;
994
+ write(`\r${" ".repeat(painted)}\r`);
995
+ painted = 0;
996
+ };
997
+ return writer;
913
998
  }
914
999
  function failure(text) {
915
1000
  return { text, json: { error: text }, code: 1 };
@@ -1053,14 +1138,20 @@ async function list() {
1053
1138
  code: 0
1054
1139
  };
1055
1140
  }
1056
- async function deployCommand(directory, site) {
1141
+ async function deployCommand(directory, site, report = silentProgress, subdomain) {
1057
1142
  const found = credentialsOr();
1058
1143
  if ("result" in found) return found.result;
1059
1144
  const project = readProject();
1060
- const target = site ?? project?.siteId;
1061
1145
  const folder = directory ?? project?.dir ?? ".";
1146
+ const target = subdomain === void 0 ? site ?? project?.siteId : void 0;
1062
1147
  try {
1063
- const result = await publishDirectory(found.credentials, resolve(folder), target);
1148
+ const result = await publishDirectory(
1149
+ found.credentials,
1150
+ resolve(folder),
1151
+ target,
1152
+ report,
1153
+ subdomain
1154
+ );
1064
1155
  const what = result.unchanged ? "Already up to date — nothing needed publishing." : `Published ${result.files.toLocaleString()} ${result.files === 1 ? "file" : "files"}.`;
1065
1156
  return {
1066
1157
  text: `${what}
@@ -1070,9 +1161,11 @@ ${result.url}`,
1070
1161
  };
1071
1162
  } catch (error) {
1072
1163
  return failureFrom(error);
1164
+ } finally {
1165
+ report.done();
1073
1166
  }
1074
1167
  }
1075
- async function init(directory, site) {
1168
+ async function init(directory, site, subdomain) {
1076
1169
  const found = credentialsOr();
1077
1170
  if ("result" in found) return found.result;
1078
1171
  if (readProject() !== null) {
@@ -1081,7 +1174,7 @@ async function init(directory, site) {
1081
1174
  );
1082
1175
  }
1083
1176
  try {
1084
- const target = site === void 0 ? await createSite(found.credentials) : await findSite(found.credentials, site);
1177
+ const target = site === void 0 ? await createSite(found.credentials, subdomain) : await findSite(found.credentials, site);
1085
1178
  const project = {
1086
1179
  siteId: target.siteId,
1087
1180
  subdomain: target.subdomain,
@@ -1219,6 +1312,7 @@ const HELP = `drop2run — publish a static site from the command line
1219
1312
  drop2run logout Remove the stored token
1220
1313
  drop2run init [dir] [--site <subdomain>] Tie this folder to a site (writes drop2run.json)
1221
1314
  drop2run deploy [dir] [--site <subdomain>] Publish a folder (default: . or drop2run.json)
1315
+ drop2run init|deploy --subdomain <name> Create a new site under a name you pick
1222
1316
  drop2run ls List your sites
1223
1317
  drop2run open [site] Open a site in a browser
1224
1318
  drop2run rollback <deployId> [--site X] Put an earlier version back live
@@ -1229,14 +1323,19 @@ const HELP = `drop2run — publish a static site from the command line
1229
1323
  drop2run --version Print the version
1230
1324
 
1231
1325
  Flags
1232
- --json Print machine-readable output instead of text
1233
- --site X Act on an existing site rather than the one in drop2run.json
1234
- --device Sign in by approving a code on another machine
1235
- --yes Confirm a deletion, which cannot be undone
1326
+ --json Print machine-readable output instead of text
1327
+ --site X Act on an existing site rather than the one in drop2run.json
1328
+ --subdomain X Create a new site under this name (init and deploy only)
1329
+ --device Sign in by approving a code on another machine
1330
+ --yes Confirm a deletion, which cannot be undone
1236
1331
 
1237
1332
  Where a site comes from
1238
- --site, then drop2run.json, then a new one. \`init\` writes that file, so
1239
- \`deploy\` in the same folder needs no arguments at all.
1333
+ --subdomain creates one under the name you give. Otherwise: --site, then
1334
+ drop2run.json, then a new site with a generated name. \`init\` writes that
1335
+ file, so \`deploy\` in the same folder needs no arguments at all.
1336
+
1337
+ --site only ever finds a site you already have; it never creates one, so a
1338
+ subdomain typed wrong fails instead of quietly becoming a second site.
1240
1339
 
1241
1340
  Signing in
1242
1341
  \`login\` opens a browser and listens on 127.0.0.1, so it needs both on this
@@ -1253,12 +1352,25 @@ function flagValue(args, flag) {
1253
1352
  const value = args[at + 1];
1254
1353
  return value === void 0 || value.startsWith("-") ? void 0 : value;
1255
1354
  }
1355
+ function subdomainFlagProblem(argv, command, site, subdomain) {
1356
+ if (!argv.includes("--subdomain")) return void 0;
1357
+ if (subdomain === void 0) {
1358
+ return "`--subdomain` needs a name after it, for example `--subdomain my-docs`.";
1359
+ }
1360
+ if (site !== void 0) {
1361
+ return "`--site` and `--subdomain` cannot both be given: `--site` publishes to a site you already have, and `--subdomain` creates a new one. Drop whichever is not what you meant.";
1362
+ }
1363
+ if (command !== "init" && command !== "deploy") {
1364
+ return `\`--subdomain\` only applies to \`init\` and \`deploy\`, which can create a site. Use \`--site\` to tell \`${command}\` which of your sites to act on.`;
1365
+ }
1366
+ return void 0;
1367
+ }
1256
1368
  async function run(argv) {
1257
1369
  const json = argv.includes("--json");
1258
1370
  const positional = argv.filter((argument) => !argument.startsWith("-"));
1259
1371
  const [command, ...rest] = positional;
1260
1372
  if (argv.includes("--version")) {
1261
- const { version } = await import("./package-D-UHpX_a.js").then(
1373
+ const { version } = await import("./package-Cz51eZ2C.js").then(
1262
1374
  (module) => module.default
1263
1375
  );
1264
1376
  return { text: version, json: { version }, code: 0 };
@@ -1267,17 +1379,24 @@ async function run(argv) {
1267
1379
  if (askedForHelp) return { text: HELP, json: { help: HELP }, code: 0 };
1268
1380
  if (command === void 0) return { text: HELP, json: { help: HELP }, code: 1 };
1269
1381
  const site = flagValue(argv, "--site");
1382
+ const subdomain = flagValue(argv, "--subdomain");
1383
+ const refusal = subdomainFlagProblem(argv, command, site, subdomain);
1384
+ if (refusal !== void 0) {
1385
+ const failed = { text: refusal, json: { error: refusal }, code: 1 };
1386
+ return json ? { ...failed, text: JSON.stringify(failed.json, null, 2) } : failed;
1387
+ }
1270
1388
  const result = await dispatch(
1271
1389
  command,
1272
1390
  rest,
1273
1391
  site,
1274
1392
  json,
1275
1393
  argv.includes("--device"),
1276
- argv.includes("--yes")
1394
+ argv.includes("--yes"),
1395
+ subdomain
1277
1396
  );
1278
1397
  return json ? { ...result, text: JSON.stringify(result.json, null, 2) } : result;
1279
1398
  }
1280
- async function dispatch(command, rest, site, json, device, confirmed) {
1399
+ async function dispatch(command, rest, site, json, device, confirmed, subdomain) {
1281
1400
  switch (command) {
1282
1401
  case "login": {
1283
1402
  const print = json ? () => {
@@ -1287,9 +1406,15 @@ async function dispatch(command, rest, site, json, device, confirmed) {
1287
1406
  case "logout":
1288
1407
  return logout();
1289
1408
  case "init":
1290
- return await init(rest[0] ?? ".", site);
1291
- case "deploy":
1292
- return await deployCommand(rest[0], site);
1409
+ return await init(rest[0] ?? ".", site, subdomain);
1410
+ case "deploy": {
1411
+ const report = json ? silentProgress : progressWriter(
1412
+ (text) => process.stderr.write(text),
1413
+ process.stderr.isTTY === true,
1414
+ process.stderr.columns ?? 80
1415
+ );
1416
+ return await deployCommand(rest[0], site, report, subdomain);
1417
+ }
1293
1418
  case "ls":
1294
1419
  case "list":
1295
1420
  return await list();
@@ -1,5 +1,5 @@
1
1
  const name = "drop2run";
2
- const version = "0.2.1";
2
+ const version = "0.4.0";
3
3
  const description = "Publish a static site to Drop2Run from the command line.";
4
4
  const license = "MIT";
5
5
  const type = "module";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drop2run",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "Publish a static site to Drop2Run from the command line.",
5
5
  "license": "MIT",
6
6
  "type": "module",