@swipt/cli 0.1.0-nightly.34 → 0.1.0-nightly.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/metro.mjs +251 -0
  2. package/package.json +9 -3
  3. package/project.mjs +16 -3
package/metro.mjs ADDED
@@ -0,0 +1,251 @@
1
+ import fs from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ import { transformAsync } from "@babel/core";
7
+ import { loadProjectEnv } from "@expo/env";
8
+ import { imageSize } from "image-size";
9
+
10
+ const require = createRequire(import.meta.url);
11
+ const cliDirectory = path.dirname(fileURLToPath(import.meta.url));
12
+
13
+ const sourceExtensions = ["js", "jsx", "json", "ts", "tsx", "cjs", "mjs"];
14
+ const assetExtensions = [
15
+ "bmp", "gif", "jpg", "jpeg", "png", "psd", "svg", "webp", "xml", "m4v", "mov", "mp4", "mpeg", "mpg", "webm", "aac", "aiff", "caf",
16
+ "m4a", "mp3", "wav", "html", "pdf", "yaml", "yml", "otf", "ttf", "zip",
17
+ ];
18
+ const imageExtensions = new Set(["png", "jpg", "jpeg", "bmp", "gif", "webp", "psd", "svg", "tiff", "ktx"]);
19
+ const babelConfigNames = [
20
+ ".babelrc", ".babelrc.js", ".babelrc.cjs", ".babelrc.mjs", ".babelrc.json", ".babelrc.cts",
21
+ "babel.config.js", "babel.config.cjs", "babel.config.mjs", "babel.config.json", "babel.config.cts", "babel.config.ts", "babel.config.mts",
22
+ ];
23
+ const contextModes = ["sync", "eager", "lazy", "lazy-once"];
24
+ const scaleSuffixes = ["", "@1x", "@1.5x", "@2x", "@3x", "@4x"];
25
+ const platformSuffixes = ["", ".ios", ".native"];
26
+ const platformRank = { ios: 2, native: 1 };
27
+
28
+ const posix = file => file.split(path.sep).join("/");
29
+ const escape = text => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
30
+
31
+ export function loadEnvironment(root) {
32
+ loadProjectEnv(root, { mode: "production", silent: true });
33
+ process.env.BABEL_ENV ??= "production";
34
+ }
35
+
36
+ function defaultPreset(root) {
37
+ try {
38
+ return createRequire(path.join(root, "package.json")).resolve("expo/internal/babel-preset");
39
+ } catch {
40
+ return require.resolve("babel-preset-expo");
41
+ }
42
+ }
43
+
44
+ function contextFiles(directory, recursive) {
45
+ if (!fs.existsSync(directory)) {
46
+ return [];
47
+ }
48
+ const extensions = new Set([...sourceExtensions, ...assetExtensions]);
49
+ return fs.readdirSync(directory, { recursive })
50
+ .map(posix)
51
+ .filter(file => extensions.has(path.extname(file).slice(1)) && fs.statSync(path.join(directory, file)).isFile())
52
+ .sort();
53
+ }
54
+
55
+ function requireContext({ types: t, template }) {
56
+ const argument = (call, index, fallback, expected, check) => {
57
+ if (call.node.arguments[index] === undefined) {
58
+ if (fallback !== undefined) {
59
+ return fallback;
60
+ }
61
+ } else {
62
+ const { confident, value } = call.get(`arguments.${index}`).evaluate();
63
+ if (confident && check(value)) {
64
+ return value;
65
+ }
66
+ }
67
+ throw call.buildCodeFrameError(`argument ${index + 1} of require.context must be ${expected}`);
68
+ };
69
+ const pattern = (call, index) => {
70
+ const node = call.node.arguments[index];
71
+ if (node === undefined) {
72
+ return /^\.\/.*$/;
73
+ }
74
+ if (!t.isRegExpLiteral(node)) {
75
+ throw call.buildCodeFrameError(`argument ${index + 1} of require.context must be a regular expression literal`);
76
+ }
77
+ return new RegExp(node.pattern, node.flags.replace(/[gy]/g, ""));
78
+ };
79
+ return {
80
+ name: "swipt-require-context",
81
+ visitor: {
82
+ CallExpression(call, state) {
83
+ const { callee } = call.node;
84
+ if (
85
+ !t.isMemberExpression(callee) || !t.isIdentifier(callee.object, { name: "require" }) || call.scope.hasBinding("require")
86
+ || !(callee.computed ? t.isStringLiteral(callee.property, { value: "context" }) : t.isIdentifier(callee.property, { name: "context" }))
87
+ ) {
88
+ return;
89
+ }
90
+ const from = path.dirname(state.filename);
91
+ const directory = path.resolve(from, argument(call, 0, undefined, "a string", value => typeof value === "string"));
92
+ const recursive = argument(call, 1, true, "a boolean", value => typeof value === "boolean");
93
+ const filter = pattern(call, 2);
94
+ const mode = argument(call, 3, "sync", `one of ${contextModes.join(", ")}`, value => contextModes.includes(value));
95
+ const entries = contextFiles(directory, recursive)
96
+ .map(file => `./${file}`)
97
+ .filter(key => filter.test(key))
98
+ .map(key => {
99
+ const target = posix(path.relative(from, path.join(directory, key)));
100
+ return `${JSON.stringify(key)}: () => require(${JSON.stringify(target.startsWith(".") ? target : `./${target}`)})`;
101
+ });
102
+ call.replaceWith(template.expression.ast(`(() => {
103
+ const modules = { ${entries.join(", ")} };
104
+ const context = key => {
105
+ if (!Object.prototype.hasOwnProperty.call(modules, key)) {
106
+ throw Object.assign(new Error("Cannot find module '" + key + "'"), { code: "MODULE_NOT_FOUND" });
107
+ }
108
+ return ${mode === "sync" ? "modules[key]()" : "Promise.resolve().then(modules[key])"};
109
+ };
110
+ context.keys = () => Object.keys(modules);
111
+ context.resolve = key => key;
112
+ context.id = ${JSON.stringify(posix(path.relative(state.cwd, directory)) || ".")};
113
+ return context;
114
+ })()`));
115
+ },
116
+ },
117
+ };
118
+ }
119
+
120
+ const loaderOf = file => {
121
+ const extension = path.extname(file);
122
+ return extension === ".tsx" ? "tsx" : [".ts", ".mts", ".cts"].includes(extension) ? "ts" : "jsx";
123
+ };
124
+
125
+ function archivedAsset(root, file) {
126
+ const relative = path.relative(root, file).split(path.sep);
127
+ return relative[0] === "assets" ? { path: relative.slice(1).join("/"), bundled: false } : { path: relative.map(part => (part === ".." ? "__" : part)).join("/"), bundled: true };
128
+ }
129
+
130
+ function assetVariants(resolved) {
131
+ const directory = path.dirname(resolved);
132
+ const extension = path.extname(resolved);
133
+ const stem = path.basename(resolved, extension).replace(/(\.(ios|native))$/, "").replace(/@[\d.]+x$/, "");
134
+ const pattern = new RegExp(`^${escape(stem)}(?:@([\\d.]+)x)?(?:\\.(ios|native))?${escape(extension)}$`);
135
+ const scales = new Map();
136
+ for (const name of fs.readdirSync(directory)) {
137
+ const match = pattern.exec(name);
138
+ if (match === null) {
139
+ continue;
140
+ }
141
+ const scale = match[1] === undefined ? 1 : Number(match[1]);
142
+ const rank = platformRank[match[2]] ?? 0;
143
+ if (Number.isFinite(scale) && (scales.get(scale)?.rank ?? -1) < rank) {
144
+ scales.set(scale, { rank, file: path.join(directory, name) });
145
+ }
146
+ }
147
+ return [...scales].sort(([a], [b]) => a - b).map(([scale, { file }]) => ({ scale, file }));
148
+ }
149
+
150
+ function assetModule(root, variants, assets) {
151
+ const [first] = variants;
152
+ const extension = path.extname(first.file).slice(1);
153
+ const size = imageExtensions.has(extension) ? imageSize(fs.readFileSync(first.file)) : undefined;
154
+ const files = variants.map(({ scale, file }) => {
155
+ const archived = archivedAsset(root, file);
156
+ if (archived.bundled) {
157
+ assets.set(archived.path, file);
158
+ }
159
+ return [scale, archived.path];
160
+ });
161
+ return `const { PixelRatio } = require("react-native");
162
+ const { assets } = require("@swipt/api/file-system");
163
+ const files = ${JSON.stringify(files)};
164
+ const ratio = PixelRatio.get();
165
+ const [scale, file] = files.find(([scale]) => scale >= ratio) ?? files[files.length - 1];
166
+ module.exports = { uri: assets.file(file).uri, scale${size ? `, width: ${size.width / first.scale}, height: ${size.height / first.scale}` : ""} };
167
+ `;
168
+ }
169
+
170
+ function checkWorklets(root) {
171
+ let plugin;
172
+ try {
173
+ plugin = require.resolve("react-native-worklets/plugin", { paths: [root] });
174
+ } catch {
175
+ return;
176
+ }
177
+ const { version } = JSON.parse(fs.readFileSync(path.join(path.dirname(plugin), "..", "package.json"), "utf8"));
178
+ const { version: expected } = require("react-native-worklets/package.json");
179
+ if (version !== expected) {
180
+ throw new Error(`the project has react-native-worklets ${version}, but Swipt runs ${expected}; install react-native-worklets@${expected} or remove it`);
181
+ }
182
+ }
183
+
184
+ export function metro(root, { assets, sourcemap }) {
185
+ checkWorklets(root);
186
+ const configName = babelConfigNames.find(name => fs.existsSync(path.join(root, name)));
187
+ const babel = {
188
+ cwd: root,
189
+ root,
190
+ babelrc: false,
191
+ configFile: false,
192
+ sourceType: "unambiguous",
193
+ envName: "production",
194
+ compact: false,
195
+ retainLines: true,
196
+ sourceMaps: sourcemap ? "inline" : false,
197
+ ...(configName ? { extends: path.join(root, configName) } : { presets: [defaultPreset(root)] }),
198
+ plugins: [requireContext],
199
+ };
200
+ const caller = {
201
+ name: "metro", bundler: "metro", platform: "ios", engine: "hermes", isDev: false, isServer: false, isReactServer: false, baseUrl: "",
202
+ routerRoot: "app", projectRoot: root, isHMREnabled: true, metroSourceType: "module", supportsStaticESM: true,
203
+ };
204
+ const internal = Symbol("internal");
205
+ return {
206
+ name: "swiplet-metro",
207
+ setup(build) {
208
+ build.onResolve({ filter: /^@babel\/runtime\// }, async args => {
209
+ if (args.pluginData === internal) {
210
+ return undefined;
211
+ }
212
+ const options = { kind: args.kind, importer: args.importer, pluginData: internal };
213
+ const found = await build.resolve(args.path, { ...options, resolveDir: args.resolveDir });
214
+ return found.errors.length === 0 ? found : build.resolve(args.path, { ...options, resolveDir: cliDirectory });
215
+ });
216
+ if (assets) {
217
+ build.onResolve({ filter: new RegExp(`\\.(${assetExtensions.join("|")})$`) }, async args => {
218
+ if (args.pluginData === internal) {
219
+ return undefined;
220
+ }
221
+ const extension = path.extname(args.path);
222
+ const stem = args.path.slice(0, -extension.length);
223
+ let first;
224
+ for (const scale of scaleSuffixes) {
225
+ for (const platform of platformSuffixes) {
226
+ const found = await build.resolve(`${stem}${scale}${platform}${extension}`, { kind: args.kind, importer: args.importer, resolveDir: args.resolveDir, pluginData: internal });
227
+ if (found.errors.length === 0) {
228
+ return { path: found.path, namespace: "swiplet-asset", pluginData: assetVariants(found.path) };
229
+ }
230
+ first ??= found;
231
+ }
232
+ }
233
+ return { errors: first.errors };
234
+ });
235
+ build.onLoad({ filter: /.*/, namespace: "swiplet-asset" }, args => ({
236
+ contents: assetModule(root, args.pluginData, assets),
237
+ loader: "js",
238
+ resolveDir: root,
239
+ }));
240
+ }
241
+ build.onLoad({ filter: /\.[cm]?[jt]sx?$/, namespace: "file" }, async args => {
242
+ const result = await transformAsync(await fs.promises.readFile(args.path, "utf8"), {
243
+ ...babel,
244
+ filename: args.path,
245
+ caller: { ...caller, isNodeModule: args.path.includes("node_modules") },
246
+ });
247
+ return result === null ? undefined : { contents: result.code, loader: loaderOf(args.path), resolveDir: path.dirname(args.path) };
248
+ });
249
+ },
250
+ };
251
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swipt/cli",
3
- "version": "0.1.0-nightly.34",
3
+ "version": "0.1.0-nightly.36",
4
4
  "license": "GPL-3.0-or-later",
5
5
  "repository": {
6
6
  "type": "git",
@@ -19,13 +19,19 @@
19
19
  "access": "public"
20
20
  },
21
21
  "dependencies": {
22
+ "@babel/core": "^7.29.7",
23
+ "@babel/runtime": "^7.29.10",
22
24
  "@bufbuild/protobuf": "^2.16.0",
25
+ "@expo/env": "^2.5.3",
23
26
  "@jridgewell/trace-mapping": "^0.3.31",
27
+ "babel-preset-expo": "~58.0.22",
24
28
  "bonjour-service": "^1.4.4",
25
- "esbuild": "^0.28.2"
29
+ "esbuild": "^0.28.2",
30
+ "image-size": "^2.0.4",
31
+ "react-native-worklets": "0.13.0"
26
32
  },
27
33
  "peerDependencies": {
28
- "@swipt/api": "0.1.0-nightly.34"
34
+ "@swipt/api": "0.1.0-nightly.36"
29
35
  },
30
36
  "devDependencies": {
31
37
  "@bufbuild/protoc-gen-es": "^2.16.0"
package/project.mjs CHANGED
@@ -9,6 +9,8 @@ import vm from "node:vm";
9
9
 
10
10
  import { build } from "esbuild";
11
11
 
12
+ import { loadEnvironment, metro } from "./metro.mjs";
13
+
12
14
  const { version } = JSON.parse(fs.readFileSync(new URL("package.json", import.meta.url), "utf8"));
13
15
  const { peerDependencies } = createRequire(import.meta.url)("@swipt/api/package.json");
14
16
 
@@ -16,6 +18,7 @@ const appModules = [
16
18
  "react",
17
19
  "react/jsx-runtime",
18
20
  "react-native",
21
+ "react-native-worklets",
19
22
  "expo",
20
23
  "expo-localization",
21
24
  "@expo/ui",
@@ -78,7 +81,8 @@ const typeImport = extension => ({
78
81
  });
79
82
 
80
83
  async function bundle(project) {
81
- const out = { app: "", extension: "", extensionMap: "" };
84
+ const out = { app: "", extension: "", extensionMap: "", assets: new Map() };
85
+ loadEnvironment(project.root);
82
86
  if (project.extension) {
83
87
  const result = await build({
84
88
  entryPoints: [project.extension],
@@ -93,6 +97,7 @@ async function bundle(project) {
93
97
  sourcesContent: false,
94
98
  write: false,
95
99
  logLevel: "silent",
100
+ plugins: [metro(project.root, { sourcemap: true })],
96
101
  });
97
102
  out.extension = result.outputFiles.find(file => file.path.endsWith(".js")).text;
98
103
  out.extensionMap = result.outputFiles.find(file => file.path.endsWith(".map")).text;
@@ -112,7 +117,7 @@ async function bundle(project) {
112
117
  external: appModules,
113
118
  write: false,
114
119
  logLevel: "silent",
115
- plugins: project.extension ? [typeImport(project.extension)] : [],
120
+ plugins: [...(project.extension ? [typeImport(project.extension)] : []), metro(project.root, { assets: out.assets })],
116
121
  });
117
122
  out.app = result.outputFiles[0].text;
118
123
  }
@@ -256,10 +261,18 @@ export async function buildProject(root) {
256
261
  if (fs.existsSync(path.join(root, "assets"))) {
257
262
  fs.cpSync(path.join(root, "assets"), path.join(folder, "assets"), { recursive: true, filter: source => !path.basename(source).startsWith(".") });
258
263
  }
264
+ for (const [name, source] of output.assets) {
265
+ const target = path.join(folder, "assets", name);
266
+ if (fs.existsSync(target)) {
267
+ throw new Error(`${path.relative(root, source)} and ${path.join("assets", name)} would both be assets/${name} in the swiplet`);
268
+ }
269
+ fs.mkdirSync(path.dirname(target), { recursive: true });
270
+ fs.copyFileSync(source, target);
271
+ }
259
272
  const archive = path.join(dist, `${manifest.id}.swiplet`);
260
273
  fs.rmSync(archive, { force: true });
261
274
  fs.rmSync(path.join(dist, `${manifest.id}.zip`), { force: true });
262
- execFileSync("zip", ["-qr", "-n", storedSuffixes, archive, manifest.id, "-x", "*.map"], { cwd: dist });
275
+ execFileSync("zip", ["-qr", "-n", storedSuffixes, archive, ...fs.readdirSync(folder), "-x", "*.map"], { cwd: folder });
263
276
  return { id: manifest.id, archive, digest: folderDigest(folder) };
264
277
  }
265
278