| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent a58c98e commit db0efa3
12 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -2401,6 +2401,17 @@ error indicates that the idle loop has failed to stop. | |||
| 2401 | 2401 | An attempt was made to use operations that can only be used when building | |
| 2402 | 2402 | V8 startup snapshot even though Node.js isn't building one. | |
| 2403 | 2403 | ||
| 2404 | + <a id="ERR_NOT_IN_SINGLE_EXECUTABLE_APPLICATION"></a> | ||
| 2405 | + | ||
| 2406 | + ### `ERR_NOT_IN_SINGLE_EXECUTABLE_APPLICATION` | ||
| 2407 | + | ||
| 2408 | + <!-- YAML | ||
| 2409 | + added: REPLACEME | ||
| 2410 | + --> | ||
| 2411 | + | ||
| 2412 | + The operation cannot be performed when it's not in a single-executable | ||
| 2413 | + application. | ||
| 2414 | + | ||
| 2404 | 2415 | <a id="ERR_NOT_SUPPORTED_IN_SNAPSHOT"></a> | |
| 2405 | 2416 | ||
| 2406 | 2417 | ### `ERR_NOT_SUPPORTED_IN_SNAPSHOT` | |
@@ -2547,6 +2558,17 @@ The [`server.close()`][] method was called when a `net.Server` was not | |||
| 2547 | 2558 | running. This applies to all instances of `net.Server`, including HTTP, HTTPS, | |
| 2548 | 2559 | and HTTP/2 `Server` instances. | |
| 2549 | 2560 | ||
| 2561 | + <a id="ERR_SINGLE_EXECUTABLE_APPLICATION_ASSET_NOT_FOUND"></a> | ||
| 2562 | + | ||
| 2563 | + ### `ERR_SINGLE_EXECUTABLE_APPLICATION_ASSET_NOT_FOUND` | ||
| 2564 | + | ||
| 2565 | + <!-- YAML | ||
| 2566 | + added: REPLACEME | ||
| 2567 | + --> | ||
| 2568 | + | ||
| 2569 | + A key was passed to single executable application APIs to identify an asset, | ||
| 2570 | + but no match could be found. | ||
| 2571 | + | ||
| 2550 | 2572 | <a id="ERR_SOCKET_ALREADY_BOUND"></a> | |
| 2551 | 2573 | ||
| 2552 | 2574 | ### `ERR_SOCKET_ALREADY_BOUND` | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -178,14 +178,52 @@ The configuration currently reads the following top-level fields: | |||
| 178 | 178 | "output": "/path/to/write/the/generated/blob.blob", | |
| 179 | 179 | "disableExperimentalSEAWarning": true, // Default: false | |
| 180 | 180 | "useSnapshot": false, // Default: false | |
| 181 | - "useCodeCache": true // Default: false | ||
| 181 | + "useCodeCache": true, // Default: false | ||
| 182 | + "assets": { // Optional | ||
| 183 | + "a.dat": "/path/to/a.dat", | ||
| 184 | + "b.txt": "/path/to/b.txt" | ||
| 185 | + } | ||
| 182 | 186 | } | |
| 183 | 187 | ``` | |
| 184 | 188 | ||
| 185 | 189 | If the paths are not absolute, Node.js will use the path relative to the | |
| 186 | 190 | current working directory. The version of the Node.js binary used to produce | |
| 187 | 191 | the blob must be the same as the one to which the blob will be injected. | |
| 188 | 192 | ||
| 193 | + ### Assets | ||
| 194 | + | ||
| 195 | + Users can include assets by adding a key-path dictionary to the configuration | ||
| 196 | + as the `assets` field. At build time, Node.js would read the assets from the | ||
| 197 | + specified paths and bundle them into the preparation blob. In the generated | ||
| 198 | + executable, users can retrieve the assets using the [`sea.getAsset()`][] and | ||
| 199 | + [`sea.getAssetAsBlob()`][] APIs. | ||
| 200 | + | ||
| 201 | + ```json | ||
| 202 | + { | ||
| 203 | + "main": "/path/to/bundled/script.js", | ||
| 204 | + "output": "/path/to/write/the/generated/blob.blob", | ||
| 205 | + "assets": { | ||
| 206 | + "a.jpg": "/path/to/a.jpg", | ||
| 207 | + "b.txt": "/path/to/b.txt" | ||
| 208 | + } | ||
| 209 | + } | ||
| 210 | + ``` | ||
| 211 | + | ||
| 212 | + The single-executable application can access the assets as follows: | ||
| 213 | + | ||
| 214 | + ```cjs | ||
| 215 | + const { getAsset } = require('node:sea'); | ||
| 216 | + // Returns a copy of the data in an ArrayBuffer. | ||
| 217 | + const image = getAsset('a.jpg'); | ||
| 218 | + // Returns a string decoded from the asset as UTF8. | ||
| 219 | + const text = getAsset('b.txt', 'utf8'); | ||
| 220 | + // Returns a Blob containing the asset. | ||
| 221 | + const blob = getAssetAsBlob('a.jpg'); | ||
| 222 | + ``` | ||
| 223 | + | ||
| 224 | + See documentation of the [`sea.getAsset()`][] and [`sea.getAssetAsBlob()`][] | ||
| 225 | + APIs for more information. | ||
| 226 | + | ||
| 189 | 227 | ### Startup snapshot support | |
| 190 | 228 | ||
| 191 | 229 | The `useSnapshot` field can be used to enable startup snapshot support. In this | |
@@ -229,11 +267,58 @@ execute the script, which would improve the startup performance. | |||
| 229 | 267 | ||
| 230 | 268 | **Note:** `import()` does not work when `useCodeCache` is `true`. | |
| 231 | 269 | ||
| 232 | - ## Notes | ||
| 270 | + ## In the injected main script | ||
| 233 | 271 | ||
| 234 | - ### `require(id)` in the injected module is not file based | ||
| 272 | + ### Single-executable application API | ||
| 235 | 273 | ||
| 236 | - `require()` in the injected module is not the same as the [`require()`][] | ||
| 274 | + The `node:sea` builtin allows interaction with the single-executable application | ||
| 275 | + from the JavaScript main script embedded into the executable. | ||
| 276 | + | ||
| 277 | + #### `sea.isSea()` | ||
| 278 | + | ||
| 279 | + <!-- YAML | ||
| 280 | + added: REPLACEME | ||
| 281 | + --> | ||
| 282 | + | ||
| 283 | + * Returns: {boolean} Whether this script is running inside a single-executable | ||
| 284 | + application. | ||
| 285 | + | ||
| 286 | + ### `sea.getAsset(key[, encoding])` | ||
| 287 | + | ||
| 288 | + <!-- YAML | ||
| 289 | + added: REPLACEME | ||
| 290 | + --> | ||
| 291 | + | ||
| 292 | + This method can be used to retrieve the assets configured to be bundled into the | ||
| 293 | + single-executable application at build time. | ||
| 294 | + An error is thrown when no matching asset can be found. | ||
| 295 | + | ||
| 296 | + * `key` {string} the key for the asset in the dictionary specified by the | ||
| 297 | + `assets` field in the single-executable application configuration. | ||
| 298 | + * `encoding` {string} If specified, the asset will be decoded as | ||
| 299 | + a string. Any encoding supported by the `TextDecoder` is accepted. | ||
| 300 | + If unspecified, an `ArrayBuffer` containing a copy of the asset would be | ||
| 301 | + returned instead. | ||
| 302 | + * Returns: {string|ArrayBuffer} | ||
| 303 | + | ||
| 304 | + ### `sea.getAssetAsBlob(key[, options])` | ||
| 305 | + | ||
| 306 | + <!-- YAML | ||
| 307 | + added: REPLACEME | ||
| 308 | + --> | ||
| 309 | + | ||
| 310 | + Similar to [`sea.getAsset()`][], but returns the result in a [`Blob`][]. | ||
| 311 | + An error is thrown when no matching asset can be found. | ||
| 312 | + | ||
| 313 | + * `key` {string} the key for the asset in the dictionary specified by the | ||
| 314 | + `assets` field in the single-executable application configuration. | ||
| 315 | + * `options` {Object} | ||
| 316 | + * `type` {string} An optional mime type for the blob. | ||
| 317 | + * Returns: {Blob} | ||
| 318 | + | ||
| 319 | + ### `require(id)` in the injected main script is not file based | ||
| 320 | + | ||
| 321 | + `require()` in the injected main script is not the same as the [`require()`][] | ||
| 237 | 322 | available to modules that are not injected. It also does not have any of the | |
| 238 | 323 | properties that non-injected [`require()`][] has except [`require.main`][]. It | |
| 239 | 324 | can only be used to load built-in modules. Attempting to load a module that can | |
@@ -250,15 +335,17 @@ const { createRequire } = require('node:module'); | |||
| 250 | 335 | require = createRequire(__filename); | |
| 251 | 336 | ``` | |
| 252 | 337 | ||
| 253 | - ### `__filename` and `module.filename` in the injected module | ||
| 338 | + ### `__filename` and `module.filename` in the injected main script | ||
| 254 | 339 | ||
| 255 | - The values of `__filename` and `module.filename` in the injected module are | ||
| 256 | - equal to [`process.execPath`][]. | ||
| 340 | + The values of `__filename` and `module.filename` in the injected main script | ||
| 341 | + are equal to [`process.execPath`][]. | ||
| 257 | 342 | ||
| 258 | - ### `__dirname` in the injected module | ||
| 343 | + ### `__dirname` in the injected main script | ||
| 259 | 344 | ||
| 260 | - The value of `__dirname` in the injected module is equal to the directory name | ||
| 261 | - of [`process.execPath`][]. | ||
| 345 | + The value of `__dirname` in the injected main script is equal to the directory | ||
| 346 | + name of [`process.execPath`][]. | ||
| 347 | + | ||
| 348 | + ## Notes | ||
| 262 | 349 | ||
| 263 | 350 | ### Single executable application creation process | |
| 264 | 351 | ||
@@ -298,9 +385,12 @@ to help us document them. | |||
| 298 | 385 | [Mach-O]: https://en.wikipedia.org/wiki/Mach-O | |
| 299 | 386 | [PE]: https://en.wikipedia.org/wiki/Portable_Executable | |
| 300 | 387 | [Windows SDK]: https://developer.microsoft.com/en-us/windows/downloads/windows-sdk/ | |
| 388 | + [`Blob`]: https://developer.mozilla.org/en-US/docs/Web/API/Blob | ||
| 301 | 389 | [`process.execPath`]: process.md#processexecpath | |
| 302 | 390 | [`require()`]: modules.md#requireid | |
| 303 | 391 | [`require.main`]: modules.md#accessing-the-main-module | |
| 392 | + [`sea.getAsset()`]: #seagetassetkey-encoding | ||
| 393 | + [`sea.getAssetAsBlob()`]: #seagetassetasblobkey-options | ||
| 304 | 394 | [`v8.startupSnapshot.setDeserializeMainFunction()`]: v8.md#v8startupsnapshotsetdeserializemainfunctioncallback-data | |
| 305 | 395 | [`v8.startupSnapshot` API]: v8.md#startup-snapshot-api | |
| 306 | 396 | [documentation about startup snapshot support in Node.js]: cli.md#--build-snapshot | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -128,6 +128,7 @@ const legacyWrapperList = new SafeSet([ | |||
| 128 | 128 | // beginning with "internal/". | |
| 129 | 129 | // Modules that can only be imported via the node: scheme. | |
| 130 | 130 | const schemelessBlockList = new SafeSet([ | |
| 131 | + 'sea', | ||
| 131 | 132 | 'test', | |
| 132 | 133 | 'test/reporters', | |
| 133 | 134 | ]); | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1637,6 +1637,8 @@ E('ERR_NETWORK_IMPORT_DISALLOWED', | |||
| 1637 | 1637 | "import of '%s' by %s is not supported: %s", Error); | |
| 1638 | 1638 | E('ERR_NOT_BUILDING_SNAPSHOT', | |
| 1639 | 1639 | 'Operation cannot be invoked when not building startup snapshot', Error); | |
| 1640 | + E('ERR_NOT_IN_SINGLE_EXECUTABLE_APPLICATION', | ||
| 1641 | + 'Operation cannot be invoked when not in a single-executable application', Error); | ||
| 1640 | 1642 | E('ERR_NOT_SUPPORTED_IN_SNAPSHOT', '%s is not supported in startup snapshot', Error); | |
| 1641 | 1643 | E('ERR_NO_CRYPTO', | |
| 1642 | 1644 | 'Node.js is not compiled with OpenSSL crypto support', Error); | |
@@ -1720,6 +1722,8 @@ E('ERR_SCRIPT_EXECUTION_INTERRUPTED', | |||
| 1720 | 1722 | E('ERR_SERVER_ALREADY_LISTEN', | |
| 1721 | 1723 | 'Listen method has been called more than once without closing.', Error); | |
| 1722 | 1724 | E('ERR_SERVER_NOT_RUNNING', 'Server is not running.', Error); | |
| 1725 | + E('ERR_SINGLE_EXECUTABLE_APPLICATION_ASSET_NOT_FOUND', | ||
| 1726 | + 'Cannot find asset %s for the single executable application', Error); | ||
| 1723 | 1727 | E('ERR_SOCKET_ALREADY_BOUND', 'Socket is already bound', Error); | |
| 1724 | 1728 | E('ERR_SOCKET_BAD_BUFFER_SIZE', | |
| 1725 | 1729 | 'Buffer size must be a positive integer', TypeError); | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,75 @@ | |||
| 1 | + 'use strict'; | ||
| 2 | + const { | ||
| 3 | + ArrayBufferPrototypeSlice, | ||
| 4 | + } = primordials; | ||
| 5 | + | ||
| 6 | + const { isSea, getAsset: getAssetInternal } = internalBinding('sea'); | ||
| 7 | + const { TextDecoder } = require('internal/encoding'); | ||
| 8 | + const { validateString } = require('internal/validators'); | ||
| 9 | + const { | ||
| 10 | + ERR_NOT_IN_SINGLE_EXECUTABLE_APPLICATION, | ||
| 11 | + ERR_SINGLE_EXECUTABLE_APPLICATION_ASSET_NOT_FOUND, | ||
| 12 | + } = require('internal/errors').codes; | ||
| 13 | + const { Blob } = require('internal/blob'); | ||
| 14 | + | ||
| 15 | + /** | ||
| 16 | + * Look for the asset in the injected SEA blob using the key. If | ||
| 17 | + * no matching asset is found an error is thrown. The returned | ||
| 18 | + * ArrayBuffer should not be mutated or otherwise the process | ||
| 19 | + * can crash due to access violation. | ||
| 20 | + * @param {string} key | ||
| 21 | + * @returns {ArrayBuffer} | ||
| 22 | + */ | ||
| 23 | + function getRawAsset(key) { | ||
| 24 | + validateString(key, 'key'); | ||
| 25 | + | ||
| 26 | + if (!isSea()) { | ||
| 27 | + throw new ERR_NOT_IN_SINGLE_EXECUTABLE_APPLICATION(); | ||
| 28 | + } | ||
| 29 | + | ||
| 30 | + const asset = getAssetInternal(key); | ||
| 31 | + if (asset === undefined) { | ||
| 32 | + throw new ERR_SINGLE_EXECUTABLE_APPLICATION_ASSET_NOT_FOUND(key); | ||
| 33 | + } | ||
| 34 | + return asset; | ||
| 35 | + } | ||
| 36 | + | ||
| 37 | + /** | ||
| 38 | + * Look for the asset in the injected SEA blob using the key. If the | ||
| 39 | + * encoding is specified, return a string decoded from it by TextDecoder, | ||
| 40 | + * otherwise return *a copy* of the original data in an ArrayBuffer. If | ||
| 41 | + * no matching asset is found an error is thrown. | ||
| 42 | + * @param {string} key | ||
| 43 | + * @param {string|undefined} encoding | ||
| 44 | + * @returns {string|ArrayBuffer} | ||
| 45 | + */ | ||
| 46 | + function getAsset(key, encoding) { | ||
| 47 | + if (encoding !== undefined) { | ||
| 48 | + validateString(encoding, 'encoding'); | ||
| 49 | + } | ||
| 50 | + const asset = getRawAsset(key); | ||
| 51 | + if (encoding === undefined) { | ||
| 52 | + return ArrayBufferPrototypeSlice(asset); | ||
| 53 | + } | ||
| 54 | + const decoder = new TextDecoder(encoding); | ||
| 55 | + return decoder.decode(asset); | ||
| 56 | + } | ||
| 57 | + | ||
| 58 | + /** | ||
| 59 | + * Look for the asset in the injected SEA blob using the key. If | ||
| 60 | + * no matching asset is found an error is thrown. The data is returned | ||
| 61 | + * in a Blob. If no matching asset is found an error is thrown. | ||
| 62 | + * @param {string} key | ||
| 63 | + * @param {ConstructorParameters<Blob>[1]} [options] | ||
| 64 | + * @returns {Blob} | ||
| 65 | + */ | ||
| 66 | + function getAssetAsBlob(key, options) { | ||
| 67 | + const asset = getRawAsset(key); | ||
| 68 | + return new Blob([asset], options); | ||
| 69 | + } | ||
| 70 | + | ||
| 71 | + module.exports = { | ||
| 72 | + isSea, | ||
| 73 | + getAsset, | ||
| 74 | + getAssetAsBlob, | ||
| 75 | + }; | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -4,6 +4,7 @@ | |||
| 4 | 4 | #include "util-inl.h" | |
| 5 | 5 | ||
| 6 | 6 | namespace node { | |
| 7 | + using v8::Array; | ||
| 7 | 8 | using v8::Context; | |
| 8 | 9 | using v8::Isolate; | |
| 9 | 10 | using v8::Local; | |
@@ -101,4 +102,51 @@ std::optional<bool> JSONParser::GetTopLevelBoolField(std::string_view field) { | |||
| 101 | 102 | return value->BooleanValue(isolate); | |
| 102 | 103 | } | |
| 103 | 104 | ||
| 105 | + std::optional<JSONParser::StringDict> JSONParser::GetTopLevelStringDict( | ||
| 106 | + std::string_view field) { | ||
| 107 | + Isolate* isolate = isolate_.get(); | ||
| 108 | + v8::HandleScope handle_scope(isolate); | ||
| 109 | + Local<Context> context = context_.Get(isolate); | ||
| 110 | + Local<Object> content_object = content_.Get(isolate); | ||
| 111 | + Local<Value> value; | ||
| 112 | + bool has_field; | ||
| 113 | + // It's not a real script, so don't print the source line. | ||
| 114 | + errors::PrinterTryCatch bootstrapCatch( | ||
| 115 | + isolate, errors::PrinterTryCatch::kDontPrintSourceLine); | ||
| 116 | + Local<Value> field_local; | ||
| 117 | + if (!ToV8Value(context, field, isolate).ToLocal(&field_local)) { | ||
| 118 | + return std::nullopt; | ||
| 119 | + } | ||
| 120 | + if (!content_object->Has(context, field_local).To(&has_field)) { | ||
| 121 | + return std::nullopt; | ||
| 122 | + } | ||
| 123 | + if (!has_field) { | ||
| 124 | + return StringDict(); | ||
| 125 | + } | ||
| 126 | + if (!content_object->Get(context, field_local).ToLocal(&value) || | ||
| 127 | + !value->IsObject()) { | ||
| 128 | + return std::nullopt; | ||
| 129 | + } | ||
| 130 | + Local<Object> dict = value.As<Object>(); | ||
| 131 | + Local<Array> keys; | ||
| 132 | + if (!dict->GetOwnPropertyNames(context).ToLocal(&keys)) { | ||
| 133 | + return std::nullopt; | ||
| 134 | + } | ||
| 135 | + std::unordered_map<std::string, std::string> result; | ||
| 136 | + uint32_t length = keys->Length(); | ||
| 137 | + for (uint32_t i = 0; i < length; ++i) { | ||
| 138 | + Local<Value> key; | ||
| 139 | + Local<Value> value; | ||
| 140 | + if (!keys->Get(context, i).ToLocal(&key) || !key->IsString()) | ||
| 141 | + return StringDict(); | ||
| 142 | + if (!dict->Get(context, key).ToLocal(&value) || !value->IsString()) | ||
| 143 | + return StringDict(); | ||
| 144 | + | ||
| 145 | + Utf8Value key_utf8(isolate, key); | ||
| 146 | + Utf8Value value_utf8(isolate, value); | ||
| 147 | + result.emplace(*key_utf8, *value_utf8); | ||
| 148 | + } | ||
| 149 | + return result; | ||
| 150 | + } | ||
| 151 | + | ||
| 104 | 152 | } // namespace node | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -6,6 +6,7 @@ | |||
| 6 | 6 | #include <memory> | |
| 7 | 7 | #include <optional> | |
| 8 | 8 | #include <string> | |
| 9 | + #include <unordered_map> | ||
| 9 | 10 | #include "util.h" | |
| 10 | 11 | #include "v8.h" | |
| 11 | 12 | ||
@@ -15,11 +16,13 @@ namespace node { | |||
| 15 | 16 | // complicates things. | |
| 16 | 17 | class JSONParser { | |
| 17 | 18 | public: | |
| 19 | + using StringDict = std::unordered_map<std::string, std::string>; | ||
| 18 | 20 | JSONParser(); | |
| 19 | 21 | ~JSONParser() = default; | |
| 20 | 22 | bool Parse(const std::string& content); | |
| 21 | 23 | std::optional<std::string> GetTopLevelStringField(std::string_view field); | |
| 22 | 24 | std::optional<bool> GetTopLevelBoolField(std::string_view field); | |
| 25 | + std::optional<StringDict> GetTopLevelStringDict(std::string_view field); | ||
| 23 | 26 | ||
| 24 | 27 | private: | |
| 25 | 28 | // We might want a lighter-weight JSON parser for this use case. But for now | |
| Back | FazBrowse Home | New Git URL |
0 commit comments