@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.
Files changed (34) hide show
  1. package/README.md +49 -0
  2. package/dist/cli.d.ts +3 -0
  3. package/dist/cli.js +80 -0
  4. package/dist/index.d.ts +5 -0
  5. package/dist/index.js +3 -0
  6. package/dist/lib/abort-signal.d.ts +6 -0
  7. package/dist/lib/abort-signal.js +20 -0
  8. package/dist/lib/data-to-files.d.ts +8 -0
  9. package/dist/lib/data-to-files.js +36 -0
  10. package/dist/lib/download-offline-zip.d.ts +4 -0
  11. package/dist/lib/download-offline-zip.js +12 -0
  12. package/dist/lib/exec-script-in-sandbox.d.ts +3 -0
  13. package/dist/lib/exec-script-in-sandbox.js +50 -0
  14. package/dist/lib/generate-offline-data-legacy.d.ts +6 -0
  15. package/dist/lib/generate-offline-data-legacy.js +85 -0
  16. package/dist/lib/generate-offline-data.d.ts +6 -0
  17. package/dist/lib/generate-offline-data.js +90 -0
  18. package/dist/lib/generate-offline-map-data.d.ts +16 -0
  19. package/dist/lib/generate-offline-map-data.js +463 -0
  20. package/dist/lib/generate-runtime-files-data.d.ts +5 -0
  21. package/dist/lib/generate-runtime-files-data.js +16 -0
  22. package/dist/lib/offlinize-asset-url.d.ts +6 -0
  23. package/dist/lib/offlinize-asset-url.js +35 -0
  24. package/dist/lib/offlinize-assets-in-place.d.ts +6 -0
  25. package/dist/lib/offlinize-assets-in-place.js +139 -0
  26. package/dist/lib/offlinize-css-asset-text.d.ts +6 -0
  27. package/dist/lib/offlinize-css-asset-text.js +52 -0
  28. package/dist/lib/resolve-floorplan-dir.d.ts +8 -0
  29. package/dist/lib/resolve-floorplan-dir.js +34 -0
  30. package/dist/lib/save-offline-zip.d.ts +4 -0
  31. package/dist/lib/save-offline-zip.js +21 -0
  32. package/dist/lib/types.d.ts +18 -0
  33. package/dist/lib/types.js +1 -0
  34. 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
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
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
+ });
@@ -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,3 @@
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';
@@ -0,0 +1,6 @@
1
+ export interface TimedAbortSignal {
2
+ signal: AbortSignal;
3
+ dispose(): void;
4
+ }
5
+ export declare function withTimeoutSignal(signal: AbortSignal | undefined, timeoutMs: number): TimedAbortSignal;
6
+ //# sourceMappingURL=abort-signal.d.ts.map
@@ -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,8 @@
1
+ import type { LocalData } from './types.js';
2
+ export declare function dataToFiles(data: AsyncIterable<LocalData>, options?: {
3
+ signal?: AbortSignal;
4
+ }): AsyncIterable<{
5
+ path: string;
6
+ data: Uint8Array;
7
+ }>;
8
+ //# sourceMappingURL=data-to-files.d.ts.map
@@ -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,3 @@
1
+ /** Fetch a legacy data script and execute it in a compatibility vm context. */
2
+ export declare function execScriptInSandbox(url: string, signal?: AbortSignal): Promise<Record<string, unknown>>;
3
+ //# sourceMappingURL=exec-script-in-sandbox.d.ts.map
@@ -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