diff --git a/lib/internal/webstorage.js b/lib/internal/webstorage.js index cf8820bda0c..10ecef7db69 100644 --- a/lib/internal/webstorage.js +++ b/lib/internal/webstorage.js @@ -6,6 +6,11 @@ const { hasInspector } = internalBinding('config'); const { getOptionValue } = require('internal/options'); const { kConstructorKey, Storage } = internalBinding('webstorage'); const { getValidatedPath } = require('internal/fs/utils'); +const { + addAfterUserSerializeCallback, + namespace: { isBuildingSnapshot }, + throwIfBuildingSnapshot, +} = require('internal/v8/startup_snapshot'); const kInMemoryPath = ':memory:'; module.exports = { Storage }; @@ -14,12 +19,19 @@ let lazyLocalStorage; let lazySessionStorage; let lazyInspectorStorage; let localStorageWarned = false; +if (isBuildingSnapshot()) { + // User serialize callbacks can still read localStorage. + addAfterUserSerializeCallback(() => { + localStorageWarned = false; + }); +} -// Check at load time if localStorage file is provided to determine enumerability. -// If not provided, localStorage is non-enumerable to avoid breaking {...globalThis}. -const localStorageLocation = getOptionValue('--localstorage-file'); -const experimentalStorageInspection = - hasInspector && getOptionValue('--experimental-storage-inspection'); +// This module can be loaded while building a startup snapshot and then run +// with different options, so the getters read the options when they are +// called instead of at load time. +function isStorageInspectionEnabled() { + return hasInspector && getOptionValue('--experimental-storage-inspection'); +} function getInspectorStorage() { if (lazyInspectorStorage === undefined) { @@ -33,9 +45,11 @@ ObjectDefineProperties(module.exports, { localStorage: { __proto__: null, configurable: true, - enumerable: localStorageLocation !== '', + // If no file is provided, localStorage is non-enumerable to avoid breaking {...globalThis}. + enumerable: getOptionValue('--localstorage-file') !== '', get() { if (lazyLocalStorage === undefined) { + const localStorageLocation = getOptionValue('--localstorage-file'); if (localStorageLocation === '') { if (!localStorageWarned) { localStorageWarned = true; @@ -47,7 +61,8 @@ ObjectDefineProperties(module.exports, { return undefined; } - if (experimentalStorageInspection) { + throwIfBuildingSnapshot('localStorage'); + if (isStorageInspectionEnabled()) { const { InspectorLocalStorage } = getInspectorStorage(); lazyLocalStorage = new InspectorLocalStorage(kConstructorKey, getValidatedPath(localStorageLocation), true); } else { @@ -63,7 +78,8 @@ ObjectDefineProperties(module.exports, { enumerable: true, get() { if (lazySessionStorage === undefined) { - if (experimentalStorageInspection) { + throwIfBuildingSnapshot('sessionStorage'); + if (isStorageInspectionEnabled()) { const { InspectorSessionStorage } = getInspectorStorage(); lazySessionStorage = new InspectorSessionStorage(kConstructorKey, kInMemoryPath, false); } else { diff --git a/src/node_external_reference.h b/src/node_external_reference.h index abdf6a36cbc..1be7bab3ebf 100644 --- a/src/node_external_reference.h +++ b/src/node_external_reference.h @@ -145,12 +145,19 @@ class ExternalReferenceRegistry { #define EXTERNAL_REFERENCE_BINDING_LIST_QUIC(V) #endif +#if HAVE_SQLITE +#define EXTERNAL_REFERENCE_BINDING_LIST_SQLITE(V) V(webstorage) +#else +#define EXTERNAL_REFERENCE_BINDING_LIST_SQLITE(V) +#endif // HAVE_SQLITE + #define EXTERNAL_REFERENCE_BINDING_LIST(V) \ EXTERNAL_REFERENCE_BINDING_LIST_BASE(V) \ EXTERNAL_REFERENCE_BINDING_LIST_INSPECTOR(V) \ EXTERNAL_REFERENCE_BINDING_LIST_I18N(V) \ EXTERNAL_REFERENCE_BINDING_LIST_CRYPTO(V) \ - EXTERNAL_REFERENCE_BINDING_LIST_QUIC(V) + EXTERNAL_REFERENCE_BINDING_LIST_QUIC(V) \ + EXTERNAL_REFERENCE_BINDING_LIST_SQLITE(V) } // namespace node diff --git a/src/node_webstorage.cc b/src/node_webstorage.cc index 204fd463126..ccfcaf7db6c 100644 --- a/src/node_webstorage.cc +++ b/src/node_webstorage.cc @@ -7,6 +7,7 @@ #include "memory_tracker-inl.h" #include "node.h" #include "node_errors.h" +#include "node_external_reference.h" #include "node_mem-inl.h" #include "path.h" #include "simdutf.h" @@ -813,7 +814,30 @@ static void Initialize(Local target, .Check(); } +static void RegisterExternalReferences(ExternalReferenceRegistry* registry) { + registry->Register(Storage::New); + registry->Register(StorageGetter); + registry->Register(StorageSetter); + registry->Register(StorageQuery); + registry->Register(StorageDeleter); + registry->Register(StorageEnumerator); + registry->Register(StorageDefiner); + registry->Register(IndexedGetter); + registry->Register(IndexedSetter); + registry->Register(IndexedQuery); + registry->Register(IndexedDeleter); + registry->Register(IndexedDefiner); + registry->Register(StorageLengthGetter); + registry->Register(Clear); + registry->Register(GetItem); + registry->Register(Key); + registry->Register(RemoveItem); + registry->Register(SetItem); +} + } // namespace webstorage } // namespace node NODE_BINDING_CONTEXT_AWARE_INTERNAL(webstorage, node::webstorage::Initialize) +NODE_BINDING_EXTERNAL_REFERENCE(webstorage, + node::webstorage::RegisterExternalReferences) diff --git a/test/parallel/test-snapshot-webstorage.js b/test/parallel/test-snapshot-webstorage.js new file mode 100644 index 00000000000..14ab41cfc71 --- /dev/null +++ b/test/parallel/test-snapshot-webstorage.js @@ -0,0 +1,86 @@ +'use strict'; + +// Tests that web storage can be touched by a snapshot builder script: the +// binding can be serialized, creating a Storage is rejected with an error, and +// the options passed after deserialization are honored. + +const common = require('../common'); +common.skipIfSQLiteMissing(); +const assert = require('assert'); +const fs = require('fs'); +const tmpdir = require('../common/tmpdir'); +const { + spawnSyncAndAssert, + spawnSyncAndExitWithoutError, +} = require('../common/child_process'); + +tmpdir.refresh(); +const blobPath = tmpdir.resolve('snapshot.blob'); + +for (const [name, args] of [ + ['localStorage', [`--localstorage-file=${tmpdir.resolve('build.db')}`]], + ['sessionStorage', []], +]) { + const entry = tmpdir.resolve(`${name}.js`); + fs.writeFileSync(entry, `globalThis.${name};\n`); + spawnSyncAndAssert(process.execPath, [ + ...args, + '--snapshot-blob', + blobPath, + '--build-snapshot', + entry, + ], { + cwd: tmpdir.path, + }, { + status: 1, + stderr: new RegExp( + `ERR_NOT_SUPPORTED_IN_SNAPSHOT.*${name} is not supported in startup snapshot`, 's'), + }); + assert(!fs.existsSync(blobPath)); +} + +{ + const entry = tmpdir.resolve('no-file.js'); + fs.writeFileSync(entry, ` + const { startupSnapshot } = require('v8'); + if (globalThis.localStorage !== undefined) throw new Error('unexpected'); + // Registered after the web storage module has registered its own. + startupSnapshot.addSerializeCallback(() => globalThis.localStorage); + startupSnapshot.setDeserializeMainFunction(() => { + localStorage?.setItem('local', 'a'); + sessionStorage.setItem('session', 'b'); + console.log(localStorage?.getItem('local'), sessionStorage.getItem('session')); + }); + `); + spawnSyncAndExitWithoutError(process.execPath, [ + '--snapshot-blob', + blobPath, + '--build-snapshot', + entry, + ], { + cwd: tmpdir.path, + }); + + spawnSyncAndAssert(process.execPath, [ + `--localstorage-file=${tmpdir.resolve('run.db')}`, + '--snapshot-blob', + blobPath, + ], { + cwd: tmpdir.path, + }, { + stdout: 'a b', + trim: true, + }); + + // The warning from the build is not carried over into the snapshot. + spawnSyncAndAssert(process.execPath, [ + '--snapshot-blob', + blobPath, + ], { + cwd: tmpdir.path, + }, { + stdout: 'undefined b', + stderr: /localStorage is not available because --localstorage-file was not provided/, + trim: true, + }); +}