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/tools/play.mjs ADDED
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import { realpathSync } from "node:fs";
4
+ import { mkdtemp, readFile, writeFile } from "node:fs/promises";
5
+ import os from "node:os";
6
+ import path from "node:path";
7
+ import { fileURLToPath, pathToFileURL } from "node:url";
8
+ import { resolveProjectPath } from "./hostProject.mjs";
9
+ import { extractMemory } from "./memoryFormat.mjs";
10
+
11
+ const PLAY_DIR = path.dirname(fileURLToPath(import.meta.url));
12
+ const PLAY_BUNDLE_PATH = path.join(PLAY_DIR, "playLauncher.bundle.js");
13
+
14
+ export const resolvePlayEntry = (manifest, filePaths) => {
15
+ if (filePaths.includes(manifest.entry)) {
16
+ return manifest.entry;
17
+ }
18
+
19
+ const indexEntry = filePaths.find(
20
+ (filePath) => filePath === "index.html" || filePath.endsWith("/index.html")
21
+ );
22
+
23
+ return indexEntry ?? "";
24
+ };
25
+
26
+ export { resolveMemoryPlayMode } from "./playMemory.mjs";
27
+
28
+ export const buildPlayLauncherPage = (pngBuffer, bundleScript) => `<!DOCTYPE html>
29
+ <html lang="en">
30
+ <head>
31
+ <meta charset="utf-8" />
32
+ <title>Loading memory...</title>
33
+ <style>
34
+ body {
35
+ font-family: system-ui, sans-serif;
36
+ margin: 2rem;
37
+ }
38
+ </style>
39
+ </head>
40
+ <body>
41
+ <p id="status">Loading memory...</p>
42
+ <script>${bundleScript}</script>
43
+ <script>
44
+ (async () => {
45
+ const status = document.getElementById("status");
46
+
47
+ try {
48
+ const pngBytes = Uint8Array.from(
49
+ atob(${JSON.stringify(pngBuffer.toString("base64"))}),
50
+ (char) => char.charCodeAt(0)
51
+ );
52
+ const png = new Blob([pngBytes], { type: "image/png" });
53
+ const launch = await MemoryExtract.createMemoryBlob(png);
54
+ location.replace(URL.createObjectURL(launch));
55
+ } catch (error) {
56
+ status.textContent = error.message ?? "Failed to launch memory.";
57
+ }
58
+ })();
59
+ </script>
60
+ </body>
61
+ </html>
62
+ `;
63
+
64
+ export const writePlayLauncherPage = async (pngBuffer, bundleScript = "") => {
65
+ const script = bundleScript || await readFile(PLAY_BUNDLE_PATH, "utf8");
66
+ const tempDir = await mkdtemp(path.join(os.tmpdir(), "memory-play-"));
67
+ const pagePath = path.join(tempDir, "index.html");
68
+
69
+ await writeFile(
70
+ pagePath,
71
+ buildPlayLauncherPage(pngBuffer, script),
72
+ "utf8"
73
+ );
74
+
75
+ return {
76
+ pagePath,
77
+ pageUrl: pathToFileURL(pagePath).href,
78
+ };
79
+ };
80
+
81
+ export const isCliEntry = (entryArg = process.argv[1]) => {
82
+ try {
83
+ return (
84
+ realpathSync(entryArg) ===
85
+ realpathSync(fileURLToPath(import.meta.url))
86
+ );
87
+ } catch {
88
+ return false;
89
+ }
90
+ };
91
+
92
+ const spawnDetached = (command, args, options = {}) =>
93
+ new Promise((resolve, reject) => {
94
+ const child = spawn(command, args, {
95
+ detached: true,
96
+ stdio: "ignore",
97
+ ...options,
98
+ });
99
+
100
+ child.once("error", reject);
101
+ child.unref();
102
+ resolve();
103
+ });
104
+
105
+ const openBrowser = async (url) => {
106
+ if (process.env.BROWSER) {
107
+ await spawnDetached(process.env.BROWSER, [url]);
108
+ return;
109
+ }
110
+
111
+ const platform = process.platform;
112
+
113
+ if (platform === "darwin") {
114
+ await spawnDetached("open", [url]);
115
+ return;
116
+ }
117
+
118
+ if (platform === "win32") {
119
+ await spawnDetached("cmd", ["/c", "start", "", url], { shell: true });
120
+ return;
121
+ }
122
+
123
+ await spawnDetached("xdg-open", [url]);
124
+ };
125
+
126
+ export const ensureMemoryPng = (buffer, filePath) => {
127
+ try {
128
+ extractMemory(buffer);
129
+ } catch {
130
+ throw new Error(`${filePath} doesn't contain memory`);
131
+ }
132
+ };
133
+
134
+ export const resolvePlayInput = (argv, projectPath = process.cwd()) => {
135
+ const input = argv.find((arg) => !arg.startsWith("-"));
136
+
137
+ if (!input) {
138
+ throw new Error("Specify a memory PNG path.");
139
+ }
140
+
141
+ return resolveProjectPath(projectPath, input);
142
+ };
143
+
144
+ const playMemoryPng = async (input, label = input) => {
145
+ const pngBuffer = await readFile(input);
146
+ ensureMemoryPng(pngBuffer, label);
147
+
148
+ const { pagePath, pageUrl } = await writePlayLauncherPage(pngBuffer);
149
+
150
+ console.log(`Playing memory from ${input}`);
151
+ console.log(`Launcher: ${pageUrl}`);
152
+
153
+ try {
154
+ await openBrowser(pageUrl);
155
+ } catch (error) {
156
+ console.warn(`Could not open browser: ${error.message ?? error}`);
157
+ console.warn(`Open this file manually: ${pagePath}`);
158
+ }
159
+ };
160
+
161
+ const main = async () => {
162
+ const projectPath = process.cwd();
163
+ const argv = process.argv.slice(2);
164
+ const label = argv.find((arg) => !arg.startsWith("-")) ?? "";
165
+ const input = resolvePlayInput(argv, projectPath);
166
+
167
+ await playMemoryPng(input, label);
168
+ };
169
+
170
+ const isMain = isCliEntry();
171
+
172
+ if (isMain) {
173
+ main().catch((error) => {
174
+ console.error(error.message ?? error);
175
+ process.exit(1);
176
+ });
177
+ }
@@ -0,0 +1,156 @@
1
+ import { readdir, readFile, stat } from "node:fs/promises";
2
+ import http from "node:http";
3
+ import path from "node:path";
4
+ import { guessMimeType, resolveSafePath } from "../src/payloadFormat.js";
5
+
6
+ const INDEX_CANDIDATES = ["index.html", "index.htm"];
7
+
8
+ export const findDirectoryIndex = async (dirPath) => {
9
+ for (const name of INDEX_CANDIDATES) {
10
+ try {
11
+ const entryStat = await stat(path.join(dirPath, name));
12
+ if (entryStat.isFile()) {
13
+ return name;
14
+ }
15
+ } catch {
16
+ continue;
17
+ }
18
+ }
19
+
20
+ return "";
21
+ };
22
+
23
+ const normalizeUrlPath = (pathname) => {
24
+ const decoded = decodeURIComponent(pathname || "/");
25
+ const trimmed = decoded.replace(/\/+$/, "");
26
+ return trimmed || "/";
27
+ };
28
+
29
+ const urlPathToFsPath = (rootDir, urlPath) => {
30
+ const relative = urlPath === "/"
31
+ ? ""
32
+ : urlPath.slice(1).split("/").join(path.sep);
33
+
34
+ if (!relative) {
35
+ return rootDir;
36
+ }
37
+
38
+ return resolveSafePath(rootDir, relative, path.sep, path.resolve);
39
+ };
40
+
41
+ const hrefForEntry = (urlPath, name, isDirectory) => {
42
+ const base = urlPath === "/" ? "" : urlPath;
43
+ const href = `${base}/${name}${isDirectory ? "/" : ""}`.replace(/\/+/g, "/");
44
+ return encodeURI(href);
45
+ };
46
+
47
+ export const renderDirectoryListing = (urlPath, entries, { showParent = false, parentHref = "../" } = {}) => {
48
+ const sorted = [...entries].sort((left, right) => {
49
+ if (left.isDirectory() !== right.isDirectory()) {
50
+ return left.isDirectory() ? -1 : 1;
51
+ }
52
+
53
+ return left.name.localeCompare(right.name);
54
+ });
55
+
56
+ const rows = sorted.map((entry) => {
57
+ const suffix = entry.isDirectory() ? "/" : "";
58
+ const href = hrefForEntry(urlPath, entry.name, entry.isDirectory());
59
+ return `<li><a href="${href}">${entry.name}${suffix}</a></li>`;
60
+ });
61
+
62
+ if (showParent) {
63
+ rows.unshift(`<li><a href="${parentHref}">../</a></li>`);
64
+ }
65
+
66
+ return `<!DOCTYPE html>
67
+ <html lang="en">
68
+ <head>
69
+ <meta charset="utf-8" />
70
+ <title>Index of ${urlPath}</title>
71
+ <style>
72
+ body { font-family: system-ui, sans-serif; margin: 2rem; }
73
+ h1 { font-size: 1.1rem; font-weight: 600; }
74
+ ul { list-style: none; padding: 0; }
75
+ li { margin: 0.35rem 0; }
76
+ a { text-decoration: none; }
77
+ a:hover { text-decoration: underline; }
78
+ </style>
79
+ </head>
80
+ <body>
81
+ <h1>Index of ${urlPath}</h1>
82
+ <ul>
83
+ ${rows.join("\n ")}
84
+ </ul>
85
+ </body>
86
+ </html>
87
+ `;
88
+ };
89
+
90
+ export const createDirectoryPlayServer = (rootDir) =>
91
+ http.createServer(async (request, response) => {
92
+ try {
93
+ const url = new URL(request.url ?? "/", "http://localhost");
94
+ const urlPath = normalizeUrlPath(url.pathname);
95
+ const fsPath = urlPathToFsPath(rootDir, urlPath);
96
+ const entryStat = await stat(fsPath);
97
+
98
+ if (entryStat.isFile()) {
99
+ const body = await readFile(fsPath);
100
+ response.writeHead(200, {
101
+ "Content-Type": guessMimeType(path.basename(fsPath)),
102
+ });
103
+ response.end(body);
104
+ return;
105
+ }
106
+
107
+ const indexName = await findDirectoryIndex(fsPath);
108
+ const wantsDirectoryListing = url.searchParams.has("dir");
109
+
110
+ if (indexName && !wantsDirectoryListing) {
111
+ const body = await readFile(path.join(fsPath, indexName));
112
+ response.writeHead(200, {
113
+ "Content-Type": guessMimeType(indexName),
114
+ });
115
+ response.end(body);
116
+ return;
117
+ }
118
+
119
+ const entries = await readdir(fsPath, { withFileTypes: true });
120
+ const parentHref = urlPath === "/"
121
+ ? null
122
+ : encodeURI(`${path.posix.dirname(urlPath) || ""}/`.replace(/\/+/g, "/"));
123
+ const html = renderDirectoryListing(urlPath, entries, {
124
+ showParent: urlPath !== "/",
125
+ parentHref: parentHref ?? "../",
126
+ });
127
+
128
+ response.writeHead(200, {
129
+ "Content-Type": "text/html; charset=utf-8",
130
+ });
131
+ response.end(html);
132
+ } catch (error) {
133
+ response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
134
+ response.end(error.message ?? "Not found");
135
+ }
136
+ });
137
+
138
+ export const startDirectoryPlayServer = async (rootDir) => {
139
+ const server = createDirectoryPlayServer(rootDir);
140
+ const indexName = await findDirectoryIndex(rootDir);
141
+
142
+ await new Promise((resolve, reject) => {
143
+ server.once("error", reject);
144
+ server.listen(0, "127.0.0.1", resolve);
145
+ });
146
+
147
+ const { port } = server.address();
148
+ const baseUrl = `http://127.0.0.1:${port}/`;
149
+
150
+ return {
151
+ server,
152
+ baseUrl,
153
+ indexName,
154
+ mode: indexName ? "run" : "browse",
155
+ };
156
+ };