memory-extract 0.1.0 → 0.1.2
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/package.json +6 -6
- package/src/index.js +1 -1
- package/src/loadMemory.js +140 -21
- package/src/memoryPlay.js +246 -0
- package/src/payloadFormat.js +4 -1
- package/tools/buildPlayLauncher.mjs +30 -0
- package/tools/hostProject.mjs +129 -11
- package/tools/memoryFormat.mjs +13 -3
- package/tools/pack.mjs +155 -99
- package/tools/play.mjs +177 -0
- package/tools/playDirectory.mjs +156 -0
- package/tools/playLauncher.bundle.js +737 -0
- package/tools/playMemory.mjs +10 -0
- package/tools/unpack.mjs +18 -7
- package/tools/packCache.mjs +0 -62
- package/tools/resizeCover.mjs +0 -55
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memory-extract",
|
|
3
|
-
"description": "
|
|
3
|
+
"description": "Tool for embedding data into PNG images",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.2",
|
|
6
6
|
"author": "semigarden",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -22,14 +22,14 @@
|
|
|
22
22
|
},
|
|
23
23
|
"bin": {
|
|
24
24
|
"memory-pack": "./tools/pack.mjs",
|
|
25
|
-
"memory-unpack": "./tools/unpack.mjs"
|
|
25
|
+
"memory-unpack": "./tools/unpack.mjs",
|
|
26
|
+
"memory-play": "./tools/play.mjs"
|
|
26
27
|
},
|
|
27
28
|
"scripts": {
|
|
28
29
|
"pack": "node tools/pack.mjs",
|
|
29
30
|
"unpack": "node tools/unpack.mjs",
|
|
31
|
+
"play": "node tools/play.mjs",
|
|
32
|
+
"build:launcher": "node tools/buildPlayLauncher.mjs",
|
|
30
33
|
"test": "node --test test/memory.test.mjs"
|
|
31
|
-
},
|
|
32
|
-
"dependencies": {
|
|
33
|
-
"sharp": "^0.34.5"
|
|
34
34
|
}
|
|
35
35
|
}
|
package/src/index.js
CHANGED
package/src/loadMemory.js
CHANGED
|
@@ -3,6 +3,11 @@ import {
|
|
|
3
3
|
readMemoryFile,
|
|
4
4
|
extractMemory,
|
|
5
5
|
} from "./memoryFormat.js";
|
|
6
|
+
import {
|
|
7
|
+
buildInteractiveBrowseDocument,
|
|
8
|
+
buildMemoryBrowseListings,
|
|
9
|
+
resolveMemoryPlayMode,
|
|
10
|
+
} from "./memoryPlay.js";
|
|
6
11
|
|
|
7
12
|
const textDecoder = new TextDecoder();
|
|
8
13
|
|
|
@@ -36,6 +41,9 @@ const readInput = async (input) => {
|
|
|
36
41
|
|
|
37
42
|
const normalizeAssetPath = (value) => value.replace(/^\.\//, "");
|
|
38
43
|
|
|
44
|
+
const stripLeadingSlash = (value) =>
|
|
45
|
+
value.startsWith("/") ? value.slice(1) : value;
|
|
46
|
+
|
|
39
47
|
const guessMimeType = (filePath, record) => {
|
|
40
48
|
if (record?.mime) return record.mime;
|
|
41
49
|
if (filePath.endsWith(".html")) return "text/html";
|
|
@@ -59,7 +67,7 @@ const resolveAssetUrl = (rawPath, urlByPath) => {
|
|
|
59
67
|
|
|
60
68
|
const resolveManifestPath = (basePath, rawPath, manifestPaths) => {
|
|
61
69
|
const manifestSet = new Set(manifestPaths);
|
|
62
|
-
const normalized = normalizeAssetPath(rawPath);
|
|
70
|
+
const normalized = stripLeadingSlash(normalizeAssetPath(rawPath));
|
|
63
71
|
|
|
64
72
|
if (manifestSet.has(normalized)) {
|
|
65
73
|
return normalized;
|
|
@@ -87,9 +95,9 @@ const extractCssAssetPaths = (cssText) => {
|
|
|
87
95
|
return paths;
|
|
88
96
|
};
|
|
89
97
|
|
|
90
|
-
const collectRequiredFiles = (manifest, fileBytes) => {
|
|
98
|
+
const collectRequiredFiles = (manifest, fileBytes, entryPath) => {
|
|
91
99
|
const manifestPaths = listManifestFiles(manifest);
|
|
92
|
-
const required = new Set([
|
|
100
|
+
const required = new Set([entryPath]);
|
|
93
101
|
|
|
94
102
|
for (const filePath of manifestPaths) {
|
|
95
103
|
if (filePath.endsWith(".js") || filePath.endsWith(".css")) {
|
|
@@ -131,13 +139,61 @@ const collectRequiredFiles = (manifest, fileBytes) => {
|
|
|
131
139
|
return required;
|
|
132
140
|
};
|
|
133
141
|
|
|
134
|
-
const
|
|
142
|
+
const BLOB_LAUNCH_BASE = "http://localhost/";
|
|
143
|
+
|
|
144
|
+
const injectBlobLaunchShim = (doc) => {
|
|
145
|
+
const base = doc.createElement("base");
|
|
146
|
+
base.setAttribute("href", BLOB_LAUNCH_BASE);
|
|
147
|
+
doc.head.prepend(base);
|
|
148
|
+
|
|
149
|
+
const shim = doc.createElement("script");
|
|
150
|
+
shim.textContent = `(function () {
|
|
151
|
+
var base = ${JSON.stringify(BLOB_LAUNCH_BASE)};
|
|
152
|
+
var NativeURL = URL;
|
|
153
|
+
var invalidBase = function (value) {
|
|
154
|
+
if (!value) return true;
|
|
155
|
+
var text = String(value);
|
|
156
|
+
return text === "null" || text.indexOf("blob:") === 0;
|
|
157
|
+
};
|
|
158
|
+
URL = function (url, baseUrl) {
|
|
159
|
+
if (typeof url === "string" && url.charAt(0) === "/" && invalidBase(baseUrl)) {
|
|
160
|
+
return new NativeURL(url, base);
|
|
161
|
+
}
|
|
162
|
+
return new NativeURL(url, baseUrl);
|
|
163
|
+
};
|
|
164
|
+
URL.prototype = NativeURL.prototype;
|
|
165
|
+
URL.createObjectURL = NativeURL.createObjectURL.bind(NativeURL);
|
|
166
|
+
URL.revokeObjectURL = NativeURL.revokeObjectURL.bind(NativeURL);
|
|
167
|
+
})();`;
|
|
168
|
+
doc.head.prepend(shim);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
export const resolveLaunchAssetUrl = (rawPath, urlByPath, entryPath, manifestPaths) => {
|
|
172
|
+
const direct = resolveAssetUrl(rawPath, urlByPath);
|
|
173
|
+
if (direct) {
|
|
174
|
+
return direct;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (!rawPath || !entryPath || !manifestPaths) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const resolved = resolveManifestPath(entryPath, rawPath, manifestPaths);
|
|
182
|
+
return resolved ? urlByPath.get(resolved) ?? null : null;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const buildLaunchDocument = (html, urlByPath, entryPath, manifestPaths) => {
|
|
135
186
|
const doc = new DOMParser().parseFromString(html, "text/html");
|
|
136
187
|
|
|
137
188
|
REWRITABLE_ATTRIBUTES.forEach(([tag, attribute]) => {
|
|
138
189
|
doc.querySelectorAll(`${tag}[${attribute}]`).forEach((element) => {
|
|
139
190
|
const rawPath = element.getAttribute(attribute);
|
|
140
|
-
const assetUrl =
|
|
191
|
+
const assetUrl = resolveLaunchAssetUrl(
|
|
192
|
+
rawPath,
|
|
193
|
+
urlByPath,
|
|
194
|
+
entryPath,
|
|
195
|
+
manifestPaths
|
|
196
|
+
);
|
|
141
197
|
|
|
142
198
|
if (assetUrl) {
|
|
143
199
|
element.setAttribute(attribute, assetUrl);
|
|
@@ -151,6 +207,8 @@ const buildLaunchDocument = (html, urlByPath) => {
|
|
|
151
207
|
element.removeAttribute("crossorigin");
|
|
152
208
|
});
|
|
153
209
|
|
|
210
|
+
injectBlobLaunchShim(doc);
|
|
211
|
+
|
|
154
212
|
return `<!DOCTYPE html>\n${doc.documentElement.outerHTML}`;
|
|
155
213
|
};
|
|
156
214
|
|
|
@@ -162,37 +220,98 @@ const getManifestFileMeta = (manifest, filePath) => {
|
|
|
162
220
|
return manifest.files?.[filePath] ?? null;
|
|
163
221
|
};
|
|
164
222
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
223
|
+
const attachBlobDispose = (blob, blobUrls) => {
|
|
224
|
+
blob.dispose = () => {
|
|
225
|
+
blobUrls.forEach((url) => URL.revokeObjectURL(url));
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
return blob;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const createFileLaunchBlob = (manifest, fileBytes, filePath, blobUrls) => {
|
|
232
|
+
const fileMeta = getManifestFileMeta(manifest, filePath);
|
|
233
|
+
const fileContent = readMemoryFile(manifest, filePath, fileBytes);
|
|
234
|
+
const mime = guessMimeType(filePath, fileMeta);
|
|
235
|
+
const launchBlob = new Blob([fileContent], { type: mime });
|
|
236
|
+
|
|
237
|
+
return attachBlobDispose(launchBlob, blobUrls);
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const createBrowseLaunchBlob = (manifest, fileBytes, filePaths, blobUrls) => {
|
|
241
|
+
const fileUrlByPath = new Map();
|
|
242
|
+
|
|
243
|
+
for (const filePath of filePaths) {
|
|
244
|
+
const fileMeta = getManifestFileMeta(manifest, filePath);
|
|
245
|
+
const fileContent = readMemoryFile(manifest, filePath, fileBytes);
|
|
246
|
+
const mime = guessMimeType(filePath, fileMeta);
|
|
247
|
+
const assetUrl = URL.createObjectURL(new Blob([fileContent], { type: mime }));
|
|
248
|
+
blobUrls.push(assetUrl);
|
|
249
|
+
fileUrlByPath.set(filePath, assetUrl);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const listings = buildMemoryBrowseListings(filePaths, fileUrlByPath);
|
|
253
|
+
const launchDocument = buildInteractiveBrowseDocument(listings);
|
|
254
|
+
const launchBlob = new Blob([launchDocument], { type: "text/html" });
|
|
255
|
+
|
|
256
|
+
return attachBlobDispose(launchBlob, blobUrls);
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const createPlayLaunchBlob = (manifest, fileBytes, entryPath, blobUrls) => {
|
|
169
260
|
const urlByPath = new Map();
|
|
170
|
-
const requiredFiles = collectRequiredFiles(manifest, fileBytes);
|
|
261
|
+
const requiredFiles = collectRequiredFiles(manifest, fileBytes, entryPath);
|
|
171
262
|
|
|
172
263
|
for (const filePath of requiredFiles) {
|
|
173
264
|
const fileMeta = getManifestFileMeta(manifest, filePath);
|
|
174
265
|
const fileContent = readMemoryFile(manifest, filePath, fileBytes);
|
|
175
266
|
const mime = guessMimeType(filePath, fileMeta);
|
|
176
|
-
const
|
|
177
|
-
const assetUrl = URL.createObjectURL(assetBlob);
|
|
267
|
+
const assetUrl = URL.createObjectURL(new Blob([fileContent], { type: mime }));
|
|
178
268
|
blobUrls.push(assetUrl);
|
|
179
269
|
urlByPath.set(filePath, assetUrl);
|
|
180
270
|
urlByPath.set(normalizeAssetPath(filePath), assetUrl);
|
|
181
271
|
}
|
|
182
272
|
|
|
183
|
-
if (!requiredFiles.has(
|
|
184
|
-
|
|
185
|
-
throw new Error(`Manifest entry not found: ${manifest.entry}`);
|
|
273
|
+
if (!requiredFiles.has(entryPath)) {
|
|
274
|
+
throw new Error(`Manifest entry not found: ${entryPath}`);
|
|
186
275
|
}
|
|
187
276
|
|
|
188
|
-
const htmlBytes = readMemoryFile(manifest,
|
|
277
|
+
const htmlBytes = readMemoryFile(manifest, entryPath, fileBytes);
|
|
189
278
|
const html = textDecoder.decode(htmlBytes);
|
|
190
|
-
const
|
|
279
|
+
const manifestPaths = listManifestFiles(manifest);
|
|
280
|
+
const launchDocument = buildLaunchDocument(
|
|
281
|
+
html,
|
|
282
|
+
urlByPath,
|
|
283
|
+
entryPath,
|
|
284
|
+
manifestPaths
|
|
285
|
+
);
|
|
191
286
|
const launchBlob = new Blob([launchDocument], { type: "text/html" });
|
|
192
287
|
|
|
193
|
-
launchBlob
|
|
194
|
-
|
|
195
|
-
|
|
288
|
+
return attachBlobDispose(launchBlob, blobUrls);
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
export const createMemoryBlob = async (input) => {
|
|
292
|
+
const bytes = await readInput(input);
|
|
293
|
+
const { manifest, fileBytes } = await extractMemory(bytes);
|
|
294
|
+
const filePaths = listManifestFiles(manifest);
|
|
295
|
+
const playMode = resolveMemoryPlayMode(manifest, filePaths);
|
|
296
|
+
const blobUrls = [];
|
|
196
297
|
|
|
197
|
-
|
|
298
|
+
try {
|
|
299
|
+
if (playMode.mode === "file") {
|
|
300
|
+
return createFileLaunchBlob(manifest, fileBytes, playMode.path, blobUrls);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (playMode.mode === "browse") {
|
|
304
|
+
return createBrowseLaunchBlob(manifest, fileBytes, filePaths, blobUrls);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return createPlayLaunchBlob(
|
|
308
|
+
manifest,
|
|
309
|
+
fileBytes,
|
|
310
|
+
playMode.entry ?? manifest.entry,
|
|
311
|
+
blobUrls
|
|
312
|
+
);
|
|
313
|
+
} catch (error) {
|
|
314
|
+
blobUrls.forEach((url) => URL.revokeObjectURL(url));
|
|
315
|
+
throw error;
|
|
316
|
+
}
|
|
198
317
|
};
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
export const INDEX_NAMES = ["index.html", "index.htm"];
|
|
2
|
+
|
|
3
|
+
export const isHtmlEntry = (filePath) =>
|
|
4
|
+
filePath.endsWith(".html") || filePath.endsWith(".htm");
|
|
5
|
+
|
|
6
|
+
export const resolveMemoryPlayMode = (
|
|
7
|
+
manifest,
|
|
8
|
+
filePaths
|
|
9
|
+
) => {
|
|
10
|
+
if (filePaths.length === 1) {
|
|
11
|
+
return { mode: "file", path: filePaths[0] };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const indexEntry = filePaths.find(
|
|
15
|
+
(filePath) =>
|
|
16
|
+
filePath === "index.html" ||
|
|
17
|
+
filePath.endsWith("/index.html") ||
|
|
18
|
+
filePath === "index.htm" ||
|
|
19
|
+
filePath.endsWith("/index.htm")
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
if (indexEntry) {
|
|
23
|
+
return { mode: "play", entry: indexEntry };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (filePaths.includes(manifest.entry) && isHtmlEntry(manifest.entry)) {
|
|
27
|
+
return { mode: "play", entry: manifest.entry };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return { mode: "browse" };
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const urlPathToPrefix = (urlPath) =>
|
|
34
|
+
urlPath === "/" ? "" : urlPath.slice(1);
|
|
35
|
+
|
|
36
|
+
export const listVirtualEntries = (filePaths, urlPath = "/") => {
|
|
37
|
+
const prefix = urlPathToPrefix(urlPath);
|
|
38
|
+
const entries = new Map();
|
|
39
|
+
|
|
40
|
+
for (const filePath of filePaths) {
|
|
41
|
+
if (prefix && !filePath.startsWith(`${prefix}/`)) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const rest = prefix ? filePath.slice(prefix.length + 1) : filePath;
|
|
46
|
+
if (!rest) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const slash = rest.indexOf("/");
|
|
51
|
+
|
|
52
|
+
if (slash === -1) {
|
|
53
|
+
entries.set(rest, { name: rest, isDirectory: false });
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
entries.set(rest.slice(0, slash), {
|
|
58
|
+
name: rest.slice(0, slash),
|
|
59
|
+
isDirectory: true,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return [...entries.values()];
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const collectVirtualDirectoryPaths = (filePaths) => {
|
|
67
|
+
const dirs = new Set(["/"]);
|
|
68
|
+
|
|
69
|
+
for (const filePath of filePaths) {
|
|
70
|
+
const parts = filePath.split("/");
|
|
71
|
+
|
|
72
|
+
for (let index = 1; index < parts.length; index += 1) {
|
|
73
|
+
dirs.add(`/${parts.slice(0, index).join("/")}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return [...dirs].sort(
|
|
78
|
+
(left, right) => right.split("/").length - left.split("/").length
|
|
79
|
+
);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const parentVirtualPath = (urlPath) => {
|
|
83
|
+
if (urlPath === "/") {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const trimmed = urlPath.replace(/\/+$/, "");
|
|
88
|
+
const slash = trimmed.lastIndexOf("/");
|
|
89
|
+
return slash <= 0 ? "/" : trimmed.slice(0, slash);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const buildBrowseListingDocument = (
|
|
93
|
+
urlPath,
|
|
94
|
+
entries,
|
|
95
|
+
{ showParent = false, parentHref = "../" } = {}
|
|
96
|
+
) => {
|
|
97
|
+
const sorted = [...entries].sort((left, right) => {
|
|
98
|
+
if (left.isDirectory !== right.isDirectory) {
|
|
99
|
+
return left.isDirectory ? -1 : 1;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return left.name.localeCompare(right.name);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const rows = sorted.map((entry) => {
|
|
106
|
+
const suffix = entry.isDirectory ? "/" : "";
|
|
107
|
+
return `<li><a href="${entry.href}">${entry.name}${suffix}</a></li>`;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (showParent) {
|
|
111
|
+
rows.unshift(`<li><a href="${parentHref}">../</a></li>`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return `<!DOCTYPE html>
|
|
115
|
+
<html lang="en">
|
|
116
|
+
<head>
|
|
117
|
+
<meta charset="utf-8" />
|
|
118
|
+
<title>Index of ${urlPath}</title>
|
|
119
|
+
<style>
|
|
120
|
+
body { font-family: system-ui, sans-serif; margin: 2rem; }
|
|
121
|
+
h1 { font-size: 1.1rem; font-weight: 600; }
|
|
122
|
+
ul { list-style: none; padding: 0; }
|
|
123
|
+
li { margin: 0.35rem 0; }
|
|
124
|
+
a { text-decoration: none; }
|
|
125
|
+
a:hover { text-decoration: underline; }
|
|
126
|
+
</style>
|
|
127
|
+
</head>
|
|
128
|
+
<body>
|
|
129
|
+
<h1>Index of ${urlPath}</h1>
|
|
130
|
+
<ul>
|
|
131
|
+
${rows.join("\n ")}
|
|
132
|
+
</ul>
|
|
133
|
+
</body>
|
|
134
|
+
</html>
|
|
135
|
+
`;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export const buildMemoryBrowseListings = (filePaths, fileUrlByPath) => {
|
|
139
|
+
const listings = {};
|
|
140
|
+
|
|
141
|
+
for (const urlPath of collectVirtualDirectoryPaths(filePaths)) {
|
|
142
|
+
const prefix = urlPath === "/" ? "" : urlPath.slice(1);
|
|
143
|
+
|
|
144
|
+
listings[urlPath] = listVirtualEntries(filePaths, urlPath).map((entry) => {
|
|
145
|
+
const fullPath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
146
|
+
|
|
147
|
+
if (entry.isDirectory) {
|
|
148
|
+
return {
|
|
149
|
+
name: entry.name,
|
|
150
|
+
isDirectory: true,
|
|
151
|
+
path: `/${fullPath}`.replace(/\/+/g, "/"),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
name: entry.name,
|
|
157
|
+
isDirectory: false,
|
|
158
|
+
href: fileUrlByPath.get(fullPath) ?? "#",
|
|
159
|
+
};
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return listings;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
export const buildInteractiveBrowseDocument = (listings, rootPath = "/") => `<!DOCTYPE html>
|
|
167
|
+
<html lang="en">
|
|
168
|
+
<head>
|
|
169
|
+
<meta charset="utf-8" />
|
|
170
|
+
<title>Index of ${rootPath}</title>
|
|
171
|
+
<style>
|
|
172
|
+
body { font-family: system-ui, sans-serif; margin: 2rem; }
|
|
173
|
+
h1 { font-size: 1.1rem; font-weight: 600; }
|
|
174
|
+
ul { list-style: none; padding: 0; }
|
|
175
|
+
li { margin: 0.35rem 0; }
|
|
176
|
+
a { text-decoration: none; color: inherit; }
|
|
177
|
+
a:hover { text-decoration: underline; }
|
|
178
|
+
</style>
|
|
179
|
+
</head>
|
|
180
|
+
<body>
|
|
181
|
+
<h1 id="title">Index of ${rootPath}</h1>
|
|
182
|
+
<ul id="list"></ul>
|
|
183
|
+
<script>
|
|
184
|
+
const listings = ${JSON.stringify(listings)};
|
|
185
|
+
const title = document.getElementById("title");
|
|
186
|
+
const list = document.getElementById("list");
|
|
187
|
+
|
|
188
|
+
const parentPath = (path) => {
|
|
189
|
+
if (path === "/") return "/";
|
|
190
|
+
const trimmed = path.replace(/\\/+$/, "");
|
|
191
|
+
const slash = trimmed.lastIndexOf("/");
|
|
192
|
+
return slash <= 0 ? "/" : trimmed.slice(0, slash);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const render = (path) => {
|
|
196
|
+
const entries = listings[path] || [];
|
|
197
|
+
title.textContent = "Index of " + path;
|
|
198
|
+
list.replaceChildren();
|
|
199
|
+
|
|
200
|
+
if (path !== "/") {
|
|
201
|
+
const parentItem = document.createElement("li");
|
|
202
|
+
const parentLink = document.createElement("a");
|
|
203
|
+
parentLink.href = "#";
|
|
204
|
+
parentLink.textContent = "../";
|
|
205
|
+
parentLink.addEventListener("click", (event) => {
|
|
206
|
+
event.preventDefault();
|
|
207
|
+
render(parentPath(path));
|
|
208
|
+
});
|
|
209
|
+
parentItem.appendChild(parentLink);
|
|
210
|
+
list.appendChild(parentItem);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
entries
|
|
214
|
+
.slice()
|
|
215
|
+
.sort((left, right) => {
|
|
216
|
+
if (left.isDirectory !== right.isDirectory) {
|
|
217
|
+
return left.isDirectory ? -1 : 1;
|
|
218
|
+
}
|
|
219
|
+
return left.name.localeCompare(right.name);
|
|
220
|
+
})
|
|
221
|
+
.forEach((entry) => {
|
|
222
|
+
const item = document.createElement("li");
|
|
223
|
+
const link = document.createElement("a");
|
|
224
|
+
|
|
225
|
+
if (entry.isDirectory) {
|
|
226
|
+
link.href = "#";
|
|
227
|
+
link.textContent = entry.name + "/";
|
|
228
|
+
link.addEventListener("click", (event) => {
|
|
229
|
+
event.preventDefault();
|
|
230
|
+
render(entry.path);
|
|
231
|
+
});
|
|
232
|
+
} else {
|
|
233
|
+
link.href = entry.href;
|
|
234
|
+
link.textContent = entry.name;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
item.appendChild(link);
|
|
238
|
+
list.appendChild(item);
|
|
239
|
+
});
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
render(${JSON.stringify(rootPath)});
|
|
243
|
+
</script>
|
|
244
|
+
</body>
|
|
245
|
+
</html>
|
|
246
|
+
`;
|
package/src/payloadFormat.js
CHANGED
|
@@ -83,6 +83,7 @@ export const buildV2Manifest = ({
|
|
|
83
83
|
files,
|
|
84
84
|
kind = "web-app",
|
|
85
85
|
runtime = "iframe-sandbox",
|
|
86
|
+
source = "",
|
|
86
87
|
}) => {
|
|
87
88
|
const sortedPaths = Object.keys(files).sort();
|
|
88
89
|
|
|
@@ -93,6 +94,7 @@ export const buildV2Manifest = ({
|
|
|
93
94
|
name,
|
|
94
95
|
entry,
|
|
95
96
|
runtime,
|
|
97
|
+
...(source ? { source } : {}),
|
|
96
98
|
files: sortedPaths.map((filePath) => {
|
|
97
99
|
const bytes = toUint8Array(files[filePath]);
|
|
98
100
|
return {
|
|
@@ -106,13 +108,14 @@ export const buildV2Manifest = ({
|
|
|
106
108
|
};
|
|
107
109
|
};
|
|
108
110
|
|
|
109
|
-
export const encodeV2Archive = ({ name, entry, files, kind, runtime }) => {
|
|
111
|
+
export const encodeV2Archive = ({ name, entry, files, kind, runtime, source }) => {
|
|
110
112
|
const { manifest, sortedPaths } = buildV2Manifest({
|
|
111
113
|
name,
|
|
112
114
|
entry,
|
|
113
115
|
files,
|
|
114
116
|
kind,
|
|
115
117
|
runtime,
|
|
118
|
+
source,
|
|
116
119
|
});
|
|
117
120
|
const json = new TextEncoder().encode(JSON.stringify(manifest));
|
|
118
121
|
const header = new Uint8Array(4);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const toolsDir = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const packageRoot = path.dirname(toolsDir);
|
|
8
|
+
const outfile = path.join(toolsDir, "playLauncher.bundle.js");
|
|
9
|
+
|
|
10
|
+
const result = spawnSync(
|
|
11
|
+
"npx",
|
|
12
|
+
[
|
|
13
|
+
"--yes",
|
|
14
|
+
"esbuild",
|
|
15
|
+
path.join(packageRoot, "src/index.js"),
|
|
16
|
+
"--bundle",
|
|
17
|
+
"--platform=browser",
|
|
18
|
+
"--format=iife",
|
|
19
|
+
"--global-name=MemoryExtract",
|
|
20
|
+
"--target=chrome109,firefox115,safari16",
|
|
21
|
+
`--outfile=${outfile}`,
|
|
22
|
+
],
|
|
23
|
+
{ cwd: packageRoot, stdio: "inherit" }
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
if (result.status !== 0) {
|
|
27
|
+
process.exit(result.status ?? 1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
console.log("Built tools/playLauncher.bundle.js");
|