memory-extract 0.1.2 → 0.1.3

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 CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "memory-extract",
3
3
  "description": "Tool for embedding data into PNG images",
4
4
  "license": "MIT",
5
- "version": "0.1.2",
5
+ "version": "0.1.3",
6
6
  "author": "semigarden",
7
7
  "repository": {
8
8
  "type": "git",
@@ -29,7 +29,6 @@
29
29
  "pack": "node tools/pack.mjs",
30
30
  "unpack": "node tools/unpack.mjs",
31
31
  "play": "node tools/play.mjs",
32
- "build:launcher": "node tools/buildPlayLauncher.mjs",
33
32
  "test": "node --test test/memory.test.mjs"
34
33
  }
35
34
  }
package/tools/play.mjs CHANGED
@@ -6,10 +6,12 @@ import os from "node:os";
6
6
  import path from "node:path";
7
7
  import { fileURLToPath, pathToFileURL } from "node:url";
8
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");
9
+ import { extractMemory, listManifestFiles, readMemoryFile } from "./memoryFormat.mjs";
10
+ import { resolveMemoryPlayMode } from "./playMemory.mjs";
11
+ import {
12
+ startMemoryBrowseServer,
13
+ startMemoryPlayServer,
14
+ } from "./playMemoryServer.mjs";
13
15
 
14
16
  export const resolvePlayEntry = (manifest, filePaths) => {
15
17
  if (filePaths.includes(manifest.entry)) {
@@ -25,59 +27,6 @@ export const resolvePlayEntry = (manifest, filePaths) => {
25
27
 
26
28
  export { resolveMemoryPlayMode } from "./playMemory.mjs";
27
29
 
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
30
  export const isCliEntry = (entryArg = process.argv[1]) => {
82
31
  try {
83
32
  return (
@@ -141,23 +90,94 @@ export const resolvePlayInput = (argv, projectPath = process.cwd()) => {
141
90
  return resolveProjectPath(projectPath, input);
142
91
  };
143
92
 
144
- const playMemoryPng = async (input, label = input) => {
145
- const pngBuffer = await readFile(input);
146
- ensureMemoryPng(pngBuffer, label);
93
+ const waitForShutdown = (server) =>
94
+ new Promise((resolve) => {
95
+ const shutdown = () => {
96
+ server.close(() => resolve());
97
+ };
147
98
 
148
- const { pagePath, pageUrl } = await writePlayLauncherPage(pngBuffer);
99
+ process.once("SIGINT", shutdown);
100
+ process.once("SIGTERM", shutdown);
101
+ });
149
102
 
103
+ const runServerSession = async ({ input, label, openUrl, server }) => {
150
104
  console.log(`Playing memory from ${input}`);
151
- console.log(`Launcher: ${pageUrl}`);
105
+ console.log(`Local server: ${openUrl}`);
106
+
107
+ try {
108
+ await openBrowser(openUrl);
109
+ } catch (error) {
110
+ console.warn(`Could not open browser: ${error.message ?? error}`);
111
+ console.warn(`Open this URL manually: ${openUrl}`);
112
+ }
113
+
114
+ console.log("Press Ctrl+C to stop.");
115
+ await waitForShutdown(server);
116
+ };
117
+
118
+ const openSingleFile = async (manifest, fileBytes, filePath) => {
119
+ const tempDir = await mkdtemp(path.join(os.tmpdir(), "memory-play-file-"));
120
+ const outPath = path.join(tempDir, path.basename(filePath));
121
+ await writeFile(outPath, readMemoryFile(manifest, filePath, fileBytes));
122
+
123
+ const fileUrl = pathToFileURL(outPath).href;
124
+
125
+ console.log(`Opening ${filePath} from memory`);
126
+ console.log(`File: ${outPath}`);
152
127
 
153
128
  try {
154
- await openBrowser(pageUrl);
129
+ await openBrowser(fileUrl);
155
130
  } catch (error) {
156
131
  console.warn(`Could not open browser: ${error.message ?? error}`);
157
- console.warn(`Open this file manually: ${pagePath}`);
132
+ console.warn(`Open this file manually: ${outPath}`);
158
133
  }
159
134
  };
160
135
 
136
+ const playMemoryPng = async (input, label = input) => {
137
+ const pngBuffer = await readFile(input);
138
+ ensureMemoryPng(pngBuffer, label);
139
+
140
+ const { manifest, fileBytes } = extractMemory(pngBuffer);
141
+ const filePaths = listManifestFiles(manifest);
142
+ const playMode = resolveMemoryPlayMode(manifest, filePaths);
143
+
144
+ if (playMode.mode === "file") {
145
+ await openSingleFile(manifest, fileBytes, playMode.path);
146
+ return;
147
+ }
148
+
149
+ if (playMode.mode === "play") {
150
+ const { server, baseUrl } = await startMemoryPlayServer(
151
+ manifest,
152
+ fileBytes,
153
+ filePaths,
154
+ playMode.entry
155
+ );
156
+ const openUrl = new URL(playMode.entry, baseUrl).href;
157
+
158
+ await runServerSession({
159
+ input,
160
+ label,
161
+ openUrl,
162
+ server,
163
+ });
164
+ return;
165
+ }
166
+
167
+ const { server, baseUrl } = await startMemoryBrowseServer(
168
+ manifest,
169
+ fileBytes,
170
+ filePaths
171
+ );
172
+
173
+ await runServerSession({
174
+ input,
175
+ label,
176
+ openUrl: baseUrl,
177
+ server,
178
+ });
179
+ };
180
+
161
181
  const main = async () => {
162
182
  const projectPath = process.cwd();
163
183
  const argv = process.argv.slice(2);
@@ -0,0 +1,162 @@
1
+ import http from "node:http";
2
+ import path from "node:path";
3
+ import { guessMimeType } from "../src/payloadFormat.js";
4
+ import {
5
+ buildBrowseListingDocument,
6
+ listVirtualEntries,
7
+ } from "../src/memoryPlay.js";
8
+ import { listManifestFiles, readMemoryFile } from "./memoryFormat.mjs";
9
+
10
+ export const normalizeUrlPath = (pathname) => {
11
+ const decoded = decodeURIComponent(pathname || "/");
12
+ const trimmed = decoded.replace(/\/+$/, "");
13
+ return trimmed || "/";
14
+ };
15
+
16
+ export const urlPathToManifestPath = (urlPath) =>
17
+ urlPath === "/" ? "" : urlPath.slice(1);
18
+
19
+ const hrefForEntry = (urlPath, name, isDirectory) => {
20
+ const base = urlPath === "/" ? "" : urlPath;
21
+ const href = `${base}/${name}${isDirectory ? "/" : ""}`.replace(/\/+/g, "/");
22
+ return encodeURI(href);
23
+ };
24
+
25
+ export const resolveManifestPathForUrl = (urlPath, manifestPaths, entryPath = "") => {
26
+ const manifestSet = new Set(manifestPaths);
27
+ const direct = urlPathToManifestPath(urlPath);
28
+
29
+ if (direct && manifestSet.has(direct)) {
30
+ return direct;
31
+ }
32
+
33
+ const entryDir = entryPath.includes("/")
34
+ ? entryPath.slice(0, entryPath.lastIndexOf("/"))
35
+ : "";
36
+
37
+ if (direct && entryDir) {
38
+ const joined = `${entryDir}/${direct}`.replace(/\/+/g, "/");
39
+ if (manifestSet.has(joined)) {
40
+ return joined;
41
+ }
42
+ }
43
+
44
+ return null;
45
+ };
46
+
47
+ const startHttpServer = async (handler) => {
48
+ const server = http.createServer(handler);
49
+
50
+ await new Promise((resolve, reject) => {
51
+ server.once("error", reject);
52
+ server.listen(0, "127.0.0.1", resolve);
53
+ });
54
+
55
+ const { port } = server.address();
56
+
57
+ return {
58
+ server,
59
+ baseUrl: `http://127.0.0.1:${port}/`,
60
+ };
61
+ };
62
+
63
+ const serveManifestFile = (response, manifest, fileBytes, manifestPath) => {
64
+ const body = readMemoryFile(manifest, manifestPath, fileBytes);
65
+ response.writeHead(200, {
66
+ "Content-Type": guessMimeType(manifestPath),
67
+ });
68
+ response.end(body);
69
+ };
70
+
71
+ export const createMemoryPlayHandler = (manifest, fileBytes, filePaths, entryPath) => {
72
+ const manifestPaths = filePaths ?? listManifestFiles(manifest);
73
+
74
+ return (request, response) => {
75
+ try {
76
+ const url = new URL(request.url ?? "/", "http://localhost");
77
+ const urlPath = normalizeUrlPath(url.pathname);
78
+
79
+ if (urlPath === "/") {
80
+ response.writeHead(302, {
81
+ Location: `/${entryPath}`,
82
+ });
83
+ response.end();
84
+ return;
85
+ }
86
+
87
+ const manifestPath = resolveManifestPathForUrl(
88
+ urlPath,
89
+ manifestPaths,
90
+ entryPath
91
+ );
92
+
93
+ if (!manifestPath) {
94
+ response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
95
+ response.end("Not found");
96
+ return;
97
+ }
98
+
99
+ serveManifestFile(response, manifest, fileBytes, manifestPath);
100
+ } catch (error) {
101
+ response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
102
+ response.end(error.message ?? "Not found");
103
+ }
104
+ };
105
+ };
106
+
107
+ export const createMemoryBrowseHandler = (manifest, fileBytes, filePaths) => {
108
+ const manifestPaths = filePaths ?? listManifestFiles(manifest);
109
+
110
+ return (request, response) => {
111
+ try {
112
+ const url = new URL(request.url ?? "/", "http://localhost");
113
+ const urlPath = normalizeUrlPath(url.pathname);
114
+ const manifestPath = resolveManifestPathForUrl(urlPath, manifestPaths);
115
+
116
+ if (manifestPath) {
117
+ serveManifestFile(response, manifest, fileBytes, manifestPath);
118
+ return;
119
+ }
120
+
121
+ const entries = listVirtualEntries(manifestPaths, urlPath);
122
+
123
+ if (entries.length === 0) {
124
+ response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
125
+ response.end("Not found");
126
+ return;
127
+ }
128
+
129
+ const listingEntries = entries.map((entry) => ({
130
+ name: entry.name,
131
+ isDirectory: entry.isDirectory,
132
+ href: hrefForEntry(urlPath, entry.name, entry.isDirectory),
133
+ }));
134
+ const parentHref =
135
+ urlPath === "/"
136
+ ? null
137
+ : encodeURI(`${normalizeUrlPath(path.posix.dirname(urlPath) || "/")}/`.replace(/\/+/g, "/"));
138
+ const html = buildBrowseListingDocument(urlPath, listingEntries, {
139
+ showParent: urlPath !== "/",
140
+ parentHref: parentHref ?? "../",
141
+ });
142
+
143
+ response.writeHead(200, {
144
+ "Content-Type": "text/html; charset=utf-8",
145
+ });
146
+ response.end(html);
147
+ } catch (error) {
148
+ response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
149
+ response.end(error.message ?? "Not found");
150
+ }
151
+ };
152
+ };
153
+
154
+ export const startMemoryPlayServer = async (manifest, fileBytes, filePaths, entryPath) => {
155
+ const handler = createMemoryPlayHandler(manifest, fileBytes, filePaths, entryPath);
156
+ return startHttpServer(handler);
157
+ };
158
+
159
+ export const startMemoryBrowseServer = async (manifest, fileBytes, filePaths) => {
160
+ const handler = createMemoryBrowseHandler(manifest, fileBytes, filePaths);
161
+ return startHttpServer(handler);
162
+ };