@shotkit/shotium 0.0.1 → 0.2.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 +168 -1
- package/dist/daemon_main.d.ts +1 -0
- package/dist/daemon_main.js +305 -0
- package/dist/daemon_main.js.map +1 -0
- package/dist/engine-Xe7nH-1i.js +267 -0
- package/dist/engine-Xe7nH-1i.js.map +1 -0
- package/dist/index.d.ts +268 -0
- package/dist/index.js +355 -0
- package/dist/index.js.map +1 -0
- package/native/binding.cc +283 -0
- package/native/binding.gyp +54 -0
- package/native/stage_header.js +53 -0
- package/package.json +56 -3
- package/src/daemon_main.ts +76 -0
- package/src/index.ts +139 -0
- package/src/lib/binding.ts +89 -0
- package/src/lib/client.ts +373 -0
- package/src/lib/config.ts +31 -0
- package/src/lib/daemon.ts +382 -0
- package/src/lib/endpoint.ts +70 -0
- package/src/lib/engine.ts +168 -0
- package/src/lib/platform.ts +69 -0
- package/src/lib/protocol.ts +53 -0
- package/src/lib/request.ts +97 -0
- package/src/types.ts +143 -0
|
@@ -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,59 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shotkit/shotium",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.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
|
+
"./package.json": "./package.json"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist/",
|
|
31
|
+
"src/",
|
|
32
|
+
"native/binding.cc",
|
|
33
|
+
"native/binding.gyp",
|
|
34
|
+
"native/stage_header.js",
|
|
35
|
+
"README.md"
|
|
36
|
+
],
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18"
|
|
39
|
+
},
|
|
40
|
+
"optionalDependencies": {
|
|
41
|
+
"@shotkit/shotium-darwin-arm64": "0.2.0",
|
|
42
|
+
"@shotkit/shotium-darwin-x64": "0.2.0",
|
|
43
|
+
"@shotkit/shotium-linux-arm64": "0.2.0",
|
|
44
|
+
"@shotkit/shotium-linux-x64": "0.2.0",
|
|
45
|
+
"@shotkit/shotium-win32-arm64": "0.2.0",
|
|
46
|
+
"@shotkit/shotium-win32-x64": "0.2.0"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "tsdown",
|
|
50
|
+
"check:types": "tsc --noEmit",
|
|
51
|
+
"prepack": "tsdown"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/node": "22.20.1",
|
|
55
|
+
"tsdown": "0.22.14",
|
|
56
|
+
"typescript": "5.9.3"
|
|
57
|
+
},
|
|
58
|
+
"license": "BSD-3-Clause"
|
|
6
59
|
}
|
|
@@ -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();
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import * as client from './lib/client.js';
|
|
2
|
+
import type {DaemonClient} from './lib/client.js';
|
|
3
|
+
import {Engine} from './lib/engine.js';
|
|
4
|
+
import type {
|
|
5
|
+
DaemonOptions,
|
|
6
|
+
DaemonStatus,
|
|
7
|
+
PurgeOptions,
|
|
8
|
+
ScreenshotOptions,
|
|
9
|
+
StartOptions,
|
|
10
|
+
} from './types.js';
|
|
11
|
+
|
|
12
|
+
export type {
|
|
13
|
+
Clip,
|
|
14
|
+
DaemonOptions,
|
|
15
|
+
DaemonStatus,
|
|
16
|
+
PageGotoParams,
|
|
17
|
+
PurgeOptions,
|
|
18
|
+
ScreenshotOptions,
|
|
19
|
+
StartOptions,
|
|
20
|
+
Viewport,
|
|
21
|
+
} from './types.js';
|
|
22
|
+
export type {DaemonClient} from './lib/client.js';
|
|
23
|
+
|
|
24
|
+
/** The five things a caller does with the resident engine. */
|
|
25
|
+
export interface Daemon {
|
|
26
|
+
/** Connects, starting a daemon if none is listening. */
|
|
27
|
+
connect(options?: DaemonOptions): Promise<DaemonClient>;
|
|
28
|
+
/** One screenshot through the daemon, connection and all. */
|
|
29
|
+
screenshot(options: ScreenshotOptions&{daemon?: DaemonOptions}):
|
|
30
|
+
Promise<Buffer|null>;
|
|
31
|
+
/** Starts one if it is not up, and reports what is there either way. */
|
|
32
|
+
start(options?: DaemonOptions): Promise<DaemonStatus&{spawned: boolean}>;
|
|
33
|
+
status(options?: DaemonOptions):
|
|
34
|
+
Promise<Partial<DaemonStatus>&{running: boolean, endpoint: string}>;
|
|
35
|
+
stop(options?: DaemonOptions): Promise<{stopped: boolean, endpoint: string}>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The engine, and its lifecycle, in this process.
|
|
40
|
+
*
|
|
41
|
+
* import shotium from '@shotkit/shotium';
|
|
42
|
+
*
|
|
43
|
+
* shotium.runtime.start();
|
|
44
|
+
* const png = await shotium.screenshot({file: 'https://example.com'});
|
|
45
|
+
* await shotium.runtime.stop();
|
|
46
|
+
*
|
|
47
|
+
* `start` and `stop` are explicit because starting Blink is the expensive part
|
|
48
|
+
* -- tens of milliseconds and a working set that stays resident -- and only
|
|
49
|
+
* the caller knows whether the next screenshot is coming in a moment or never.
|
|
50
|
+
* Neither call is required: a screenshot starts the engine if it is not up.
|
|
51
|
+
* What they buy is control over when that cost is paid, and the certainty that
|
|
52
|
+
* it has been given back.
|
|
53
|
+
*
|
|
54
|
+
* `runtime` below is the singleton because there is nothing else it could be:
|
|
55
|
+
* Blink starts once per process and cannot be restarted, so a second Runtime
|
|
56
|
+
* in the same process has no engine to have. Construct one directly only to
|
|
57
|
+
* own the lifecycle yourself instead of using `runtime`. Parallelism is more
|
|
58
|
+
* processes, not more Runtimes.
|
|
59
|
+
*
|
|
60
|
+
* `daemon` is the same engine in a process of its own, behind a socket, for
|
|
61
|
+
* callers whose own process does not live long enough to be worth starting
|
|
62
|
+
* one.
|
|
63
|
+
*/
|
|
64
|
+
export class Runtime {
|
|
65
|
+
private engine = new Engine();
|
|
66
|
+
|
|
67
|
+
get running(): boolean {
|
|
68
|
+
return this.engine.running;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Starts the engine. Safe to call twice; the second call is a no-op, so that
|
|
73
|
+
* library code can call it defensively. Not safe after `stop()` -- see there.
|
|
74
|
+
*
|
|
75
|
+
* Every option has a default. `cacheDir` is the HTTP disk cache and `null`
|
|
76
|
+
* disables it; `resourceDir` is where `shotium_data.pak` and
|
|
77
|
+
* `shotium_strings.pak` are, and defaults to the directory the engine was
|
|
78
|
+
* loaded from, which is where they ship.
|
|
79
|
+
*/
|
|
80
|
+
start(options: StartOptions = {}): this {
|
|
81
|
+
this.engine.start(options);
|
|
82
|
+
return this;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Stops the engine, after whatever is queued.
|
|
87
|
+
*
|
|
88
|
+
* Final for this process. Blink writes process-wide state that it has no
|
|
89
|
+
* path to undo, so starting again -- here or on another Runtime -- throws
|
|
90
|
+
* rather than quietly handing back something that cannot render. A program
|
|
91
|
+
* that wants another screenshot later should stay started and `purge()`.
|
|
92
|
+
*/
|
|
93
|
+
stop(): Promise<void> {
|
|
94
|
+
return this.engine.stop();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Hands back what the engine is holding but can rebuild. Worth calling when
|
|
99
|
+
* a batch has ended and the next one may be a while away.
|
|
100
|
+
*/
|
|
101
|
+
purge(options: PurgeOptions = {}): void {
|
|
102
|
+
this.engine.purge(options);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Renders one screenshot. Resolves to the encoded image, or to `null` when
|
|
107
|
+
* `path` was given and the engine wrote the file itself.
|
|
108
|
+
*/
|
|
109
|
+
screenshot(options: ScreenshotOptions): Promise<Buffer|null> {
|
|
110
|
+
return this.engine.screenshot(options);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The shared engine: one per process, started on first use. */
|
|
115
|
+
const runtime = new Runtime();
|
|
116
|
+
|
|
117
|
+
/** One screenshot through the shared engine, starting it if it is not up. */
|
|
118
|
+
const screenshot = (options: ScreenshotOptions): Promise<Buffer|null> =>
|
|
119
|
+
runtime.screenshot(options);
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The resident engine: a process that outlives the one that started it,
|
|
123
|
+
* reachable over a named pipe on Windows and a unix socket elsewhere. For
|
|
124
|
+
* callers that are short-lived themselves. See lib/daemon.ts.
|
|
125
|
+
*/
|
|
126
|
+
const daemon: Daemon = {
|
|
127
|
+
connect: client.connect,
|
|
128
|
+
screenshot: client.screenshot,
|
|
129
|
+
start: client.start,
|
|
130
|
+
status: client.status,
|
|
131
|
+
stop: client.stop,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export {runtime, screenshot, daemon};
|
|
135
|
+
|
|
136
|
+
// A default as well as the names, because `import shotium from` is what a
|
|
137
|
+
// caller coming from `require` writes first, and the two have to be the same
|
|
138
|
+
// object rather than two views that drift.
|
|
139
|
+
export default {Runtime, runtime, screenshot, daemon};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import {createRequire} from 'node:module';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {fileURLToPath} from 'node:url';
|
|
5
|
+
|
|
6
|
+
import * as platformPackage from './platform.js';
|
|
7
|
+
|
|
8
|
+
// A .node addon is a CommonJS artefact: there is no ESM loader for one.
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
10
|
+
|
|
11
|
+
// ESM has no __dirname. This is the same thing, from the module's own URL.
|
|
12
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The engine handle the addon hands back. Opaque on purpose: everything that
|
|
16
|
+
* can be done with it is a call on the binding below.
|
|
17
|
+
*/
|
|
18
|
+
export type Engine = unknown;
|
|
19
|
+
|
|
20
|
+
/** What native/binding.cc exports. See shot/shot_api.h for the C ABI. */
|
|
21
|
+
export interface NativeBinding {
|
|
22
|
+
create(optionsJson: string): Engine;
|
|
23
|
+
destroy(engine: Engine): void;
|
|
24
|
+
purge(engine: Engine, releaseWorkingSet: boolean): void;
|
|
25
|
+
capture(engine: Engine, requestJson: string): Promise<Buffer>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Where the addon and the library beside it live.
|
|
29
|
+
//
|
|
30
|
+
// The platform package is what ships -- the .node sits next to the shared
|
|
31
|
+
// library it is linked against, which is the whole reason the two travel in
|
|
32
|
+
// one package rather than two. native/build/Release is where node-gyp puts a
|
|
33
|
+
// local build; it exists in a checkout and not in an install, so the two never
|
|
34
|
+
// compete in practice. Both paths are relative to this file's build output,
|
|
35
|
+
// which is one directory below the package root.
|
|
36
|
+
function candidates(): string[] {
|
|
37
|
+
const found: string[] = [];
|
|
38
|
+
const dir = platformPackage.packageDir();
|
|
39
|
+
if (dir) {
|
|
40
|
+
found.push(path.join(dir, 'shotium.node'));
|
|
41
|
+
}
|
|
42
|
+
found.push(
|
|
43
|
+
path.join(HERE, '..', 'native', 'build', 'Release', 'shotium.node'));
|
|
44
|
+
return found;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let binding: NativeBinding|null = null;
|
|
48
|
+
let loadedFrom: string|null = null;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The addon, loaded once. Throws if there is none for this platform, which is
|
|
52
|
+
* the only failure this package cannot work around: there is nothing else to
|
|
53
|
+
* fall back to.
|
|
54
|
+
*/
|
|
55
|
+
export function load(): NativeBinding {
|
|
56
|
+
if (binding) {
|
|
57
|
+
return binding;
|
|
58
|
+
}
|
|
59
|
+
const tried = candidates();
|
|
60
|
+
for (const candidate of tried) {
|
|
61
|
+
if (!fs.existsSync(candidate)) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
// Not wrapped in a try: a .node that is there and will not load is a
|
|
65
|
+
// broken installation, and the loader's own message -- a missing
|
|
66
|
+
// dependency, an architecture mismatch -- says more than anything that
|
|
67
|
+
// could be substituted for it.
|
|
68
|
+
binding = require(candidate) as NativeBinding;
|
|
69
|
+
loadedFrom = path.dirname(candidate);
|
|
70
|
+
return binding;
|
|
71
|
+
}
|
|
72
|
+
const expected = platformPackage.packageName();
|
|
73
|
+
throw new Error(
|
|
74
|
+
'shotium: no engine for this platform.\n' +
|
|
75
|
+
` looked in:\n ${tried.join('\n ')}\n` +
|
|
76
|
+
(expected ?
|
|
77
|
+
` It ships in ${expected}, which npm installs as an optional ` +
|
|
78
|
+
'dependency of this package. If the install skipped optional ' +
|
|
79
|
+
'dependencies, it is not there.\n' :
|
|
80
|
+
` There is no build for ${process.platform}-${process.arch}.\n`));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The directory the addon came from, or null before the first load(). The
|
|
85
|
+
* resource packs ship beside it, which is what this is for.
|
|
86
|
+
*/
|
|
87
|
+
export function directory(): string|null {
|
|
88
|
+
return loadedFrom;
|
|
89
|
+
}
|