@swipt/cli 0.1.0-nightly.42 → 0.1.0-nightly.44

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
@@ -7,12 +7,16 @@ import path from "node:path";
7
7
  import { parseArgs } from "node:util";
8
8
  import vm from "node:vm";
9
9
 
10
+ import { transformAsync } from "@babel/core";
10
11
  import { build } from "esbuild";
11
12
 
12
13
  import { loadEnvironment, metro } from "./metro.mjs";
13
14
 
14
- const { version } = JSON.parse(fs.readFileSync(new URL("package.json", import.meta.url), "utf8"));
15
- const { peerDependencies } = createRequire(import.meta.url)("@swipt/api/package.json");
15
+ const { version } = JSON.parse(
16
+ fs.readFileSync(new URL("package.json", import.meta.url), "utf8"),
17
+ );
18
+ const require = createRequire(import.meta.url);
19
+ const { peerDependencies } = require("@swipt/api/package.json");
16
20
 
17
21
  const appModules = [
18
22
  "react",
@@ -27,6 +31,8 @@ const appModules = [
27
31
  "@swipt/api/swiplet",
28
32
  "@swipt/api/file-system",
29
33
  "expo-sqlite",
34
+ "expo-sqlite/kv-store",
35
+ "expo-sqlite/localStorage/install",
30
36
  ];
31
37
  const extensionModules = [
32
38
  "@swipt/api",
@@ -39,45 +45,98 @@ const extensionModules = [
39
45
  "@swipt/api/image",
40
46
  "expo-localization",
41
47
  "expo-sqlite",
48
+ "expo-sqlite/kv-store",
49
+ "expo-sqlite/localStorage/install",
42
50
  ];
43
51
 
44
52
  const idPattern = /^[a-z0-9][a-z0-9._-]{0,127}$/;
45
53
 
46
- 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(":");
47
75
 
48
76
  export function loadProject(root) {
49
- 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
+ );
50
80
  const config = manifest.swiplet ?? {};
51
81
  if (typeof config.id !== "string" || !idPattern.test(config.id)) {
52
- 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
+ );
53
85
  }
54
86
  if (typeof config.name !== "string" || config.name === "") {
55
- 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
+ );
56
90
  }
57
91
  const localizedNames = config.localizedNames ?? {};
58
- if (localizedNames === null || typeof localizedNames !== "object" || !Object.values(localizedNames).every(name => typeof name === "string")) {
59
- 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
+ );
60
100
  }
61
101
  for (const field of ["description", "updateURL"]) {
62
102
  if (config[field] !== undefined && typeof config[field] !== "string") {
63
103
  throw new Error(`"swiplet.${field}" in package.json must be a string`);
64
104
  }
65
105
  }
66
- if (config.versionCode !== undefined && (!Number.isInteger(config.versionCode) || config.versionCode < 1)) {
67
- 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
+ );
68
113
  }
69
- const entry = name => (config[name] === undefined ? undefined : path.resolve(root, config[name]));
70
- 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
+ };
71
124
  }
72
125
 
73
- const typeImport = extension => ({
126
+ const typeImport = (extension) => ({
74
127
  name: "swiplet-type-import",
75
128
  setup(build) {
76
- build.onLoad({ filter: /.*/ }, args => {
129
+ build.onLoad({ filter: /.*/ }, (args) => {
77
130
  if (args.path !== extension) {
78
131
  return undefined;
79
132
  }
80
- 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
+ };
81
140
  });
82
141
  },
83
142
  });
@@ -86,7 +145,13 @@ async function bundle(project) {
86
145
  const out = { app: "", extension: "", extensionMap: "", assets: new Map() };
87
146
  loadEnvironment(project.root);
88
147
  if (project.extension) {
89
- 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
+ );
90
155
  const result = await build({
91
156
  entryPoints: [project.extension],
92
157
  outfile,
@@ -102,10 +167,26 @@ async function bundle(project) {
102
167
  logLevel: "silent",
103
168
  plugins: [metro(project.root, { sourcemap: true })],
104
169
  });
105
- out.extension = result.outputFiles.find(file => file.path.endsWith(".js")).text;
106
- const map = JSON.parse(result.outputFiles.find(file => file.path.endsWith(".map")).text);
107
- const sources = map.sources.map(source => path.relative(project.root, path.resolve(path.dirname(outfile), map.sourceRoot ?? "", source)).split(path.sep).join("/"));
108
- 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
+ });
109
190
  }
110
191
  if (project.app) {
111
192
  const result = await build({
@@ -115,19 +196,44 @@ async function bundle(project) {
115
196
  platform: "neutral",
116
197
  mainFields: ["react-native", "browser", "module", "main"],
117
198
  conditions: ["react-native"],
118
- 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
+ ],
119
213
  jsx: "automatic",
120
214
  target: "es2020",
121
215
  define: { "process.env.NODE_ENV": '"production"', __DEV__: "false" },
122
216
  external: appModules,
123
217
  write: false,
124
218
  logLevel: "silent",
125
- 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
+ ],
223
+ });
224
+ const scoped = await transformAsync(result.outputFiles[0].text, {
225
+ babelrc: false,
226
+ configFile: false,
227
+ sourceType: "script",
228
+ compact: false,
229
+ plugins: [require.resolve("@babel/plugin-transform-block-scoping")],
126
230
  });
127
- out.app = result.outputFiles[0].text;
231
+ out.app = scoped.code;
128
232
  }
129
233
  if (out.app === "" && out.extension === "") {
130
- 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
+ );
131
237
  }
132
238
  return out;
133
239
  }
@@ -138,28 +244,45 @@ function describeExtension(source) {
138
244
  if (source === "") {
139
245
  return { service: false, actions: [] };
140
246
  }
141
- const loading = name => {
247
+ const loading = (name) => {
142
248
  const fail = () => {
143
- 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
+ );
144
252
  };
145
- 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();
146
259
  return new Proxy(fail, { get, apply: fail, construct: fail });
147
260
  };
148
261
  const modules = {
149
262
  "@swipt/api": {},
150
263
  "@swipt/api/swiplet": {
151
- defineExtension: definition => ({ [definitionKey]: definition }),
152
- defineAction: definition => definition,
264
+ defineExtension: (definition) => ({ [definitionKey]: definition }),
265
+ defineAction: (definition) => definition,
153
266
  actions: loading("actions"),
154
267
  useService: loading("useService"),
155
- preferredLocalizations: localizations => (localizations.includes("en") ? ["en"] : localizations.slice(0, 1)),
268
+ preferredLocalizations: (localizations) =>
269
+ localizations.includes("en") ? ["en"] : localizations.slice(0, 1),
156
270
  },
157
271
  "@swipt/api/file-system": {
158
272
  get dataDirectory() {
159
- 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
+ );
276
+ },
277
+ get cacheDirectory() {
278
+ throw new Error(
279
+ "cacheDirectory is used while extension.js loads; use it inside run or an action",
280
+ );
160
281
  },
161
282
  get assets() {
162
- 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
+ );
163
286
  },
164
287
  },
165
288
  "@swipt/api/accessibility": loading("accessibility"),
@@ -167,46 +290,148 @@ function describeExtension(source) {
167
290
  "@swipt/api/device": loading("device"),
168
291
  "@swipt/api/screen": loading("screen"),
169
292
  "@swipt/api/image": loading("image"),
170
- "expo-localization": { getLocales: () => [{ languageTag: "en", languageCode: "en" }], getCalendars: () => [] },
293
+ "expo-localization": {
294
+ getLocales: () => [{ languageTag: "en", languageCode: "en" }],
295
+ getCalendars: () => [],
296
+ },
171
297
  "expo-sqlite": {
298
+ SQLiteDatabase: loading("SQLiteDatabase"),
299
+ SQLiteStatement: loading("SQLiteStatement"),
300
+ SQLiteTaggedQuery: loading("SQLiteTaggedQuery"),
301
+ SQLiteSession: loading("SQLiteSession"),
172
302
  openDatabaseSync: () => loading("the database"),
173
303
  openDatabaseAsync: () => Promise.resolve(loading("the database")),
304
+ deserializeDatabaseSync: () => loading("the database"),
305
+ deserializeDatabaseAsync: () => Promise.resolve(loading("the database")),
174
306
  deleteDatabaseSync: () => {},
175
307
  deleteDatabaseAsync: () => Promise.resolve(),
308
+ backupDatabaseSync: () => {},
309
+ backupDatabaseAsync: () => Promise.resolve(),
310
+ importDatabaseFromAssetAsync: () => Promise.resolve(),
176
311
  addDatabaseChangeListener: () => ({ remove: () => {} }),
177
312
  defaultDatabaseDirectory: "",
313
+ bundledExtensions: {},
314
+ configureWeb: () => {},
315
+ deepEqual: function deepEqual(a, b) {
316
+ if (a === b) {
317
+ return true;
318
+ }
319
+ if (
320
+ a === null ||
321
+ b === null ||
322
+ typeof a !== "object" ||
323
+ typeof b !== "object"
324
+ ) {
325
+ return false;
326
+ }
327
+ const keys = Object.keys(a);
328
+ return (
329
+ keys.length === Object.keys(b).length &&
330
+ keys.every((key) => deepEqual(a[key], b[key]))
331
+ );
332
+ },
333
+ SQLiteProvider: () => {
334
+ throw new Error(
335
+ "SQLiteProvider renders React, so it works only in the swiplet interface",
336
+ );
337
+ },
338
+ useSQLiteContext: () => {
339
+ throw new Error(
340
+ "useSQLiteContext renders React, so it works only in the swiplet interface",
341
+ );
342
+ },
343
+ },
344
+ "expo-sqlite/kv-store": {
345
+ SQLiteStorage: loading("SQLiteStorage"),
346
+ AsyncStorage: loading("the storage"),
347
+ Storage: loading("the storage"),
348
+ default: loading("the storage"),
349
+ },
350
+ get "expo-sqlite/localStorage/install"() {
351
+ context.localStorage = loading("localStorage");
352
+ return {};
178
353
  },
179
354
  };
180
355
  const ignored = () => 0;
181
356
  const webGlobals = [
182
- "MessageEvent", "CloseEvent", "ErrorEvent", "File", "FormData", "ReadableStream", "ReadableStreamDefaultReader", "ReadableStreamBYOBReader",
183
- "ReadableStreamBYOBRequest", "ReadableStreamDefaultController", "ReadableByteStreamController", "WritableStream", "WritableStreamDefaultWriter",
184
- "WritableStreamDefaultController", "TransformStream", "TransformStreamDefaultController", "ByteLengthQueuingStrategy", "CountQueuingStrategy",
185
- "TextEncoderStream", "TextDecoderStream", "CompressionStream", "DecompressionStream", "WebSocket", "EventSource",
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",
186
384
  ];
187
385
  const context = vm.createContext({
188
- console, URL, URLSearchParams, TextEncoder, TextDecoder, atob, btoa, structuredClone, queueMicrotask, crypto,
189
- AbortController, AbortSignal, Event, EventTarget, CustomEvent, DOMException, Headers, Request, Response, Blob,
190
- ...Object.fromEntries(webGlobals.map(name => [name, globalThis[name]])),
191
- fetch: loading("fetch"),
192
- 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,
193
408
  navigator: { userAgent: "Swipt", language: "en", languages: ["en"] },
194
409
  });
195
- 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
+ );
196
415
  const module = { exports: {} };
197
416
  factory(
198
- specifier => {
417
+ (specifier) => {
199
418
  if (!Object.hasOwn(modules, specifier)) {
200
- 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
+ );
201
422
  }
202
423
  return modules[specifier];
203
424
  },
204
425
  module,
205
426
  module.exports,
206
427
  );
207
- const definition = (module.exports.default ?? module.exports)?.[definitionKey];
428
+ const definition = (module.exports.default ?? module.exports)?.[
429
+ definitionKey
430
+ ];
208
431
  if (definition === undefined) {
209
- 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
+ );
210
435
  }
211
436
  const schema = (name, value, side) => {
212
437
  if (value === undefined) {
@@ -214,27 +439,38 @@ function describeExtension(source) {
214
439
  }
215
440
  const converter = value?.["~standard"]?.jsonSchema;
216
441
  if (typeof converter?.[side] !== "function") {
217
- 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
+ );
218
445
  }
219
446
  return converter[side]({ target: "draft-2020-12" });
220
447
  };
221
- const actions = Object.entries(definition.actions ?? {}).map(([name, action]) => {
222
- if (typeof action?.title !== "string" || action.title === "") {
223
- throw new Error(`the action ${JSON.stringify(name)} needs a title`);
224
- }
225
- const expose = action.expose ?? [];
226
- if (!Array.isArray(expose) || !expose.every(caller => caller === "shortcuts")) {
227
- throw new Error(`the action ${JSON.stringify(name)} can only expose itself to "shortcuts"`);
228
- }
229
- return {
230
- name,
231
- title: action.title,
232
- ...(action.description ? { description: action.description } : {}),
233
- ...(expose.length > 0 ? { expose } : {}),
234
- ...(action.input ? { input: schema(name, action.input, "input") } : {}),
235
- ...(action.output ? { output: schema(name, action.output, "output") } : {}),
236
- };
237
- });
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
+ );
238
474
  return { service: typeof definition.run === "function", actions };
239
475
  }
240
476
 
@@ -256,7 +492,7 @@ function swipletManifest(project, output) {
256
492
  };
257
493
  }
258
494
 
259
- const sourceFiles = output => ({
495
+ const sourceFiles = (output) => ({
260
496
  ...(output.app ? { "app.js": output.app } : {}),
261
497
  ...(output.extension ? { "extension.js": output.extension } : {}),
262
498
  });
@@ -269,23 +505,34 @@ export async function buildProject(root) {
269
505
  const folder = path.join(dist, manifest.id);
270
506
  fs.rmSync(folder, { recursive: true, force: true });
271
507
  fs.mkdirSync(path.join(folder, "src"), { recursive: true });
272
- 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
+ );
273
512
  for (const [file, contents] of Object.entries(sourceFiles(output))) {
274
513
  fs.writeFileSync(path.join(folder, "src", file), contents);
275
514
  }
276
515
  if (output.extensionMap) {
277
- 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
+ );
278
520
  }
279
521
  if (fs.existsSync(path.join(root, "icon.png"))) {
280
522
  fs.copyFileSync(path.join(root, "icon.png"), path.join(folder, "icon.png"));
281
523
  }
282
524
  if (fs.existsSync(path.join(root, "assets"))) {
283
- 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
+ });
284
529
  }
285
530
  for (const [name, source] of output.assets) {
286
531
  const target = path.join(folder, "assets", name);
287
532
  if (fs.existsSync(target)) {
288
- 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
+ );
289
536
  }
290
537
  fs.mkdirSync(path.dirname(target), { recursive: true });
291
538
  fs.copyFileSync(source, target);
@@ -293,14 +540,20 @@ export async function buildProject(root) {
293
540
  const archive = path.join(dist, `${manifest.id}.swiplet`);
294
541
  fs.rmSync(archive, { force: true });
295
542
  fs.rmSync(path.join(dist, `${manifest.id}.zip`), { force: true });
296
- 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
+ );
297
548
  return { id: manifest.id, archive, digest: folderDigest(folder) };
298
549
  }
299
550
 
300
551
  function folderDigest(folder) {
301
552
  const hash = createHash("sha256");
302
- const walk = directory => {
303
- 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))) {
304
557
  const name = path.join(directory, entry.name);
305
558
  if (entry.isDirectory()) {
306
559
  walk(name);
@@ -318,7 +571,9 @@ function folderDigest(folder) {
318
571
  const manifestEntry = /^(?:[^/]+\/)?manifest\.json$/;
319
572
 
320
573
  function loadSource(root) {
321
- 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
+ );
322
577
  const config = manifest.source ?? {};
323
578
  for (const field of ["name", "image", "url"]) {
324
579
  if (config[field] !== undefined && typeof config[field] !== "string") {
@@ -326,21 +581,38 @@ function loadSource(root) {
326
581
  }
327
582
  }
328
583
  const localizedNames = config.localizedNames ?? {};
329
- if (localizedNames === null || typeof localizedNames !== "object" || !Object.values(localizedNames).every(name => typeof name === "string")) {
330
- 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
+ );
331
592
  }
332
- if (config.icon !== undefined && (typeof config.icon?.symbol !== "string" || (config.icon.color !== undefined && typeof config.icon.color !== "string"))) {
333
- 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
+ );
334
602
  }
335
603
  let prefix;
336
604
  if (config.url !== undefined) {
337
605
  try {
338
- prefix = new URL(config.url.endsWith("/") ? config.url : `${config.url}/`);
606
+ prefix = new URL(
607
+ config.url.endsWith("/") ? config.url : `${config.url}/`,
608
+ );
339
609
  } catch {
340
610
  throw new Error('"source.url" in package.json is not an absolute URL');
341
611
  }
342
612
  }
343
- 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 ?? []);
344
616
  return { root, config, localizedNames, prefix, workspaces };
345
617
  }
346
618
 
@@ -358,7 +630,9 @@ function workspaceSwiplets(source) {
358
630
  }
359
631
  const built = path.join(source.root, directory, "dist", `${id}.swiplet`);
360
632
  if (!fs.existsSync(built)) {
361
- 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
+ );
362
636
  }
363
637
  files.push(built);
364
638
  }
@@ -367,11 +641,16 @@ function workspaceSwiplets(source) {
367
641
  }
368
642
 
369
643
  function readSwiplet(file) {
370
- 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];
371
648
  if (entry === undefined) {
372
649
  throw new Error(`${file} has no manifest.json`);
373
650
  }
374
- 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
+ );
375
654
  if (typeof manifest.id !== "string" || !idPattern.test(manifest.id)) {
376
655
  throw new Error(`${file} has no valid swiplet id in its manifest`);
377
656
  }
@@ -380,47 +659,90 @@ function readSwiplet(file) {
380
659
 
381
660
  export function sourceCommand(root, given) {
382
661
  const source = loadSource(root);
383
- 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);
384
666
  if (files.length === 0) {
385
- 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
+ );
386
670
  }
387
671
  const swiplets = new Map();
388
672
  for (const read of files.map(readSwiplet)) {
389
673
  const { id } = read.manifest;
390
674
  if (swiplets.has(id)) {
391
- 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
+ );
392
678
  }
393
679
  swiplets.set(id, read);
394
680
  }
395
- const image = source.config.image === undefined ? undefined : { name: path.basename(source.config.image), data: fs.readFileSync(path.resolve(root, source.config.image)) };
396
- 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;
397
690
  const dist = path.join(root, "dist");
398
691
  fs.rmSync(dist, { recursive: true, force: true });
399
692
  fs.mkdirSync(dist, { recursive: true });
400
- const entries = [...swiplets.keys()].sort().map(id => {
693
+ const entries = [...swiplets.keys()].sort().map((id) => {
401
694
  const { manifest, data } = swiplets.get(id);
402
695
  const name = `${id}.swiplet`;
403
696
  fs.writeFileSync(path.join(dist, name), data);
404
- 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
+ };
405
703
  });
406
704
  if (image) {
407
705
  fs.writeFileSync(path.join(dist, image.name), image.data);
408
706
  }
409
707
  const index = {
410
708
  ...(source.config.name ? { name: source.config.name } : {}),
411
- ...(Object.keys(source.localizedNames).length > 0 ? { localizedNames: source.localizedNames } : {}),
412
- ...(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
+ : {}),
413
722
  ...(image ? { image: located(image.name) } : {}),
414
723
  swiplets: entries,
415
724
  };
416
- 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
+ );
417
729
  console.log(path.relative(process.cwd(), path.join(dist, "swiplets.json")));
418
730
  }
419
731
 
420
- const idOf = name => name.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[._-]+/, "").slice(0, 128).replace(/[._-]+$/, "") || "swiplet";
732
+ const swiptRange = version.includes("-")
733
+ ? version
734
+ : `>=${version}-0 <=${version}`;
735
+
736
+ const idOf = (name) =>
737
+ name
738
+ .toLowerCase()
739
+ .replace(/[^a-z0-9._-]+/g, "-")
740
+ .replace(/^[._-]+/, "")
741
+ .slice(0, 128)
742
+ .replace(/[._-]+$/, "") || "swiplet";
421
743
 
422
744
  const templates = {
423
- "package.json": name =>
745
+ "package.json": (name) =>
424
746
  `${JSON.stringify(
425
747
  {
426
748
  name: idOf(name),
@@ -437,10 +759,15 @@ const templates = {
437
759
  scripts: { build: "swipt project build", check: "tsc" },
438
760
  dependencies: { zod: "^4.6.7" },
439
761
  devDependencies: {
440
- "@swipt/api": version,
441
- "@swipt/cli": version,
762
+ "@swipt/api": swiptRange,
763
+ "@swipt/cli": swiptRange,
442
764
  typescript: "^7.0.2",
443
- ...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
+ ),
444
771
  },
445
772
  },
446
773
  null,
@@ -450,8 +777,14 @@ const templates = {
450
777
  `${JSON.stringify(
451
778
  {
452
779
  compilerOptions: {
453
- target: "ES2022", module: "ESNext", moduleResolution: "Bundler", jsx: "react-jsx", lib: ["ES2023", "DOM"],
454
- 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,
455
788
  },
456
789
  },
457
790
  null,
@@ -470,7 +803,8 @@ export default defineExtension({
470
803
  },
471
804
  });
472
805
  `,
473
- "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";
474
808
  import { useState } from "react";
475
809
  import { actions } from "@swipt/api/swiplet";
476
810
 
@@ -494,7 +828,10 @@ export default function App() {
494
828
  `,
495
829
  };
496
830
 
497
- const quote = value => (/^[\w@%+=:,./-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`);
831
+ const quote = (value) =>
832
+ /^[\w@%+=:,./-]+$/.test(value)
833
+ ? value
834
+ : `'${value.replaceAll("'", "'\\''")}'`;
498
835
 
499
836
  export function createCommand(directory) {
500
837
  const root = path.resolve(directory);
@@ -506,5 +843,7 @@ export function createCommand(directory) {
506
843
  fs.writeFileSync(path.join(root, file), template(path.basename(root)));
507
844
  }
508
845
  const manager = process.env.npm_config_user_agent?.split("/")[0] ?? "npm";
509
- console.log(`cd ${quote(directory)} && ${manager} install && ${manager} run build`);
846
+ console.log(
847
+ `cd ${quote(directory)} && ${manager} install && ${manager} run build`,
848
+ );
510
849
  }