@expofp/offline 0.0.0-experimental.d269d30
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 +49 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +80 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +3 -0
- package/dist/lib/abort-signal.d.ts +6 -0
- package/dist/lib/abort-signal.js +20 -0
- package/dist/lib/data-to-files.d.ts +8 -0
- package/dist/lib/data-to-files.js +36 -0
- package/dist/lib/download-offline-zip.d.ts +4 -0
- package/dist/lib/download-offline-zip.js +12 -0
- package/dist/lib/exec-script-in-sandbox.d.ts +3 -0
- package/dist/lib/exec-script-in-sandbox.js +50 -0
- package/dist/lib/generate-offline-data-legacy.d.ts +6 -0
- package/dist/lib/generate-offline-data-legacy.js +85 -0
- package/dist/lib/generate-offline-data.d.ts +6 -0
- package/dist/lib/generate-offline-data.js +90 -0
- package/dist/lib/generate-offline-map-data.d.ts +16 -0
- package/dist/lib/generate-offline-map-data.js +463 -0
- package/dist/lib/generate-runtime-files-data.d.ts +5 -0
- package/dist/lib/generate-runtime-files-data.js +16 -0
- package/dist/lib/offlinize-asset-url.d.ts +6 -0
- package/dist/lib/offlinize-asset-url.js +35 -0
- package/dist/lib/offlinize-assets-in-place.d.ts +6 -0
- package/dist/lib/offlinize-assets-in-place.js +139 -0
- package/dist/lib/offlinize-css-asset-text.d.ts +6 -0
- package/dist/lib/offlinize-css-asset-text.js +52 -0
- package/dist/lib/resolve-floorplan-dir.d.ts +8 -0
- package/dist/lib/resolve-floorplan-dir.js +34 -0
- package/dist/lib/save-offline-zip.d.ts +4 -0
- package/dist/lib/save-offline-zip.js +21 -0
- package/dist/lib/types.d.ts +18 -0
- package/dist/lib/types.js +1 -0
- package/package.json +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
## Offline: generating ZIPs
|
|
2
|
+
|
|
3
|
+
The `@expofp/offline` package provides a CLI (`expofp-offline`) that creates self-contained ZIP archives of floor plans. The ZIP can be opened in any browser without internet connectivity.
|
|
4
|
+
|
|
5
|
+
### Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx @expofp/offline@<tag-or-version> <manifest-url> [options]
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Use a dist-tag or exact version to match the deployed floor plan (e.g. `@latest`, `@next`, `@customer1`, `@3.0.0-alpha.9`).
|
|
12
|
+
|
|
13
|
+
| Option | Description | Default |
|
|
14
|
+
| ----------------- | -------------------------------------------------- | -------------------------------------- |
|
|
15
|
+
| `-o`, `--output` | Output file path | `offline.zip` |
|
|
16
|
+
| `--offline-map` | Include an offline MapLibre basemap | |
|
|
17
|
+
| `--map-source` | Global PMTiles source URL/path for `--offline-map` | Latest available Protomaps daily build |
|
|
18
|
+
| `-h`, `--help` | Show help message | |
|
|
19
|
+
| `-v`, `--version` | Show version number | |
|
|
20
|
+
|
|
21
|
+
### Example
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx @expofp/offline@next https://demo.expofp.com/manifest.json -o offline.zip
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
With an offline MapLibre basemap:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npx @expofp/offline@next https://demo.expofp.com/manifest.json \
|
|
31
|
+
--offline-map \
|
|
32
|
+
-o offline.zip
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
If `--map-source` is omitted, the CLI checks `https://build.protomaps.com/YYYYMMDD.pmtiles`
|
|
36
|
+
starting from the current date and falls back to previous dates for up to 7 days.
|
|
37
|
+
Pass `--map-source` explicitly to use a specific PMTiles URL or local file.
|
|
38
|
+
|
|
39
|
+
Offline map extraction requires the `pmtiles` CLI binary to be available in `PATH`.
|
|
40
|
+
This is separate from the `pmtiles` npm package used by the browser runtime.
|
|
41
|
+
On macOS, install the CLI with `brew install pmtiles`.
|
|
42
|
+
|
|
43
|
+
### Debugging
|
|
44
|
+
|
|
45
|
+
Enable debug output with the `DEBUG` env variable:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
DEBUG=efp:offline:* npx @expofp/offline https://...
|
|
49
|
+
```
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { parseArgs } from 'node:util';
|
|
4
|
+
import { resolveFloorplanDir } from './lib/resolve-floorplan-dir.js';
|
|
5
|
+
import { saveOfflineZip } from './lib/save-offline-zip.js';
|
|
6
|
+
const HELP = `
|
|
7
|
+
Usage: expofp-offline <manifest-url> [options]
|
|
8
|
+
|
|
9
|
+
Create an offline ZIP archive of an ExpoFP floor plan.
|
|
10
|
+
|
|
11
|
+
Arguments:
|
|
12
|
+
manifest-url URL to the floor plan manifest JSON
|
|
13
|
+
|
|
14
|
+
Options:
|
|
15
|
+
-o, --output Output file path (default: offline.zip)
|
|
16
|
+
--offline-map
|
|
17
|
+
Include an offline MapLibre basemap in the ZIP
|
|
18
|
+
--map-source <source>
|
|
19
|
+
Global PMTiles source URL/path used with --offline-map
|
|
20
|
+
Defaults to the latest available Protomaps daily build
|
|
21
|
+
-h, --help Show this help message
|
|
22
|
+
-v, --version Show version number
|
|
23
|
+
`.trim();
|
|
24
|
+
async function main() {
|
|
25
|
+
const { values, positionals } = parseArgs({
|
|
26
|
+
allowPositionals: true,
|
|
27
|
+
options: {
|
|
28
|
+
output: { type: 'string', short: 'o', default: 'offline.zip' },
|
|
29
|
+
'offline-map': { type: 'boolean', default: false },
|
|
30
|
+
'map-source': { type: 'string' },
|
|
31
|
+
help: { type: 'boolean', short: 'h', default: false },
|
|
32
|
+
version: { type: 'boolean', short: 'v', default: false },
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
if (values.help) {
|
|
36
|
+
console.log(HELP);
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
if (values.version) {
|
|
40
|
+
const require = createRequire(import.meta.url);
|
|
41
|
+
const pkg = require('../package.json');
|
|
42
|
+
console.log(pkg.version);
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
const manifestUrl = positionals[0];
|
|
46
|
+
if (!manifestUrl) {
|
|
47
|
+
console.error('Error: manifest URL is required.\n');
|
|
48
|
+
console.error(HELP);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
// Validate URL
|
|
52
|
+
try {
|
|
53
|
+
new URL(manifestUrl);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
console.error(`Error: "${manifestUrl}" is not a valid URL.\n`);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
const outputPath = values.output;
|
|
60
|
+
if (!outputPath.endsWith('.zip')) {
|
|
61
|
+
console.error('Error: output path must end with .zip\n');
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
let offlineMap;
|
|
65
|
+
if (values['offline-map']) {
|
|
66
|
+
offlineMap = { mapSource: values['map-source'] };
|
|
67
|
+
}
|
|
68
|
+
console.log(`Resolving @expofp/floorplan runtime...`);
|
|
69
|
+
const runtimeBaseUrl = await resolveFloorplanDir();
|
|
70
|
+
console.log(`Creating offline ZIP from ${manifestUrl}...`);
|
|
71
|
+
await saveOfflineZip({ $ref: manifestUrl }, outputPath, {
|
|
72
|
+
runtimeBaseUrl,
|
|
73
|
+
offlineMap,
|
|
74
|
+
});
|
|
75
|
+
console.log(`Done! Offline ZIP saved to ${outputPath}`);
|
|
76
|
+
}
|
|
77
|
+
main().catch((err) => {
|
|
78
|
+
console.error('Error:', err instanceof Error ? err.message : err);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { downloadOfflineZip } from './lib/download-offline-zip.js';
|
|
2
|
+
export { buildOfflineZip } from './lib/generate-offline-data.js';
|
|
3
|
+
export { saveOfflineZip } from './lib/save-offline-zip.js';
|
|
4
|
+
export type { OfflineOptions } from './lib/types.js';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const noop = () => undefined;
|
|
2
|
+
export function withTimeoutSignal(signal, timeoutMs) {
|
|
3
|
+
const controller = new AbortController();
|
|
4
|
+
if (signal?.aborted) {
|
|
5
|
+
controller.abort(signal.reason);
|
|
6
|
+
return { signal: controller.signal, dispose: noop };
|
|
7
|
+
}
|
|
8
|
+
const timeout = setTimeout(() => {
|
|
9
|
+
controller.abort(new Error(`Operation timed out after ${timeoutMs}ms.`));
|
|
10
|
+
}, timeoutMs);
|
|
11
|
+
const abortFromParent = () => controller.abort(signal?.reason);
|
|
12
|
+
signal?.addEventListener('abort', abortFromParent, { once: true });
|
|
13
|
+
return {
|
|
14
|
+
signal: controller.signal,
|
|
15
|
+
dispose() {
|
|
16
|
+
clearTimeout(timeout);
|
|
17
|
+
signal?.removeEventListener('abort', abortFromParent);
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { readFileFromUrl } from '@expofp/utils';
|
|
2
|
+
import debug from 'debug';
|
|
3
|
+
import { withTimeoutSignal } from './abort-signal.js';
|
|
4
|
+
const log = debug('efp:offline:data-to-files');
|
|
5
|
+
const FILE_FETCH_TIMEOUT_MS = 60_000;
|
|
6
|
+
export async function* dataToFiles(data, options) {
|
|
7
|
+
for await (const file of data) {
|
|
8
|
+
if ('url' in file) {
|
|
9
|
+
const fetchSignal = withTimeoutSignal(options?.signal, FILE_FETCH_TIMEOUT_MS);
|
|
10
|
+
let response;
|
|
11
|
+
try {
|
|
12
|
+
response = await readFileFromUrl(file.url, { signal: fetchSignal.signal });
|
|
13
|
+
}
|
|
14
|
+
finally {
|
|
15
|
+
fetchSignal.dispose();
|
|
16
|
+
}
|
|
17
|
+
if (!response.ok) {
|
|
18
|
+
console.warn(`Skipping file ${file.url} as it could not be downloaded`);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
log('Bundling file from url', file.targetFilePath);
|
|
22
|
+
yield {
|
|
23
|
+
path: file.targetFilePath,
|
|
24
|
+
data: response.data,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
log('Bundling text file', file.targetFilePath);
|
|
30
|
+
yield {
|
|
31
|
+
path: file.targetFilePath,
|
|
32
|
+
data: new TextEncoder().encode(file.text),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Manifest, Ref } from '@expofp/data';
|
|
2
|
+
import type { OfflineOptions } from './types.js';
|
|
3
|
+
export declare function downloadOfflineZip(manifest: Manifest | Ref<Manifest>, options: OfflineOptions): Promise<void>;
|
|
4
|
+
//# sourceMappingURL=download-offline-zip.d.ts.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { buildOfflineZip } from './generate-offline-data.js';
|
|
2
|
+
export async function downloadOfflineZip(manifest, options) {
|
|
3
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
|
4
|
+
throw new Error('downloadOfflineZip can only be used in a browser environment');
|
|
5
|
+
}
|
|
6
|
+
const blob = await buildOfflineZip(manifest, options);
|
|
7
|
+
const a = document.createElement('a');
|
|
8
|
+
a.href = URL.createObjectURL(blob);
|
|
9
|
+
a.download = 'offline.zip';
|
|
10
|
+
a.click();
|
|
11
|
+
URL.revokeObjectURL(a.href);
|
|
12
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { fetchWithRetry } from '@expofp/utils';
|
|
2
|
+
import { withTimeoutSignal } from './abort-signal.js';
|
|
3
|
+
const SCRIPT_FETCH_TIMEOUT_MS = 60_000;
|
|
4
|
+
const SCRIPT_EXECUTION_TIMEOUT_MS = 5_000;
|
|
5
|
+
const LEGACY_SCRIPT_ALLOWED_ORIGINS_ENV = 'EXPOFP_OFFLINE_LEGACY_SCRIPT_ORIGINS';
|
|
6
|
+
/** Fetch a legacy data script and execute it in a compatibility vm context. */
|
|
7
|
+
export async function execScriptInSandbox(url, signal) {
|
|
8
|
+
assertTrustedLegacyScriptUrl(url);
|
|
9
|
+
const fetchSignal = withTimeoutSignal(signal, SCRIPT_FETCH_TIMEOUT_MS);
|
|
10
|
+
let code;
|
|
11
|
+
try {
|
|
12
|
+
const response = await fetchWithRetry(url, { signal: fetchSignal.signal });
|
|
13
|
+
if (!response.ok) {
|
|
14
|
+
throw new Error(`Failed to fetch script (HTTP ${response.status}): ${url}`);
|
|
15
|
+
}
|
|
16
|
+
code = await response.text();
|
|
17
|
+
}
|
|
18
|
+
finally {
|
|
19
|
+
fetchSignal.dispose();
|
|
20
|
+
}
|
|
21
|
+
const { runInNewContext } = await import('node:vm');
|
|
22
|
+
const sandbox = {};
|
|
23
|
+
sandbox.window = sandbox; // legacy scripts expect window === globalThis
|
|
24
|
+
// The vm context is not a security boundary; origin validation above is required.
|
|
25
|
+
runInNewContext(code, sandbox, { timeout: SCRIPT_EXECUTION_TIMEOUT_MS });
|
|
26
|
+
return sandbox;
|
|
27
|
+
}
|
|
28
|
+
function assertTrustedLegacyScriptUrl(url) {
|
|
29
|
+
const parsed = new URL(url);
|
|
30
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
31
|
+
throw new Error(`Legacy script URL must use http or https: ${url}`);
|
|
32
|
+
}
|
|
33
|
+
const allowedOrigins = getAllowedLegacyScriptOrigins();
|
|
34
|
+
if (isDefaultAllowedHost(parsed.hostname) || allowedOrigins.includes(parsed.origin))
|
|
35
|
+
return;
|
|
36
|
+
throw new Error(`Legacy script host is not allowed: ${parsed.hostname}. Add ${parsed.origin} to ${LEGACY_SCRIPT_ALLOWED_ORIGINS_ENV} to allow it.`);
|
|
37
|
+
}
|
|
38
|
+
function getAllowedLegacyScriptOrigins() {
|
|
39
|
+
return (process.env[LEGACY_SCRIPT_ALLOWED_ORIGINS_ENV] ?? '')
|
|
40
|
+
.split(',')
|
|
41
|
+
.map((origin) => origin.trim())
|
|
42
|
+
.filter(Boolean);
|
|
43
|
+
}
|
|
44
|
+
function isDefaultAllowedHost(hostname) {
|
|
45
|
+
return (hostname === 'expofp.com' ||
|
|
46
|
+
hostname.endsWith('.expofp.com') ||
|
|
47
|
+
hostname === 'localhost' ||
|
|
48
|
+
hostname === '127.0.0.1' ||
|
|
49
|
+
hostname === '::1');
|
|
50
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Manifest } from '@expofp/data';
|
|
2
|
+
import type { LocalData } from './types.js';
|
|
3
|
+
export declare function generateOfflineDataLegacy(manifest: Manifest, options: {
|
|
4
|
+
signal?: AbortSignal;
|
|
5
|
+
}): AsyncIterable<LocalData>;
|
|
6
|
+
//# sourceMappingURL=generate-offline-data-legacy.d.ts.map
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { resolve } from '@expofp/resolve';
|
|
2
|
+
import { deepClone, makeLocalPath } from '@expofp/utils';
|
|
3
|
+
import debug from 'debug';
|
|
4
|
+
import { execScriptInSandbox } from './exec-script-in-sandbox.js';
|
|
5
|
+
import { offlinizeAssetsInPlace } from './offlinize-assets-in-place.js';
|
|
6
|
+
const log = debug('efp:offline:generate-offline-data-legacy');
|
|
7
|
+
export async function* generateOfflineDataLegacy(manifest, options) {
|
|
8
|
+
if (!manifest.legacyDataUrlBase) {
|
|
9
|
+
throw new Error('Manifest does not have legacyDataUrlBase');
|
|
10
|
+
}
|
|
11
|
+
const { signal } = options;
|
|
12
|
+
const legacyDataUrlBase = manifest.legacyDataUrlBase;
|
|
13
|
+
const version = await resolve(manifest.legacyDataVersion)
|
|
14
|
+
.then((json) => json?.version)
|
|
15
|
+
.catch(() => {
|
|
16
|
+
log('Could not resolve legacyDataVersion, proceeding without it');
|
|
17
|
+
return Date.now().toString();
|
|
18
|
+
});
|
|
19
|
+
const localDataUrlBase = await makeLocalPath(new URL(legacyDataUrlBase));
|
|
20
|
+
// data.js — execute in isolated vm context to extract __data
|
|
21
|
+
const dataCtx = await execScriptInSandbox(`${legacyDataUrlBase}data.js?v=${version}`, signal);
|
|
22
|
+
const data = deepClone(dataCtx.__data);
|
|
23
|
+
yield* offlinizeDataJs(data, legacyDataUrlBase, signal);
|
|
24
|
+
// Asset paths in data are absolute (e.g. "https-demo-expofp-com/data/exhibitors/...").
|
|
25
|
+
// The runtime resolves them relative to legacyDataUrlBase, so strip that prefix
|
|
26
|
+
// to avoid doubling (e.g. "base/base/exhibitors/...").
|
|
27
|
+
stripPathPrefix(data, localDataUrlBase);
|
|
28
|
+
yield {
|
|
29
|
+
text: `__data = ${JSON.stringify(data, null, 2)};`,
|
|
30
|
+
targetFilePath: `${localDataUrlBase}data.js`,
|
|
31
|
+
};
|
|
32
|
+
// wf.data.js
|
|
33
|
+
const wfDataUrl = `${legacyDataUrlBase}wf.data.js?v=${version}`;
|
|
34
|
+
yield { url: new URL(wfDataUrl), targetFilePath: `${localDataUrlBase}wf.data.js` };
|
|
35
|
+
// fp.svg.js — execute in isolated vm context to extract __fpLayers
|
|
36
|
+
const svgCtx = await execScriptInSandbox(`${legacyDataUrlBase}fp.svg.js?v=${version}`, signal);
|
|
37
|
+
yield {
|
|
38
|
+
url: new URL(`${legacyDataUrlBase}fp.svg.js?v=${version}`),
|
|
39
|
+
targetFilePath: `${localDataUrlBase}fp.svg.js`,
|
|
40
|
+
};
|
|
41
|
+
// fp.svg.{layer}.js
|
|
42
|
+
const fpLayers = svgCtx.__fpLayers || [];
|
|
43
|
+
for (const layer of fpLayers) {
|
|
44
|
+
const layerFile = `fp.svg.${layer.name}.js`;
|
|
45
|
+
const layerUrl = `${legacyDataUrlBase}${layerFile}?v=${version}`;
|
|
46
|
+
yield { url: new URL(layerUrl), targetFilePath: `${localDataUrlBase}${layerFile}` };
|
|
47
|
+
}
|
|
48
|
+
manifest.legacyDataUrlBase = localDataUrlBase;
|
|
49
|
+
}
|
|
50
|
+
/** Strip a path prefix from all string values in a data tree (in place). */
|
|
51
|
+
function stripPathPrefix(node, prefix) {
|
|
52
|
+
if (Array.isArray(node)) {
|
|
53
|
+
for (let i = 0; i < node.length; i++) {
|
|
54
|
+
if (typeof node[i] === 'string' && node[i].startsWith(prefix)) {
|
|
55
|
+
node[i] = node[i].slice(prefix.length);
|
|
56
|
+
}
|
|
57
|
+
else if (node[i] && typeof node[i] === 'object') {
|
|
58
|
+
stripPathPrefix(node[i], prefix);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (!node || typeof node !== 'object') {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const obj = node;
|
|
67
|
+
for (const key of Object.keys(obj)) {
|
|
68
|
+
const val = obj[key];
|
|
69
|
+
if (typeof val === 'string' && val.startsWith(prefix)) {
|
|
70
|
+
obj[key] = val.slice(prefix.length);
|
|
71
|
+
}
|
|
72
|
+
else if (val && typeof val === 'object') {
|
|
73
|
+
stripPathPrefix(val, prefix);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function* offlinizeDataJs(data, legacyDataUrlBase, signal) {
|
|
78
|
+
if (data.customCss) {
|
|
79
|
+
console.warn('data.customCss is deprecated, renaming to data.customCssAssetText');
|
|
80
|
+
data.customCssAssetText = data.customCss;
|
|
81
|
+
delete data.customCss;
|
|
82
|
+
}
|
|
83
|
+
const fetches = await offlinizeAssetsInPlace(data, { legacyDataUrlBase, signal });
|
|
84
|
+
yield* fetches;
|
|
85
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Manifest, Ref } from '@expofp/data';
|
|
2
|
+
import type { LocalData, OfflineOptions } from './types.js';
|
|
3
|
+
export declare function generateOfflineData(inputManifest: Manifest | Ref<Manifest>, options: OfflineOptions): AsyncIterable<LocalData, void>;
|
|
4
|
+
/** Build a complete offline ZIP archive from a manifest. */
|
|
5
|
+
export declare function buildOfflineZip(manifest: Manifest | Ref<Manifest>, options: OfflineOptions): Promise<Blob>;
|
|
6
|
+
//# sourceMappingURL=generate-offline-data.d.ts.map
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { resolve } from '@expofp/resolve';
|
|
2
|
+
import { buildZipArchive, deepClone } from '@expofp/utils';
|
|
3
|
+
import debug from 'debug';
|
|
4
|
+
import { dataToFiles } from './data-to-files.js';
|
|
5
|
+
import { generateOfflineDataLegacy } from './generate-offline-data-legacy.js';
|
|
6
|
+
import { generateOfflineMapData, validateOfflineMapOptions } from './generate-offline-map-data.js';
|
|
7
|
+
import { generateRuntimeFilesData } from './generate-runtime-files-data.js';
|
|
8
|
+
import { offlinizeAssetsInPlace } from './offlinize-assets-in-place.js';
|
|
9
|
+
const log = debug('efp:offline:generate-offline-data');
|
|
10
|
+
export async function* generateOfflineData(inputManifest, options) {
|
|
11
|
+
const manifest = deepClone(await resolve(inputManifest));
|
|
12
|
+
log(`Generating offline files for expo: ${manifest.expo}`);
|
|
13
|
+
const pathPrefix = manifest.expo;
|
|
14
|
+
// this is a requirement of mobile SDK, paths in the ZIP must be prefixed with the expo name
|
|
15
|
+
// TODO: make sure SDK works with unprefixed paths, and remove the prefixing logic from this function
|
|
16
|
+
const prefixPath = (path) => `${pathPrefix}/${path}`;
|
|
17
|
+
const legacyDataUrlBase = manifest.legacyDataUrlBase;
|
|
18
|
+
let maplibre;
|
|
19
|
+
const offlineMap = options.offlineMap
|
|
20
|
+
? await validateOfflineMapOptions(options.offlineMap)
|
|
21
|
+
: undefined;
|
|
22
|
+
if (manifest.legacyDataUrlBase) {
|
|
23
|
+
for await (const d of generateOfflineDataLegacy(manifest, options)) {
|
|
24
|
+
yield { ...d, targetFilePath: prefixPath(d.targetFilePath) };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (offlineMap) {
|
|
28
|
+
const mapData = generateOfflineMapData({ ...manifest, legacyDataUrlBase }, { ...offlineMap, signal: options.signal });
|
|
29
|
+
for await (const d of mapData.files) {
|
|
30
|
+
yield { ...d, targetFilePath: prefixPath(d.targetFilePath) };
|
|
31
|
+
}
|
|
32
|
+
maplibre = mapData.maplibre;
|
|
33
|
+
}
|
|
34
|
+
// transform entire manifest + assets
|
|
35
|
+
const data = await offlinizeAssetsInPlace(manifest, options);
|
|
36
|
+
for (const d of data) {
|
|
37
|
+
yield { ...d, targetFilePath: prefixPath(d.targetFilePath) };
|
|
38
|
+
}
|
|
39
|
+
// add runtime files — manually iterate to prefix paths while capturing return value
|
|
40
|
+
const runtimeGen = generateRuntimeFilesData(options.runtimeBaseUrl);
|
|
41
|
+
let runtimeResult = await runtimeGen.next();
|
|
42
|
+
while (!runtimeResult.done) {
|
|
43
|
+
yield {
|
|
44
|
+
...runtimeResult.value,
|
|
45
|
+
targetFilePath: prefixPath(runtimeResult.value.targetFilePath),
|
|
46
|
+
};
|
|
47
|
+
runtimeResult = await runtimeGen.next();
|
|
48
|
+
}
|
|
49
|
+
const { entry } = runtimeResult.value;
|
|
50
|
+
// generate index.html
|
|
51
|
+
yield {
|
|
52
|
+
text: getIndexHtml(entry, manifest, maplibre),
|
|
53
|
+
targetFilePath: prefixPath('index.html'),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/** Build a complete offline ZIP archive from a manifest. */
|
|
57
|
+
export async function buildOfflineZip(manifest, options) {
|
|
58
|
+
const data = generateOfflineData(manifest, options);
|
|
59
|
+
const files = dataToFiles(data, { signal: options.signal });
|
|
60
|
+
return await buildZipArchive(files);
|
|
61
|
+
}
|
|
62
|
+
/** Known path — file lives in `packages/floorplan/public/` and is copied to dist by Vite. */
|
|
63
|
+
const COMPAT_HELPER = 'compat-helper.js';
|
|
64
|
+
function getIndexHtml(entry, manifest, maplibre) {
|
|
65
|
+
// entry is "./runtime/index.js", derive the runtime base path
|
|
66
|
+
const runtimeBase = entry.slice(0, entry.lastIndexOf('/') + 1);
|
|
67
|
+
return `\
|
|
68
|
+
<!DOCTYPE html>
|
|
69
|
+
<script src="${runtimeBase}${COMPAT_HELPER}"></script>
|
|
70
|
+
<script type="module">
|
|
71
|
+
import { load } from ${JSON.stringify(entry)};
|
|
72
|
+
const options = ${JSON.stringify({ ...manifest, maplibre })};
|
|
73
|
+
if (options.maplibre && options.maplibre.offline && options.maplibre.styleUrl) {
|
|
74
|
+
const styleUrl = new URL(options.maplibre.styleUrl, window.location.href);
|
|
75
|
+
const style = await fetch(styleUrl).then((response) => response.json());
|
|
76
|
+
const resolveStyleUrl = (url) => new URL(url, styleUrl).href.replaceAll('%7B', '{').replaceAll('%7D', '}');
|
|
77
|
+
if (typeof style.sprite === 'string') style.sprite = resolveStyleUrl(style.sprite);
|
|
78
|
+
if (typeof style.glyphs === 'string') style.glyphs = resolveStyleUrl(style.glyphs);
|
|
79
|
+
for (const source of Object.values(style.sources || {})) {
|
|
80
|
+
if (source && typeof source.url === 'string' && source.url.startsWith('pmtiles://')) {
|
|
81
|
+
source.url = 'pmtiles://' + new URL(source.url.slice('pmtiles://'.length), styleUrl).href;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
options.maplibre.style = style;
|
|
85
|
+
delete options.maplibre.styleUrl;
|
|
86
|
+
}
|
|
87
|
+
await load(options);
|
|
88
|
+
</script>
|
|
89
|
+
`;
|
|
90
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Manifest } from '@expofp/data';
|
|
2
|
+
import type { MaplibreOptions } from '@expofp/floorplan';
|
|
3
|
+
import type { LocalData, OfflineMapOptions } from './types.js';
|
|
4
|
+
type ValidatedOfflineMapOptions = OfflineMapOptions & {
|
|
5
|
+
mapSource: string;
|
|
6
|
+
};
|
|
7
|
+
export interface OfflineMapResult {
|
|
8
|
+
files: AsyncIterable<LocalData>;
|
|
9
|
+
maplibre: MaplibreOptions;
|
|
10
|
+
}
|
|
11
|
+
export declare function generateOfflineMapData(manifest: Manifest, options: ValidatedOfflineMapOptions & {
|
|
12
|
+
signal?: AbortSignal;
|
|
13
|
+
}): OfflineMapResult;
|
|
14
|
+
export declare function validateOfflineMapOptions(options: OfflineMapOptions): Promise<ValidatedOfflineMapOptions>;
|
|
15
|
+
export {};
|
|
16
|
+
//# sourceMappingURL=generate-offline-map-data.d.ts.map
|