@pagepocket/cli 0.11.1 → 0.13.0

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 (31) hide show
  1. package/dist/commands/archive.js +46 -161
  2. package/dist/commands/plugin/set.js +1 -3
  3. package/dist/commands/strategy/doctor.js +6 -6
  4. package/dist/commands/strategy/ls.js +32 -8
  5. package/dist/commands/view.js +1 -1
  6. package/dist/services/config-service.js +8 -8
  7. package/dist/services/plugin-installer.js +10 -10
  8. package/dist/services/plugin-store.js +11 -13
  9. package/dist/services/strategy/builtin-strategy-registry.js +12 -21
  10. package/dist/services/strategy/strategy-analyze.js +6 -6
  11. package/dist/services/strategy/strategy-config.js +4 -4
  12. package/dist/services/strategy/strategy-fetch.js +2 -4
  13. package/dist/services/strategy/strategy-io.js +3 -9
  14. package/dist/services/strategy/strategy-normalize.js +43 -7
  15. package/dist/services/strategy/strategy-pack-read.js +2 -4
  16. package/dist/services/strategy/strategy-pack-store.js +1 -2
  17. package/dist/services/strategy/strategy-service.js +63 -54
  18. package/dist/services/units/unit-store.js +1 -1
  19. package/dist/services/units/unit-validate.js +4 -10
  20. package/dist/services/user-packages/parse-pinned-spec.js +3 -7
  21. package/dist/services/user-packages/user-package-installer.js +5 -5
  22. package/dist/services/user-packages/user-package-store.js +8 -8
  23. package/dist/units/network-observer-unit.js +1 -1
  24. package/dist/utils/archive-strategy.js +138 -0
  25. package/dist/utils/array.js +1 -3
  26. package/dist/utils/parse-plugin-spec.js +1 -1
  27. package/dist/vendor/content-reader.css +1 -0
  28. package/dist/vendor/content-reader.iife.js +60 -0
  29. package/dist/view-main-content.js +40 -0
  30. package/dist/view.js +71 -33
  31. package/package.json +21 -19
@@ -1,139 +1,11 @@
1
- import { createRequire } from "node:module";
2
- import { pathToFileURL } from "node:url";
3
1
  import { Args, Command, Flags } from "@oclif/core";
4
2
  import { ga, ns, PagePocket } from "@pagepocket/lib";
5
3
  import chalk from "chalk";
6
- import { loadConfiguredPlugins } from "../services/load-configured-plugins.js";
4
+ import ora from "ora";
7
5
  import { ConfigService } from "../services/config-service.js";
8
- import { readBuiltinStrategy, listBuiltinStrategyNames } from "../services/strategy/builtin-strategy-registry.js";
6
+ import { loadConfiguredPlugins } from "../services/load-configured-plugins.js";
9
7
  import { StrategyService } from "../services/strategy/strategy-service.js";
10
- import { normalizeStrategyUnits } from "../services/strategy/strategy-normalize.js";
11
- import { parsePinnedSpec } from "../services/user-packages/parse-pinned-spec.js";
12
- import { isUnitLike, resolveUnitConstructor } from "../services/units/unit-validate.js";
13
- import { UnitStore } from "../services/units/unit-store.js";
14
- import { uniq } from "../utils/array.js";
15
- import { withSpinner } from "../utils/with-spinner.js";
16
- const buildMissingStrategyError = (input) => {
17
- const available = uniq([...input.installedNames, ...listBuiltinStrategyNames()])
18
- .filter((name) => name.trim().length > 0)
19
- .sort((left, right) => left.localeCompare(right));
20
- const suffix = available.length > 0
21
- ? ` Available strategies: ${available.join(", ")}`
22
- : " No strategies found.";
23
- return new Error(`Strategy not found: ${input.strategyName}.${suffix}`);
24
- };
25
- const resolveStrategy = (input) => {
26
- const installedNames = input.strategyService.listInstalledStrategyNames();
27
- if (installedNames.includes(input.strategyName)) {
28
- return {
29
- name: input.strategyName,
30
- strategyFile: input.strategyService.readStrategy(input.strategyName),
31
- source: "installed"
32
- };
33
- }
34
- const builtin = readBuiltinStrategy(input.strategyName);
35
- if (builtin) {
36
- return {
37
- name: input.strategyName,
38
- strategyFile: builtin,
39
- source: "builtin"
40
- };
41
- }
42
- throw buildMissingStrategyError({
43
- strategyName: input.strategyName,
44
- installedNames
45
- });
46
- };
47
- const assertNoDuplicateUnitIds = (input) => {
48
- const seen = new Set();
49
- const dup = input.units.find((u) => {
50
- if (seen.has(u.id)) {
51
- return true;
52
- }
53
- seen.add(u.id);
54
- return false;
55
- });
56
- if (dup) {
57
- throw new Error(`Duplicate unit id detected in strategy ${input.strategyName}: ${dup.id}`);
58
- }
59
- };
60
- const loadInstalledStrategyUnits = async (input) => {
61
- const strategyService = new StrategyService(input.configService);
62
- const unitStore = new UnitStore(input.configService);
63
- strategyService.ensureConfigFileExists();
64
- const strategyFile = strategyService.readStrategy(input.strategyName);
65
- const normalized = normalizeStrategyUnits(strategyFile);
66
- const installed = unitStore.readInstalledDependencyVersions();
67
- const drift = normalized.flatMap((u) => {
68
- const pinned = parsePinnedSpec(u.ref);
69
- const installedVersion = installed[pinned.name];
70
- if (!installedVersion || installedVersion !== pinned.version) {
71
- return [
72
- {
73
- name: pinned.name,
74
- pinned: pinned.version,
75
- installed: installedVersion
76
- }
77
- ];
78
- }
79
- return [];
80
- });
81
- if (drift.length > 0) {
82
- const details = drift
83
- .map((d) => {
84
- const installedText = typeof d.installed === "string" ? d.installed : "(not installed)";
85
- return `${d.name}: pinned ${d.pinned}, installed ${installedText}`;
86
- })
87
- .join("; ");
88
- throw new Error(`Strategy drift detected (${input.strategyName}). ${details}. ` +
89
- `Fix: pp strategy update ${input.strategyName} OR pp strategy pin ${input.strategyName}`);
90
- }
91
- const units = await Promise.all(normalized.map(async (u) => unitStore.instantiateFromRef(u.ref, u.args)));
92
- assertNoDuplicateUnitIds({
93
- strategyName: input.strategyName,
94
- units
95
- });
96
- return units;
97
- };
98
- const instantiateBuiltinUnit = async (input) => {
99
- const req = createRequire(import.meta.url);
100
- const pinned = parsePinnedSpec(input.ref);
101
- const resolved = req.resolve(pinned.name);
102
- const mod = (await import(pathToFileURL(resolved).href));
103
- const ctor = resolveUnitConstructor(mod);
104
- if (!ctor) {
105
- throw new Error(`Unit ${pinned.name} does not export a Unit constructor.`);
106
- }
107
- const instance = new ctor(...input.args);
108
- if (!isUnitLike(instance)) {
109
- throw new Error(`Unit ${pinned.name} exported constructor did not produce a Unit.`);
110
- }
111
- return instance;
112
- };
113
- const loadBuiltinStrategyUnits = async (input) => {
114
- const normalized = normalizeStrategyUnits(input.strategyFile);
115
- const units = await Promise.all(normalized.map(async (u) => instantiateBuiltinUnit({
116
- ref: u.ref,
117
- args: u.args
118
- })));
119
- assertNoDuplicateUnitIds({
120
- strategyName: input.strategyName,
121
- units
122
- });
123
- return units;
124
- };
125
- const loadStrategyUnits = async (input) => {
126
- if (input.strategy.source === "installed") {
127
- return loadInstalledStrategyUnits({
128
- strategyName: input.strategy.name,
129
- configService: input.configService
130
- });
131
- }
132
- return loadBuiltinStrategyUnits({
133
- strategyName: input.strategy.name,
134
- strategyFile: input.strategy.strategyFile
135
- });
136
- };
8
+ import { loadStrategyUnits, resolveStrategy } from "../utils/archive-strategy.js";
137
9
  export default class ArchiveCommand extends Command {
138
10
  static description = "Archive a web page as an offline snapshot.";
139
11
  static args = {
@@ -165,38 +37,51 @@ export default class ArchiveCommand extends Command {
165
37
  const timeoutMs = typeof flags.timeout === "number" ? flags.timeout : undefined;
166
38
  const maxDurationMs = typeof flags.maxDuration === "number" ? flags.maxDuration : undefined;
167
39
  const strategyName = typeof flags.strategy === "string" ? flags.strategy.trim() : undefined;
40
+ const configService = new ConfigService();
41
+ const name = strategyName && strategyName.length > 0 ? strategyName : "default";
42
+ const strategyService = new StrategyService(configService);
43
+ strategyService.ensureConfigFileExists();
44
+ const strategy = resolveStrategy({ strategyName: name, strategyService });
45
+ const units = await loadStrategyUnits({ strategy, configService });
46
+ const strategyFile = strategy.strategyFile;
47
+ const captureOptions = strategyFile.pipeline.captureOptions;
48
+ const effectiveTimeoutMs = typeof timeoutMs === "number" ? timeoutMs : captureOptions?.timeoutMs;
49
+ const effectiveMaxDurationMs = typeof maxDurationMs === "number" ? maxDurationMs : captureOptions?.maxDurationMs;
168
50
  const pagepocket = PagePocket.fromURL(targetUrl);
169
- const result = await withSpinner(async (spinner) => {
170
- {
171
- const configService = new ConfigService();
172
- const name = strategyName && strategyName.length > 0 ? strategyName : "default";
173
- const strategyService = new StrategyService(configService);
174
- strategyService.ensureConfigFileExists();
175
- const strategy = resolveStrategy({
176
- strategyName: name,
177
- strategyService
178
- });
179
- const units = await loadStrategyUnits({
180
- strategy,
181
- configService
182
- });
183
- const entryTarget = pagepocket;
184
- const strategyFile = strategy.strategyFile;
185
- const captureOptions = strategyFile.pipeline.captureOptions;
186
- const effectiveTimeoutMs = typeof timeoutMs === "number" ? timeoutMs : captureOptions?.timeoutMs;
187
- const effectiveMaxDurationMs = typeof maxDurationMs === "number" ? maxDurationMs : captureOptions?.maxDurationMs;
188
- const result = await entryTarget.capture({
189
- ...(typeof effectiveTimeoutMs === "number" ? { timeoutMs: effectiveTimeoutMs } : {}),
190
- ...(typeof effectiveMaxDurationMs === "number"
191
- ? { maxDurationMs: effectiveMaxDurationMs }
192
- : {}),
193
- blacklist: [...ga, ...ns],
194
- units,
195
- plugins: await loadConfiguredPlugins().catch(() => [])
196
- });
197
- return result;
51
+ let spinner = ora();
52
+ let activeUnitId = "";
53
+ let activeUnitLabel = "";
54
+ const formatUnitLabel = (index, total, unitDescription) => `[${index + 1}/${total}] ${unitDescription}`;
55
+ pagepocket.on("unit:start", (e) => {
56
+ activeUnitId = e.unitId;
57
+ activeUnitLabel = formatUnitLabel(e.index, e.total, e.unitDescription);
58
+ spinner = ora(activeUnitLabel).start();
59
+ });
60
+ pagepocket.on("unit:log", (e) => {
61
+ if (e.unitId !== activeUnitId) {
62
+ return;
198
63
  }
199
- }, "Freezing page");
64
+ spinner.text = `${activeUnitLabel} ${chalk.gray(e.message)}`;
65
+ });
66
+ pagepocket.on("unit:end", (e) => {
67
+ spinner.succeed(formatUnitLabel(e.index, e.total, e.unitDescription));
68
+ });
69
+ let result;
70
+ try {
71
+ result = await pagepocket.capture({
72
+ ...(typeof effectiveTimeoutMs === "number" ? { timeoutMs: effectiveTimeoutMs } : {}),
73
+ ...(typeof effectiveMaxDurationMs === "number"
74
+ ? { maxDurationMs: effectiveMaxDurationMs }
75
+ : {}),
76
+ blacklist: [...ga, ...ns],
77
+ units,
78
+ plugins: await loadConfiguredPlugins().catch(() => [])
79
+ });
80
+ }
81
+ catch (error) {
82
+ spinner.fail();
83
+ throw error;
84
+ }
200
85
  this.log(chalk.green("All done! Snapshot created."));
201
86
  if (result.kind === "zip") {
202
87
  this.log(`Snapshot saved to ${chalk.cyan(result.zip.outputPath)}`);
@@ -38,9 +38,7 @@ export default class PluginSetCommand extends Command {
38
38
  const configService = new ConfigService();
39
39
  configService.ensureConfigFileExists();
40
40
  const config = configService.readConfigOrDefault();
41
- const targetIndex = config.plugins.findIndex((entry) => {
42
- return (typeof entry === "string" ? entry : entry.name) === targetName;
43
- });
41
+ const targetIndex = config.plugins.findIndex((entry) => (typeof entry === "string" ? entry : entry.name) === targetName);
44
42
  if (targetIndex < 0) {
45
43
  this.log(chalk.gray(`Plugin not found in config: ${targetName}`));
46
44
  return;
@@ -6,15 +6,15 @@ export default class StrategyDoctorCommand extends Command {
6
6
  async run() {
7
7
  const service = new StrategyService();
8
8
  const report = service.doctor();
9
- report.conflicts.forEach((c) => {
10
- this.log(chalk.red(`[CONFLICT] ${c.name}`));
11
- Object.entries(c.versions).forEach(([version, strategies]) => {
9
+ report.conflicts.forEach((conflict) => {
10
+ this.log(chalk.red(`[CONFLICT] ${conflict.name}`));
11
+ Object.entries(conflict.versions).forEach(([version, strategies]) => {
12
12
  this.log(chalk.red(` ${version}: ${strategies.join(", ")}`));
13
13
  });
14
14
  });
15
- report.drift.forEach((d) => {
16
- this.log(chalk.yellow(`[DRIFT] ${d.strategy}`));
17
- d.items.forEach((item) => {
15
+ report.drift.forEach((drift) => {
16
+ this.log(chalk.yellow(`[DRIFT] ${drift.strategy}`));
17
+ drift.items.forEach((item) => {
18
18
  const installed = typeof item.installed === "string" ? item.installed : "(not installed)";
19
19
  this.log(chalk.yellow(` ${item.name}: pinned ${item.pinned}, installed ${installed}`));
20
20
  });
@@ -1,22 +1,46 @@
1
1
  import { Command } from "@oclif/core";
2
- import { listBuiltinStrategyNames } from "../../services/strategy/builtin-strategy-registry.js";
2
+ import chalk from "chalk";
3
+ import { listBuiltinStrategyNames, readBuiltinStrategy } from "../../services/strategy/builtin-strategy-registry.js";
4
+ import { normalizeBuiltinStrategyUnits, normalizeStrategyUnits } from "../../services/strategy/strategy-normalize.js";
3
5
  import { StrategyService } from "../../services/strategy/strategy-service.js";
4
6
  import { uniq } from "../../utils/array.js";
7
+ const formatInstalledStrategy = (name, strategyFile) => {
8
+ const units = normalizeStrategyUnits(strategyFile);
9
+ const pipeline = units.map((unit) => unit.ref).join(" -> ");
10
+ return pipeline.length > 0 ? `${name}${chalk.dim(`(${pipeline})`)}` : name;
11
+ };
12
+ const formatBuiltinStrategy = (name, strategyFile) => {
13
+ const units = normalizeBuiltinStrategyUnits(strategyFile);
14
+ const pipeline = units.map((unit) => unit.name).join(" -> ");
15
+ return pipeline.length > 0 ? `${name}${chalk.dim(`(${pipeline})`)}` : name;
16
+ };
5
17
  export default class StrategyLsCommand extends Command {
6
18
  static description = "List available strategies.";
7
19
  async run() {
8
20
  const service = new StrategyService();
9
21
  service.ensureConfigFileExists();
10
- const builtin = listBuiltinStrategyNames();
11
- const installed = service.listInstalledStrategyNames();
12
- const names = uniq([...builtin, ...installed])
13
- .map((n) => n.trim())
14
- .filter((n) => n.length > 0)
15
- .sort((a, b) => a.localeCompare(b));
22
+ const builtinNames = listBuiltinStrategyNames();
23
+ const installedNames = service.listInstalledStrategyNames();
24
+ const names = uniq([...builtinNames, ...installedNames])
25
+ .map((name) => name.trim())
26
+ .filter((name) => name.length > 0)
27
+ .sort((left, right) => left.localeCompare(right));
16
28
  if (names.length === 0) {
17
29
  this.log("No strategies available.");
18
30
  return;
19
31
  }
20
- names.forEach((n) => this.log(n));
32
+ for (const name of names) {
33
+ const isInstalled = installedNames.includes(name);
34
+ const strategyFile = isInstalled ? service.readStrategy(name) : readBuiltinStrategy(name);
35
+ if (strategyFile) {
36
+ const formatted = isInstalled
37
+ ? formatInstalledStrategy(name, strategyFile)
38
+ : formatBuiltinStrategy(name, strategyFile);
39
+ this.log(formatted);
40
+ }
41
+ else {
42
+ this.log(name);
43
+ }
44
+ }
21
45
  }
22
46
  }
@@ -51,7 +51,7 @@ export default class ViewCommand extends Command {
51
51
  host: flags.host,
52
52
  port
53
53
  });
54
- this.log(`Serving ${chalk.cyan(rootDir)}`);
54
+ this.log(`Serving ${chalk.cyan(rootDir)} ${chalk.dim(`(${server.snapshotType})`)}`);
55
55
  this.log(chalk.green(server.url));
56
56
  }
57
57
  }
@@ -15,8 +15,8 @@ export class ConfigService {
15
15
  }
16
16
  getConfigDir() {
17
17
  if (process.platform === "win32") {
18
- const p = envPaths(this.appName, { suffix: "" });
19
- return p.config;
18
+ const envPathsResult = envPaths(this.appName, { suffix: "" });
19
+ return envPathsResult.config;
20
20
  }
21
21
  const homeDir = os.homedir();
22
22
  const xdgConfigHome = process.env.XDG_CONFIG_HOME;
@@ -24,8 +24,8 @@ export class ConfigService {
24
24
  return path.join(base, this.appName);
25
25
  }
26
26
  getDataDir() {
27
- const p = envPaths(this.appName, { suffix: "" });
28
- return p.data;
27
+ const envPathsResult = envPaths(this.appName, { suffix: "" });
28
+ return envPathsResult.data;
29
29
  }
30
30
  getConfigPath() {
31
31
  return path.join(this.getConfigDir(), "config.json");
@@ -73,13 +73,13 @@ export class ConfigService {
73
73
  }
74
74
  return undefined;
75
75
  })
76
- .filter((x) => typeof x !== "undefined")
76
+ .filter((pluginEntry) => typeof pluginEntry !== "undefined")
77
77
  : [];
78
78
  const strategiesRaw = value.strategies;
79
79
  const strategies = Array.isArray(strategiesRaw)
80
80
  ? strategiesRaw
81
- .map((x) => (typeof x === "string" ? x.trim() : ""))
82
- .filter((x) => x.length > 0)
81
+ .map((strategyName) => (typeof strategyName === "string" ? strategyName.trim() : ""))
82
+ .filter((strategyName) => strategyName.length > 0)
83
83
  : [];
84
84
  const packsRaw = value.strategyPacks;
85
85
  const strategyPacks = Array.isArray(packsRaw)
@@ -98,7 +98,7 @@ export class ConfigService {
98
98
  }
99
99
  return undefined;
100
100
  })
101
- .filter((x) => Boolean(x))
101
+ .filter((strategyPackEntry) => Boolean(strategyPackEntry))
102
102
  : [];
103
103
  return {
104
104
  plugins,
@@ -7,16 +7,16 @@ import { spawnSync } from "node:child_process";
7
7
  */
8
8
  const tryInstallWithFallback = (installDir, packageSpec) => {
9
9
  const tryRun = (cmd, args) => {
10
- const r = spawnSync(cmd, args, {
10
+ const result = spawnSync(cmd, args, {
11
11
  cwd: installDir,
12
12
  stdio: "inherit",
13
13
  shell: false
14
14
  });
15
- if (r.error) {
16
- return { ok: false, error: r.error };
15
+ if (result.error) {
16
+ return { ok: false, error: result.error };
17
17
  }
18
- if (typeof r.status === "number" && r.status !== 0) {
19
- return { ok: false, error: new Error(`${cmd} exited with code ${r.status}`) };
18
+ if (typeof result.status === "number" && result.status !== 0) {
19
+ return { ok: false, error: new Error(`${cmd} exited with code ${result.status}`) };
20
20
  }
21
21
  return { ok: true };
22
22
  };
@@ -51,16 +51,16 @@ export const uninstallPluginPackage = (store, input) => {
51
51
  store.ensurePluginPackageJson();
52
52
  const installDir = store.getInstallDir();
53
53
  const tryRun = (cmd, args) => {
54
- const r = spawnSync(cmd, args, {
54
+ const result = spawnSync(cmd, args, {
55
55
  cwd: installDir,
56
56
  stdio: "inherit",
57
57
  shell: false
58
58
  });
59
- if (r.error) {
60
- return { ok: false, error: r.error };
59
+ if (result.error) {
60
+ return { ok: false, error: result.error };
61
61
  }
62
- if (typeof r.status === "number" && r.status !== 0) {
63
- return { ok: false, error: new Error(`${cmd} exited with code ${r.status}`) };
62
+ if (typeof result.status === "number" && result.status !== 0) {
63
+ return { ok: false, error: new Error(`${cmd} exited with code ${result.status}`) };
64
64
  }
65
65
  return { ok: true };
66
66
  };
@@ -15,17 +15,13 @@ export const normalizePluginConfigEntry = (entry) => {
15
15
  };
16
16
  };
17
17
  export const getPluginNameFromSpec = (spec) => spec.name;
18
- const isRecord = (value) => {
19
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
20
- };
21
- const isCallable = (value) => {
22
- return typeof value === "function";
23
- };
18
+ const isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
19
+ const isCallable = (value) => typeof value === "function";
24
20
  const isStringRecord = (value) => {
25
21
  if (!value || typeof value !== "object" || Array.isArray(value)) {
26
22
  return false;
27
23
  }
28
- return Object.values(value).every((v) => typeof v === "string");
24
+ return Object.values(value).every((entryValue) => typeof entryValue === "string");
29
25
  };
30
26
  export class PluginStore {
31
27
  configService;
@@ -80,11 +76,13 @@ export class PluginStore {
80
76
  if (!json.ok || !json.value || typeof json.value !== "object") {
81
77
  return { name: pluginName };
82
78
  }
83
- const o = json.value;
79
+ const packageJsonRecord = json.value;
84
80
  return {
85
- name: typeof o.name === "string" ? o.name : pluginName,
86
- description: typeof o.description === "string" ? o.description : undefined,
87
- version: typeof o.version === "string" ? o.version : undefined
81
+ name: typeof packageJsonRecord.name === "string" ? packageJsonRecord.name : pluginName,
82
+ description: typeof packageJsonRecord.description === "string"
83
+ ? packageJsonRecord.description
84
+ : undefined,
85
+ version: typeof packageJsonRecord.version === "string" ? packageJsonRecord.version : undefined
88
86
  };
89
87
  }
90
88
  catch {
@@ -146,7 +144,7 @@ export class PluginStore {
146
144
  this.configService.writeConfig(next);
147
145
  }
148
146
  hasPlugin(config, pluginName) {
149
- return config.plugins.some((e) => getPluginNameFromSpec(normalizePluginConfigEntry(e)) === pluginName);
147
+ return config.plugins.some((pluginEntry) => getPluginNameFromSpec(normalizePluginConfigEntry(pluginEntry)) === pluginName);
150
148
  }
151
149
  addPluginToConfig(config, entry) {
152
150
  const spec = normalizePluginConfigEntry(entry);
@@ -157,7 +155,7 @@ export class PluginStore {
157
155
  }
158
156
  removePluginFromConfig(config, pluginName) {
159
157
  const before = config.plugins;
160
- const after = before.filter((e) => getPluginNameFromSpec(normalizePluginConfigEntry(e)) !== pluginName);
158
+ const after = before.filter((pluginEntry) => getPluginNameFromSpec(normalizePluginConfigEntry(pluginEntry)) !== pluginName);
161
159
  return {
162
160
  config: { ...config, plugins: after },
163
161
  removed: after.length !== before.length
@@ -1,23 +1,14 @@
1
- import path from "node:path";
2
1
  import { createRequire } from "node:module";
2
+ import path from "node:path";
3
+ import { MemoryCache } from "@pagepocket/shared";
3
4
  import { readStrategiesFromPackRoot } from "./strategy-pack-read.js";
4
- const readBuiltinStrategiesUnsafe = () => {
5
- const req = createRequire(import.meta.url);
6
- const pkgJsonPath = req.resolve("@pagepocket/builtin-strategy/package.json");
7
- const packRoot = path.dirname(pkgJsonPath);
8
- return readStrategiesFromPackRoot(packRoot);
9
- };
10
- export const readBuiltinStrategies = () => {
11
- try {
12
- return readBuiltinStrategiesUnsafe();
13
- }
14
- catch {
15
- return [];
16
- }
17
- };
18
- export const listBuiltinStrategyNames = () => {
19
- return readBuiltinStrategies().map((strategy) => strategy.name);
20
- };
21
- export const readBuiltinStrategy = (name) => {
22
- return readBuiltinStrategies().find((strategy) => strategy.name === name);
23
- };
5
+ const BUILTIN_KEY = "builtin";
6
+ const cache = new MemoryCache();
7
+ const resolveBuiltinPackRoot = () => {
8
+ const require = createRequire(import.meta.url);
9
+ const packageJsonPath = require.resolve("@pagepocket/builtin-strategy/package.json");
10
+ return path.dirname(packageJsonPath);
11
+ };
12
+ const loadBuiltinStrategies = () => cache.getOrSet(BUILTIN_KEY, () => readStrategiesFromPackRoot(resolveBuiltinPackRoot()));
13
+ export const listBuiltinStrategyNames = () => loadBuiltinStrategies().map((strategy) => strategy.name);
14
+ export const readBuiltinStrategy = (name) => loadBuiltinStrategies().find((strategy) => strategy.name === name);
@@ -1,12 +1,12 @@
1
1
  import { parsePinnedSpec } from "../user-packages/parse-pinned-spec.js";
2
2
  export const collectWantedVersions = (strategies) => {
3
3
  const wanted = {};
4
- for (const s of strategies) {
5
- for (const u of s.units) {
6
- const pinned = parsePinnedSpec(u.ref);
4
+ for (const strategy of strategies) {
5
+ for (const unit of strategy.units) {
6
+ const pinned = parsePinnedSpec(unit.ref);
7
7
  wanted[pinned.name] ??= {};
8
8
  wanted[pinned.name][pinned.version] ??= [];
9
- wanted[pinned.name][pinned.version].push(s.name);
9
+ wanted[pinned.name][pinned.version].push(strategy.name);
10
10
  }
11
11
  }
12
12
  return wanted;
@@ -23,8 +23,8 @@ export const computeConflicts = (wanted) => {
23
23
  return conflicts;
24
24
  };
25
25
  export const computeDrift = (input) => {
26
- const items = input.units.flatMap((u) => {
27
- const pinned = parsePinnedSpec(u.ref);
26
+ const items = input.units.flatMap((unit) => {
27
+ const pinned = parsePinnedSpec(unit.ref);
28
28
  const installedVersion = input.installed[pinned.name];
29
29
  if (!installedVersion || installedVersion !== pinned.version) {
30
30
  return [
@@ -1,7 +1,7 @@
1
1
  import { uniq } from "../../utils/array.js";
2
- export const listStrategyNamesFromConfig = (config) => {
3
- return uniq((config.strategies ?? []).map((s) => s.trim()).filter((s) => s.length > 0));
4
- };
2
+ export const listStrategyNamesFromConfig = (config) => uniq((config.strategies ?? [])
3
+ .map((strategyName) => strategyName.trim())
4
+ .filter((strategyName) => strategyName.length > 0));
5
5
  export const requireStrategyInstalled = (config, name) => {
6
6
  const installed = listStrategyNamesFromConfig(config);
7
7
  if (!installed.includes(name)) {
@@ -13,6 +13,6 @@ export const withStrategyInConfig = (config, name) => {
13
13
  return { ...config, strategies };
14
14
  };
15
15
  export const withoutStrategyInConfig = (config, name) => {
16
- const strategies = (config.strategies ?? []).filter((s) => s !== name);
16
+ const strategies = (config.strategies ?? []).filter((strategyName) => strategyName !== name);
17
17
  return { ...config, strategies };
18
18
  };
@@ -1,10 +1,8 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { parseJson } from "../../utils/parse-json.js";
4
3
  import { isUrlLike } from "../../utils/normalize-argv.js";
5
- const isRecord = (value) => {
6
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
7
- };
4
+ import { parseJson } from "../../utils/parse-json.js";
5
+ const isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
8
6
  const isStrategyFile = (value) => {
9
7
  if (!isRecord(value)) {
10
8
  return false;
@@ -1,9 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { parseJson } from "../../utils/parse-json.js";
4
- const isRecord = (value) => {
5
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
6
- };
4
+ const isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
7
5
  const isStrategyFile = (value) => {
8
6
  if (!isRecord(value)) {
9
7
  return false;
@@ -23,12 +21,8 @@ const isStrategyFile = (value) => {
23
21
  }
24
22
  return true;
25
23
  };
26
- export const getStrategiesDir = (configService) => {
27
- return path.join(configService.getConfigDir(), "strategies");
28
- };
29
- export const getStrategyPath = (configService, name) => {
30
- return path.join(getStrategiesDir(configService), `${name}.json`);
31
- };
24
+ export const getStrategiesDir = (configService) => path.join(configService.getConfigDir(), "strategies");
25
+ export const getStrategyPath = (configService, name) => path.join(getStrategiesDir(configService), `${name}.json`);
32
26
  export const readStrategyFile = (filePath) => {
33
27
  const text = fs.readFileSync(filePath, "utf8");
34
28
  const parsed = parseJson(text);