@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/client.mjs +103 -30
- package/connection.mjs +86 -26
- package/device.mjs +268 -90
- package/ignore.mjs +17 -5
- package/metro.mjs +235 -65
- package/output.mjs +32 -7
- package/package.json +3 -2
- package/project.mjs +442 -103
- package/swipt.mjs +300 -54
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(
|
|
15
|
-
|
|
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 = [
|
|
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(
|
|
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(
|
|
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(
|
|
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 (
|
|
59
|
-
|
|
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 (
|
|
67
|
-
|
|
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 =>
|
|
70
|
-
|
|
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 {
|
|
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(
|
|
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 =>
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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: [
|
|
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: [
|
|
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 =
|
|
231
|
+
out.app = scoped.code;
|
|
128
232
|
}
|
|
129
233
|
if (out.app === "" && out.extension === "") {
|
|
130
|
-
throw new Error(
|
|
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(
|
|
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) =>
|
|
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:
|
|
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(
|
|
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(
|
|
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": {
|
|
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
|
-
"
|
|
183
|
-
"
|
|
184
|
-
"
|
|
185
|
-
"
|
|
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,
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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(
|
|
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(
|
|
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)?.[
|
|
428
|
+
const definition = (module.exports.default ?? module.exports)?.[
|
|
429
|
+
definitionKey
|
|
430
|
+
];
|
|
208
431
|
if (definition === undefined) {
|
|
209
|
-
throw new Error(
|
|
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(
|
|
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(
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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(
|
|
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(
|
|
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"), {
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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 (
|
|
330
|
-
|
|
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 (
|
|
333
|
-
|
|
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(
|
|
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)
|
|
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(
|
|
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" })
|
|
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(
|
|
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 =
|
|
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(
|
|
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(
|
|
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 =
|
|
396
|
-
|
|
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 {
|
|
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
|
|
412
|
-
|
|
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(
|
|
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
|
|
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":
|
|
441
|
-
"@swipt/cli":
|
|
762
|
+
"@swipt/api": swiptRange,
|
|
763
|
+
"@swipt/cli": swiptRange,
|
|
442
764
|
typescript: "^7.0.2",
|
|
443
|
-
...Object.fromEntries(
|
|
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",
|
|
454
|
-
|
|
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":
|
|
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 =
|
|
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(
|
|
846
|
+
console.log(
|
|
847
|
+
`cd ${quote(directory)} && ${manager} install && ${manager} run build`,
|
|
848
|
+
);
|
|
510
849
|
}
|