@swipt/cli 0.1.0-nightly.43 → 0.1.0-nightly.45

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/project.mjs CHANGED
@@ -12,7 +12,9 @@ import { build } from "esbuild";
12
12
 
13
13
  import { loadEnvironment, metro } from "./metro.mjs";
14
14
 
15
- const { version } = JSON.parse(fs.readFileSync(new URL("package.json", import.meta.url), "utf8"));
15
+ const { version } = JSON.parse(
16
+ fs.readFileSync(new URL("package.json", import.meta.url), "utf8"),
17
+ );
16
18
  const require = createRequire(import.meta.url);
17
19
  const { peerDependencies } = require("@swipt/api/package.json");
18
20
 
@@ -49,41 +51,92 @@ const extensionModules = [
49
51
 
50
52
  const idPattern = /^[a-z0-9][a-z0-9._-]{0,127}$/;
51
53
 
52
- const storedSuffixes = [".webp", ".jxl", ".avif", ".heic", ".heif", ".jpg", ".jpeg", ".png", ".gif", ".mp4", ".mov", ".m4a", ".mp3", ".aac", ".zip", ".gz", ".br", ".zst", ".xz"].join(":");
54
+ const storedSuffixes = [
55
+ ".webp",
56
+ ".jxl",
57
+ ".avif",
58
+ ".heic",
59
+ ".heif",
60
+ ".jpg",
61
+ ".jpeg",
62
+ ".png",
63
+ ".gif",
64
+ ".mp4",
65
+ ".mov",
66
+ ".m4a",
67
+ ".mp3",
68
+ ".aac",
69
+ ".zip",
70
+ ".gz",
71
+ ".br",
72
+ ".zst",
73
+ ".xz",
74
+ ].join(":");
53
75
 
54
76
  export function loadProject(root) {
55
- const manifest = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
77
+ const manifest = JSON.parse(
78
+ fs.readFileSync(path.join(root, "package.json"), "utf8"),
79
+ );
56
80
  const config = manifest.swiplet ?? {};
57
81
  if (typeof config.id !== "string" || !idPattern.test(config.id)) {
58
- throw new Error('"swiplet.id" in package.json must be 1 to 128 lowercase letters, digits, dots, underscores and hyphens starting with a letter or a digit');
82
+ throw new Error(
83
+ '"swiplet.id" in package.json must be 1 to 128 lowercase letters, digits, dots, underscores and hyphens starting with a letter or a digit',
84
+ );
59
85
  }
60
86
  if (typeof config.name !== "string" || config.name === "") {
61
- throw new Error('"swiplet.name" in package.json must be a non-empty string');
87
+ throw new Error(
88
+ '"swiplet.name" in package.json must be a non-empty string',
89
+ );
62
90
  }
63
91
  const localizedNames = config.localizedNames ?? {};
64
- if (localizedNames === null || typeof localizedNames !== "object" || !Object.values(localizedNames).every(name => typeof name === "string")) {
65
- throw new Error('"swiplet.localizedNames" in package.json must map languages to names');
92
+ if (
93
+ localizedNames === null ||
94
+ typeof localizedNames !== "object" ||
95
+ !Object.values(localizedNames).every((name) => typeof name === "string")
96
+ ) {
97
+ throw new Error(
98
+ '"swiplet.localizedNames" in package.json must map languages to names',
99
+ );
66
100
  }
67
101
  for (const field of ["description", "updateURL"]) {
68
102
  if (config[field] !== undefined && typeof config[field] !== "string") {
69
103
  throw new Error(`"swiplet.${field}" in package.json must be a string`);
70
104
  }
71
105
  }
72
- if (config.versionCode !== undefined && (!Number.isInteger(config.versionCode) || config.versionCode < 1)) {
73
- throw new Error('"swiplet.versionCode" in package.json must be a positive integer');
106
+ if (
107
+ config.versionCode !== undefined &&
108
+ (!Number.isInteger(config.versionCode) || config.versionCode < 1)
109
+ ) {
110
+ throw new Error(
111
+ '"swiplet.versionCode" in package.json must be a positive integer',
112
+ );
74
113
  }
75
- const entry = name => (config[name] === undefined ? undefined : path.resolve(root, config[name]));
76
- return { root, manifest, config, localizedNames, app: entry("app"), extension: entry("extension") };
114
+ const entry = (name) =>
115
+ config[name] === undefined ? undefined : path.resolve(root, config[name]);
116
+ return {
117
+ root,
118
+ manifest,
119
+ config,
120
+ localizedNames,
121
+ app: entry("app"),
122
+ extension: entry("extension"),
123
+ };
77
124
  }
78
125
 
79
- const typeImport = extension => ({
126
+ const typeImport = (extension) => ({
80
127
  name: "swiplet-type-import",
81
128
  setup(build) {
82
- build.onLoad({ filter: /.*/ }, args => {
129
+ build.onLoad({ filter: /.*/ }, (args) => {
83
130
  if (args.path !== extension) {
84
131
  return undefined;
85
132
  }
86
- return { errors: [{ text: `the interface imports ${path.basename(extension)} at run time; import its type with \`import type\`` }] };
133
+ return {
134
+ errors: [
135
+ {
136
+ text: `the interface imports ${path.basename(extension)} at run time; import its type with \`import type\``,
137
+ },
138
+ ],
139
+ };
87
140
  });
88
141
  },
89
142
  });
@@ -92,7 +145,13 @@ async function bundle(project) {
92
145
  const out = { app: "", extension: "", extensionMap: "", assets: new Map() };
93
146
  loadEnvironment(project.root);
94
147
  if (project.extension) {
95
- const outfile = path.join(project.root, "dist", project.config.id, "src", "extension.js");
148
+ const outfile = path.join(
149
+ project.root,
150
+ "dist",
151
+ project.config.id,
152
+ "src",
153
+ "extension.js",
154
+ );
96
155
  const result = await build({
97
156
  entryPoints: [project.extension],
98
157
  outfile,
@@ -108,10 +167,26 @@ async function bundle(project) {
108
167
  logLevel: "silent",
109
168
  plugins: [metro(project.root, { sourcemap: true })],
110
169
  });
111
- out.extension = result.outputFiles.find(file => file.path.endsWith(".js")).text;
112
- const map = JSON.parse(result.outputFiles.find(file => file.path.endsWith(".map")).text);
113
- const sources = map.sources.map(source => path.relative(project.root, path.resolve(path.dirname(outfile), map.sourceRoot ?? "", source)).split(path.sep).join("/"));
114
- out.extensionMap = JSON.stringify({ ...map, sourceRoot: undefined, sources });
170
+ out.extension = result.outputFiles.find((file) =>
171
+ file.path.endsWith(".js"),
172
+ ).text;
173
+ const map = JSON.parse(
174
+ result.outputFiles.find((file) => file.path.endsWith(".map")).text,
175
+ );
176
+ const sources = map.sources.map((source) =>
177
+ path
178
+ .relative(
179
+ project.root,
180
+ path.resolve(path.dirname(outfile), map.sourceRoot ?? "", source),
181
+ )
182
+ .split(path.sep)
183
+ .join("/"),
184
+ );
185
+ out.extensionMap = JSON.stringify({
186
+ ...map,
187
+ sourceRoot: undefined,
188
+ sources,
189
+ });
115
190
  }
116
191
  if (project.app) {
117
192
  const result = await build({
@@ -121,22 +196,44 @@ async function bundle(project) {
121
196
  platform: "neutral",
122
197
  mainFields: ["react-native", "browser", "module", "main"],
123
198
  conditions: ["react-native"],
124
- resolveExtensions: [".ios.tsx", ".ios.ts", ".native.tsx", ".native.ts", ".tsx", ".ts", ".ios.jsx", ".ios.js", ".native.js", ".jsx", ".js", ".json"],
199
+ resolveExtensions: [
200
+ ".ios.tsx",
201
+ ".ios.ts",
202
+ ".native.tsx",
203
+ ".native.ts",
204
+ ".tsx",
205
+ ".ts",
206
+ ".ios.jsx",
207
+ ".ios.js",
208
+ ".native.js",
209
+ ".jsx",
210
+ ".js",
211
+ ".json",
212
+ ],
125
213
  jsx: "automatic",
126
214
  target: "es2020",
127
215
  define: { "process.env.NODE_ENV": '"production"', __DEV__: "false" },
128
216
  external: appModules,
129
217
  write: false,
130
218
  logLevel: "silent",
131
- plugins: [...(project.extension ? [typeImport(project.extension)] : []), metro(project.root, { assets: out.assets })],
219
+ plugins: [
220
+ ...(project.extension ? [typeImport(project.extension)] : []),
221
+ metro(project.root, { assets: out.assets }),
222
+ ],
132
223
  });
133
224
  const scoped = await transformAsync(result.outputFiles[0].text, {
134
- babelrc: false, configFile: false, sourceType: "script", compact: false, plugins: [require.resolve("@babel/plugin-transform-block-scoping")],
225
+ babelrc: false,
226
+ configFile: false,
227
+ sourceType: "script",
228
+ compact: false,
229
+ plugins: [require.resolve("@babel/plugin-transform-block-scoping")],
135
230
  });
136
231
  out.app = scoped.code;
137
232
  }
138
233
  if (out.app === "" && out.extension === "") {
139
- throw new Error('package.json names neither "swiplet.app" nor "swiplet.extension"');
234
+ throw new Error(
235
+ 'package.json names neither "swiplet.app" nor "swiplet.extension"',
236
+ );
140
237
  }
141
238
  return out;
142
239
  }
@@ -147,31 +244,45 @@ function describeExtension(source) {
147
244
  if (source === "") {
148
245
  return { service: false, actions: [] };
149
246
  }
150
- const loading = name => {
247
+ const loading = (name) => {
151
248
  const fail = () => {
152
- throw new Error(`${name} is used while extension.js loads; use it inside run or an action`);
249
+ throw new Error(
250
+ `${name} is used while extension.js loads; use it inside run or an action`,
251
+ );
153
252
  };
154
- const get = (_, key) => (key === "__esModule" ? true : typeof key === "symbol" || key === "then" ? undefined : fail());
253
+ const get = (_, key) =>
254
+ key === "__esModule"
255
+ ? true
256
+ : typeof key === "symbol" || key === "then"
257
+ ? undefined
258
+ : fail();
155
259
  return new Proxy(fail, { get, apply: fail, construct: fail });
156
260
  };
157
261
  const modules = {
158
262
  "@swipt/api": {},
159
263
  "@swipt/api/swiplet": {
160
- defineExtension: definition => ({ [definitionKey]: definition }),
161
- defineAction: definition => definition,
264
+ defineExtension: (definition) => ({ [definitionKey]: definition }),
265
+ defineAction: (definition) => definition,
162
266
  actions: loading("actions"),
163
267
  useService: loading("useService"),
164
- preferredLocalizations: localizations => (localizations.includes("en") ? ["en"] : localizations.slice(0, 1)),
268
+ preferredLocalizations: (localizations) =>
269
+ localizations.includes("en") ? ["en"] : localizations.slice(0, 1),
165
270
  },
166
271
  "@swipt/api/file-system": {
167
272
  get dataDirectory() {
168
- throw new Error("dataDirectory is used while extension.js loads; use it inside run or an action");
273
+ throw new Error(
274
+ "dataDirectory is used while extension.js loads; use it inside run or an action",
275
+ );
169
276
  },
170
277
  get cacheDirectory() {
171
- throw new Error("cacheDirectory is used while extension.js loads; use it inside run or an action");
278
+ throw new Error(
279
+ "cacheDirectory is used while extension.js loads; use it inside run or an action",
280
+ );
172
281
  },
173
282
  get assets() {
174
- throw new Error("assets is used while extension.js loads; use it inside run or an action");
283
+ throw new Error(
284
+ "assets is used while extension.js loads; use it inside run or an action",
285
+ );
175
286
  },
176
287
  },
177
288
  "@swipt/api/accessibility": loading("accessibility"),
@@ -179,7 +290,10 @@ function describeExtension(source) {
179
290
  "@swipt/api/device": loading("device"),
180
291
  "@swipt/api/screen": loading("screen"),
181
292
  "@swipt/api/image": loading("image"),
182
- "expo-localization": { getLocales: () => [{ languageTag: "en", languageCode: "en" }], getCalendars: () => [] },
293
+ "expo-localization": {
294
+ getLocales: () => [{ languageTag: "en", languageCode: "en" }],
295
+ getCalendars: () => [],
296
+ },
183
297
  "expo-sqlite": {
184
298
  SQLiteDatabase: loading("SQLiteDatabase"),
185
299
  SQLiteStatement: loading("SQLiteStatement"),
@@ -202,17 +316,29 @@ function describeExtension(source) {
202
316
  if (a === b) {
203
317
  return true;
204
318
  }
205
- if (a === null || b === null || typeof a !== "object" || typeof b !== "object") {
319
+ if (
320
+ a === null ||
321
+ b === null ||
322
+ typeof a !== "object" ||
323
+ typeof b !== "object"
324
+ ) {
206
325
  return false;
207
326
  }
208
327
  const keys = Object.keys(a);
209
- return keys.length === Object.keys(b).length && keys.every(key => deepEqual(a[key], b[key]));
328
+ return (
329
+ keys.length === Object.keys(b).length &&
330
+ keys.every((key) => deepEqual(a[key], b[key]))
331
+ );
210
332
  },
211
333
  SQLiteProvider: () => {
212
- throw new Error("SQLiteProvider renders React, so it works only in the swiplet interface");
334
+ throw new Error(
335
+ "SQLiteProvider renders React, so it works only in the swiplet interface",
336
+ );
213
337
  },
214
338
  useSQLiteContext: () => {
215
- throw new Error("useSQLiteContext renders React, so it works only in the swiplet interface");
339
+ throw new Error(
340
+ "useSQLiteContext renders React, so it works only in the swiplet interface",
341
+ );
216
342
  },
217
343
  },
218
344
  "expo-sqlite/kv-store": {
@@ -228,34 +354,84 @@ function describeExtension(source) {
228
354
  };
229
355
  const ignored = () => 0;
230
356
  const webGlobals = [
231
- "ErrorEvent", "File", "FormData", "ReadableStream", "ReadableStreamDefaultReader", "ReadableStreamBYOBReader", "ReadableStreamBYOBRequest",
232
- "ReadableStreamDefaultController", "ReadableByteStreamController", "WritableStream", "WritableStreamDefaultWriter", "WritableStreamDefaultController",
233
- "TransformStream", "TransformStreamDefaultController", "ByteLengthQueuingStrategy", "CountQueuingStrategy", "TextEncoderStream", "TextDecoderStream",
234
- "CompressionStream", "DecompressionStream", "performance", "Performance", "PerformanceEntry", "PerformanceMark", "PerformanceMeasure",
235
- "PerformanceObserver", "PerformanceObserverEntryList",
357
+ "ErrorEvent",
358
+ "File",
359
+ "FormData",
360
+ "ReadableStream",
361
+ "ReadableStreamDefaultReader",
362
+ "ReadableStreamBYOBReader",
363
+ "ReadableStreamBYOBRequest",
364
+ "ReadableStreamDefaultController",
365
+ "ReadableByteStreamController",
366
+ "WritableStream",
367
+ "WritableStreamDefaultWriter",
368
+ "WritableStreamDefaultController",
369
+ "TransformStream",
370
+ "TransformStreamDefaultController",
371
+ "ByteLengthQueuingStrategy",
372
+ "CountQueuingStrategy",
373
+ "TextEncoderStream",
374
+ "TextDecoderStream",
375
+ "CompressionStream",
376
+ "DecompressionStream",
377
+ "performance",
378
+ "Performance",
379
+ "PerformanceEntry",
380
+ "PerformanceMark",
381
+ "PerformanceMeasure",
382
+ "PerformanceObserver",
383
+ "PerformanceObserverEntryList",
236
384
  ];
237
385
  const context = vm.createContext({
238
- console, URL, URLSearchParams, TextEncoder, TextDecoder, atob, btoa, structuredClone, queueMicrotask, crypto,
239
- AbortController, AbortSignal, Event, EventTarget, CustomEvent, DOMException, Blob,
240
- ...Object.fromEntries(webGlobals.map(name => [name, globalThis[name]])),
241
- setTimeout: ignored, setInterval: ignored, clearTimeout: ignored, clearInterval: ignored,
386
+ console,
387
+ URL,
388
+ URLSearchParams,
389
+ TextEncoder,
390
+ TextDecoder,
391
+ atob,
392
+ btoa,
393
+ structuredClone,
394
+ queueMicrotask,
395
+ crypto,
396
+ AbortController,
397
+ AbortSignal,
398
+ Event,
399
+ EventTarget,
400
+ CustomEvent,
401
+ DOMException,
402
+ Blob,
403
+ ...Object.fromEntries(webGlobals.map((name) => [name, globalThis[name]])),
404
+ setTimeout: ignored,
405
+ setInterval: ignored,
406
+ clearTimeout: ignored,
407
+ clearInterval: ignored,
242
408
  navigator: { userAgent: "Swipt", language: "en", languages: ["en"] },
243
409
  });
244
- const factory = vm.runInContext(`(function (require, module, exports) {${source}\n})`, context, { filename: "extension.js" });
410
+ const factory = vm.runInContext(
411
+ `(function (require, module, exports) {${source}\n})`,
412
+ context,
413
+ { filename: "extension.js" },
414
+ );
245
415
  const module = { exports: {} };
246
416
  factory(
247
- specifier => {
417
+ (specifier) => {
248
418
  if (!Object.hasOwn(modules, specifier)) {
249
- throw new Error(`Swipt doesn't provide "${specifier}" to extensions; bundle it into extension.js`);
419
+ throw new Error(
420
+ `Swipt doesn't provide "${specifier}" to extensions; bundle it into extension.js`,
421
+ );
250
422
  }
251
423
  return modules[specifier];
252
424
  },
253
425
  module,
254
426
  module.exports,
255
427
  );
256
- const definition = (module.exports.default ?? module.exports)?.[definitionKey];
428
+ const definition = (module.exports.default ?? module.exports)?.[
429
+ definitionKey
430
+ ];
257
431
  if (definition === undefined) {
258
- throw new Error("the extension's default export is not the result of defineExtension");
432
+ throw new Error(
433
+ "the extension's default export is not the result of defineExtension",
434
+ );
259
435
  }
260
436
  const schema = (name, value, side) => {
261
437
  if (value === undefined) {
@@ -263,27 +439,38 @@ function describeExtension(source) {
263
439
  }
264
440
  const converter = value?.["~standard"]?.jsonSchema;
265
441
  if (typeof converter?.[side] !== "function") {
266
- throw new Error(`the ${side} schema of the action ${JSON.stringify(name)} doesn't implement Standard JSON Schema; use a library that does, such as Zod 4`);
442
+ throw new Error(
443
+ `the ${side} schema of the action ${JSON.stringify(name)} doesn't implement Standard JSON Schema; use a library that does, such as Zod 4`,
444
+ );
267
445
  }
268
446
  return converter[side]({ target: "draft-2020-12" });
269
447
  };
270
- const actions = Object.entries(definition.actions ?? {}).map(([name, action]) => {
271
- if (typeof action?.title !== "string" || action.title === "") {
272
- throw new Error(`the action ${JSON.stringify(name)} needs a title`);
273
- }
274
- const expose = action.expose ?? [];
275
- if (!Array.isArray(expose) || !expose.every(caller => caller === "shortcuts")) {
276
- throw new Error(`the action ${JSON.stringify(name)} can only expose itself to "shortcuts"`);
277
- }
278
- return {
279
- name,
280
- title: action.title,
281
- ...(action.description ? { description: action.description } : {}),
282
- ...(expose.length > 0 ? { expose } : {}),
283
- ...(action.input ? { input: schema(name, action.input, "input") } : {}),
284
- ...(action.output ? { output: schema(name, action.output, "output") } : {}),
285
- };
286
- });
448
+ const actions = Object.entries(definition.actions ?? {}).map(
449
+ ([name, action]) => {
450
+ if (typeof action?.title !== "string" || action.title === "") {
451
+ throw new Error(`the action ${JSON.stringify(name)} needs a title`);
452
+ }
453
+ const expose = action.expose ?? [];
454
+ if (
455
+ !Array.isArray(expose) ||
456
+ !expose.every((caller) => caller === "shortcuts")
457
+ ) {
458
+ throw new Error(
459
+ `the action ${JSON.stringify(name)} can only expose itself to "shortcuts"`,
460
+ );
461
+ }
462
+ return {
463
+ name,
464
+ title: action.title,
465
+ ...(action.description ? { description: action.description } : {}),
466
+ ...(expose.length > 0 ? { expose } : {}),
467
+ ...(action.input ? { input: schema(name, action.input, "input") } : {}),
468
+ ...(action.output
469
+ ? { output: schema(name, action.output, "output") }
470
+ : {}),
471
+ };
472
+ },
473
+ );
287
474
  return { service: typeof definition.run === "function", actions };
288
475
  }
289
476
 
@@ -305,7 +492,7 @@ function swipletManifest(project, output) {
305
492
  };
306
493
  }
307
494
 
308
- const sourceFiles = output => ({
495
+ const sourceFiles = (output) => ({
309
496
  ...(output.app ? { "app.js": output.app } : {}),
310
497
  ...(output.extension ? { "extension.js": output.extension } : {}),
311
498
  });
@@ -318,23 +505,34 @@ export async function buildProject(root) {
318
505
  const folder = path.join(dist, manifest.id);
319
506
  fs.rmSync(folder, { recursive: true, force: true });
320
507
  fs.mkdirSync(path.join(folder, "src"), { recursive: true });
321
- fs.writeFileSync(path.join(folder, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
508
+ fs.writeFileSync(
509
+ path.join(folder, "manifest.json"),
510
+ `${JSON.stringify(manifest, null, 2)}\n`,
511
+ );
322
512
  for (const [file, contents] of Object.entries(sourceFiles(output))) {
323
513
  fs.writeFileSync(path.join(folder, "src", file), contents);
324
514
  }
325
515
  if (output.extensionMap) {
326
- fs.writeFileSync(path.join(folder, "src", "extension.js.map"), output.extensionMap);
516
+ fs.writeFileSync(
517
+ path.join(folder, "src", "extension.js.map"),
518
+ output.extensionMap,
519
+ );
327
520
  }
328
521
  if (fs.existsSync(path.join(root, "icon.png"))) {
329
522
  fs.copyFileSync(path.join(root, "icon.png"), path.join(folder, "icon.png"));
330
523
  }
331
524
  if (fs.existsSync(path.join(root, "assets"))) {
332
- fs.cpSync(path.join(root, "assets"), path.join(folder, "assets"), { recursive: true, filter: source => !path.basename(source).startsWith(".") });
525
+ fs.cpSync(path.join(root, "assets"), path.join(folder, "assets"), {
526
+ recursive: true,
527
+ filter: (source) => !path.basename(source).startsWith("."),
528
+ });
333
529
  }
334
530
  for (const [name, source] of output.assets) {
335
531
  const target = path.join(folder, "assets", name);
336
532
  if (fs.existsSync(target)) {
337
- throw new Error(`${path.relative(root, source)} and ${path.join("assets", name)} would both be assets/${name} in the swiplet`);
533
+ throw new Error(
534
+ `${path.relative(root, source)} and ${path.join("assets", name)} would both be assets/${name} in the swiplet`,
535
+ );
338
536
  }
339
537
  fs.mkdirSync(path.dirname(target), { recursive: true });
340
538
  fs.copyFileSync(source, target);
@@ -342,14 +540,20 @@ export async function buildProject(root) {
342
540
  const archive = path.join(dist, `${manifest.id}.swiplet`);
343
541
  fs.rmSync(archive, { force: true });
344
542
  fs.rmSync(path.join(dist, `${manifest.id}.zip`), { force: true });
345
- execFileSync("zip", ["-qr", "-n", storedSuffixes, archive, ...fs.readdirSync(folder)], { cwd: folder });
543
+ execFileSync(
544
+ "zip",
545
+ ["-qr", "-n", storedSuffixes, archive, ...fs.readdirSync(folder)],
546
+ { cwd: folder },
547
+ );
346
548
  return { id: manifest.id, archive, digest: folderDigest(folder) };
347
549
  }
348
550
 
349
551
  function folderDigest(folder) {
350
552
  const hash = createHash("sha256");
351
- const walk = directory => {
352
- for (const entry of fs.readdirSync(directory, { withFileTypes: true }).sort((a, b) => (a.name < b.name ? -1 : 1))) {
553
+ const walk = (directory) => {
554
+ for (const entry of fs
555
+ .readdirSync(directory, { withFileTypes: true })
556
+ .sort((a, b) => (a.name < b.name ? -1 : 1))) {
353
557
  const name = path.join(directory, entry.name);
354
558
  if (entry.isDirectory()) {
355
559
  walk(name);
@@ -367,7 +571,9 @@ function folderDigest(folder) {
367
571
  const manifestEntry = /^(?:[^/]+\/)?manifest\.json$/;
368
572
 
369
573
  function loadSource(root) {
370
- const manifest = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
574
+ const manifest = JSON.parse(
575
+ fs.readFileSync(path.join(root, "package.json"), "utf8"),
576
+ );
371
577
  const config = manifest.source ?? {};
372
578
  for (const field of ["name", "image", "url"]) {
373
579
  if (config[field] !== undefined && typeof config[field] !== "string") {
@@ -375,21 +581,38 @@ function loadSource(root) {
375
581
  }
376
582
  }
377
583
  const localizedNames = config.localizedNames ?? {};
378
- if (localizedNames === null || typeof localizedNames !== "object" || !Object.values(localizedNames).every(name => typeof name === "string")) {
379
- throw new Error('"source.localizedNames" in package.json must map languages to names');
584
+ if (
585
+ localizedNames === null ||
586
+ typeof localizedNames !== "object" ||
587
+ !Object.values(localizedNames).every((name) => typeof name === "string")
588
+ ) {
589
+ throw new Error(
590
+ '"source.localizedNames" in package.json must map languages to names',
591
+ );
380
592
  }
381
- if (config.icon !== undefined && (typeof config.icon?.symbol !== "string" || (config.icon.color !== undefined && typeof config.icon.color !== "string"))) {
382
- throw new Error('"source.icon" in package.json must have a symbol and may have a color');
593
+ if (
594
+ config.icon !== undefined &&
595
+ (typeof config.icon?.symbol !== "string" ||
596
+ (config.icon.color !== undefined &&
597
+ typeof config.icon.color !== "string"))
598
+ ) {
599
+ throw new Error(
600
+ '"source.icon" in package.json must have a symbol and may have a color',
601
+ );
383
602
  }
384
603
  let prefix;
385
604
  if (config.url !== undefined) {
386
605
  try {
387
- prefix = new URL(config.url.endsWith("/") ? config.url : `${config.url}/`);
606
+ prefix = new URL(
607
+ config.url.endsWith("/") ? config.url : `${config.url}/`,
608
+ );
388
609
  } catch {
389
610
  throw new Error('"source.url" in package.json is not an absolute URL');
390
611
  }
391
612
  }
392
- const workspaces = Array.isArray(manifest.workspaces) ? manifest.workspaces : manifest.workspaces?.packages ?? [];
613
+ const workspaces = Array.isArray(manifest.workspaces)
614
+ ? manifest.workspaces
615
+ : (manifest.workspaces?.packages ?? []);
393
616
  return { root, config, localizedNames, prefix, workspaces };
394
617
  }
395
618
 
@@ -407,7 +630,9 @@ function workspaceSwiplets(source) {
407
630
  }
408
631
  const built = path.join(source.root, directory, "dist", `${id}.swiplet`);
409
632
  if (!fs.existsSync(built)) {
410
- throw new Error(`${path.relative(source.root, built)} is missing; build ${directory} first`);
633
+ throw new Error(
634
+ `${path.relative(source.root, built)} is missing; build ${directory} first`,
635
+ );
411
636
  }
412
637
  files.push(built);
413
638
  }
@@ -416,11 +641,16 @@ function workspaceSwiplets(source) {
416
641
  }
417
642
 
418
643
  function readSwiplet(file) {
419
- const entry = execFileSync("unzip", ["-Z1", file], { encoding: "utf8" }).split("\n").filter(name => manifestEntry.test(name)).sort((a, b) => a.length - b.length)[0];
644
+ const entry = execFileSync("unzip", ["-Z1", file], { encoding: "utf8" })
645
+ .split("\n")
646
+ .filter((name) => manifestEntry.test(name))
647
+ .sort((a, b) => a.length - b.length)[0];
420
648
  if (entry === undefined) {
421
649
  throw new Error(`${file} has no manifest.json`);
422
650
  }
423
- const manifest = JSON.parse(execFileSync("unzip", ["-p", file, entry], { encoding: "utf8" }));
651
+ const manifest = JSON.parse(
652
+ execFileSync("unzip", ["-p", file, entry], { encoding: "utf8" }),
653
+ );
424
654
  if (typeof manifest.id !== "string" || !idPattern.test(manifest.id)) {
425
655
  throw new Error(`${file} has no valid swiplet id in its manifest`);
426
656
  }
@@ -429,49 +659,90 @@ function readSwiplet(file) {
429
659
 
430
660
  export function sourceCommand(root, given) {
431
661
  const source = loadSource(root);
432
- const files = given.length > 0 ? given.map(file => path.resolve(file)) : workspaceSwiplets(source);
662
+ const files =
663
+ given.length > 0
664
+ ? given.map((file) => path.resolve(file))
665
+ : workspaceSwiplets(source);
433
666
  if (files.length === 0) {
434
- throw new Error("package.json lists no workspaces with swiplets; name the swiplets to list");
667
+ throw new Error(
668
+ "package.json lists no workspaces with swiplets; name the swiplets to list",
669
+ );
435
670
  }
436
671
  const swiplets = new Map();
437
672
  for (const read of files.map(readSwiplet)) {
438
673
  const { id } = read.manifest;
439
674
  if (swiplets.has(id)) {
440
- throw new Error(`${id} is both ${swiplets.get(id).file} and ${read.file}`);
675
+ throw new Error(
676
+ `${id} is both ${swiplets.get(id).file} and ${read.file}`,
677
+ );
441
678
  }
442
679
  swiplets.set(id, read);
443
680
  }
444
- const image = source.config.image === undefined ? undefined : { name: path.basename(source.config.image), data: fs.readFileSync(path.resolve(root, source.config.image)) };
445
- const located = name => (source.prefix ? new URL(name, source.prefix).href : name);
681
+ const image =
682
+ source.config.image === undefined
683
+ ? undefined
684
+ : {
685
+ name: path.basename(source.config.image),
686
+ data: fs.readFileSync(path.resolve(root, source.config.image)),
687
+ };
688
+ const located = (name) =>
689
+ source.prefix ? new URL(name, source.prefix).href : name;
446
690
  const dist = path.join(root, "dist");
447
691
  fs.rmSync(dist, { recursive: true, force: true });
448
692
  fs.mkdirSync(dist, { recursive: true });
449
- const entries = [...swiplets.keys()].sort().map(id => {
693
+ const entries = [...swiplets.keys()].sort().map((id) => {
450
694
  const { manifest, data } = swiplets.get(id);
451
695
  const name = `${id}.swiplet`;
452
696
  fs.writeFileSync(path.join(dist, name), data);
453
- return { ...manifest, url: located(name), size: data.length, sha256: createHash("sha256").update(data).digest("hex") };
697
+ return {
698
+ ...manifest,
699
+ url: located(name),
700
+ size: data.length,
701
+ sha256: createHash("sha256").update(data).digest("hex"),
702
+ };
454
703
  });
455
704
  if (image) {
456
705
  fs.writeFileSync(path.join(dist, image.name), image.data);
457
706
  }
458
707
  const index = {
459
708
  ...(source.config.name ? { name: source.config.name } : {}),
460
- ...(Object.keys(source.localizedNames).length > 0 ? { localizedNames: source.localizedNames } : {}),
461
- ...(source.config.icon ? { icon: { symbol: source.config.icon.symbol, ...(source.config.icon.color ? { color: source.config.icon.color } : {}) } } : {}),
709
+ ...(Object.keys(source.localizedNames).length > 0
710
+ ? { localizedNames: source.localizedNames }
711
+ : {}),
712
+ ...(source.config.icon
713
+ ? {
714
+ icon: {
715
+ symbol: source.config.icon.symbol,
716
+ ...(source.config.icon.color
717
+ ? { color: source.config.icon.color }
718
+ : {}),
719
+ },
720
+ }
721
+ : {}),
462
722
  ...(image ? { image: located(image.name) } : {}),
463
723
  swiplets: entries,
464
724
  };
465
- fs.writeFileSync(path.join(dist, "swiplets.json"), `${JSON.stringify(index, null, 2)}\n`);
725
+ fs.writeFileSync(
726
+ path.join(dist, "swiplets.json"),
727
+ `${JSON.stringify(index, null, 2)}\n`,
728
+ );
466
729
  console.log(path.relative(process.cwd(), path.join(dist, "swiplets.json")));
467
730
  }
468
731
 
469
- const swiptRange = version.includes("-") ? version : `>=${version}-0 <=${version}`;
732
+ const swiptRange = version.includes("-")
733
+ ? version
734
+ : `>=${version}-0 <=${version}`;
470
735
 
471
- const idOf = name => name.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[._-]+/, "").slice(0, 128).replace(/[._-]+$/, "") || "swiplet";
736
+ const idOf = (name) =>
737
+ name
738
+ .toLowerCase()
739
+ .replace(/[^a-z0-9._-]+/g, "-")
740
+ .replace(/^[._-]+/, "")
741
+ .slice(0, 128)
742
+ .replace(/[._-]+$/, "") || "swiplet";
472
743
 
473
744
  const templates = {
474
- "package.json": name =>
745
+ "package.json": (name) =>
475
746
  `${JSON.stringify(
476
747
  {
477
748
  name: idOf(name),
@@ -491,7 +762,12 @@ const templates = {
491
762
  "@swipt/api": swiptRange,
492
763
  "@swipt/cli": swiptRange,
493
764
  typescript: "^7.0.2",
494
- ...Object.fromEntries(["@expo/ui", "@types/react", "react"].map(name => [name, peerDependencies[name]])),
765
+ ...Object.fromEntries(
766
+ ["@expo/ui", "@types/react", "react"].map((name) => [
767
+ name,
768
+ peerDependencies[name],
769
+ ]),
770
+ ),
495
771
  },
496
772
  },
497
773
  null,
@@ -501,8 +777,14 @@ const templates = {
501
777
  `${JSON.stringify(
502
778
  {
503
779
  compilerOptions: {
504
- target: "ES2022", module: "ESNext", moduleResolution: "Bundler", jsx: "react-jsx", lib: ["ES2023", "DOM"],
505
- strict: true, noEmit: true, skipLibCheck: true,
780
+ target: "ES2022",
781
+ module: "ESNext",
782
+ moduleResolution: "Bundler",
783
+ jsx: "react-jsx",
784
+ lib: ["ES2023", "DOM"],
785
+ strict: true,
786
+ noEmit: true,
787
+ skipLibCheck: true,
506
788
  },
507
789
  },
508
790
  null,
@@ -521,7 +803,8 @@ export default defineExtension({
521
803
  },
522
804
  });
523
805
  `,
524
- "src/App.tsx": () => `import { Button, Form, Host, Section, Text } from "@expo/ui/swift-ui";
806
+ "src/App.tsx":
807
+ () => `import { Button, Form, Host, Section, Text } from "@expo/ui/swift-ui";
525
808
  import { useState } from "react";
526
809
  import { actions } from "@swipt/api/swiplet";
527
810
 
@@ -545,7 +828,10 @@ export default function App() {
545
828
  `,
546
829
  };
547
830
 
548
- const quote = value => (/^[\w@%+=:,./-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`);
831
+ const quote = (value) =>
832
+ /^[\w@%+=:,./-]+$/.test(value)
833
+ ? value
834
+ : `'${value.replaceAll("'", "'\\''")}'`;
549
835
 
550
836
  export function createCommand(directory) {
551
837
  const root = path.resolve(directory);
@@ -557,5 +843,7 @@ export function createCommand(directory) {
557
843
  fs.writeFileSync(path.join(root, file), template(path.basename(root)));
558
844
  }
559
845
  const manager = process.env.npm_config_user_agent?.split("/")[0] ?? "npm";
560
- console.log(`cd ${quote(directory)} && ${manager} install && ${manager} run build`);
846
+ console.log(
847
+ `cd ${quote(directory)} && ${manager} install && ${manager} run build`,
848
+ );
561
849
  }