@shotkit/shotium 0.0.1 → 0.1.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.
- package/README.md +169 -1
- package/dist/daemon_main.d.ts +1 -0
- package/dist/daemon_main.js +322 -0
- package/dist/daemon_main.js.map +1 -0
- package/dist/index.d.ts +135 -0
- package/dist/index.js +351 -0
- package/dist/index.js.map +1 -0
- package/dist/native.d.ts +66 -0
- package/dist/native.js +127 -0
- package/dist/native.js.map +1 -0
- package/dist/platform-DU8DYqmA.js +32 -0
- package/dist/platform-DU8DYqmA.js.map +1 -0
- package/dist/pool-BSgS6vkr.js +356 -0
- package/dist/pool-BSgS6vkr.js.map +1 -0
- package/dist/request-qZXS3N9f.js +43 -0
- package/dist/request-qZXS3N9f.js.map +1 -0
- package/dist/types-x9HtkzeE.d.ts +156 -0
- package/native/binding.cc +283 -0
- package/native/binding.gyp +54 -0
- package/native/stage_header.js +53 -0
- package/package.json +60 -3
- package/src/daemon_main.ts +76 -0
- package/src/index.ts +161 -0
- package/src/lib/client.ts +377 -0
- package/src/lib/config.ts +86 -0
- package/src/lib/daemon.ts +370 -0
- package/src/lib/endpoint.ts +63 -0
- package/src/lib/platform.ts +75 -0
- package/src/lib/pool.ts +243 -0
- package/src/lib/protocol.ts +53 -0
- package/src/lib/request.ts +108 -0
- package/src/lib/worker.ts +220 -0
- package/src/native.ts +234 -0
- package/src/types.ts +169 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
// Copyright 2026 The Chromium Authors
|
|
2
|
+
// Use of this source code is governed by a BSD-style license that can be
|
|
3
|
+
// found in the LICENSE file.
|
|
4
|
+
|
|
5
|
+
// shotium's node addon: shot in this process, over shot_api.h.
|
|
6
|
+
//
|
|
7
|
+
// It is deliberately thin. Everything about what a screenshot means -- which
|
|
8
|
+
// fields exist, what they default to, what an unknown one is -- lives in
|
|
9
|
+
// shotium/src/lib/request.ts and shot/shot_request.cc, and this file carries
|
|
10
|
+
// JSON between them without reading it. Anything it understood would be a
|
|
11
|
+
// third opinion about the request format, and the third opinion is always the
|
|
12
|
+
// one that drifts.
|
|
13
|
+
//
|
|
14
|
+
// Node-API rather than V8: the ABI is stable across node versions, so one
|
|
15
|
+
// prebuilt .node per platform is enough and the addon does not have to be
|
|
16
|
+
// rebuilt every time node's internals move.
|
|
17
|
+
|
|
18
|
+
#include <node_api.h>
|
|
19
|
+
|
|
20
|
+
#include <cstring>
|
|
21
|
+
#include <string>
|
|
22
|
+
#include <utility>
|
|
23
|
+
|
|
24
|
+
#include "shot_api.h"
|
|
25
|
+
|
|
26
|
+
namespace {
|
|
27
|
+
|
|
28
|
+
// A shot_engine, as node sees it.
|
|
29
|
+
//
|
|
30
|
+
// Wrapped rather than handed over as a bare pointer so that a script dropping
|
|
31
|
+
// the handle on the floor still shuts the engine down: the finalizer runs when
|
|
32
|
+
// the object is collected. `engine` is cleared by an explicit destroy() so the
|
|
33
|
+
// finalizer does not do it twice.
|
|
34
|
+
struct EngineHandle {
|
|
35
|
+
shot_engine* engine = nullptr;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
void FinalizeEngine(napi_env env, void* data, void* hint) {
|
|
39
|
+
auto* handle = static_cast<EngineHandle*>(data);
|
|
40
|
+
if (handle->engine) {
|
|
41
|
+
shot_engine_destroy(handle->engine);
|
|
42
|
+
}
|
|
43
|
+
delete handle;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// One capture in flight: a promise, and the strings on both sides of it.
|
|
47
|
+
//
|
|
48
|
+
// The request is copied rather than referenced because Execute runs on a
|
|
49
|
+
// libuv thread where no JS value may be touched, and the JS string it came
|
|
50
|
+
// from can be collected before then.
|
|
51
|
+
struct CaptureTask {
|
|
52
|
+
napi_deferred deferred = nullptr;
|
|
53
|
+
napi_async_work work = nullptr;
|
|
54
|
+
shot_engine* engine = nullptr;
|
|
55
|
+
std::string request;
|
|
56
|
+
shot_status status = SHOT_ERR_CAPTURE;
|
|
57
|
+
shot_buffer* image = nullptr;
|
|
58
|
+
shot_buffer* error = nullptr;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
bool ReadUtf8(napi_env env, napi_value value, std::string* out) {
|
|
62
|
+
size_t length = 0;
|
|
63
|
+
if (napi_get_value_string_utf8(env, value, nullptr, 0, &length) != napi_ok) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
// Room for the NUL node insists on writing, then cut back to what it says
|
|
67
|
+
// it wrote.
|
|
68
|
+
std::string text(length + 1, '\0');
|
|
69
|
+
size_t written = 0;
|
|
70
|
+
if (napi_get_value_string_utf8(env, value, text.data(), length + 1,
|
|
71
|
+
&written) != napi_ok) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
text.resize(written);
|
|
75
|
+
*out = std::move(text);
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
napi_value Undefined(napi_env env) {
|
|
80
|
+
napi_value value = nullptr;
|
|
81
|
+
napi_get_undefined(env, &value);
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Turns a shot_buffer carrying a message into a thrown JS error. Freeing it is
|
|
86
|
+
// this function's job either way, because every caller is on its way out.
|
|
87
|
+
void ThrowFromBuffer(napi_env env, shot_buffer* message, const char* fallback) {
|
|
88
|
+
const char* text = fallback;
|
|
89
|
+
if (message && shot_buffer_size(message) > 0) {
|
|
90
|
+
text = reinterpret_cast<const char*>(shot_buffer_data(message));
|
|
91
|
+
}
|
|
92
|
+
napi_throw_error(env, nullptr, text);
|
|
93
|
+
shot_buffer_free(message);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
bool ReadHandle(napi_env env, napi_value value, EngineHandle** out) {
|
|
97
|
+
void* data = nullptr;
|
|
98
|
+
if (napi_get_value_external(env, value, &data) != napi_ok || !data) {
|
|
99
|
+
napi_throw_type_error(env, nullptr, "shotium: expected an engine handle");
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
*out = static_cast<EngineHandle*>(data);
|
|
103
|
+
if (!(*out)->engine) {
|
|
104
|
+
napi_throw_error(env, nullptr, "shotium: this engine has been destroyed");
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
napi_value Create(napi_env env, napi_callback_info info) {
|
|
111
|
+
size_t argc = 1;
|
|
112
|
+
napi_value argv[1] = {};
|
|
113
|
+
napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);
|
|
114
|
+
|
|
115
|
+
std::string options;
|
|
116
|
+
if (argc > 0 && !ReadUtf8(env, argv[0], &options)) {
|
|
117
|
+
napi_throw_type_error(env, nullptr,
|
|
118
|
+
"shotium: create(optionsJson) wants a string");
|
|
119
|
+
return nullptr;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (shot_abi_version() != SHOT_ABI_VERSION) {
|
|
123
|
+
napi_throw_error(env, nullptr,
|
|
124
|
+
"shotium: the shot library beside this addon speaks a "
|
|
125
|
+
"different ABI version; they ship together and one of "
|
|
126
|
+
"them has been replaced");
|
|
127
|
+
return nullptr;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
shot_engine* engine = nullptr;
|
|
131
|
+
shot_buffer* error = nullptr;
|
|
132
|
+
if (shot_engine_create(options.c_str(), &engine, &error) != SHOT_OK) {
|
|
133
|
+
ThrowFromBuffer(env, error, "shotium: the engine would not start");
|
|
134
|
+
return nullptr;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
auto* handle = new EngineHandle{engine};
|
|
138
|
+
napi_value external = nullptr;
|
|
139
|
+
if (napi_create_external(env, handle, FinalizeEngine, nullptr, &external) !=
|
|
140
|
+
napi_ok) {
|
|
141
|
+
shot_engine_destroy(engine);
|
|
142
|
+
delete handle;
|
|
143
|
+
napi_throw_error(env, nullptr, "shotium: could not wrap the engine");
|
|
144
|
+
return nullptr;
|
|
145
|
+
}
|
|
146
|
+
return external;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
napi_value Destroy(napi_env env, napi_callback_info info) {
|
|
150
|
+
size_t argc = 1;
|
|
151
|
+
napi_value argv[1] = {};
|
|
152
|
+
napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);
|
|
153
|
+
|
|
154
|
+
void* data = nullptr;
|
|
155
|
+
if (argc < 1 || napi_get_value_external(env, argv[0], &data) != napi_ok ||
|
|
156
|
+
!data) {
|
|
157
|
+
napi_throw_type_error(env, nullptr, "shotium: expected an engine handle");
|
|
158
|
+
return nullptr;
|
|
159
|
+
}
|
|
160
|
+
auto* handle = static_cast<EngineHandle*>(data);
|
|
161
|
+
if (handle->engine) {
|
|
162
|
+
shot_engine_destroy(handle->engine);
|
|
163
|
+
handle->engine = nullptr;
|
|
164
|
+
}
|
|
165
|
+
return Undefined(env);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Runs on a libuv thread. No napi call is legal here beyond the ones that take
|
|
169
|
+
// no env, which is why everything it needs was copied out first.
|
|
170
|
+
void ExecuteCapture(napi_env env, void* data) {
|
|
171
|
+
auto* task = static_cast<CaptureTask*>(data);
|
|
172
|
+
task->status = shot_engine_capture(task->engine, task->request.c_str(),
|
|
173
|
+
&task->image, &task->error);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
void CompleteCapture(napi_env env, napi_status status, void* data) {
|
|
177
|
+
auto* task = static_cast<CaptureTask*>(data);
|
|
178
|
+
|
|
179
|
+
if (status == napi_ok && task->status == SHOT_OK) {
|
|
180
|
+
// Copied into a node Buffer rather than handed over as external memory.
|
|
181
|
+
// An external buffer would save a memcpy of a few hundred kilobytes
|
|
182
|
+
// against a render that took tens of milliseconds, and would put the
|
|
183
|
+
// lifetime of shot's allocation in the hands of node's GC -- across an
|
|
184
|
+
// allocator boundary the whole C ABI exists to keep closed.
|
|
185
|
+
napi_value buffer = nullptr;
|
|
186
|
+
napi_create_buffer_copy(env, shot_buffer_size(task->image),
|
|
187
|
+
shot_buffer_data(task->image), nullptr, &buffer);
|
|
188
|
+
napi_resolve_deferred(env, task->deferred, buffer);
|
|
189
|
+
} else {
|
|
190
|
+
const char* text = "shotium: the capture failed";
|
|
191
|
+
if (task->error && shot_buffer_size(task->error) > 0) {
|
|
192
|
+
text = reinterpret_cast<const char*>(shot_buffer_data(task->error));
|
|
193
|
+
}
|
|
194
|
+
napi_value message = nullptr;
|
|
195
|
+
napi_value error_value = nullptr;
|
|
196
|
+
napi_create_string_utf8(env, text, NAPI_AUTO_LENGTH, &message);
|
|
197
|
+
napi_create_error(env, nullptr, message, &error_value);
|
|
198
|
+
napi_reject_deferred(env, task->deferred, error_value);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
shot_buffer_free(task->image);
|
|
202
|
+
shot_buffer_free(task->error);
|
|
203
|
+
napi_delete_async_work(env, task->work);
|
|
204
|
+
delete task;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
napi_value Capture(napi_env env, napi_callback_info info) {
|
|
208
|
+
size_t argc = 2;
|
|
209
|
+
napi_value argv[2] = {};
|
|
210
|
+
napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);
|
|
211
|
+
|
|
212
|
+
EngineHandle* handle = nullptr;
|
|
213
|
+
if (argc < 2 || !ReadHandle(env, argv[0], &handle)) {
|
|
214
|
+
return nullptr;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
auto* task = new CaptureTask;
|
|
218
|
+
task->engine = handle->engine;
|
|
219
|
+
if (!ReadUtf8(env, argv[1], &task->request)) {
|
|
220
|
+
delete task;
|
|
221
|
+
napi_throw_type_error(env, nullptr,
|
|
222
|
+
"shotium: capture(engine, requestJson) wants a "
|
|
223
|
+
"string");
|
|
224
|
+
return nullptr;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
napi_value promise = nullptr;
|
|
228
|
+
if (napi_create_promise(env, &task->deferred, &promise) != napi_ok) {
|
|
229
|
+
delete task;
|
|
230
|
+
napi_throw_error(env, nullptr, "shotium: could not make a promise");
|
|
231
|
+
return nullptr;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
napi_value name = nullptr;
|
|
235
|
+
napi_create_string_utf8(env, "shot:capture", NAPI_AUTO_LENGTH, &name);
|
|
236
|
+
napi_create_async_work(env, nullptr, name, ExecuteCapture, CompleteCapture,
|
|
237
|
+
task, &task->work);
|
|
238
|
+
napi_queue_async_work(env, task->work);
|
|
239
|
+
return promise;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Synchronous on purpose. A purge is milliseconds and happens when the caller
|
|
243
|
+
// has decided it has nothing else to do; queuing it behind the event loop
|
|
244
|
+
// would mean the process that just went idle stays large until something wakes
|
|
245
|
+
// it up.
|
|
246
|
+
napi_value Purge(napi_env env, napi_callback_info info) {
|
|
247
|
+
size_t argc = 2;
|
|
248
|
+
napi_value argv[2] = {};
|
|
249
|
+
napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);
|
|
250
|
+
|
|
251
|
+
EngineHandle* handle = nullptr;
|
|
252
|
+
if (argc < 1 || !ReadHandle(env, argv[0], &handle)) {
|
|
253
|
+
return nullptr;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
bool release = false;
|
|
257
|
+
if (argc > 1) {
|
|
258
|
+
napi_get_value_bool(env, argv[1], &release);
|
|
259
|
+
}
|
|
260
|
+
shot_engine_purge(handle->engine, release ? 1 : 0);
|
|
261
|
+
return Undefined(env);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
napi_value Init(napi_env env, napi_value exports) {
|
|
265
|
+
const napi_property_descriptor properties[] = {
|
|
266
|
+
{"create", nullptr, Create, nullptr, nullptr, nullptr, napi_default,
|
|
267
|
+
nullptr},
|
|
268
|
+
{"destroy", nullptr, Destroy, nullptr, nullptr, nullptr, napi_default,
|
|
269
|
+
nullptr},
|
|
270
|
+
{"capture", nullptr, Capture, nullptr, nullptr, nullptr, napi_default,
|
|
271
|
+
nullptr},
|
|
272
|
+
{"purge", nullptr, Purge, nullptr, nullptr, nullptr, napi_default,
|
|
273
|
+
nullptr},
|
|
274
|
+
};
|
|
275
|
+
napi_define_properties(env, exports,
|
|
276
|
+
sizeof(properties) / sizeof(properties[0]),
|
|
277
|
+
properties);
|
|
278
|
+
return exports;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
} // namespace
|
|
282
|
+
|
|
283
|
+
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
# How the addon is built. It is not built by `npm install`: shot itself is a
|
|
3
|
+
# Chromium fork that takes hours and a checkout to compile, so the library
|
|
4
|
+
# this links against is a release artifact, and the addon is built against it
|
|
5
|
+
# once per platform and shipped prebuilt.
|
|
6
|
+
#
|
|
7
|
+
# SHOT_INCLUDE_DIR=/path/to/shot (the directory holding shot_api.h)
|
|
8
|
+
# SHOT_LIB_DIR=/path/to/out/Shot (the directory holding the library)
|
|
9
|
+
# npx node-gyp rebuild
|
|
10
|
+
"variables": {
|
|
11
|
+
# Not the engine's source directory: a directory holding a copy of
|
|
12
|
+
# shot_api.h and nothing else. An include directory pointed at shot/ makes
|
|
13
|
+
# libc++'s `#include <version>` resolve to shot/VERSION on any
|
|
14
|
+
# case-insensitive filesystem, which is macOS out of the box. See
|
|
15
|
+
# stage_header.js, which does the copying and explains the rest.
|
|
16
|
+
#
|
|
17
|
+
# SHOT_INCLUDE_DIR still says where shot_api.h is; stage_header.js reads it.
|
|
18
|
+
"shot_include_dir%": "<!(node stage_header.js)",
|
|
19
|
+
"shot_lib_dir%": "<!(node -p \"process.env.SHOT_LIB_DIR || require('path').resolve('../../out/Shot')\")"
|
|
20
|
+
},
|
|
21
|
+
"targets": [
|
|
22
|
+
{
|
|
23
|
+
"target_name": "shotium",
|
|
24
|
+
"sources": ["binding.cc"],
|
|
25
|
+
"include_dirs": ["<(shot_include_dir)"],
|
|
26
|
+
# Node-API 8 is Node 12.22 / 14.17 and up. Declaring it pins the surface
|
|
27
|
+
# this addon may use, so a build cannot quietly start depending on a
|
|
28
|
+
# newer node than the one it claims to support.
|
|
29
|
+
"defines": ["NAPI_VERSION=8"],
|
|
30
|
+
"cflags!": ["-fno-exceptions"],
|
|
31
|
+
"cflags_cc!": ["-fno-exceptions"],
|
|
32
|
+
"conditions": [
|
|
33
|
+
["OS=='win'", {
|
|
34
|
+
# The import library GN writes beside the DLL. The DLL itself is
|
|
35
|
+
# found at run time in the directory the .node was loaded from.
|
|
36
|
+
"libraries": ["<(shot_lib_dir)/shotium.dll.lib"]
|
|
37
|
+
}],
|
|
38
|
+
["OS=='linux'", {
|
|
39
|
+
"libraries": [
|
|
40
|
+
"-L<(shot_lib_dir)",
|
|
41
|
+
"-lshotium",
|
|
42
|
+
# $ORIGIN, escaped past make: the library ships beside the .node,
|
|
43
|
+
# not in a system directory, and nothing should be searching the
|
|
44
|
+
# host's library path for something named this generally.
|
|
45
|
+
"-Wl,-rpath,'$$ORIGIN'"
|
|
46
|
+
]
|
|
47
|
+
}],
|
|
48
|
+
["OS=='mac'", {
|
|
49
|
+
"libraries": ["-L<(shot_lib_dir)", "-lshotium", "-Wl,-rpath,@loader_path"]
|
|
50
|
+
}]
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Puts shot_api.h somewhere it is the only thing there, and prints where.
|
|
2
|
+
//
|
|
3
|
+
// binding.gyp calls this at generate time and uses the result as the addon's
|
|
4
|
+
// one include directory. The obvious thing -- pointing the include directory
|
|
5
|
+
// straight at shot/ -- does not survive a case-insensitive filesystem:
|
|
6
|
+
//
|
|
7
|
+
// src/shot/version:1:10: error: expected ';' after top level declarator
|
|
8
|
+
// 1 | MAJOR=153
|
|
9
|
+
//
|
|
10
|
+
// That is libc++'s <string> including <version>, the compiler searching the
|
|
11
|
+
// include directories before the system ones, and macOS answering `version`
|
|
12
|
+
// with `shot/VERSION`, which is this tree's build number file. The same trap is
|
|
13
|
+
// set on Windows -- NTFS is case-insensitive too -- and springs only because
|
|
14
|
+
// MSVC's <string> happens not to reach for <version>.
|
|
15
|
+
//
|
|
16
|
+
// Renaming the file is not available: shot/VERSION is what this fork calls
|
|
17
|
+
// chrome/VERSION, and //base, //build and a dozen .gni files name it. Nor is
|
|
18
|
+
// -iquote, which would say exactly the right thing and has a different
|
|
19
|
+
// spelling in each of the three generators node-gyp drives. Copying the one
|
|
20
|
+
// header the addon needs into a directory of its own is what is left, and it
|
|
21
|
+
// has the advantage that nothing about it can be undone by an STL that starts
|
|
22
|
+
// including one more thing.
|
|
23
|
+
//
|
|
24
|
+
// SHOT_INCLUDE_DIR still names where shot_api.h is *found*; it just is not
|
|
25
|
+
// handed to the compiler any more.
|
|
26
|
+
|
|
27
|
+
import fs from 'node:fs';
|
|
28
|
+
import path from 'node:path';
|
|
29
|
+
import {fileURLToPath} from 'node:url';
|
|
30
|
+
|
|
31
|
+
// ESM has no __dirname. This is the same thing, from the module's own URL.
|
|
32
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
|
|
34
|
+
const HEADER = 'shot_api.h';
|
|
35
|
+
|
|
36
|
+
const source = process.env.SHOT_INCLUDE_DIR ||
|
|
37
|
+
path.resolve(HERE, '..', '..', 'shot');
|
|
38
|
+
const staged = path.resolve(HERE, 'build', 'include');
|
|
39
|
+
|
|
40
|
+
const from = path.join(source, HEADER);
|
|
41
|
+
if (!fs.existsSync(from)) {
|
|
42
|
+
process.stderr.write(
|
|
43
|
+
`stage_header.js: no ${HEADER} in ${source}\n` +
|
|
44
|
+
' Set SHOT_INCLUDE_DIR to the directory holding it.\n');
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
fs.mkdirSync(staged, {recursive: true});
|
|
49
|
+
fs.copyFileSync(from, path.join(staged, HEADER));
|
|
50
|
+
|
|
51
|
+
// gyp takes this whole line as the variable's value, so it is the only thing
|
|
52
|
+
// written to stdout.
|
|
53
|
+
process.stdout.write(staged);
|
package/package.json
CHANGED
|
@@ -1,6 +1,63 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shotkit/shotium",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Static screenshots from a stripped Chromium: DOM, CSS, layout, paint, no JavaScript engine.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"screenshot",
|
|
7
|
+
"chromium",
|
|
8
|
+
"blink",
|
|
9
|
+
"html-to-image",
|
|
10
|
+
"png",
|
|
11
|
+
"render"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/sj817/shotium#readme",
|
|
14
|
+
"bugs": "https://github.com/sj817/shotium/issues",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/sj817/shotium.git"
|
|
18
|
+
},
|
|
19
|
+
"type": "module",
|
|
20
|
+
"main": "./dist/index.js",
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"default": "./dist/index.js"
|
|
26
|
+
},
|
|
27
|
+
"./native": {
|
|
28
|
+
"types": "./dist/native.d.ts",
|
|
29
|
+
"default": "./dist/native.js"
|
|
30
|
+
},
|
|
31
|
+
"./package.json": "./package.json"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist/",
|
|
35
|
+
"src/",
|
|
36
|
+
"native/binding.cc",
|
|
37
|
+
"native/binding.gyp",
|
|
38
|
+
"native/stage_header.js",
|
|
39
|
+
"README.md"
|
|
40
|
+
],
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=18"
|
|
43
|
+
},
|
|
44
|
+
"optionalDependencies": {
|
|
45
|
+
"@shotkit/shotium-darwin-arm64": "0.1.0",
|
|
46
|
+
"@shotkit/shotium-darwin-x64": "0.1.0",
|
|
47
|
+
"@shotkit/shotium-linux-arm64": "0.1.0",
|
|
48
|
+
"@shotkit/shotium-linux-x64": "0.1.0",
|
|
49
|
+
"@shotkit/shotium-win32-arm64": "0.1.0",
|
|
50
|
+
"@shotkit/shotium-win32-x64": "0.1.0"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "tsdown",
|
|
54
|
+
"check:types": "tsc --noEmit",
|
|
55
|
+
"prepack": "tsdown"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@types/node": "22.20.1",
|
|
59
|
+
"tsdown": "0.22.14",
|
|
60
|
+
"typescript": "5.9.3"
|
|
61
|
+
},
|
|
62
|
+
"license": "BSD-3-Clause"
|
|
6
63
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// The entry point of a detached daemon process.
|
|
2
|
+
//
|
|
3
|
+
// The configuration arrives as one base64 argument rather than as flags,
|
|
4
|
+
// because it contains paths that a Windows command line would otherwise quote
|
|
5
|
+
// badly, and because the client and the daemon have to agree on it exactly:
|
|
6
|
+
// the endpoint is a hash of these fields, so a value mangled in transit would
|
|
7
|
+
// produce a daemon listening where nobody looks. See endpoint.ts.
|
|
8
|
+
//
|
|
9
|
+
// It is a build entry of its own, and not a chunk, because lib/client.ts
|
|
10
|
+
// spawns it by path -- `node dist/daemon_main.js <base64 json>` -- and a name
|
|
11
|
+
// the bundler chose would be a name that changes.
|
|
12
|
+
|
|
13
|
+
import {Daemon} from './lib/daemon.js';
|
|
14
|
+
import type {DaemonOptions} from './types.js';
|
|
15
|
+
|
|
16
|
+
async function main(): Promise<void> {
|
|
17
|
+
const encoded = process.argv[2];
|
|
18
|
+
if (!encoded) {
|
|
19
|
+
process.stderr.write('shotium: daemon_main expects a base64 config\n');
|
|
20
|
+
process.exit(2);
|
|
21
|
+
}
|
|
22
|
+
const options = JSON.parse(Buffer.from(encoded, 'base64').toString('utf8')) as
|
|
23
|
+
DaemonOptions;
|
|
24
|
+
const daemon = new Daemon(options);
|
|
25
|
+
|
|
26
|
+
daemon.on('stderr', ({worker, line}: {worker: number, line: string}) => {
|
|
27
|
+
process.stderr.write(`shotium worker ${worker}: ${line}\n`);
|
|
28
|
+
});
|
|
29
|
+
for (const event of ['crash', 'timeout', 'worker-restart', 'worker-error',
|
|
30
|
+
'idle-exit']) {
|
|
31
|
+
daemon.on(event, (payload: {error?: unknown}) => {
|
|
32
|
+
// An Error does not survive JSON.stringify -- it comes out as {} -- and
|
|
33
|
+
// its message is the whole point of logging a worker that would not
|
|
34
|
+
// start.
|
|
35
|
+
const detail = payload && payload.error ?
|
|
36
|
+
{
|
|
37
|
+
...payload,
|
|
38
|
+
error: String(
|
|
39
|
+
(payload.error as Error).message ?? payload.error),
|
|
40
|
+
} :
|
|
41
|
+
payload;
|
|
42
|
+
process.stderr.write(
|
|
43
|
+
`shotium daemon ${event}: ${JSON.stringify(detail)}\n`);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
// An 'error' with nobody listening is thrown by EventEmitter itself, which
|
|
47
|
+
// would turn a socket that failed after binding -- something the daemon can
|
|
48
|
+
// survive -- into a dead pool.
|
|
49
|
+
daemon.on('error', (error: Error) => {
|
|
50
|
+
process.stderr.write(
|
|
51
|
+
`shotium daemon error: ${(error && error.message) || error}\n`);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
await daemon.listen();
|
|
56
|
+
} catch (error) {
|
|
57
|
+
// Losing the race to bind is the ordinary outcome when two clients start a
|
|
58
|
+
// daemon at the same moment: the other one is up, this one is not needed,
|
|
59
|
+
// and the client that spawned it will connect to the winner. Anything else
|
|
60
|
+
// is a real failure and says so.
|
|
61
|
+
if ((error as NodeJS.ErrnoException | null)?.code === 'EADDRINUSE') {
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
64
|
+
process.stderr.write(`shotium: daemon failed to start: ${error}\n`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const shutdown = () => {
|
|
69
|
+
daemon.close().then(() => process.exit(0), () => process.exit(1));
|
|
70
|
+
};
|
|
71
|
+
process.on('SIGINT', shutdown);
|
|
72
|
+
process.on('SIGTERM', shutdown);
|
|
73
|
+
daemon.on('close', () => process.exit(0));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
void main();
|