memory-extract 0.1.0 → 0.1.1

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.
@@ -1,7 +1,68 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
 
4
- const DEFAULT_EXCLUDE = ["**/*.map"];
4
+ const DEFAULT_EXCLUDE = [
5
+ "**/*.map",
6
+ "node_modules",
7
+ "node_modules/**",
8
+ ".git",
9
+ ".git/**",
10
+ ];
11
+ const DEFAULT_PATH = ".";
12
+
13
+ const isFlag = (arg) => arg.startsWith("-");
14
+
15
+ export const parsePackArgv = (argv, defaultInput = DEFAULT_PATH) => {
16
+ let inputPath = defaultInput;
17
+ let outName = "";
18
+ let index = 0;
19
+
20
+ const first = argv[index];
21
+ if (first && !isFlag(first)) {
22
+ inputPath = first;
23
+ index += 1;
24
+
25
+ const second = argv[index];
26
+ if (second && !isFlag(second)) {
27
+ outName = second;
28
+ index += 1;
29
+ }
30
+ }
31
+
32
+ return {
33
+ input: path.resolve(inputPath),
34
+ outName,
35
+ argv: argv.slice(index),
36
+ };
37
+ };
38
+
39
+ export const parseUnpackArgv = (argv) => {
40
+ const positional = [];
41
+ const remaining = [];
42
+
43
+ for (const arg of argv) {
44
+ if (isFlag(arg)) {
45
+ remaining.push(arg);
46
+ continue;
47
+ }
48
+
49
+ if (positional.length < 2) {
50
+ positional.push(arg);
51
+ continue;
52
+ }
53
+
54
+ remaining.push(arg);
55
+ }
56
+
57
+ return {
58
+ input: positional[0] ? path.resolve(positional[0]) : "",
59
+ out: positional[1] ? path.resolve(positional[1]) : "",
60
+ argv: remaining,
61
+ };
62
+ };
63
+
64
+ export const resolveProjectPath = (rootDir, targetPath = "") =>
65
+ path.resolve(rootDir, targetPath);
5
66
 
6
67
  export const loadHostProject = async (rootDir = process.cwd()) => {
7
68
  try {
@@ -12,34 +73,91 @@ export const loadHostProject = async (rootDir = process.cwd()) => {
12
73
  const dist = path.resolve(rootDir, memory.dist ?? "dist");
13
74
  const out = memory.out
14
75
  ? path.resolve(rootDir, memory.out)
15
- : path.join(dist, `${name}.png`);
76
+ : "";
16
77
 
17
78
  return {
79
+ root: rootDir,
18
80
  name,
19
81
  dist,
20
82
  out,
21
83
  cover: memory.cover ? path.resolve(rootDir, memory.cover) : "",
22
84
  entry: memory.entry ?? "index.html",
23
85
  exclude: [...new Set([...DEFAULT_EXCLUDE, ...(memory.exclude ?? [])])],
24
- coverMaxSize: memory.coverMaxSize ?? 512,
25
- coverResize: memory.coverResize !== false,
26
- cache: memory.cache !== false,
27
86
  unpackDir: memory.unpackDir
28
87
  ? path.resolve(rootDir, memory.unpackDir)
29
- : path.join(dist, "unpack"),
88
+ : "",
30
89
  };
31
90
  } catch {
32
91
  return {
92
+ root: rootDir,
33
93
  name: "memory",
34
94
  dist: path.resolve(rootDir, "dist"),
35
- out: path.resolve(rootDir, "dist", "memory.png"),
95
+ out: "",
36
96
  cover: "",
37
97
  entry: "index.html",
38
98
  exclude: [...DEFAULT_EXCLUDE],
39
- coverMaxSize: 512,
40
- coverResize: true,
41
- cache: true,
42
- unpackDir: path.resolve(rootDir, "dist", "unpack"),
99
+ unpackDir: "",
43
100
  };
44
101
  }
45
102
  };
103
+
104
+ export const resolveInputIdentity = (inputPath, { isFile = false } = {}) => {
105
+ const resolved = path.resolve(inputPath);
106
+ const baseName = path.basename(resolved);
107
+
108
+ if (isFile) {
109
+ const extension = path.extname(baseName);
110
+ return extension ? baseName.slice(0, -extension.length) : baseName;
111
+ }
112
+
113
+ return baseName;
114
+ };
115
+
116
+ export const prefixFilePaths = (files, prefix) => {
117
+ if (!prefix) {
118
+ return files;
119
+ }
120
+
121
+ const normalized = prefix.replace(/\\/g, "/");
122
+
123
+ return Object.fromEntries(
124
+ Object.entries(files).map(([filePath, buffer]) => [
125
+ filePath ? `${normalized}/${filePath}` : normalized,
126
+ buffer,
127
+ ])
128
+ );
129
+ };
130
+
131
+ export const resolvePackSource = (projectPath, inputPath, { isFile = false } = {}) => {
132
+ if (isFile) {
133
+ return "";
134
+ }
135
+
136
+ const relative = path.relative(projectPath, path.resolve(inputPath));
137
+
138
+ if (!relative || relative === ".") {
139
+ return "";
140
+ }
141
+
142
+ return relative.split(path.sep).join("/");
143
+ };
144
+
145
+ export const resolveUnpackDir = ({
146
+ manifestSource,
147
+ explicitOut,
148
+ host,
149
+ }) => {
150
+ if (explicitOut) {
151
+ return path.resolve(explicitOut);
152
+ }
153
+
154
+ if (host.unpackDir) {
155
+ return host.unpackDir;
156
+ }
157
+
158
+ if (manifestSource) {
159
+ return path.join(host.root ?? process.cwd(), manifestSource);
160
+ }
161
+
162
+ return host.root ?? process.cwd();
163
+ };
@@ -115,12 +115,14 @@ export const buildManifest = ({
115
115
  files,
116
116
  kind = "web-app",
117
117
  runtime = "iframe-sandbox",
118
+ source = "",
118
119
  }) => ({
119
120
  v: 1,
120
121
  kind,
121
122
  name,
122
123
  entry,
123
124
  runtime,
125
+ ...(source ? { source } : {}),
124
126
  files,
125
127
  });
126
128
 
@@ -136,6 +138,7 @@ export const encodeMemoryPayload = ({
136
138
  files,
137
139
  kind = "web-app",
138
140
  runtime = "iframe-sandbox",
141
+ source = "",
139
142
  version = MEMORY_VERSION,
140
143
  }) => {
141
144
  if (version === 1) {
@@ -145,7 +148,14 @@ export const encodeMemoryPayload = ({
145
148
  encodedFiles[filePath] = encodeManifestFile(buffer, filePath);
146
149
  }
147
150
 
148
- const manifest = buildManifest({ name, entry, files: encodedFiles, kind, runtime });
151
+ const manifest = buildManifest({
152
+ name,
153
+ entry,
154
+ files: encodedFiles,
155
+ kind,
156
+ runtime,
157
+ source,
158
+ });
149
159
 
150
160
  return {
151
161
  version: 1,
@@ -158,8 +168,8 @@ export const encodeMemoryPayload = ({
158
168
  throw new Error(`Unsupported memory version: ${version}`);
159
169
  }
160
170
 
161
- const archive = encodeV2Archive({ name, entry, files, kind, runtime });
162
- const { manifest } = buildV2Manifest({ name, entry, files, kind, runtime });
171
+ const archive = encodeV2Archive({ name, entry, files, kind, runtime, source });
172
+ const { manifest } = buildV2Manifest({ name, entry, files, kind, runtime, source });
163
173
 
164
174
  return {
165
175
  version: 2,
package/tools/pack.mjs CHANGED
@@ -3,13 +3,13 @@ import { readdir, readFile, rename, stat, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { createBlankCover } from "./blankCover.mjs";
5
5
  import { matchesAnyGlob } from "./glob.mjs";
6
- import { loadHostProject } from "./hostProject.mjs";
7
6
  import {
8
- buildPackFingerprint,
9
- isPackUpToDate,
10
- savePackCache,
11
- } from "./packCache.mjs";
12
- import { prepareCover } from "./resizeCover.mjs";
7
+ loadHostProject,
8
+ parsePackArgv,
9
+ prefixFilePaths,
10
+ resolveInputIdentity,
11
+ resolveProjectPath,
12
+ } from "./hostProject.mjs";
13
13
  import {
14
14
  embedMemory,
15
15
  encodeMemoryPayload,
@@ -27,6 +27,21 @@ const shouldSkipPath = (absolutePath, skipPaths) => {
27
27
  });
28
28
  };
29
29
 
30
+ const shouldExcludePath = (relativePath, excludePatterns) => {
31
+ if (matchesAnyGlob(relativePath, excludePatterns)) {
32
+ return true;
33
+ }
34
+
35
+ return excludePatterns.some((pattern) => {
36
+ if (!pattern.endsWith("/**")) {
37
+ return false;
38
+ }
39
+
40
+ const root = pattern.slice(0, -3);
41
+ return relativePath === root || relativePath.startsWith(`${root}/`);
42
+ });
43
+ };
44
+
30
45
  const collectFiles = async (dir, baseDir, skipPaths, excludePatterns) => {
31
46
  const entries = await readdir(dir, { withFileTypes: true });
32
47
  const files = {};
@@ -40,16 +55,33 @@ const collectFiles = async (dir, baseDir, skipPaths, excludePatterns) => {
40
55
  continue;
41
56
  }
42
57
 
58
+ const relativePath = path.relative(baseDir, absolutePath)
59
+ .split(path.sep)
60
+ .join("/");
61
+
43
62
  if (entry.isDirectory()) {
63
+ if (shouldExcludePath(relativePath, excludePatterns)) {
64
+ continue;
65
+ }
66
+
44
67
  subdirs.push(absolutePath);
45
68
  continue;
46
69
  }
47
70
 
48
- const relativePath = path.relative(baseDir, absolutePath)
49
- .split(path.sep)
50
- .join("/");
71
+ if (entry.isSymbolicLink()) {
72
+ const linkStat = await stat(absolutePath);
73
+
74
+ if (linkStat.isDirectory()) {
75
+ if (shouldExcludePath(relativePath, excludePatterns)) {
76
+ continue;
77
+ }
51
78
 
52
- if (matchesAnyGlob(relativePath, excludePatterns)) {
79
+ subdirs.push(absolutePath);
80
+ continue;
81
+ }
82
+ }
83
+
84
+ if (shouldExcludePath(relativePath, excludePatterns)) {
53
85
  continue;
54
86
  }
55
87
 
@@ -75,67 +107,120 @@ const collectFiles = async (dir, baseDir, skipPaths, excludePatterns) => {
75
107
  };
76
108
 
77
109
  const buildSkipPaths = (options) =>
78
- [options.out, path.join(path.resolve(options.dist), "unpack")].map((entry) =>
79
- path.resolve(entry)
80
- );
110
+ [options.out, `${options.out}.tmp`].map((entry) => path.resolve(entry));
111
+
112
+ const resolveEntry = (files, preferredEntry) => {
113
+ const filePaths = Object.keys(files).sort();
114
+
115
+ if (filePaths.length === 0) {
116
+ throw new Error("No files to pack.");
117
+ }
118
+
119
+ if (files[preferredEntry]) {
120
+ return preferredEntry;
121
+ }
122
+
123
+ const htmlEntry = filePaths.find((filePath) => filePath.endsWith(".html"));
124
+ if (htmlEntry) {
125
+ return htmlEntry;
126
+ }
127
+
128
+ return filePaths[0];
129
+ };
130
+
131
+ const collectInput = async (inputPath, skipPaths, excludePatterns) => {
132
+ let inputStat;
133
+
134
+ try {
135
+ inputStat = await stat(inputPath);
136
+ } catch {
137
+ throw new Error(`Input not found at ${inputPath}.`);
138
+ }
139
+
140
+ if (inputStat.isFile()) {
141
+ const relativePath = path.basename(inputPath);
142
+ return {
143
+ files: {
144
+ [relativePath]: await readFile(inputPath),
145
+ },
146
+ entry: relativePath,
147
+ source: "",
148
+ };
149
+ }
150
+
151
+ if (inputStat.isDirectory()) {
152
+ return {
153
+ files: await collectFiles(inputPath, inputPath, skipPaths, excludePatterns),
154
+ entry: "",
155
+ source: "",
156
+ };
157
+ }
158
+
159
+ throw new Error(`Input is not a file or directory: ${inputPath}`);
160
+ };
161
+
162
+ const ensurePngExtension = (fileName) =>
163
+ fileName.toLowerCase().endsWith(".png") ? fileName : `${fileName}.png`;
81
164
 
82
165
  const parseArgs = async (argv) => {
83
- const rootDir = process.cwd();
84
- const host = await loadHostProject(rootDir);
166
+ const projectPath = process.cwd();
167
+ const host = await loadHostProject(projectPath);
168
+ const { input: defaultInput, outName, argv: packArgv } = parsePackArgv(argv);
85
169
  const options = {
86
- root: rootDir,
87
- dist: host.dist,
170
+ root: projectPath,
171
+ input: defaultInput,
172
+ outName,
88
173
  cover: host.cover,
89
174
  out: host.out,
90
175
  name: host.name,
91
176
  entry: host.entry,
92
177
  exclude: [...host.exclude],
93
- coverMaxSize: host.coverMaxSize,
94
- coverResize: host.coverResize,
95
- cache: host.cache,
96
- force: false,
97
- check: false,
178
+ entryExplicit: false,
179
+ nameExplicit: false,
180
+ outExplicit: Boolean(host.out),
98
181
  };
99
182
 
100
- for (let index = 0; index < argv.length; index += 1) {
101
- const arg = argv[index];
102
- if (arg === "--root") {
103
- options.root = path.resolve(argv[index + 1] ?? "");
104
- index += 1;
105
- } else if (arg === "--dist") {
106
- options.dist = path.resolve(argv[index + 1] ?? "");
183
+ for (let index = 0; index < packArgv.length; index += 1) {
184
+ const arg = packArgv[index];
185
+ if (arg === "--input" || arg === "--dist") {
186
+ options.input = resolveProjectPath(projectPath, packArgv[index + 1] ?? "");
107
187
  index += 1;
108
188
  } else if (arg === "--cover") {
109
- options.cover = path.resolve(argv[index + 1] ?? "");
189
+ options.cover = resolveProjectPath(projectPath, packArgv[index + 1] ?? "");
110
190
  index += 1;
111
191
  } else if (arg === "--out") {
112
- options.out = path.resolve(argv[index + 1] ?? "");
192
+ options.out = resolveProjectPath(projectPath, packArgv[index + 1] ?? "");
193
+ options.outExplicit = true;
113
194
  index += 1;
114
195
  } else if (arg === "--name") {
115
- options.name = argv[index + 1] ?? options.name;
196
+ options.name = packArgv[index + 1] ?? options.name;
197
+ options.nameExplicit = true;
116
198
  index += 1;
117
199
  } else if (arg === "--entry") {
118
- options.entry = argv[index + 1] ?? options.entry;
200
+ options.entry = packArgv[index + 1] ?? options.entry;
201
+ options.entryExplicit = true;
119
202
  index += 1;
120
203
  } else if (arg === "--exclude") {
121
- options.exclude.push(argv[index + 1] ?? "");
122
- index += 1;
123
- } else if (arg === "--cover-max-size") {
124
- options.coverMaxSize = Number(argv[index + 1] ?? options.coverMaxSize);
204
+ options.exclude.push(packArgv[index + 1] ?? "");
125
205
  index += 1;
126
- } else if (arg === "--no-cover-resize") {
127
- options.coverResize = false;
128
- } else if (arg === "--no-cache") {
129
- options.cache = false;
130
- } else if (arg === "--force") {
131
- options.force = true;
132
- } else if (arg === "--check") {
133
- options.check = true;
134
206
  }
135
207
  }
136
208
 
137
209
  options.exclude = [...new Set(options.exclude.filter(Boolean))];
138
210
 
211
+ if (options.outName && !options.outExplicit) {
212
+ options.out = resolveProjectPath(
213
+ projectPath,
214
+ ensurePngExtension(options.outName)
215
+ );
216
+ options.outExplicit = true;
217
+ }
218
+
219
+ if (options.outName && !options.nameExplicit) {
220
+ options.name = options.outName;
221
+ options.nameExplicit = true;
222
+ }
223
+
139
224
  return options;
140
225
  };
141
226
 
@@ -143,29 +228,21 @@ const formatKb = (bytes) => `${(bytes / 1024).toFixed(1)} KB`;
143
228
 
144
229
  const main = async () => {
145
230
  const options = await parseArgs(process.argv.slice(2));
231
+ const inputStat = await stat(options.input);
232
+ const inputIsFile = inputStat.isFile();
233
+ const identity = resolveInputIdentity(options.input, { isFile: inputIsFile });
146
234
 
147
- try {
148
- await stat(options.dist);
149
- } catch {
150
- throw new Error(`Build output not found at ${options.dist}. Run your build first.`);
235
+ if (!options.nameExplicit) {
236
+ options.name = identity;
237
+ }
238
+
239
+ if (!options.outExplicit) {
240
+ options.out = path.join(options.root, `${identity}.png`);
151
241
  }
152
242
 
153
243
  let cover;
154
244
  if (options.cover) {
155
- const sourceCover = await readFile(options.cover);
156
- const prepared = await prepareCover(sourceCover, {
157
- maxSize: options.coverMaxSize,
158
- resize: options.coverResize,
159
- });
160
-
161
- cover = prepared.buffer;
162
-
163
- if (prepared.changed && prepared.from && prepared.info) {
164
- console.log(
165
- `Resized cover from ${prepared.from.width}x${prepared.from.height} (${formatKb(prepared.from.bytes)}) to ${prepared.info.width}x${prepared.info.height} (${formatKb(prepared.info.bytes)})`
166
- );
167
- }
168
-
245
+ cover = await readFile(options.cover);
169
246
  console.log(`Using cover image ${options.cover}`);
170
247
  } else {
171
248
  cover = createBlankCover();
@@ -173,48 +250,31 @@ const main = async () => {
173
250
  }
174
251
 
175
252
  const skipPaths = buildSkipPaths(options);
176
- const files = await collectFiles(
177
- options.dist,
178
- options.dist,
253
+ const collected = await collectInput(
254
+ options.input,
179
255
  skipPaths,
180
256
  options.exclude
181
257
  );
258
+ let { files, entry: fileEntry } = collected;
259
+ const preferredEntry = inputIsFile
260
+ ? options.entry
261
+ : path.posix.join(identity, options.entry);
182
262
 
183
- if (!files[options.entry]) {
184
- throw new Error(
185
- `Entry not found in dist: ${options.entry}. Checked ${Object.keys(files).length} files.`
186
- );
187
- }
188
-
189
- const fingerprint = buildPackFingerprint(options, files, cover);
190
-
191
- if (options.check && !options.cache) {
192
- throw new Error("Pack cache is disabled. Remove --check or enable cache in memory config.");
263
+ if (!inputIsFile) {
264
+ files = prefixFilePaths(files, identity);
193
265
  }
194
266
 
195
- const upToDate =
196
- options.cache &&
197
- !options.force &&
198
- (await isPackUpToDate(options.root, fingerprint, options.out));
267
+ const entry = fileEntry || resolveEntry(files, preferredEntry);
199
268
 
200
- if (options.check) {
201
- if (upToDate) {
202
- console.log(`Memory pack is up to date: ${options.out}`);
203
- return;
204
- }
205
-
206
- console.error(`Memory pack is stale: ${options.out}`);
207
- process.exit(1);
208
- }
209
-
210
- if (upToDate) {
211
- console.log(`Memory unchanged, skipping pack: ${options.out}`);
212
- return;
269
+ if (options.entryExplicit && !files[options.entry]) {
270
+ throw new Error(
271
+ `Entry not found: ${options.entry}. Checked ${Object.keys(files).length} files.`
272
+ );
213
273
  }
214
274
 
215
275
  const { payload, version } = encodeMemoryPayload({
216
276
  name: options.name,
217
- entry: options.entry,
277
+ entry,
218
278
  files,
219
279
  });
220
280
  const memory = embedMemory(cover, payload, version);
@@ -223,12 +283,8 @@ const main = async () => {
223
283
  await writeFile(tempPath, memory);
224
284
  await rename(tempPath, options.out);
225
285
 
226
- if (options.cache) {
227
- await savePackCache(options.root, fingerprint);
228
- }
229
-
230
286
  console.log(
231
- `Packed ${Object.keys(files).length} files into ${options.out} (${formatKb(memory.length)})`
287
+ `Packed ${Object.keys(files).length} files from ${options.input} into ${options.out} (${formatKb(memory.length)})`
232
288
  );
233
289
  console.log(` cover: ${formatKb(cover.length)}, payload: ${formatKb(payload.length)}, format: v${version}`);
234
290
  };