@electron-stagewright/origin-migration 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +94 -2
- package/dist/blank.d.ts +5 -0
- package/dist/blank.d.ts.map +1 -0
- package/dist/blank.js +7 -0
- package/dist/blank.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/migrate-origin.d.ts +28 -0
- package/dist/migrate-origin.d.ts.map +1 -0
- package/dist/migrate-origin.js +28 -0
- package/dist/migrate-origin.js.map +1 -0
- package/dist/migration.d.ts +82 -0
- package/dist/migration.d.ts.map +1 -0
- package/dist/migration.js +265 -0
- package/dist/migration.js.map +1 -0
- package/dist/pages.d.ts +16 -0
- package/dist/pages.d.ts.map +1 -0
- package/dist/pages.js +114 -0
- package/dist/pages.js.map +1 -0
- package/dist/scripts.d.ts +24 -0
- package/dist/scripts.d.ts.map +1 -0
- package/dist/scripts.js +110 -0
- package/dist/scripts.js.map +1 -0
- package/package.json +57 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Electron Stagewright contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,95 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @electron-stagewright/origin-migration
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Moving an Electron renderer from `file://` to a custom scheme changes its storage origin, so the
|
|
4
|
+
first launch on the new scheme starts with empty storage. This package copies the old data across
|
|
5
|
+
once, from your app's main process.
|
|
6
|
+
|
|
7
|
+
**v0 copies `localStorage` and the origin private file system (OPFS) only.** IndexedDB, cookies,
|
|
8
|
+
`sessionStorage` and Cache Storage are not copied. If your app keeps data there, this package does
|
|
9
|
+
not move it yet.
|
|
10
|
+
|
|
11
|
+
## Guarantees
|
|
12
|
+
|
|
13
|
+
- A `localStorage` key the new origin already holds is never overwritten.
|
|
14
|
+
- The `file://` copy is never modified, so an older build still opens with its data.
|
|
15
|
+
- OPFS files are copied in 1 MiB chunks to staging files, and moved into place only after every
|
|
16
|
+
file is staged. A copied file replaces a file of the same path on the new origin.
|
|
17
|
+
- A database and its `.wal` move together, database first. A `.wal` without its database is
|
|
18
|
+
skipped. Only `<name>.wal` sidecars are grouped (DuckDB); SQLite's `-wal`, `-journal` and
|
|
19
|
+
`-shm` files are not supported yet.
|
|
20
|
+
- A marker in `userData` makes the copy run once. A failed attempt is retried on the next launch,
|
|
21
|
+
three attempts in all. A crash mid-copy counts as an attempt. A marker that exists but cannot be
|
|
22
|
+
read counts as done.
|
|
23
|
+
- Data the user deletes on the new origin after it was copied is never restored by a retry.
|
|
24
|
+
|
|
25
|
+
## Use
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { app, BrowserWindow, protocol, session } from 'electron'
|
|
29
|
+
import { blankDocumentResponse, migrateOrigin } from '@electron-stagewright/origin-migration'
|
|
30
|
+
|
|
31
|
+
const BLANK_URL = 'app://example/.origin-migration/blank.html'
|
|
32
|
+
|
|
33
|
+
protocol.registerSchemesAsPrivileged([
|
|
34
|
+
{ scheme: 'app', privileges: { standard: true, secure: true, supportFetchAPI: true } },
|
|
35
|
+
])
|
|
36
|
+
|
|
37
|
+
app.whenReady().then(async () => {
|
|
38
|
+
protocol.handle('app', (request) => {
|
|
39
|
+
if (request.url === BLANK_URL) return blankDocumentResponse()
|
|
40
|
+
return serveRenderer(request) // your handler
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
// Create the window first: closing the migration's pages must not close the app.
|
|
44
|
+
const window = new BrowserWindow({ show: false })
|
|
45
|
+
const result = await migrateOrigin({
|
|
46
|
+
userDataDir: app.getPath('userData'),
|
|
47
|
+
BrowserWindow,
|
|
48
|
+
session: session.defaultSession,
|
|
49
|
+
targetUrl: BLANK_URL,
|
|
50
|
+
onProgress: (done, total) => window.setProgressBar(total > 0 ? done / total : -1),
|
|
51
|
+
})
|
|
52
|
+
window.setProgressBar(-1)
|
|
53
|
+
if (result.status === 'failed' && result.final) {
|
|
54
|
+
// Tell the user their old data could not be copied.
|
|
55
|
+
}
|
|
56
|
+
await window.loadURL('app://example/index.html')
|
|
57
|
+
window.show()
|
|
58
|
+
})
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`migrateOrigin` resolves with one of:
|
|
62
|
+
|
|
63
|
+
| `status` | Meaning |
|
|
64
|
+
| ---------- | ---------------------------------------------------------------------------- |
|
|
65
|
+
| `skipped` | Already done, out of attempts, or the marker is unreadable. Nothing opened. |
|
|
66
|
+
| `empty` | Nothing was left to copy on the `file://` origin. |
|
|
67
|
+
| `migrated` | Data was copied. `marker` has key, file and byte counts. |
|
|
68
|
+
| `failed` | This attempt failed. `marker.error` names the error; `final` means no retry. |
|
|
69
|
+
|
|
70
|
+
Options:
|
|
71
|
+
|
|
72
|
+
| Option | Default | Notes |
|
|
73
|
+
| --------------- | ----------------------- | ----------------------------------------------------------- |
|
|
74
|
+
| `targetUrl` | required | An empty document on the new origin. Must not be `file://`. |
|
|
75
|
+
| `session` | required | The session whose storage moves; partitions work. |
|
|
76
|
+
| `stores` | both | `['localStorage']`, `['opfs']` or both. |
|
|
77
|
+
| `markerFile` | `origin-migration.json` | A file name in `userDataDir`. Keep an existing marker name. |
|
|
78
|
+
| `stepTimeoutMs` | 30000 | Bounds each page load and each page step. |
|
|
79
|
+
| `signal` | | Aborts the attempt; moves already started still finish. |
|
|
80
|
+
|
|
81
|
+
## Requirements
|
|
82
|
+
|
|
83
|
+
- Electron 42 or later.
|
|
84
|
+
- The release that migrates must keep the `GrantFileProtocolExtraPrivileges` fuse on. Without it a
|
|
85
|
+
`file://` page cannot read `localStorage`; the marker then records `localStorageUnreadable` and
|
|
86
|
+
only OPFS files move.
|
|
87
|
+
- Call it before the renderer loads from the new origin, so the app never boots on empty settings.
|
|
88
|
+
|
|
89
|
+
## Lower-level API
|
|
90
|
+
|
|
91
|
+
`runOriginMigration` takes a `LegacyOriginReader` and a `TargetOriginWriter` instead of windows,
|
|
92
|
+
for apps that reach the origins another way. `openLegacyOriginPage` and `openTargetOriginPage`
|
|
93
|
+
build the page-backed ones `migrateOrigin` uses.
|
|
94
|
+
|
|
95
|
+
See [ADR-031](../../docs/adr/031-origin-migration-library.md) for the design.
|
package/dist/blank.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** An empty document with no script, style or network access. */
|
|
2
|
+
export declare const BLANK_DOCUMENT_HTML = "<!doctype html><meta charset=\"utf-8\"><meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'none'\"><title></title>";
|
|
3
|
+
/** The blank document as a `protocol.handle` response for the migration's target URL. */
|
|
4
|
+
export declare function blankDocumentResponse(): Response;
|
|
5
|
+
//# sourceMappingURL=blank.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blank.d.ts","sourceRoot":"","sources":["../src/blank.ts"],"names":[],"mappings":"AAAA,iEAAiE;AACjE,eAAO,MAAM,mBAAmB,uIACkG,CAAA;AAElI,yFAAyF;AACzF,wBAAgB,qBAAqB,IAAI,QAAQ,CAEhD"}
|
package/dist/blank.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** An empty document with no script, style or network access. */
|
|
2
|
+
export const BLANK_DOCUMENT_HTML = '<!doctype html><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="default-src \'none\'"><title></title>';
|
|
3
|
+
/** The blank document as a `protocol.handle` response for the migration's target URL. */
|
|
4
|
+
export function blankDocumentResponse() {
|
|
5
|
+
return new Response(BLANK_DOCUMENT_HTML, { headers: { 'content-type': 'text/html' } });
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=blank.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blank.js","sourceRoot":"","sources":["../src/blank.ts"],"names":[],"mappings":"AAAA,iEAAiE;AACjE,MAAM,CAAC,MAAM,mBAAmB,GAC9B,gIAAgI,CAAA;AAElI,yFAAyF;AACzF,MAAM,UAAU,qBAAqB;IACnC,OAAO,IAAI,QAAQ,CAAC,mBAAmB,EAAE,EAAE,OAAO,EAAE,EAAE,cAAc,EAAE,WAAW,EAAE,EAAE,CAAC,CAAA;AACxF,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { BLANK_DOCUMENT_HTML, blankDocumentResponse } from './blank.js';
|
|
2
|
+
export { migrateOrigin, type MigrateOriginOptions } from './migrate-origin.js';
|
|
3
|
+
export { ORIGIN_MIGRATION_CHUNK_BYTES, ORIGIN_MIGRATION_MARKER, ORIGIN_MIGRATION_MAX_ATTEMPTS, ORIGIN_STORES, runOriginMigration, type LegacyOriginReader, type OriginFile, type OriginMigrationDeps, type OriginMigrationMarker, type OriginMigrationResult, type OriginStore, type TargetOriginWriter, } from './migration.js';
|
|
4
|
+
export { openLegacyOriginPage, openTargetOriginPage, ORIGIN_PAGE_STEP_TIMEOUT_MS, type OriginPageOptions, type WindowFactory, } from './pages.js';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AACvE,OAAO,EAAE,aAAa,EAAE,KAAK,oBAAoB,EAAE,MAAM,qBAAqB,CAAA;AAC9E,OAAO,EACL,4BAA4B,EAC5B,uBAAuB,EACvB,6BAA6B,EAC7B,aAAa,EACb,kBAAkB,EAClB,KAAK,kBAAkB,EACvB,KAAK,UAAU,EACf,KAAK,mBAAmB,EACxB,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC1B,KAAK,WAAW,EAChB,KAAK,kBAAkB,GACxB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,2BAA2B,EAC3B,KAAK,iBAAiB,EACtB,KAAK,aAAa,GACnB,MAAM,YAAY,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { BLANK_DOCUMENT_HTML, blankDocumentResponse } from './blank.js';
|
|
2
|
+
export { migrateOrigin } from './migrate-origin.js';
|
|
3
|
+
export { ORIGIN_MIGRATION_CHUNK_BYTES, ORIGIN_MIGRATION_MARKER, ORIGIN_MIGRATION_MAX_ATTEMPTS, ORIGIN_STORES, runOriginMigration, } from './migration.js';
|
|
4
|
+
export { openLegacyOriginPage, openTargetOriginPage, ORIGIN_PAGE_STEP_TIMEOUT_MS, } from './pages.js';
|
|
5
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AACvE,OAAO,EAAE,aAAa,EAA6B,MAAM,qBAAqB,CAAA;AAC9E,OAAO,EACL,4BAA4B,EAC5B,uBAAuB,EACvB,6BAA6B,EAC7B,aAAa,EACb,kBAAkB,GAQnB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,2BAA2B,GAG5B,MAAM,YAAY,CAAA"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Session } from 'electron';
|
|
2
|
+
import { type OriginMigrationResult, type OriginStore } from './migration.js';
|
|
3
|
+
import { type WindowFactory } from './pages.js';
|
|
4
|
+
export interface MigrateOriginOptions {
|
|
5
|
+
/** `app.getPath('userData')`; the marker lives here. */
|
|
6
|
+
userDataDir: string;
|
|
7
|
+
/** Electron's `BrowserWindow`, used for two hidden, sandboxed pages. */
|
|
8
|
+
BrowserWindow: WindowFactory;
|
|
9
|
+
/** The session whose storage moves, usually `session.defaultSession`. */
|
|
10
|
+
session: Session;
|
|
11
|
+
/** An empty document on the new origin, served by the app's protocol handler. */
|
|
12
|
+
targetUrl: string;
|
|
13
|
+
/** Default both. */
|
|
14
|
+
stores?: readonly OriginStore[];
|
|
15
|
+
/** A file name inside `userDataDir`. Default `origin-migration.json`. */
|
|
16
|
+
markerFile?: string;
|
|
17
|
+
/** Bounds each page load and page step. Default 30 s. */
|
|
18
|
+
stepTimeoutMs?: number;
|
|
19
|
+
onProgress?: (done: number, total: number) => void;
|
|
20
|
+
signal?: AbortSignal;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Copies localStorage and the origin private file system from `file://` to the origin of
|
|
24
|
+
* `targetUrl`, once. Resolves with the outcome; an attempt that fails is retried on later
|
|
25
|
+
* launches, up to three attempts in all.
|
|
26
|
+
*/
|
|
27
|
+
export declare function migrateOrigin(options: MigrateOriginOptions): Promise<OriginMigrationResult>;
|
|
28
|
+
//# sourceMappingURL=migrate-origin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrate-origin.d.ts","sourceRoot":"","sources":["../src/migrate-origin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAA;AAEvC,OAAO,EAAsB,KAAK,qBAAqB,EAAE,KAAK,WAAW,EAAE,MAAM,gBAAgB,CAAA;AACjG,OAAO,EAA8C,KAAK,aAAa,EAAE,MAAM,YAAY,CAAA;AAE3F,MAAM,WAAW,oBAAoB;IACnC,wDAAwD;IACxD,WAAW,EAAE,MAAM,CAAA;IACnB,wEAAwE;IACxE,aAAa,EAAE,aAAa,CAAA;IAC5B,yEAAyE;IACzE,OAAO,EAAE,OAAO,CAAA;IAChB,iFAAiF;IACjF,SAAS,EAAE,MAAM,CAAA;IACjB,oBAAoB;IACpB,MAAM,CAAC,EAAE,SAAS,WAAW,EAAE,CAAA;IAC/B,yEAAyE;IACzE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,yDAAyD;IACzD,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAClD,MAAM,CAAC,EAAE,WAAW,CAAA;CACrB;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAmBjG"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { runOriginMigration } from './migration.js';
|
|
2
|
+
import { openLegacyOriginPage, openTargetOriginPage } from './pages.js';
|
|
3
|
+
/**
|
|
4
|
+
* Copies localStorage and the origin private file system from `file://` to the origin of
|
|
5
|
+
* `targetUrl`, once. Resolves with the outcome; an attempt that fails is retried on later
|
|
6
|
+
* launches, up to three attempts in all.
|
|
7
|
+
*/
|
|
8
|
+
export async function migrateOrigin(options) {
|
|
9
|
+
if (new URL(options.targetUrl).protocol === 'file:') {
|
|
10
|
+
throw new TypeError('targetUrl must be on the new origin, not file://');
|
|
11
|
+
}
|
|
12
|
+
const pageOptions = {
|
|
13
|
+
session: options.session,
|
|
14
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
15
|
+
...(options.stepTimeoutMs ? { stepTimeoutMs: options.stepTimeoutMs } : {}),
|
|
16
|
+
};
|
|
17
|
+
return runOriginMigration({
|
|
18
|
+
userDataDir: options.userDataDir,
|
|
19
|
+
openLegacy: () => openLegacyOriginPage(options.BrowserWindow, pageOptions),
|
|
20
|
+
openTarget: () => openTargetOriginPage(options.BrowserWindow, options.targetUrl, pageOptions),
|
|
21
|
+
flush: () => options.session.flushStorageData(),
|
|
22
|
+
...(options.markerFile ? { markerFile: options.markerFile } : {}),
|
|
23
|
+
...(options.stores ? { stores: options.stores } : {}),
|
|
24
|
+
...(options.onProgress ? { onProgress: options.onProgress } : {}),
|
|
25
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=migrate-origin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrate-origin.js","sourceRoot":"","sources":["../src/migrate-origin.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kBAAkB,EAAgD,MAAM,gBAAgB,CAAA;AACjG,OAAO,EAAE,oBAAoB,EAAE,oBAAoB,EAAsB,MAAM,YAAY,CAAA;AAqB3F;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,OAA6B;IAC/D,IAAI,IAAI,GAAG,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QACpD,MAAM,IAAI,SAAS,CAAC,kDAAkD,CAAC,CAAA;IACzE,CAAC;IACD,MAAM,WAAW,GAAG;QAClB,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACrD,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,OAAO,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC3E,CAAA;IACD,OAAO,kBAAkB,CAAC;QACxB,WAAW,EAAE,OAAO,CAAC,WAAW;QAChC,UAAU,EAAE,GAAG,EAAE,CAAC,oBAAoB,CAAC,OAAO,CAAC,aAAa,EAAE,WAAW,CAAC;QAC1E,UAAU,EAAE,GAAG,EAAE,CAAC,oBAAoB,CAAC,OAAO,CAAC,aAAa,EAAE,OAAO,CAAC,SAAS,EAAE,WAAW,CAAC;QAC7F,KAAK,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,gBAAgB,EAAE;QAC/C,GAAG,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACjE,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACrD,GAAG,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACjE,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACtD,CAAC,CAAA;AACJ,CAAC"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-time copy of renderer storage from one origin to another; Chromium keys storage by origin.
|
|
3
|
+
* The legacy copy is left on disk. Keys the new origin holds are never overwritten. Files are
|
|
4
|
+
* staged, then moved database-first with any old target WAL removed. A database and its WAL count
|
|
5
|
+
* as copied only after both moves succeed; until then, a retry copies both again over files the
|
|
6
|
+
* app created after a failed attempt. The marker's progress keeps retries from restoring a
|
|
7
|
+
* completed group the user deleted since.
|
|
8
|
+
*/
|
|
9
|
+
export declare const ORIGIN_MIGRATION_MARKER = "origin-migration.json";
|
|
10
|
+
export declare const ORIGIN_MIGRATION_MAX_ATTEMPTS = 3;
|
|
11
|
+
export declare const ORIGIN_MIGRATION_CHUNK_BYTES: number;
|
|
12
|
+
export type OriginStore = 'localStorage' | 'opfs';
|
|
13
|
+
export declare const ORIGIN_STORES: readonly OriginStore[];
|
|
14
|
+
export interface OriginFile {
|
|
15
|
+
readonly path: string[];
|
|
16
|
+
readonly size: number;
|
|
17
|
+
}
|
|
18
|
+
export interface LegacyOriginReader {
|
|
19
|
+
/** Null when Chromium denies the page its localStorage, as for file:// without the fuse. */
|
|
20
|
+
readLocalStorage: () => Promise<Array<[string, string]> | null>;
|
|
21
|
+
listFiles: () => Promise<OriginFile[]>;
|
|
22
|
+
readChunk: (path: readonly string[], offset: number, length: number) => Promise<Uint8Array>;
|
|
23
|
+
close: () => void;
|
|
24
|
+
}
|
|
25
|
+
export interface TargetOriginWriter {
|
|
26
|
+
/** Sets each key the origin does not hold yet; resolves with how many it set. */
|
|
27
|
+
writeLocalStorage: (entries: Array<[string, string]>) => Promise<number>;
|
|
28
|
+
fileSize: (path: readonly string[]) => Promise<number | null>;
|
|
29
|
+
beginFile: (path: readonly string[]) => Promise<void>;
|
|
30
|
+
writeChunk: (bytes: Uint8Array) => Promise<void>;
|
|
31
|
+
endFile: () => Promise<void>;
|
|
32
|
+
abortFile: () => Promise<void>;
|
|
33
|
+
/** Moves a staged file over `name` in the same folder, replacing what is there. */
|
|
34
|
+
commitFile: (path: readonly string[], name: string) => Promise<void>;
|
|
35
|
+
/** Removes a file; a missing one is not an error. */
|
|
36
|
+
removeFile: (path: readonly string[]) => Promise<void>;
|
|
37
|
+
close: () => void;
|
|
38
|
+
}
|
|
39
|
+
export interface OriginMigrationMarker {
|
|
40
|
+
version: 1;
|
|
41
|
+
state: 'done' | 'failed';
|
|
42
|
+
attempts: number;
|
|
43
|
+
localStorageKeys: number;
|
|
44
|
+
files: number;
|
|
45
|
+
fileBytes: number;
|
|
46
|
+
durationMs: number;
|
|
47
|
+
error?: string;
|
|
48
|
+
localStorageCopied: boolean;
|
|
49
|
+
localStorageUnreadable?: boolean;
|
|
50
|
+
/** Paths joined with `/`. */
|
|
51
|
+
copiedFiles: string[];
|
|
52
|
+
}
|
|
53
|
+
export type OriginMigrationResult = {
|
|
54
|
+
status: 'skipped';
|
|
55
|
+
} | {
|
|
56
|
+
status: 'empty' | 'migrated';
|
|
57
|
+
marker: OriginMigrationMarker;
|
|
58
|
+
}
|
|
59
|
+
/** `final` when no attempt is left, so the next launch skips the copy. */
|
|
60
|
+
| {
|
|
61
|
+
status: 'failed';
|
|
62
|
+
marker: OriginMigrationMarker;
|
|
63
|
+
final: boolean;
|
|
64
|
+
};
|
|
65
|
+
export interface OriginMigrationDeps {
|
|
66
|
+
userDataDir: string;
|
|
67
|
+
openLegacy: () => Promise<LegacyOriginReader>;
|
|
68
|
+
openTarget: () => Promise<TargetOriginWriter>;
|
|
69
|
+
/** Writes pending localStorage to disk before the marker records success. */
|
|
70
|
+
flush: () => Promise<void> | void;
|
|
71
|
+
/** A file name inside `userDataDir`. Default `origin-migration.json`. */
|
|
72
|
+
markerFile?: string;
|
|
73
|
+
/** Default both. */
|
|
74
|
+
stores?: readonly OriginStore[];
|
|
75
|
+
/** Bytes staged so far out of the bytes this attempt copies. */
|
|
76
|
+
onProgress?: (done: number, total: number) => void;
|
|
77
|
+
signal?: AbortSignal;
|
|
78
|
+
now?: () => number;
|
|
79
|
+
}
|
|
80
|
+
/** Runs one attempt with injected origin access. Never rejects once the marker path is valid. */
|
|
81
|
+
export declare function runOriginMigration(deps: OriginMigrationDeps): Promise<OriginMigrationResult>;
|
|
82
|
+
//# sourceMappingURL=migration.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migration.d.ts","sourceRoot":"","sources":["../src/migration.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { open, readFile, rename } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { STAGING_SUFFIX } from './scripts.js';
|
|
4
|
+
/**
|
|
5
|
+
* One-time copy of renderer storage from one origin to another; Chromium keys storage by origin.
|
|
6
|
+
* The legacy copy is left on disk. Keys the new origin holds are never overwritten. Files are
|
|
7
|
+
* staged, then moved database-first with any old target WAL removed. A database and its WAL count
|
|
8
|
+
* as copied only after both moves succeed; until then, a retry copies both again over files the
|
|
9
|
+
* app created after a failed attempt. The marker's progress keeps retries from restoring a
|
|
10
|
+
* completed group the user deleted since.
|
|
11
|
+
*/
|
|
12
|
+
export const ORIGIN_MIGRATION_MARKER = 'origin-migration.json';
|
|
13
|
+
export const ORIGIN_MIGRATION_MAX_ATTEMPTS = 3;
|
|
14
|
+
export const ORIGIN_MIGRATION_CHUNK_BYTES = 1 << 20;
|
|
15
|
+
export const ORIGIN_STORES = ['localStorage', 'opfs'];
|
|
16
|
+
/** Null only when no marker exists; any marker this version cannot read counts as final. */
|
|
17
|
+
async function readMarker(markerPath) {
|
|
18
|
+
let text;
|
|
19
|
+
try {
|
|
20
|
+
text = await readFile(markerPath, 'utf8');
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
return error.code === 'ENOENT' ? null : 'unreadable';
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const parsed = JSON.parse(text);
|
|
27
|
+
if (parsed?.version !== 1 || (parsed.state !== 'done' && parsed.state !== 'failed')) {
|
|
28
|
+
return 'unreadable';
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
...parsed,
|
|
32
|
+
attempts: Number.isInteger(parsed.attempts) ? parsed.attempts : 1,
|
|
33
|
+
localStorageCopied: parsed.localStorageCopied === true,
|
|
34
|
+
copiedFiles: Array.isArray(parsed.copiedFiles)
|
|
35
|
+
? parsed.copiedFiles.filter((entry) => typeof entry === 'string')
|
|
36
|
+
: [],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return 'unreadable';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function writeMarker(markerPath, marker) {
|
|
44
|
+
const temporary = `${markerPath}.tmp`;
|
|
45
|
+
const handle = await open(temporary, 'w', 0o600);
|
|
46
|
+
try {
|
|
47
|
+
await handle.writeFile(`${JSON.stringify(marker, null, 2)}\n`);
|
|
48
|
+
// Flushed before the rename, so a power loss cannot leave a truncated marker in place.
|
|
49
|
+
await handle.sync();
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
await handle.close();
|
|
53
|
+
}
|
|
54
|
+
await rename(temporary, markerPath);
|
|
55
|
+
}
|
|
56
|
+
/** An error name or code, never a message: messages can carry paths. */
|
|
57
|
+
function errorCode(error) {
|
|
58
|
+
if (error && typeof error === 'object') {
|
|
59
|
+
const { code, name } = error;
|
|
60
|
+
if (typeof code === 'string' && /^[\w-]{1,64}$/.test(code))
|
|
61
|
+
return code;
|
|
62
|
+
if (typeof name === 'string' && /^\w{1,64}$/.test(name))
|
|
63
|
+
return name;
|
|
64
|
+
}
|
|
65
|
+
return 'unknown';
|
|
66
|
+
}
|
|
67
|
+
const WAL_SUFFIX = '.wal';
|
|
68
|
+
/** Where the target's own WAL waits while the legacy database moves in. */
|
|
69
|
+
const PREVIOUS_SUFFIX = '.origin-migration-previous';
|
|
70
|
+
const keyOf = (file) => file.path.join('/');
|
|
71
|
+
const nameOf = (file) => file.path.at(-1) ?? '';
|
|
72
|
+
const siblingPath = (file, suffix) => [
|
|
73
|
+
...file.path.slice(0, -1),
|
|
74
|
+
`${nameOf(file)}${suffix}`,
|
|
75
|
+
];
|
|
76
|
+
const stagingPath = (file) => siblingPath(file, STAGING_SUFFIX);
|
|
77
|
+
/**
|
|
78
|
+
* A database and its WAL share retry ownership, regardless of listing order. A WAL whose database
|
|
79
|
+
* is gone is never copied: it is useless alone and could replay onto another database.
|
|
80
|
+
*/
|
|
81
|
+
function fileGroups(files) {
|
|
82
|
+
const byKey = new Map(files.map((file) => [keyOf(file), file]));
|
|
83
|
+
const groups = [];
|
|
84
|
+
for (const file of files) {
|
|
85
|
+
const key = keyOf(file);
|
|
86
|
+
if (key.endsWith(WAL_SUFFIX))
|
|
87
|
+
continue;
|
|
88
|
+
const wal = byKey.get(`${key}${WAL_SUFFIX}`);
|
|
89
|
+
groups.push(wal ? [file, wal] : [file]);
|
|
90
|
+
}
|
|
91
|
+
return groups;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Moves one staged group in, database first. The target's own WAL is set aside rather than
|
|
95
|
+
* deleted, so a failed database move leaves the target pair intact; once the legacy database
|
|
96
|
+
* lands, that WAL no longer matches and goes.
|
|
97
|
+
*/
|
|
98
|
+
async function commitGroup(target, group) {
|
|
99
|
+
const [database, wal] = group;
|
|
100
|
+
const targetWal = siblingPath(database, WAL_SUFFIX);
|
|
101
|
+
const aside = siblingPath(database, `${WAL_SUFFIX}${PREVIOUS_SUFFIX}`);
|
|
102
|
+
const hadWal = (await target.fileSize(targetWal)) !== null;
|
|
103
|
+
if (hadWal)
|
|
104
|
+
await target.commitFile(targetWal, aside.at(-1) ?? '');
|
|
105
|
+
try {
|
|
106
|
+
await target.commitFile(stagingPath(database), nameOf(database));
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
if (hadWal) {
|
|
110
|
+
await target.commitFile(aside, `${nameOf(database)}${WAL_SUFFIX}`).catch(() => undefined);
|
|
111
|
+
}
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
// Unconditional: a crash after the set-aside but before this move leaves an aside behind.
|
|
115
|
+
await target.removeFile(aside).catch(() => undefined);
|
|
116
|
+
if (wal)
|
|
117
|
+
await target.commitFile(stagingPath(wal), nameOf(wal));
|
|
118
|
+
}
|
|
119
|
+
async function stageFile(legacy, target, file, onChunk, signal) {
|
|
120
|
+
const staging = stagingPath(file);
|
|
121
|
+
await target.beginFile(staging);
|
|
122
|
+
try {
|
|
123
|
+
for (let offset = 0; offset < file.size;) {
|
|
124
|
+
signal?.throwIfAborted();
|
|
125
|
+
const length = Math.min(ORIGIN_MIGRATION_CHUNK_BYTES, file.size - offset);
|
|
126
|
+
const bytes = await legacy.readChunk(file.path, offset, length);
|
|
127
|
+
if (bytes.byteLength !== length) {
|
|
128
|
+
throw Object.assign(new Error('Legacy file changed during migration'), { code: 'ESIZE' });
|
|
129
|
+
}
|
|
130
|
+
await target.writeChunk(bytes);
|
|
131
|
+
offset += length;
|
|
132
|
+
onChunk(length);
|
|
133
|
+
}
|
|
134
|
+
await target.endFile();
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
await target.abortFile().catch(() => undefined);
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
if ((await target.fileSize(staging)) !== file.size) {
|
|
141
|
+
throw Object.assign(new Error('Copied file size differs'), { code: 'ESIZE' });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/** Runs one attempt with injected origin access. Never rejects once the marker path is valid. */
|
|
145
|
+
export async function runOriginMigration(deps) {
|
|
146
|
+
const markerFile = deps.markerFile ?? ORIGIN_MIGRATION_MARKER;
|
|
147
|
+
if (!markerFile ||
|
|
148
|
+
markerFile !== path.basename(markerFile) ||
|
|
149
|
+
markerFile === '.' ||
|
|
150
|
+
markerFile === '..') {
|
|
151
|
+
throw new TypeError('markerFile must be a file name, not a path');
|
|
152
|
+
}
|
|
153
|
+
const stores = new Set(deps.stores ?? ORIGIN_STORES);
|
|
154
|
+
const now = deps.now ?? Date.now;
|
|
155
|
+
const markerPath = path.join(deps.userDataDir, markerFile);
|
|
156
|
+
const previous = await readMarker(markerPath);
|
|
157
|
+
if (previous === 'unreadable' ||
|
|
158
|
+
(previous && (previous.state === 'done' || previous.attempts >= ORIGIN_MIGRATION_MAX_ATTEMPTS))) {
|
|
159
|
+
return { status: 'skipped' };
|
|
160
|
+
}
|
|
161
|
+
const started = now();
|
|
162
|
+
// Recorded as interrupted up front, so a crash mid-copy still spends an attempt.
|
|
163
|
+
const marker = {
|
|
164
|
+
version: 1,
|
|
165
|
+
state: 'failed',
|
|
166
|
+
error: 'interrupted',
|
|
167
|
+
attempts: (previous?.attempts ?? 0) + 1,
|
|
168
|
+
localStorageKeys: previous?.localStorageKeys ?? 0,
|
|
169
|
+
files: previous?.files ?? 0,
|
|
170
|
+
fileBytes: previous?.fileBytes ?? 0,
|
|
171
|
+
durationMs: 0,
|
|
172
|
+
localStorageCopied: previous?.localStorageCopied ?? false,
|
|
173
|
+
...(previous?.localStorageUnreadable ? { localStorageUnreadable: true } : {}),
|
|
174
|
+
copiedFiles: [...(previous?.copiedFiles ?? [])],
|
|
175
|
+
};
|
|
176
|
+
// No disk probe: Chromium creates Local Storage before any page loads, so only the legacy
|
|
177
|
+
// origin itself can say whether it holds data.
|
|
178
|
+
let legacy;
|
|
179
|
+
let target;
|
|
180
|
+
try {
|
|
181
|
+
await writeMarker(markerPath, marker);
|
|
182
|
+
legacy = await deps.openLegacy();
|
|
183
|
+
// Settings go first, so a failed file step cannot leave the app to boot on defaults.
|
|
184
|
+
if (stores.has('localStorage') && !marker.localStorageCopied) {
|
|
185
|
+
const entries = await legacy.readLocalStorage();
|
|
186
|
+
if (entries === null)
|
|
187
|
+
marker.localStorageUnreadable = true;
|
|
188
|
+
if (entries && entries.length > 0) {
|
|
189
|
+
target = await deps.openTarget();
|
|
190
|
+
marker.localStorageKeys = await target.writeLocalStorage(entries);
|
|
191
|
+
await deps.flush();
|
|
192
|
+
}
|
|
193
|
+
marker.localStorageCopied = true;
|
|
194
|
+
await writeMarker(markerPath, marker);
|
|
195
|
+
}
|
|
196
|
+
const copied = new Set(marker.copiedFiles);
|
|
197
|
+
const listed = stores.has('opfs') ? await legacy.listFiles() : [];
|
|
198
|
+
const pendingGroups = fileGroups(listed).filter((group) => group.some((file) => !copied.has(keyOf(file))));
|
|
199
|
+
const files = pendingGroups.flat();
|
|
200
|
+
if (files.length > 0)
|
|
201
|
+
target ??= await deps.openTarget();
|
|
202
|
+
const empty = !target;
|
|
203
|
+
if (target) {
|
|
204
|
+
const writer = target;
|
|
205
|
+
const reader = legacy;
|
|
206
|
+
const staged = [];
|
|
207
|
+
const total = files.reduce((sum, file) => sum + file.size, 0);
|
|
208
|
+
let done = 0;
|
|
209
|
+
const onChunk = (length) => deps.onProgress?.((done += length), total);
|
|
210
|
+
try {
|
|
211
|
+
for (const file of files) {
|
|
212
|
+
deps.signal?.throwIfAborted();
|
|
213
|
+
staged.push(file);
|
|
214
|
+
await stageFile(reader, writer, file, onChunk, deps.signal);
|
|
215
|
+
}
|
|
216
|
+
// Past this point a started move runs to the end, so an abort cannot split a database from its WAL.
|
|
217
|
+
deps.signal?.throwIfAborted();
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
await Promise.all(staged.map((file) => writer.removeFile(stagingPath(file)).catch(() => undefined)));
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
223
|
+
let committed = 0;
|
|
224
|
+
try {
|
|
225
|
+
for (const group of pendingGroups) {
|
|
226
|
+
await commitGroup(writer, group);
|
|
227
|
+
committed += 1;
|
|
228
|
+
// Recorded per group, so a crash after it never restores this group over later changes.
|
|
229
|
+
// An older failed marker may already name one of its files; that one is not counted twice.
|
|
230
|
+
for (const file of group) {
|
|
231
|
+
if (copied.has(keyOf(file)))
|
|
232
|
+
continue;
|
|
233
|
+
copied.add(keyOf(file));
|
|
234
|
+
marker.files += 1;
|
|
235
|
+
marker.fileBytes += file.size;
|
|
236
|
+
marker.copiedFiles.push(keyOf(file));
|
|
237
|
+
}
|
|
238
|
+
await writeMarker(markerPath, marker);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
const left = pendingGroups.slice(committed).flat();
|
|
243
|
+
await Promise.all(left.map((file) => writer.removeFile(stagingPath(file)).catch(() => undefined)));
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
marker.state = 'done';
|
|
248
|
+
delete marker.error;
|
|
249
|
+
marker.durationMs = Math.round(now() - started);
|
|
250
|
+
await writeMarker(markerPath, marker);
|
|
251
|
+
return { status: empty ? 'empty' : 'migrated', marker };
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
marker.state = 'failed';
|
|
255
|
+
marker.error = errorCode(error);
|
|
256
|
+
marker.durationMs = Math.round(now() - started);
|
|
257
|
+
await writeMarker(markerPath, marker).catch(() => undefined);
|
|
258
|
+
return { status: 'failed', marker, final: marker.attempts >= ORIGIN_MIGRATION_MAX_ATTEMPTS };
|
|
259
|
+
}
|
|
260
|
+
finally {
|
|
261
|
+
legacy?.close();
|
|
262
|
+
target?.close();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
//# sourceMappingURL=migration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migration.js","sourceRoot":"","sources":["../src/migration.ts"],"names":[],"mappings":"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"}
|
package/dist/pages.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { BrowserWindow, BrowserWindowConstructorOptions, Session } from 'electron';
|
|
2
|
+
import type { LegacyOriginReader, TargetOriginWriter } from './migration.js';
|
|
3
|
+
/** Bounds each page load and script; a stalled step fails the attempt instead of the launch. */
|
|
4
|
+
export declare const ORIGIN_PAGE_STEP_TIMEOUT_MS = 30000;
|
|
5
|
+
export type WindowFactory = new (options: BrowserWindowConstructorOptions) => BrowserWindow;
|
|
6
|
+
export interface OriginPageOptions {
|
|
7
|
+
/** The session whose storage moves. Default: Electron's default session. */
|
|
8
|
+
session?: Session;
|
|
9
|
+
signal?: AbortSignal;
|
|
10
|
+
stepTimeoutMs?: number;
|
|
11
|
+
}
|
|
12
|
+
/** Reads the file:// origin through an empty page written to a private temp folder. */
|
|
13
|
+
export declare function openLegacyOriginPage(Window: WindowFactory, options?: OriginPageOptions): Promise<LegacyOriginReader>;
|
|
14
|
+
/** Writes the new origin through `blankUrl`, an empty document the app's handler serves. */
|
|
15
|
+
export declare function openTargetOriginPage(Window: WindowFactory, blankUrl: string, options?: OriginPageOptions): Promise<TargetOriginWriter>;
|
|
16
|
+
//# sourceMappingURL=pages.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pages.d.ts","sourceRoot":"","sources":["../src/pages.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,+BAA+B,EAAE,OAAO,EAAE,MAAM,UAAU,CAAA;AAGvF,OAAO,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AAgB5E,gGAAgG;AAChG,eAAO,MAAM,2BAA2B,QAAS,CAAA;AAEjD,MAAM,MAAM,aAAa,GAAG,KAAK,OAAO,EAAE,+BAA+B,KAAK,aAAa,CAAA;AAE3F,MAAM,WAAW,iBAAiB;IAChC,4EAA4E;IAC5E,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAmGD,uFAAuF;AACvF,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,aAAa,EACrB,OAAO,GAAE,iBAAsB,GAC9B,OAAO,CAAC,kBAAkB,CAAC,CAkB7B;AAED,4FAA4F;AAC5F,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,aAAa,EACrB,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,iBAAsB,GAC9B,OAAO,CAAC,kBAAkB,CAAC,CAwB7B"}
|
package/dist/pages.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { BLANK_DOCUMENT_HTML } from './blank.js';
|
|
5
|
+
import { ABORT_FILE, BEGIN_FILE, call, COMMIT_FILE, END_FILE, FILE_SIZE, LIST_FILES, READ_CHUNK, READ_LOCAL_STORAGE, REMOVE_FILE, WRITE_CHUNK, WRITE_LOCAL_STORAGE, } from './scripts.js';
|
|
6
|
+
/** Bounds each page load and script; a stalled step fails the attempt instead of the launch. */
|
|
7
|
+
export const ORIGIN_PAGE_STEP_TIMEOUT_MS = 30_000;
|
|
8
|
+
function withTimeout(promise, timeoutMs, signal) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const fail = (reason) => {
|
|
11
|
+
clearTimeout(timer);
|
|
12
|
+
signal?.removeEventListener('abort', onAbort);
|
|
13
|
+
reject(reason);
|
|
14
|
+
};
|
|
15
|
+
const onAbort = () => fail(signal?.reason);
|
|
16
|
+
const timer = setTimeout(() => fail(Object.assign(new Error('Origin migration step timed out'), { code: 'ETIMEDOUT' })), timeoutMs);
|
|
17
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
18
|
+
if (signal?.aborted)
|
|
19
|
+
onAbort();
|
|
20
|
+
promise.then((value) => {
|
|
21
|
+
clearTimeout(timer);
|
|
22
|
+
signal?.removeEventListener('abort', onAbort);
|
|
23
|
+
resolve(value);
|
|
24
|
+
}, fail);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
async function openStoragePage(Window, load, cleanup, options) {
|
|
28
|
+
const timeoutMs = options.stepTimeoutMs ?? ORIGIN_PAGE_STEP_TIMEOUT_MS;
|
|
29
|
+
let window;
|
|
30
|
+
try {
|
|
31
|
+
window = new Window({
|
|
32
|
+
show: false,
|
|
33
|
+
webPreferences: {
|
|
34
|
+
sandbox: true,
|
|
35
|
+
contextIsolation: true,
|
|
36
|
+
nodeIntegration: false,
|
|
37
|
+
webSecurity: true,
|
|
38
|
+
spellcheck: false,
|
|
39
|
+
backgroundThrottling: false,
|
|
40
|
+
...(options.session ? { session: options.session } : {}),
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
cleanup();
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
window.webContents.setWindowOpenHandler(() => ({ action: 'deny' }));
|
|
49
|
+
window.webContents.on('will-navigate', (event) => event.preventDefault());
|
|
50
|
+
window.webContents.on('will-redirect', (event) => event.preventDefault());
|
|
51
|
+
const close = () => {
|
|
52
|
+
if (!window.isDestroyed())
|
|
53
|
+
window.destroy();
|
|
54
|
+
cleanup();
|
|
55
|
+
};
|
|
56
|
+
try {
|
|
57
|
+
await withTimeout(load(window), timeoutMs, options.signal);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
close();
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
evaluate: (script) => runSettled(window, script, timeoutMs, options.signal),
|
|
65
|
+
evaluateToEnd: (script) => runSettled(window, script, timeoutMs),
|
|
66
|
+
close,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
async function runSettled(window, script, timeoutMs, signal) {
|
|
70
|
+
const result = await withTimeout(window.webContents.executeJavaScript(settled(script)), timeoutMs, signal);
|
|
71
|
+
if ('value' in result)
|
|
72
|
+
return result.value;
|
|
73
|
+
throw Object.assign(new Error('Origin page step failed'), { name: result.name });
|
|
74
|
+
}
|
|
75
|
+
// executeJavaScript rejects with neither name nor message, so the page reports the error name.
|
|
76
|
+
function settled(script) {
|
|
77
|
+
return `(async () => { try { return { value: await (${script}) }; } catch (error) { return { name: String((error && error.name) || 'Error') }; } })()`;
|
|
78
|
+
}
|
|
79
|
+
/** Reads the file:// origin through an empty page written to a private temp folder. */
|
|
80
|
+
export async function openLegacyOriginPage(Window, options = {}) {
|
|
81
|
+
const folder = await mkdtemp(path.join(os.tmpdir(), 'origin-migration-'));
|
|
82
|
+
const document = path.join(folder, 'blank.html');
|
|
83
|
+
const removeFolder = () => void rm(folder, { recursive: true, force: true }).catch(() => { });
|
|
84
|
+
try {
|
|
85
|
+
await writeFile(document, BLANK_DOCUMENT_HTML);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
removeFolder();
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
const page = await openStoragePage(Window, (w) => w.loadFile(document), removeFolder, options);
|
|
92
|
+
return {
|
|
93
|
+
readLocalStorage: () => page.evaluate(READ_LOCAL_STORAGE),
|
|
94
|
+
listFiles: () => page.evaluate(LIST_FILES),
|
|
95
|
+
readChunk: (filePath, offset, length) => page.evaluate(call(READ_CHUNK, filePath, offset, length)),
|
|
96
|
+
close: page.close,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/** Writes the new origin through `blankUrl`, an empty document the app's handler serves. */
|
|
100
|
+
export async function openTargetOriginPage(Window, blankUrl, options = {}) {
|
|
101
|
+
const page = await openStoragePage(Window, (w) => w.loadURL(blankUrl), () => { }, options);
|
|
102
|
+
return {
|
|
103
|
+
writeLocalStorage: (entries) => page.evaluate(call(WRITE_LOCAL_STORAGE, entries)),
|
|
104
|
+
fileSize: (filePath) => page.evaluate(call(FILE_SIZE, filePath)),
|
|
105
|
+
beginFile: (filePath) => page.evaluate(call(BEGIN_FILE, filePath)),
|
|
106
|
+
writeChunk: (bytes) => page.evaluate(call(WRITE_CHUNK, Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString('base64'))),
|
|
107
|
+
endFile: () => page.evaluate(END_FILE),
|
|
108
|
+
abortFile: () => page.evaluateToEnd(ABORT_FILE),
|
|
109
|
+
commitFile: (filePath, name) => page.evaluateToEnd(call(COMMIT_FILE, filePath, name)),
|
|
110
|
+
removeFile: (filePath) => page.evaluateToEnd(call(REMOVE_FILE, filePath)),
|
|
111
|
+
close: page.close,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=pages.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pages.js","sourceRoot":"","sources":["../src/pages.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scripts main runs in the hidden migration pages. They stay strings so a bundler's minifier
|
|
3
|
+
* cannot rename or wrap what runs in another realm. Arguments are spliced in with `call`, as JSON.
|
|
4
|
+
*/
|
|
5
|
+
export declare const STAGING_SUFFIX = ".origin-migration-staging";
|
|
6
|
+
/** Every key and value of the page's localStorage, or null when the page may not read it. */
|
|
7
|
+
export declare const READ_LOCAL_STORAGE = "(() => {\n try {\n void localStorage.length;\n } catch {\n return null;\n }\n const entries = [];\n for (let index = 0; index < localStorage.length; index += 1) {\n const key = localStorage.key(index);\n const value = key === null ? null : localStorage.getItem(key);\n if (key !== null && value !== null) entries.push([key, value]);\n }\n return entries;\n})()";
|
|
8
|
+
/** Sets each key the page does not hold yet; resolves with how many it set. */
|
|
9
|
+
export declare const WRITE_LOCAL_STORAGE = "((entries) => {\n let written = 0;\n for (const [key, value] of entries) {\n if (localStorage.getItem(key) !== null) continue;\n localStorage.setItem(key, value);\n written += 1;\n }\n return written;\n})";
|
|
10
|
+
/** Every file in the origin private file system, with its size. Skips swap and staging files. */
|
|
11
|
+
export declare const LIST_FILES: string;
|
|
12
|
+
/** `length` bytes of a file from `offset`. */
|
|
13
|
+
export declare const READ_CHUNK = "(async (path, offset, length) => {\n const create = false;\n let dir = await navigator.storage.getDirectory();\n for (const name of path.slice(0, -1)) dir = await dir.getDirectoryHandle(name, { create });\n const file = await (await dir.getFileHandle(path[path.length - 1])).getFile();\n return new Uint8Array(await file.slice(offset, offset + length).arrayBuffer());\n})";
|
|
14
|
+
/** A file's size, or null when the page has no file at that path. */
|
|
15
|
+
export declare const FILE_SIZE = "(async (path) => {\n const create = false;\n try {\n let dir = await navigator.storage.getDirectory();\n for (const name of path.slice(0, -1)) dir = await dir.getDirectoryHandle(name, { create });\n return (await (await dir.getFileHandle(path[path.length - 1])).getFile()).size;\n } catch (error) {\n if (error && error.name === 'NotFoundError') return null;\n throw error;\n }\n})";
|
|
16
|
+
export declare const BEGIN_FILE = "(async (path) => {\n const create = true;\n let dir = await navigator.storage.getDirectory();\n for (const name of path.slice(0, -1)) dir = await dir.getDirectoryHandle(name, { create });\n const handle = await dir.getFileHandle(path[path.length - 1], { create: true });\n globalThis.__originMigrationWritable = await handle.createWritable({ keepExistingData: false });\n})";
|
|
17
|
+
export declare const WRITE_CHUNK = "(async (data) => {\n let bytes;\n if (typeof Uint8Array.fromBase64 === 'function') {\n bytes = Uint8Array.fromBase64(data);\n } else {\n const binary = atob(data);\n bytes = new Uint8Array(binary.length);\n for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);\n }\n await globalThis.__originMigrationWritable.write(bytes);\n})";
|
|
18
|
+
export declare const END_FILE = "(async () => {\n const writable = globalThis.__originMigrationWritable;\n globalThis.__originMigrationWritable = undefined;\n await writable.close();\n})()";
|
|
19
|
+
export declare const ABORT_FILE = "(async () => {\n const writable = globalThis.__originMigrationWritable;\n globalThis.__originMigrationWritable = undefined;\n if (writable) await writable.abort();\n})()";
|
|
20
|
+
export declare const COMMIT_FILE = "(async (path, name) => {\n const create = false;\n let dir = await navigator.storage.getDirectory();\n for (const name of path.slice(0, -1)) dir = await dir.getDirectoryHandle(name, { create });\n await (await dir.getFileHandle(path[path.length - 1])).move(name);\n})";
|
|
21
|
+
export declare const REMOVE_FILE = "(async (path) => {\n const create = false;\n try {\n let dir = await navigator.storage.getDirectory();\n for (const name of path.slice(0, -1)) dir = await dir.getDirectoryHandle(name, { create });\n await dir.removeEntry(path[path.length - 1]);\n } catch (error) {\n if (!error || error.name !== 'NotFoundError') throw error;\n }\n})";
|
|
22
|
+
/** A call of one of the function scripts above. */
|
|
23
|
+
export declare function call(script: string, ...args: unknown[]): string;
|
|
24
|
+
//# sourceMappingURL=scripts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scripts.d.ts","sourceRoot":"","sources":["../src/scripts.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,eAAO,MAAM,cAAc,8BAA8B,CAAA;AAEzD,6FAA6F;AAC7F,eAAO,MAAM,kBAAkB,mYAa1B,CAAA;AAEL,+EAA+E;AAC/E,eAAO,MAAM,mBAAmB,8NAQ7B,CAAA;AAEH,iGAAiG;AACjG,eAAO,MAAM,UAAU,QAWlB,CAAA;AAKL,8CAA8C;AAC9C,eAAO,MAAM,UAAU,6XAKpB,CAAA;AAEH,qEAAqE;AACrE,eAAO,MAAM,SAAS,mZASnB,CAAA;AAGH,eAAO,MAAM,UAAU,+XAKpB,CAAA;AAEH,eAAO,MAAM,WAAW,uYAUrB,CAAA;AAEH,eAAO,MAAM,QAAQ,mKAIhB,CAAA;AAEL,eAAO,MAAM,UAAU,iLAIlB,CAAA;AAEL,eAAO,MAAM,WAAW,oRAIrB,CAAA;AAEH,eAAO,MAAM,WAAW,gWAQrB,CAAA;AAEH,mDAAmD;AACnD,wBAAgB,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,MAAM,CAE/D"}
|
package/dist/scripts.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scripts main runs in the hidden migration pages. They stay strings so a bundler's minifier
|
|
3
|
+
* cannot rename or wrap what runs in another realm. Arguments are spliced in with `call`, as JSON.
|
|
4
|
+
*/
|
|
5
|
+
export const STAGING_SUFFIX = '.origin-migration-staging';
|
|
6
|
+
/** Every key and value of the page's localStorage, or null when the page may not read it. */
|
|
7
|
+
export const READ_LOCAL_STORAGE = `(() => {
|
|
8
|
+
try {
|
|
9
|
+
void localStorage.length;
|
|
10
|
+
} catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
const entries = [];
|
|
14
|
+
for (let index = 0; index < localStorage.length; index += 1) {
|
|
15
|
+
const key = localStorage.key(index);
|
|
16
|
+
const value = key === null ? null : localStorage.getItem(key);
|
|
17
|
+
if (key !== null && value !== null) entries.push([key, value]);
|
|
18
|
+
}
|
|
19
|
+
return entries;
|
|
20
|
+
})()`;
|
|
21
|
+
/** Sets each key the page does not hold yet; resolves with how many it set. */
|
|
22
|
+
export const WRITE_LOCAL_STORAGE = `((entries) => {
|
|
23
|
+
let written = 0;
|
|
24
|
+
for (const [key, value] of entries) {
|
|
25
|
+
if (localStorage.getItem(key) !== null) continue;
|
|
26
|
+
localStorage.setItem(key, value);
|
|
27
|
+
written += 1;
|
|
28
|
+
}
|
|
29
|
+
return written;
|
|
30
|
+
})`;
|
|
31
|
+
/** Every file in the origin private file system, with its size. Skips swap and staging files. */
|
|
32
|
+
export const LIST_FILES = `(async () => {
|
|
33
|
+
const files = [];
|
|
34
|
+
const pending = [{ dir: await navigator.storage.getDirectory(), path: [] }];
|
|
35
|
+
while (pending.length > 0) {
|
|
36
|
+
const { dir, path } = pending.pop();
|
|
37
|
+
for await (const [name, handle] of dir.entries()) {
|
|
38
|
+
if (handle.kind === 'directory') pending.push({ dir: handle, path: [...path, name] });
|
|
39
|
+
else if (!name.endsWith('.crswap') && !name.endsWith(${JSON.stringify(STAGING_SUFFIX)})) files.push({ path: [...path, name], size: (await handle.getFile()).size });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return files;
|
|
43
|
+
})()`;
|
|
44
|
+
const OPEN_PARENT = `let dir = await navigator.storage.getDirectory();
|
|
45
|
+
for (const name of path.slice(0, -1)) dir = await dir.getDirectoryHandle(name, { create });`;
|
|
46
|
+
/** `length` bytes of a file from `offset`. */
|
|
47
|
+
export const READ_CHUNK = `(async (path, offset, length) => {
|
|
48
|
+
const create = false;
|
|
49
|
+
${OPEN_PARENT}
|
|
50
|
+
const file = await (await dir.getFileHandle(path[path.length - 1])).getFile();
|
|
51
|
+
return new Uint8Array(await file.slice(offset, offset + length).arrayBuffer());
|
|
52
|
+
})`;
|
|
53
|
+
/** A file's size, or null when the page has no file at that path. */
|
|
54
|
+
export const FILE_SIZE = `(async (path) => {
|
|
55
|
+
const create = false;
|
|
56
|
+
try {
|
|
57
|
+
${OPEN_PARENT}
|
|
58
|
+
return (await (await dir.getFileHandle(path[path.length - 1])).getFile()).size;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (error && error.name === 'NotFoundError') return null;
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
})`;
|
|
64
|
+
// The writable commits on close, so an interrupted copy never leaves partial content.
|
|
65
|
+
export const BEGIN_FILE = `(async (path) => {
|
|
66
|
+
const create = true;
|
|
67
|
+
${OPEN_PARENT}
|
|
68
|
+
const handle = await dir.getFileHandle(path[path.length - 1], { create: true });
|
|
69
|
+
globalThis.__originMigrationWritable = await handle.createWritable({ keepExistingData: false });
|
|
70
|
+
})`;
|
|
71
|
+
export const WRITE_CHUNK = `(async (data) => {
|
|
72
|
+
let bytes;
|
|
73
|
+
if (typeof Uint8Array.fromBase64 === 'function') {
|
|
74
|
+
bytes = Uint8Array.fromBase64(data);
|
|
75
|
+
} else {
|
|
76
|
+
const binary = atob(data);
|
|
77
|
+
bytes = new Uint8Array(binary.length);
|
|
78
|
+
for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
|
|
79
|
+
}
|
|
80
|
+
await globalThis.__originMigrationWritable.write(bytes);
|
|
81
|
+
})`;
|
|
82
|
+
export const END_FILE = `(async () => {
|
|
83
|
+
const writable = globalThis.__originMigrationWritable;
|
|
84
|
+
globalThis.__originMigrationWritable = undefined;
|
|
85
|
+
await writable.close();
|
|
86
|
+
})()`;
|
|
87
|
+
export const ABORT_FILE = `(async () => {
|
|
88
|
+
const writable = globalThis.__originMigrationWritable;
|
|
89
|
+
globalThis.__originMigrationWritable = undefined;
|
|
90
|
+
if (writable) await writable.abort();
|
|
91
|
+
})()`;
|
|
92
|
+
export const COMMIT_FILE = `(async (path, name) => {
|
|
93
|
+
const create = false;
|
|
94
|
+
${OPEN_PARENT}
|
|
95
|
+
await (await dir.getFileHandle(path[path.length - 1])).move(name);
|
|
96
|
+
})`;
|
|
97
|
+
export const REMOVE_FILE = `(async (path) => {
|
|
98
|
+
const create = false;
|
|
99
|
+
try {
|
|
100
|
+
${OPEN_PARENT}
|
|
101
|
+
await dir.removeEntry(path[path.length - 1]);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (!error || error.name !== 'NotFoundError') throw error;
|
|
104
|
+
}
|
|
105
|
+
})`;
|
|
106
|
+
/** A call of one of the function scripts above. */
|
|
107
|
+
export function call(script, ...args) {
|
|
108
|
+
return `${script}(${args.map((arg) => JSON.stringify(arg)).join(', ')})`;
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=scripts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scripts.js","sourceRoot":"","sources":["../src/scripts.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,CAAC,MAAM,cAAc,GAAG,2BAA2B,CAAA;AAEzD,6FAA6F;AAC7F,MAAM,CAAC,MAAM,kBAAkB,GAAG;;;;;;;;;;;;;KAa7B,CAAA;AAEL,+EAA+E;AAC/E,MAAM,CAAC,MAAM,mBAAmB,GAAG;;;;;;;;GAQhC,CAAA;AAEH,iGAAiG;AACjG,MAAM,CAAC,MAAM,UAAU,GAAG;;;;;;;6DAOmC,IAAI,CAAC,SAAS,CAAC,cAAc,CAAC;;;;KAItF,CAAA;AAEL,MAAM,WAAW,GAAG;8FAC0E,CAAA;AAE9F,8CAA8C;AAC9C,MAAM,CAAC,MAAM,UAAU,GAAG;;IAEtB,WAAW;;;GAGZ,CAAA;AAEH,qEAAqE;AACrE,MAAM,CAAC,MAAM,SAAS,GAAG;;;MAGnB,WAAW;;;;;;GAMd,CAAA;AAEH,sFAAsF;AACtF,MAAM,CAAC,MAAM,UAAU,GAAG;;IAEtB,WAAW;;;GAGZ,CAAA;AAEH,MAAM,CAAC,MAAM,WAAW,GAAG;;;;;;;;;;GAUxB,CAAA;AAEH,MAAM,CAAC,MAAM,QAAQ,GAAG;;;;KAInB,CAAA;AAEL,MAAM,CAAC,MAAM,UAAU,GAAG;;;;KAIrB,CAAA;AAEL,MAAM,CAAC,MAAM,WAAW,GAAG;;IAEvB,WAAW;;GAEZ,CAAA;AAEH,MAAM,CAAC,MAAM,WAAW,GAAG;;;MAGrB,WAAW;;;;;GAKd,CAAA;AAEH,mDAAmD;AACnD,MAAM,UAAU,IAAI,CAAC,MAAc,EAAE,GAAG,IAAe;IACrD,OAAO,GAAG,MAAM,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAA;AAC1E,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,60 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@electron-stagewright/origin-migration",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "One-time copy of an Electron renderer's localStorage and origin private file system from the file:// origin to a custom-scheme origin, safe to interrupt and retry.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "johnny4young <johnny.iv.young@gmail.com>",
|
|
8
|
+
"homepage": "https://electron-stagewright.github.io/electron-stagewright/",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/electron-stagewright/electron-stagewright.git",
|
|
12
|
+
"directory": "packages/origin-migration"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/electron-stagewright/electron-stagewright/issues"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"electron",
|
|
19
|
+
"custom-protocol",
|
|
20
|
+
"localstorage",
|
|
21
|
+
"opfs",
|
|
22
|
+
"migration",
|
|
23
|
+
"file-protocol"
|
|
24
|
+
],
|
|
25
|
+
"main": "./dist/index.js",
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"files": [
|
|
28
|
+
"dist/",
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE"
|
|
31
|
+
],
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"import": "./dist/index.js"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"electron": ">=42.0.0"
|
|
40
|
+
},
|
|
41
|
+
"peerDependenciesMeta": {
|
|
42
|
+
"electron": {
|
|
43
|
+
"optional": true
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"electron": "^42.11.10",
|
|
48
|
+
"@electron-stagewright/core": "0.9.0"
|
|
49
|
+
},
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=24.0.0"
|
|
52
|
+
},
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public"
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"build": "tsc -p tsconfig.json",
|
|
58
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
59
|
+
}
|
|
6
60
|
}
|