@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/client.mjs +103 -30
- package/connection.mjs +86 -26
- package/coredevice.mjs +68 -0
- package/device.mjs +274 -90
- package/ignore.mjs +17 -5
- package/metro.mjs +235 -65
- package/output.mjs +32 -7
- package/package.json +8 -3
- package/project.mjs +394 -106
- package/proto/system_pb.js +1 -1
- package/sideload/trust.js +235 -0
- package/sideload.mjs +649 -0
- package/swipt.mjs +374 -52
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(
|
|
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 = [
|
|
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(
|
|
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(
|
|
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(
|
|
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 (
|
|
65
|
-
|
|
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 (
|
|
73
|
-
|
|
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 =>
|
|
76
|
-
|
|
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 {
|
|
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(
|
|
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 =>
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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: [
|
|
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: [
|
|
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,
|
|
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(
|
|
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(
|
|
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) =>
|
|
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:
|
|
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(
|
|
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(
|
|
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(
|
|
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": {
|
|
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 (
|
|
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
|
|
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(
|
|
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(
|
|
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",
|
|
232
|
-
"
|
|
233
|
-
"
|
|
234
|
-
"
|
|
235
|
-
"
|
|
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,
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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(
|
|
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(
|
|
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)?.[
|
|
428
|
+
const definition = (module.exports.default ?? module.exports)?.[
|
|
429
|
+
definitionKey
|
|
430
|
+
];
|
|
257
431
|
if (definition === undefined) {
|
|
258
|
-
throw new Error(
|
|
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(
|
|
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(
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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(
|
|
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(
|
|
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"), {
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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 (
|
|
379
|
-
|
|
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 (
|
|
382
|
-
|
|
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(
|
|
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)
|
|
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(
|
|
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" })
|
|
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(
|
|
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 =
|
|
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(
|
|
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(
|
|
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 =
|
|
445
|
-
|
|
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 {
|
|
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
|
|
461
|
-
|
|
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(
|
|
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("-")
|
|
732
|
+
const swiptRange = version.includes("-")
|
|
733
|
+
? version
|
|
734
|
+
: `>=${version}-0 <=${version}`;
|
|
470
735
|
|
|
471
|
-
const idOf = name =>
|
|
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(
|
|
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",
|
|
505
|
-
|
|
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":
|
|
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 =
|
|
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(
|
|
846
|
+
console.log(
|
|
847
|
+
`cd ${quote(directory)} && ${manager} install && ${manager} run build`,
|
|
848
|
+
);
|
|
561
849
|
}
|