@swipt/cli 0.1.0-nightly.31 → 0.1.0-nightly.32

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.
Files changed (2) hide show
  1. package/cli.mjs +94 -66
  2. package/package.json +2 -2
package/cli.mjs CHANGED
@@ -265,71 +265,104 @@ async function buildCommand(root) {
265
265
 
266
266
  const manifestEntry = /^(?:[^/]+\/)?manifest\.json$/;
267
267
 
268
- function sourceCommand(files, base, output, name, localizedNames, icon, image) {
269
- const names = {};
270
- for (const entry of localizedNames ?? []) {
271
- const [language, ...rest] = entry.split("=");
272
- if (!language || rest.length === 0 || !rest.join("=")) {
273
- throw new UsageError("--localized-name takes <language>=<name>, such as zh-Hans=示例");
268
+ function loadSource(root) {
269
+ const manifest = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
270
+ const config = manifest.source ?? {};
271
+ for (const field of ["name", "image", "url"]) {
272
+ if (config[field] !== undefined && typeof config[field] !== "string") {
273
+ throw new Error(`"source.${field}" in package.json must be a string`);
274
274
  }
275
- names[language] = rest.join("=");
276
275
  }
277
- let shownIcon;
278
- if (icon !== undefined) {
279
- const [color, ...symbol] = icon.split("/");
280
- if (!color || !symbol.join("/")) {
281
- throw new UsageError("--icon takes <color>/<symbol>, such as blue/sparkles");
282
- }
283
- shownIcon = { color, symbol: symbol.join("/") };
276
+ const localizedNames = config.localizedNames ?? {};
277
+ if (localizedNames === null || typeof localizedNames !== "object" || !Object.values(localizedNames).every(name => typeof name === "string")) {
278
+ throw new Error('"source.localizedNames" in package.json must map languages to names');
279
+ }
280
+ if (config.icon !== undefined && (typeof config.icon?.color !== "string" || typeof config.icon?.symbol !== "string")) {
281
+ throw new Error('"source.icon" in package.json must have a color and a symbol');
284
282
  }
285
283
  let prefix;
286
- if (base !== undefined) {
284
+ if (config.url !== undefined) {
287
285
  try {
288
- prefix = new URL(base.endsWith("/") ? base : `${base}/`);
286
+ prefix = new URL(config.url.endsWith("/") ? config.url : `${config.url}/`);
289
287
  } catch {
290
- throw new UsageError(`--url ${JSON.stringify(base)} is not an absolute URL`);
288
+ throw new Error('"source.url" in package.json is not an absolute URL');
291
289
  }
292
290
  }
293
- const swiplets = new Map();
294
- for (const file of files) {
295
- const entry = execFileSync("unzip", ["-Z1", file], { encoding: "utf8" }).split("\n").filter(name => manifestEntry.test(name)).sort((a, b) => a.length - b.length)[0];
296
- if (entry === undefined) {
297
- throw new Error(`${file} has no manifest.json`);
298
- }
299
- const manifest = JSON.parse(execFileSync("unzip", ["-p", file, entry], { encoding: "utf8" }));
300
- if (typeof manifest.id !== "string" || !idPattern.test(manifest.id)) {
301
- throw new Error(`${file} has no valid swiplet id in its manifest`);
291
+ const workspaces = Array.isArray(manifest.workspaces) ? manifest.workspaces : manifest.workspaces?.packages ?? [];
292
+ return { root, config, localizedNames, prefix, workspaces };
293
+ }
294
+
295
+ function workspaceSwiplets(source) {
296
+ const files = [];
297
+ for (const pattern of source.workspaces) {
298
+ for (const directory of fs.globSync(pattern, { cwd: source.root }).sort()) {
299
+ const project = path.join(source.root, directory, "package.json");
300
+ if (!fs.existsSync(project)) {
301
+ continue;
302
+ }
303
+ const id = JSON.parse(fs.readFileSync(project, "utf8")).swiplet?.id;
304
+ if (typeof id !== "string") {
305
+ continue;
306
+ }
307
+ const built = path.join(source.root, directory, "dist", `${id}.swiplet`);
308
+ if (!fs.existsSync(built)) {
309
+ throw new Error(`${path.relative(source.root, built)} is missing; build ${directory} first`);
310
+ }
311
+ files.push(built);
302
312
  }
303
- if (swiplets.has(manifest.id)) {
304
- throw new Error(`${manifest.id} is both ${swiplets.get(manifest.id).file} and ${file}`);
313
+ }
314
+ return files;
315
+ }
316
+
317
+ function readSwiplet(file) {
318
+ const entry = execFileSync("unzip", ["-Z1", file], { encoding: "utf8" }).split("\n").filter(name => manifestEntry.test(name)).sort((a, b) => a.length - b.length)[0];
319
+ if (entry === undefined) {
320
+ throw new Error(`${file} has no manifest.json`);
321
+ }
322
+ const manifest = JSON.parse(execFileSync("unzip", ["-p", file, entry], { encoding: "utf8" }));
323
+ if (typeof manifest.id !== "string" || !idPattern.test(manifest.id)) {
324
+ throw new Error(`${file} has no valid swiplet id in its manifest`);
325
+ }
326
+ return { file, manifest, data: fs.readFileSync(file) };
327
+ }
328
+
329
+ function sourceCommand(root, given) {
330
+ const source = loadSource(root);
331
+ const files = given.length > 0 ? given.map(file => path.resolve(file)) : workspaceSwiplets(source);
332
+ if (files.length === 0) {
333
+ throw new Error("package.json lists no workspaces with swiplets; name the swiplets to list");
334
+ }
335
+ const swiplets = new Map();
336
+ for (const read of files.map(readSwiplet)) {
337
+ const { id } = read.manifest;
338
+ if (swiplets.has(id)) {
339
+ throw new Error(`${id} is both ${swiplets.get(id).file} and ${read.file}`);
305
340
  }
306
- const data = fs.readFileSync(file);
307
- swiplets.set(manifest.id, {
308
- file,
309
- entry: {
310
- ...manifest,
311
- url: prefix ? new URL(path.basename(file), prefix).href : path.basename(file),
312
- size: data.length,
313
- sha256: createHash("sha256").update(data).digest("hex"),
314
- },
315
- });
341
+ swiplets.set(id, read);
316
342
  }
317
- const listed = `${JSON.stringify(
318
- {
319
- ...(name ? { name } : {}),
320
- ...(Object.keys(names).length > 0 ? { localizedNames: names } : {}),
321
- ...(shownIcon ? { icon: shownIcon } : {}),
322
- ...(image ? { image: prefix ? new URL(image, prefix).href : image } : {}),
323
- swiplets: [...swiplets.keys()].sort().map(id => swiplets.get(id).entry),
324
- },
325
- null,
326
- 2,
327
- )}\n`;
328
- if (output) {
329
- fs.writeFileSync(output, listed);
330
- } else {
331
- process.stdout.write(listed);
343
+ const image = source.config.image === undefined ? undefined : { name: path.basename(source.config.image), data: fs.readFileSync(path.resolve(root, source.config.image)) };
344
+ const located = name => (source.prefix ? new URL(name, source.prefix).href : name);
345
+ const dist = path.join(root, "dist");
346
+ fs.rmSync(dist, { recursive: true, force: true });
347
+ fs.mkdirSync(dist, { recursive: true });
348
+ const entries = [...swiplets.keys()].sort().map(id => {
349
+ const { manifest, data } = swiplets.get(id);
350
+ const name = `${id}.swiplet`;
351
+ fs.writeFileSync(path.join(dist, name), data);
352
+ return { ...manifest, url: located(name), size: data.length, sha256: createHash("sha256").update(data).digest("hex") };
353
+ });
354
+ if (image) {
355
+ fs.writeFileSync(path.join(dist, image.name), image.data);
332
356
  }
357
+ const index = {
358
+ ...(source.config.name ? { name: source.config.name } : {}),
359
+ ...(Object.keys(source.localizedNames).length > 0 ? { localizedNames: source.localizedNames } : {}),
360
+ ...(source.config.icon ? { icon: { color: source.config.icon.color, symbol: source.config.icon.symbol } } : {}),
361
+ ...(image ? { image: located(image.name) } : {}),
362
+ swiplets: entries,
363
+ };
364
+ fs.writeFileSync(path.join(dist, "swiplets.json"), `${JSON.stringify(index, null, 2)}\n`);
365
+ console.log(path.relative(process.cwd(), path.join(dist, "swiplets.json")));
333
366
  }
334
367
 
335
368
  const idOf = name => name.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[._-]+/, "").slice(0, 128).replace(/[._-]+$/, "") || "swiplet";
@@ -426,26 +459,23 @@ function createCommand(directory) {
426
459
 
427
460
  const usage = `usage: swiplet create <directory>
428
461
  swiplet build
429
- swiplet source [--name <name>] [--localized-name <language>=<name>]... [--icon <color>/<symbol>] [--image <file>]
430
- [--url <address>] [--output <file>] <swiplet>...
462
+ swiplet source [<swiplet>...]
431
463
 
432
464
  create make a swiplet project in a new or empty directory
433
465
  build bundle the project in this directory into dist/<swiplet.id>.swiplet
434
- source list built swiplets in a source; files are expected next to it unless --url says where they are
466
+ source gather the built swiplets of this directory's workspaces, or the given ones, into dist/
467
+ with a swiplets.json described by "source" in package.json
435
468
 
436
469
  Type-check a project with its "check" script.`;
437
470
 
438
- const commands = { create: 1, build: 0, source: -1 };
471
+ const commands = { create: 1, build: 0, source: null };
439
472
 
440
473
  class UsageError extends Error {}
441
474
 
442
475
  try {
443
476
  let parsed;
444
477
  try {
445
- parsed = parseArgs({ allowPositionals: true, options: {
446
- help: { type: "boolean", short: "h" }, name: { type: "string" }, "localized-name": { type: "string", multiple: true }, icon: { type: "string" },
447
- image: { type: "string" }, url: { type: "string" }, output: { type: "string" },
448
- } });
478
+ parsed = parseArgs({ allowPositionals: true, options: { help: { type: "boolean", short: "h" } } });
449
479
  } catch (error) {
450
480
  throw new UsageError(error.message);
451
481
  }
@@ -456,14 +486,12 @@ try {
456
486
  throw new UsageError("a command is missing");
457
487
  } else if (!Object.hasOwn(commands, command)) {
458
488
  throw new UsageError(`unknown command ${JSON.stringify(command)}`);
459
- } else if (commands[command] < 0 ? rest.length === 0 : rest.length !== commands[command]) {
460
- throw new UsageError(
461
- commands[command] === 0 ? `${command} takes no arguments` : commands[command] < 0 ? `${command} takes one or more swiplets` : `${command} takes one directory`,
462
- );
489
+ } else if (commands[command] !== null && rest.length !== commands[command]) {
490
+ throw new UsageError(commands[command] === 0 ? `${command} takes no arguments` : `${command} takes one directory`);
463
491
  } else if (command === "create") {
464
492
  createCommand(rest[0]);
465
493
  } else if (command === "source") {
466
- sourceCommand(rest, parsed.values.url, parsed.values.output, parsed.values.name, parsed.values["localized-name"], parsed.values.icon, parsed.values.image);
494
+ sourceCommand(process.cwd(), rest);
467
495
  } else {
468
496
  await buildCommand(process.cwd());
469
497
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swipt/cli",
3
- "version": "0.1.0-nightly.31",
3
+ "version": "0.1.0-nightly.32",
4
4
  "license": "GPL-3.0-or-later",
5
5
  "repository": {
6
6
  "type": "git",
@@ -21,6 +21,6 @@
21
21
  "esbuild": "^0.28.2"
22
22
  },
23
23
  "peerDependencies": {
24
- "@swipt/api": "0.1.0-nightly.31"
24
+ "@swipt/api": "0.1.0-nightly.32"
25
25
  }
26
26
  }