styled-map-package 1.0.1 → 2.0.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/bin/smp-view.js +1 -0
- package/dist/download.d.ts +49 -0
- package/dist/index.d.ts +9 -0
- package/dist/reader-watch.d.ts +13 -0
- package/dist/reader.d.ts +51 -0
- package/dist/reporters.d.ts +7 -0
- package/dist/server.d.ts +21 -0
- package/dist/style-downloader.d.ts +110 -0
- package/dist/tile-downloader.d.ts +72 -0
- package/dist/types.d.ts +64 -0
- package/dist/utils/errors.d.ts +16 -0
- package/dist/utils/fetch.d.ts +54 -0
- package/dist/utils/file-formats.d.ts +25 -0
- package/dist/utils/geo.d.ts +44 -0
- package/dist/utils/mapbox.d.ts +40 -0
- package/dist/utils/misc.d.ts +20 -0
- package/dist/utils/streams.d.ts +69 -0
- package/dist/utils/style.d.ts +59 -0
- package/dist/utils/templates.d.ts +68 -0
- package/dist/writer.d.ts +130 -0
- package/lib/index.js +4 -0
- package/lib/reader-watch.js +133 -0
- package/lib/reader.js +18 -3
- package/lib/server.js +44 -27
- package/lib/utils/errors.js +24 -0
- package/lib/utils/streams.js +4 -1
- package/map-viewer/index.html +1 -1
- package/package.json +51 -10
- package/.github/workflows/node.yml +0 -30
- package/.github/workflows/release.yml +0 -47
- package/.husky/pre-commit +0 -1
- package/.nvmrc +0 -1
- package/eslint.config.js +0 -17
- package/test/download-write-read.js +0 -43
- package/test/fixtures/invalid-styles/empty.json +0 -1
- package/test/fixtures/invalid-styles/missing-source.json +0 -10
- package/test/fixtures/invalid-styles/no-layers.json +0 -4
- package/test/fixtures/invalid-styles/no-sources.json +0 -4
- package/test/fixtures/invalid-styles/null.json +0 -1
- package/test/fixtures/invalid-styles/unsupported-version.json +0 -5
- package/test/fixtures/valid-styles/external-geojson.input.json +0 -66
- package/test/fixtures/valid-styles/external-geojson.output.json +0 -93
- package/test/fixtures/valid-styles/inline-geojson.input.json +0 -421
- package/test/fixtures/valid-styles/inline-geojson.output.json +0 -1573
- package/test/fixtures/valid-styles/maplibre-demotiles.input.json +0 -831
- package/test/fixtures/valid-styles/maplibre-unlabelled.input.json +0 -496
- package/test/fixtures/valid-styles/maplibre-unlabelled.output.json +0 -1573
- package/test/fixtures/valid-styles/minimal-labelled.input.json +0 -37
- package/test/fixtures/valid-styles/minimal-labelled.output.json +0 -72
- package/test/fixtures/valid-styles/minimal-sprites.input.json +0 -37
- package/test/fixtures/valid-styles/minimal-sprites.output.json +0 -58
- package/test/fixtures/valid-styles/minimal.input.json +0 -54
- package/test/fixtures/valid-styles/minimal.output.json +0 -92
- package/test/fixtures/valid-styles/multiple-sprites.input.json +0 -46
- package/test/fixtures/valid-styles/multiple-sprites.output.json +0 -128
- package/test/fixtures/valid-styles/raster-sources.input.json +0 -33
- package/test/fixtures/valid-styles/raster-sources.output.json +0 -69
- package/test/utils/assert-bbox-equal.js +0 -19
- package/test/utils/digest-stream.js +0 -36
- package/test/utils/image-streams.js +0 -30
- package/test/utils/reader-helper.js +0 -72
- package/test/write-read.js +0 -620
- package/tsconfig.json +0 -18
- package/types/buffer-peek-stream.d.ts +0 -12
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** @import { TransformOptions } from 'readable-stream' */
|
|
2
|
+
/**
|
|
3
|
+
* Create a writable stream from an async function. Default concurrecy is 16 -
|
|
4
|
+
* this is the number of parallel functions that will be pending before
|
|
5
|
+
* backpressure is applied on the stream.
|
|
6
|
+
*
|
|
7
|
+
* @template {(...args: any[]) => Promise<void>} T
|
|
8
|
+
* @param {T} fn
|
|
9
|
+
* @returns {import('readable-stream').Writable}
|
|
10
|
+
*/
|
|
11
|
+
export function writeStreamFromAsync<T extends (...args: any[]) => Promise<void>>(fn: T, { concurrency }?: {
|
|
12
|
+
concurrency?: number | undefined;
|
|
13
|
+
}): import("readable-stream").Writable;
|
|
14
|
+
/**
|
|
15
|
+
* From https://github.com/nodejs/node/blob/430c0269/lib/internal/webstreams/adapters.js#L509
|
|
16
|
+
*
|
|
17
|
+
* @param {ReadableStream} readableStream
|
|
18
|
+
* @param {{
|
|
19
|
+
* highWaterMark? : number,
|
|
20
|
+
* encoding? : string,
|
|
21
|
+
* objectMode? : boolean,
|
|
22
|
+
* signal? : AbortSignal,
|
|
23
|
+
* }} [options]
|
|
24
|
+
* @returns {import('stream').Readable}
|
|
25
|
+
*/
|
|
26
|
+
export function fromWebReadableStream(readableStream: ReadableStream, options?: {
|
|
27
|
+
highWaterMark?: number;
|
|
28
|
+
encoding?: string;
|
|
29
|
+
objectMode?: boolean;
|
|
30
|
+
signal?: AbortSignal;
|
|
31
|
+
} | undefined): import("stream").Readable;
|
|
32
|
+
/**
|
|
33
|
+
* @param {unknown} obj
|
|
34
|
+
* @returns {obj is ReadableStream}
|
|
35
|
+
*/
|
|
36
|
+
export function isWebReadableStream(obj: unknown): obj is ReadableStream;
|
|
37
|
+
/** @typedef {(opts: { totalBytes: number, chunkBytes: number }) => void} ProgressCallback */
|
|
38
|
+
/** @typedef {TransformOptions & { onprogress?: ProgressCallback }} ProgressStreamOptions */
|
|
39
|
+
/**
|
|
40
|
+
* Passthrough stream that counts the bytes passing through it. Pass an optional
|
|
41
|
+
* `onprogress` callback that will be called with the accumulated total byte
|
|
42
|
+
* count and the chunk byte count after each chunk.
|
|
43
|
+
* @extends {Transform}
|
|
44
|
+
*/
|
|
45
|
+
export class ProgressStream extends Transform {
|
|
46
|
+
/**
|
|
47
|
+
* @param {ProgressStreamOptions} [opts]
|
|
48
|
+
*/
|
|
49
|
+
constructor({ onprogress, ...opts }?: ProgressStreamOptions | undefined);
|
|
50
|
+
/** Total bytes that have passed through this stream */
|
|
51
|
+
get byteLength(): number;
|
|
52
|
+
/**
|
|
53
|
+
* @override
|
|
54
|
+
* @param {Buffer | Uint8Array} chunk
|
|
55
|
+
* @param {Parameters<Transform['_transform']>[1]} encoding
|
|
56
|
+
* @param {Parameters<Transform['_transform']>[2]} callback
|
|
57
|
+
*/
|
|
58
|
+
override _transform(chunk: Buffer | Uint8Array, encoding: Parameters<Transform["_transform"]>[1], callback: Parameters<Transform["_transform"]>[2]): void;
|
|
59
|
+
#private;
|
|
60
|
+
}
|
|
61
|
+
export type ProgressCallback = (opts: {
|
|
62
|
+
totalBytes: number;
|
|
63
|
+
chunkBytes: number;
|
|
64
|
+
}) => void;
|
|
65
|
+
export type ProgressStreamOptions = TransformOptions & {
|
|
66
|
+
onprogress?: ProgressCallback;
|
|
67
|
+
};
|
|
68
|
+
import { Transform } from 'readable-stream';
|
|
69
|
+
import type { TransformOptions } from 'readable-stream';
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** @import {StyleSpecification, ExpressionSpecification, ValidationError} from '@maplibre/maplibre-gl-style-spec' */
|
|
2
|
+
/**
|
|
3
|
+
* For a given style, replace all font stacks (`text-field` properties) with the
|
|
4
|
+
* provided fonts. If no matching font is found, the first font in the stack is
|
|
5
|
+
* used.
|
|
6
|
+
*
|
|
7
|
+
* *Modifies the input style object*
|
|
8
|
+
*
|
|
9
|
+
* @param {StyleSpecification} style
|
|
10
|
+
* @param {string[]} fonts
|
|
11
|
+
*/
|
|
12
|
+
export function replaceFontStacks(style: StyleSpecification, fonts: string[]): StyleSpecification;
|
|
13
|
+
/**
|
|
14
|
+
* From given style layers, create a new style by calling the provided callback
|
|
15
|
+
* function on every font stack defined in the style.
|
|
16
|
+
*
|
|
17
|
+
* @param {StyleSpecification['layers']} layers
|
|
18
|
+
* @param {(fontStack: string[]) => string[]} callbackFn
|
|
19
|
+
* @returns {StyleSpecification['layers']}
|
|
20
|
+
*/
|
|
21
|
+
export function mapFontStacks(layers: StyleSpecification["layers"], callbackFn: (fontStack: string[]) => string[]): StyleSpecification["layers"];
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {object} TileJSONPartial
|
|
24
|
+
* @property {string[]} tiles
|
|
25
|
+
* @property {string} [description]
|
|
26
|
+
* @property {string} [attribution]
|
|
27
|
+
* @property {object[]} [vector_layers]
|
|
28
|
+
* @property {import('./geo.js').BBox} [bounds]
|
|
29
|
+
* @property {number} [maxzoom]
|
|
30
|
+
* @property {number} [minzoom]
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
*
|
|
34
|
+
* @param {unknown} tilejson
|
|
35
|
+
* @returns {asserts tilejson is TileJSONPartial}
|
|
36
|
+
*/
|
|
37
|
+
export function assertTileJSON(tilejson: unknown): asserts tilejson is TileJSONPartial;
|
|
38
|
+
/**
|
|
39
|
+
* Check whether a source is already inlined (e.g. does not reference a TileJSON or GeoJSON url)
|
|
40
|
+
*
|
|
41
|
+
* @param {import('@maplibre/maplibre-gl-style-spec').SourceSpecification} source
|
|
42
|
+
* @returns {source is import('../types.js').InlinedSource}
|
|
43
|
+
*/
|
|
44
|
+
export function isInlinedSource(source: import("@maplibre/maplibre-gl-style-spec").SourceSpecification): source is import("../types.js").InlinedSource;
|
|
45
|
+
export const validateStyle: {
|
|
46
|
+
(style: unknown): style is StyleSpecification;
|
|
47
|
+
errors: ValidationError[];
|
|
48
|
+
};
|
|
49
|
+
export type TileJSONPartial = {
|
|
50
|
+
tiles: string[];
|
|
51
|
+
description?: string | undefined;
|
|
52
|
+
attribution?: string | undefined;
|
|
53
|
+
vector_layers?: object[] | undefined;
|
|
54
|
+
bounds?: import("./geo.js").BBox | undefined;
|
|
55
|
+
maxzoom?: number | undefined;
|
|
56
|
+
minzoom?: number | undefined;
|
|
57
|
+
};
|
|
58
|
+
import type { StyleSpecification } from '@maplibre/maplibre-gl-style-spec';
|
|
59
|
+
import type { ValidationError } from '@maplibre/maplibre-gl-style-spec';
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {string} path
|
|
3
|
+
* @returns
|
|
4
|
+
*/
|
|
5
|
+
export function getResourceType(path: string): "sprite" | "tile" | "glyph" | "style";
|
|
6
|
+
/**
|
|
7
|
+
* Determine the content type of a file based on its extension.
|
|
8
|
+
*
|
|
9
|
+
* @param {string} path
|
|
10
|
+
*/
|
|
11
|
+
export function getContentType(path: string): "application/json; charset=utf-8" | "application/x-protobuf" | "image/png" | "image/jpeg" | "image/webp" | "application/vnd.mapbox-vector-tile";
|
|
12
|
+
/**
|
|
13
|
+
* Get the filename for a tile, given the TileInfo
|
|
14
|
+
*
|
|
15
|
+
* @param {import("type-fest").SetRequired<import("../writer.js").TileInfo, 'format'>} tileInfo
|
|
16
|
+
* @returns
|
|
17
|
+
*/
|
|
18
|
+
export function getTileFilename({ sourceId, z, x, y, format }: import("type-fest").SetRequired<import("../writer.js").TileInfo, "format">): string;
|
|
19
|
+
/**
|
|
20
|
+
* Get a filename for a sprite file, given the sprite id, pixel ratio and extension
|
|
21
|
+
*
|
|
22
|
+
* @param {{ id: string, pixelRatio: number, ext: '.json' | '.png'}} spriteInfo
|
|
23
|
+
*/
|
|
24
|
+
export function getSpriteFilename({ id, pixelRatio, ext }: {
|
|
25
|
+
id: string;
|
|
26
|
+
pixelRatio: number;
|
|
27
|
+
ext: ".json" | ".png";
|
|
28
|
+
}): string;
|
|
29
|
+
/**
|
|
30
|
+
* Get the filename for a glyph file, given the fontstack and range
|
|
31
|
+
*
|
|
32
|
+
* @param {object} options
|
|
33
|
+
* @param {string} options.fontstack
|
|
34
|
+
* @param {import("../writer.js").GlyphRange} options.range
|
|
35
|
+
*/
|
|
36
|
+
export function getGlyphFilename({ fontstack, range }: {
|
|
37
|
+
fontstack: string;
|
|
38
|
+
range: import("../writer.js").GlyphRange;
|
|
39
|
+
}): string;
|
|
40
|
+
/**
|
|
41
|
+
* Get the URI template for the sprites in the style
|
|
42
|
+
*/
|
|
43
|
+
export function getSpriteUri(id?: string): string;
|
|
44
|
+
/**
|
|
45
|
+
* Get the URI template for tiles in the style
|
|
46
|
+
*
|
|
47
|
+
* @param {object} opts
|
|
48
|
+
* @param {string} opts.sourceId
|
|
49
|
+
* @param {import("../writer.js").TileFormat} opts.format
|
|
50
|
+
* @returns
|
|
51
|
+
*/
|
|
52
|
+
export function getTileUri({ sourceId, format }: {
|
|
53
|
+
sourceId: string;
|
|
54
|
+
format: import("../writer.js").TileFormat;
|
|
55
|
+
}): string;
|
|
56
|
+
/**
|
|
57
|
+
* Replaces variables in a string with values provided in an object. Variables
|
|
58
|
+
* in the string are denoted by curly braces, e.g., {variableName}.
|
|
59
|
+
*
|
|
60
|
+
* @param {string} template - The string containing variables wrapped in curly braces.
|
|
61
|
+
* @param {Record<string, string | number>} variables - An object where the keys correspond to variable names and values correspond to the replacement values.
|
|
62
|
+
* @returns {string} The string with the variables replaced by their corresponding values.
|
|
63
|
+
*/
|
|
64
|
+
export function replaceVariables(template: string, variables: Record<string, string | number>): string;
|
|
65
|
+
export const URI_SCHEME: "smp";
|
|
66
|
+
export const URI_BASE: string;
|
|
67
|
+
export const STYLE_FILE: "style.json";
|
|
68
|
+
export const GLYPH_URI: string;
|
package/dist/writer.d.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/** @typedef {string | Buffer | Uint8Array | import('stream').Readable } Source */
|
|
2
|
+
/** @typedef {string | Buffer | import('stream').Readable} SourceInternal */
|
|
3
|
+
/** @typedef {`${number}-${number}`} GlyphRange */
|
|
4
|
+
/** @typedef {'png' | 'mvt' | 'jpg' | 'webp'} TileFormat */
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {object} SourceInfo
|
|
7
|
+
* @property {import('./types.js').SMPSource} source
|
|
8
|
+
* @property {string} encodedSourceId
|
|
9
|
+
* @property {TileFormat} [format]
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {object} TileInfo
|
|
13
|
+
* @property {number} z
|
|
14
|
+
* @property {number} x
|
|
15
|
+
* @property {number} y
|
|
16
|
+
* @property {string} sourceId
|
|
17
|
+
* @property {TileFormat} [format]
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {object} GlyphInfo
|
|
21
|
+
* @property {string} font
|
|
22
|
+
* @property {GlyphRange} range
|
|
23
|
+
*/
|
|
24
|
+
/** @import { StyleSpecification } from '@maplibre/maplibre-gl-style-spec' */
|
|
25
|
+
/** @import { InputSource, SMPSource } from './types.js' */
|
|
26
|
+
export const SUPPORTED_SOURCE_TYPES: readonly ["raster", "vector", "geojson"];
|
|
27
|
+
/**
|
|
28
|
+
* Write a styled map package to a stream. Stream `writer.outputStream` to a
|
|
29
|
+
* destination, e.g. `fs.createWriteStream('my-map.styledmap')`. You must call
|
|
30
|
+
* `witer.finish()` and then wait for your writable stream to `finish` before
|
|
31
|
+
* using the output.
|
|
32
|
+
*/
|
|
33
|
+
export default class Writer extends EventEmitter<[never]> {
|
|
34
|
+
static SUPPORTED_SOURCE_TYPES: readonly ["raster", "vector", "geojson"];
|
|
35
|
+
/**
|
|
36
|
+
* @param {any} style A v7 or v8 MapLibre style. v7 styles will be migrated to
|
|
37
|
+
* v8. (There are currently no typescript declarations for v7 styles, hence
|
|
38
|
+
* this is typed as `any` and validated internally)
|
|
39
|
+
* @param {object} opts
|
|
40
|
+
* @param {number} [opts.highWaterMark=1048576] The maximum number of bytes to buffer during write
|
|
41
|
+
*/
|
|
42
|
+
constructor(style: any, { highWaterMark }?: {
|
|
43
|
+
highWaterMark?: number | undefined;
|
|
44
|
+
});
|
|
45
|
+
/**
|
|
46
|
+
* @returns {import('stream').Readable} Readable stream of the styled map package
|
|
47
|
+
*/
|
|
48
|
+
get outputStream(): Readable;
|
|
49
|
+
/**
|
|
50
|
+
* Add a tile to the styled map package
|
|
51
|
+
*
|
|
52
|
+
* @param {Source} tileData
|
|
53
|
+
* @param {TileInfo} opts
|
|
54
|
+
*/
|
|
55
|
+
addTile(tileData: Source, { z, x, y, sourceId, format }: TileInfo): Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* Create a write stream for adding tiles to the styled map package
|
|
58
|
+
*
|
|
59
|
+
* @param {object} opts
|
|
60
|
+
* @param {number} [opts.concurrency=16] The number of concurrent writes
|
|
61
|
+
*
|
|
62
|
+
* @returns
|
|
63
|
+
*/
|
|
64
|
+
createTileWriteStream({ concurrency }?: {
|
|
65
|
+
concurrency?: number | undefined;
|
|
66
|
+
}): import("readable-stream").Writable;
|
|
67
|
+
/**
|
|
68
|
+
* Add a sprite to the styled map package
|
|
69
|
+
*
|
|
70
|
+
* @param {object} options
|
|
71
|
+
* @param {Source} options.json
|
|
72
|
+
* @param {Source} options.png
|
|
73
|
+
* @param {number} [options.pixelRatio]
|
|
74
|
+
* @param {string} [options.id='default']
|
|
75
|
+
* @returns {Promise<void>}
|
|
76
|
+
*/
|
|
77
|
+
addSprite({ json, png, pixelRatio, id }: {
|
|
78
|
+
json: Source;
|
|
79
|
+
png: Source;
|
|
80
|
+
pixelRatio?: number | undefined;
|
|
81
|
+
id?: string | undefined;
|
|
82
|
+
}): Promise<void>;
|
|
83
|
+
/**
|
|
84
|
+
* Add glyphs to the styled map package
|
|
85
|
+
*
|
|
86
|
+
* @param {Source} glyphData
|
|
87
|
+
* @param {GlyphInfo} glyphInfo
|
|
88
|
+
* @returns {Promise<void>}
|
|
89
|
+
*/
|
|
90
|
+
addGlyphs(glyphData: Source, { font: fontName, range }: GlyphInfo): Promise<void>;
|
|
91
|
+
/**
|
|
92
|
+
* Create a write stream for adding glyphs to the styled map package
|
|
93
|
+
*
|
|
94
|
+
* @param {object} opts
|
|
95
|
+
* @param {number} [opts.concurrency=16] The number of concurrent writes
|
|
96
|
+
* @returns
|
|
97
|
+
*/
|
|
98
|
+
createGlyphWriteStream({ concurrency }?: {
|
|
99
|
+
concurrency?: number | undefined;
|
|
100
|
+
}): import("readable-stream").Writable;
|
|
101
|
+
/**
|
|
102
|
+
* Finalize the styled map package and write the style to the archive.
|
|
103
|
+
* This method must be called to complete the archive.
|
|
104
|
+
* You must wait for your destination write stream to 'finish' before using the output.
|
|
105
|
+
*/
|
|
106
|
+
finish(): void;
|
|
107
|
+
#private;
|
|
108
|
+
}
|
|
109
|
+
export type Source = string | Buffer | Uint8Array | import("stream").Readable;
|
|
110
|
+
export type SourceInternal = string | Buffer | import("stream").Readable;
|
|
111
|
+
export type GlyphRange = `${number}-${number}`;
|
|
112
|
+
export type TileFormat = "png" | "mvt" | "jpg" | "webp";
|
|
113
|
+
export type SourceInfo = {
|
|
114
|
+
source: import("./types.js").SMPSource;
|
|
115
|
+
encodedSourceId: string;
|
|
116
|
+
format?: TileFormat | undefined;
|
|
117
|
+
};
|
|
118
|
+
export type TileInfo = {
|
|
119
|
+
z: number;
|
|
120
|
+
x: number;
|
|
121
|
+
y: number;
|
|
122
|
+
sourceId: string;
|
|
123
|
+
format?: TileFormat | undefined;
|
|
124
|
+
};
|
|
125
|
+
export type GlyphInfo = {
|
|
126
|
+
font: string;
|
|
127
|
+
range: GlyphRange;
|
|
128
|
+
};
|
|
129
|
+
import { EventEmitter } from 'events';
|
|
130
|
+
import { Readable } from 'stream';
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
/** @typedef {import('./types.js').SMPSource} SMPSource */
|
|
2
|
+
/** @typedef {import('./types.js').SMPStyle} SMPStyle */
|
|
3
|
+
|
|
1
4
|
export { default as Reader } from './reader.js'
|
|
5
|
+
export { default as ReaderWatch } from './reader-watch.js'
|
|
2
6
|
export { default as Writer } from './writer.js'
|
|
3
7
|
export { default as Server } from './server.js'
|
|
4
8
|
export { default as StyleDownloader } from './style-downloader.js'
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { once } from 'events'
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import fsPromises from 'node:fs/promises'
|
|
5
|
+
|
|
6
|
+
import Reader from './reader.js'
|
|
7
|
+
import { ENOENT, isFileNotThereError } from './utils/errors.js'
|
|
8
|
+
import { noop } from './utils/misc.js'
|
|
9
|
+
|
|
10
|
+
/** @implements {Pick<Reader, keyof Reader>} */
|
|
11
|
+
export default class ReaderWatch {
|
|
12
|
+
/** @type {Reader | undefined} */
|
|
13
|
+
#reader
|
|
14
|
+
/** @type {Reader | undefined} */
|
|
15
|
+
#maybeReader
|
|
16
|
+
/** @type {Promise<Reader> | undefined} */
|
|
17
|
+
#readerOpeningPromise
|
|
18
|
+
#filepath
|
|
19
|
+
/** @type {fs.FSWatcher | undefined} */
|
|
20
|
+
#watch
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {string} filepath
|
|
24
|
+
*/
|
|
25
|
+
constructor(filepath) {
|
|
26
|
+
this.#filepath = filepath
|
|
27
|
+
// Call this now to catch any synchronous errors
|
|
28
|
+
this.#tryToWatchFile()
|
|
29
|
+
// eagerly open Reader
|
|
30
|
+
this.#get().catch(noop)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
#tryToWatchFile() {
|
|
34
|
+
if (this.#watch) return
|
|
35
|
+
try {
|
|
36
|
+
this.#watch = fs
|
|
37
|
+
.watch(this.#filepath, { persistent: false }, () => {
|
|
38
|
+
this.#reader?.close().catch(noop)
|
|
39
|
+
this.#reader = undefined
|
|
40
|
+
this.#maybeReader = undefined
|
|
41
|
+
this.#readerOpeningPromise = undefined
|
|
42
|
+
// Close the watcher (which on some platforms will continue watching
|
|
43
|
+
// the previous file) so on the next request we will start watching
|
|
44
|
+
// the new file
|
|
45
|
+
this.#watch?.close()
|
|
46
|
+
this.#watch = undefined
|
|
47
|
+
})
|
|
48
|
+
.on('error', noop)
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (isFileNotThereError(error)) {
|
|
51
|
+
// Ignore: File does not exist yet, but we'll try to open it later
|
|
52
|
+
} else {
|
|
53
|
+
throw error
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async #get() {
|
|
59
|
+
if (isWin() && (this.#reader || this.#readerOpeningPromise)) {
|
|
60
|
+
// On Windows, the file watcher does not recognize file deletions, so we
|
|
61
|
+
// need to check if the file still exists each time
|
|
62
|
+
try {
|
|
63
|
+
await fsPromises.stat(this.#filepath)
|
|
64
|
+
} catch {
|
|
65
|
+
this.#watch?.close()
|
|
66
|
+
this.#watch = undefined
|
|
67
|
+
this.#reader?.close().catch(noop)
|
|
68
|
+
this.#reader = undefined
|
|
69
|
+
this.#maybeReader = undefined
|
|
70
|
+
this.#readerOpeningPromise = undefined
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Need to retry this each time in case it failed initially because the file
|
|
74
|
+
// was not present, or if the file was moved or deleted.
|
|
75
|
+
this.#tryToWatchFile()
|
|
76
|
+
// A lovely promise tangle to confuse future readers... sorry.
|
|
77
|
+
//
|
|
78
|
+
// 1. If the reader is already open, return it.
|
|
79
|
+
// 2. If the reader is in the process of opening, return a promise that will
|
|
80
|
+
// return the reader instance if it opened without error, or throw.
|
|
81
|
+
// 3. If the reader threw an error during opening, try to open it again next
|
|
82
|
+
// time this is called.
|
|
83
|
+
if (this.#reader) return this.#reader
|
|
84
|
+
if (this.#readerOpeningPromise) return this.#readerOpeningPromise
|
|
85
|
+
this.#maybeReader = new Reader(this.#filepath)
|
|
86
|
+
this.#readerOpeningPromise = this.#maybeReader
|
|
87
|
+
.opened()
|
|
88
|
+
.then(() => {
|
|
89
|
+
if (!this.#maybeReader) {
|
|
90
|
+
throw new ENOENT(this.#filepath)
|
|
91
|
+
}
|
|
92
|
+
this.#reader = this.#maybeReader
|
|
93
|
+
return this.#reader
|
|
94
|
+
})
|
|
95
|
+
.finally(() => {
|
|
96
|
+
this.#maybeReader = undefined
|
|
97
|
+
this.#readerOpeningPromise = undefined
|
|
98
|
+
})
|
|
99
|
+
return this.#readerOpeningPromise
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** @type {Reader['opened']} */
|
|
103
|
+
async opened() {
|
|
104
|
+
const reader = await this.#get()
|
|
105
|
+
return reader.opened()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** @type {Reader['getStyle']} */
|
|
109
|
+
async getStyle(baseUrl = null) {
|
|
110
|
+
const reader = await this.#get()
|
|
111
|
+
return reader.getStyle(baseUrl)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** @type {Reader['getResource']} */
|
|
115
|
+
async getResource(path) {
|
|
116
|
+
const reader = await this.#get()
|
|
117
|
+
return reader.getResource(path)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async close() {
|
|
121
|
+
const reader = await this.#get()
|
|
122
|
+
if (this.#watch) {
|
|
123
|
+
this.#watch.close()
|
|
124
|
+
await once(this.#watch, 'close')
|
|
125
|
+
}
|
|
126
|
+
await reader.close()
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** @returns {boolean} */
|
|
131
|
+
function isWin() {
|
|
132
|
+
return process.platform === 'win32'
|
|
133
|
+
}
|
package/lib/reader.js
CHANGED
|
@@ -3,6 +3,8 @@ import { open } from 'yauzl-promise'
|
|
|
3
3
|
|
|
4
4
|
import { json } from 'node:stream/consumers'
|
|
5
5
|
|
|
6
|
+
import { ENOENT } from './utils/errors.js'
|
|
7
|
+
import { noop } from './utils/misc.js'
|
|
6
8
|
import { validateStyle } from './utils/style.js'
|
|
7
9
|
import {
|
|
8
10
|
getContentType,
|
|
@@ -39,6 +41,7 @@ export default class Reader {
|
|
|
39
41
|
typeof filepathOrZip === 'string'
|
|
40
42
|
? open(filepathOrZip)
|
|
41
43
|
: Promise.resolve(filepathOrZip))
|
|
44
|
+
zipPromise.catch(noop)
|
|
42
45
|
this.#entriesPromise = (async () => {
|
|
43
46
|
/** @type {Map<string, import('yauzl-promise').Entry>} */
|
|
44
47
|
const entries = new Map()
|
|
@@ -51,6 +54,15 @@ export default class Reader {
|
|
|
51
54
|
}
|
|
52
55
|
return entries
|
|
53
56
|
})()
|
|
57
|
+
this.#entriesPromise.catch(noop)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Resolves when the styled map package has been opened and the entries have
|
|
62
|
+
* been read. Throws any error that occurred during opening.
|
|
63
|
+
*/
|
|
64
|
+
async opened() {
|
|
65
|
+
await this.#entriesPromise
|
|
54
66
|
}
|
|
55
67
|
|
|
56
68
|
/**
|
|
@@ -62,7 +74,7 @@ export default class Reader {
|
|
|
62
74
|
*/
|
|
63
75
|
async getStyle(baseUrl = null) {
|
|
64
76
|
const styleEntry = (await this.#entriesPromise).get(STYLE_FILE)
|
|
65
|
-
if (!styleEntry) throw new
|
|
77
|
+
if (!styleEntry) throw new ENOENT(STYLE_FILE)
|
|
66
78
|
const stream = await styleEntry.openReadStream()
|
|
67
79
|
const style = await json(stream)
|
|
68
80
|
if (!validateStyle(style)) {
|
|
@@ -107,7 +119,7 @@ export default class Reader {
|
|
|
107
119
|
}
|
|
108
120
|
}
|
|
109
121
|
const entry = (await this.#entriesPromise).get(path)
|
|
110
|
-
if (!entry) throw new
|
|
122
|
+
if (!entry) throw new ENOENT(path)
|
|
111
123
|
const resourceType = getResourceType(path)
|
|
112
124
|
const contentType = getContentType(path)
|
|
113
125
|
const stream = await entry.openReadStream()
|
|
@@ -146,5 +158,8 @@ function getUrl(smpUri, baseUrl) {
|
|
|
146
158
|
throw new Error(`Invalid SMP URI: ${smpUri}`)
|
|
147
159
|
}
|
|
148
160
|
if (typeof baseUrl !== 'string') return smpUri
|
|
149
|
-
|
|
161
|
+
if (!baseUrl.endsWith('/')) {
|
|
162
|
+
baseUrl += '/'
|
|
163
|
+
}
|
|
164
|
+
return smpUri.replace(URI_BASE, baseUrl)
|
|
150
165
|
}
|
package/lib/server.js
CHANGED
|
@@ -1,13 +1,24 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* @property {string} filepath
|
|
4
|
-
* @property {boolean} [lazy=false]
|
|
5
|
-
*/
|
|
1
|
+
import createError from 'http-errors'
|
|
2
|
+
|
|
6
3
|
import Reader from './reader.js'
|
|
4
|
+
import { isFileNotThereError } from './utils/errors.js'
|
|
5
|
+
import { noop } from './utils/misc.js'
|
|
7
6
|
|
|
8
7
|
/** @import { FastifyPluginCallback, FastifyReply } from 'fastify' */
|
|
9
8
|
/** @import { Resource } from './reader.js' */
|
|
10
9
|
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {object} PluginOptionsFilepath
|
|
12
|
+
* @property {string} filepath Path to styled map package (`.smp`) file
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {object} PluginOptionsReader
|
|
16
|
+
* @property {Pick<Reader, keyof Reader>} reader SMP Reader interface (also supports ReaderWatch)
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {PluginOptionsFilepath | PluginOptionsReader} PluginOptions
|
|
20
|
+
*/
|
|
21
|
+
|
|
11
22
|
/**
|
|
12
23
|
* @param {FastifyReply} reply
|
|
13
24
|
* @param {Resource} resource
|
|
@@ -24,41 +35,47 @@ function sendResource(reply, resource) {
|
|
|
24
35
|
}
|
|
25
36
|
|
|
26
37
|
/**
|
|
27
|
-
* Fastify plugin for serving a styled map package.
|
|
28
|
-
*
|
|
38
|
+
* Fastify plugin for serving a styled map package.
|
|
39
|
+
*
|
|
40
|
+
* If you provide a `Reader` (or `ReaderWatch`) instance via the `reader` opt,
|
|
41
|
+
* you must manually close the instance yourself.
|
|
29
42
|
*
|
|
30
43
|
* @type {FastifyPluginCallback<PluginOptions>}
|
|
31
44
|
*/
|
|
32
|
-
export default function (fastify,
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if
|
|
36
|
-
|
|
45
|
+
export default function (fastify, opts, done) {
|
|
46
|
+
const reader = 'reader' in opts ? opts.reader : new Reader(opts.filepath)
|
|
47
|
+
|
|
48
|
+
// Only close the reader if it was created by this plugin
|
|
49
|
+
if (!('reader' in opts)) {
|
|
50
|
+
fastify.addHook('onClose', () => reader.close().catch(noop))
|
|
37
51
|
}
|
|
38
52
|
|
|
39
53
|
fastify.get('/style.json', async () => {
|
|
40
|
-
|
|
41
|
-
|
|
54
|
+
try {
|
|
55
|
+
const baseUrl = new URL(fastify.prefix, fastify.listeningOrigin)
|
|
56
|
+
const style = await reader.getStyle(baseUrl.href)
|
|
57
|
+
return style
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (isFileNotThereError(error)) {
|
|
60
|
+
throw createError(404, error.message)
|
|
61
|
+
}
|
|
62
|
+
throw error
|
|
42
63
|
}
|
|
43
|
-
return reader.getStyle(fastify.listeningOrigin)
|
|
44
64
|
})
|
|
45
65
|
|
|
46
66
|
fastify.get('*', async (request, reply) => {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
67
|
+
// @ts-expect-error - not worth the hassle of type casting this
|
|
68
|
+
const path = request.params['*']
|
|
50
69
|
|
|
51
|
-
/** @type {Resource} */
|
|
52
|
-
let resource
|
|
53
70
|
try {
|
|
54
|
-
resource = await reader.getResource(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
71
|
+
const resource = await reader.getResource(path)
|
|
72
|
+
return sendResource(reply, resource)
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (isFileNotThereError(error)) {
|
|
75
|
+
throw createError(404, error.message)
|
|
76
|
+
}
|
|
77
|
+
throw error
|
|
59
78
|
}
|
|
60
|
-
|
|
61
|
-
return sendResource(reply, resource)
|
|
62
79
|
})
|
|
63
80
|
done()
|
|
64
81
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export class ENOENT extends Error {
|
|
2
|
+
code = 'ENOENT'
|
|
3
|
+
/** @param {string} path */
|
|
4
|
+
constructor(path) {
|
|
5
|
+
const message = `ENOENT: no such file or directory, open '${path}'`
|
|
6
|
+
super(message)
|
|
7
|
+
this.path = path
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Returns true if the error if because a file is not found. On Windows, some
|
|
13
|
+
* operations like fs.watch() throw an EPERM error rather than ENOENT.
|
|
14
|
+
*
|
|
15
|
+
* @param {unknown} error
|
|
16
|
+
* @returns {error is Error & { code: 'ENOENT' | 'EPERM' }}
|
|
17
|
+
*/
|
|
18
|
+
export function isFileNotThereError(error) {
|
|
19
|
+
return (
|
|
20
|
+
error instanceof Error &&
|
|
21
|
+
'code' in error &&
|
|
22
|
+
(error.code === 'ENOENT' || error.code === 'EPERM')
|
|
23
|
+
)
|
|
24
|
+
}
|
package/lib/utils/streams.js
CHANGED
|
@@ -123,18 +123,20 @@ export function isWebReadableStream(obj) {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
/** @typedef {(opts: { totalBytes: number, chunkBytes: number }) => void} ProgressCallback */
|
|
126
|
+
/** @typedef {TransformOptions & { onprogress?: ProgressCallback }} ProgressStreamOptions */
|
|
126
127
|
|
|
127
128
|
/**
|
|
128
129
|
* Passthrough stream that counts the bytes passing through it. Pass an optional
|
|
129
130
|
* `onprogress` callback that will be called with the accumulated total byte
|
|
130
131
|
* count and the chunk byte count after each chunk.
|
|
132
|
+
* @extends {Transform}
|
|
131
133
|
*/
|
|
132
134
|
export class ProgressStream extends Transform {
|
|
133
135
|
#onprogress
|
|
134
136
|
#byteLength = 0
|
|
135
137
|
|
|
136
138
|
/**
|
|
137
|
-
* @param {
|
|
139
|
+
* @param {ProgressStreamOptions} [opts]
|
|
138
140
|
*/
|
|
139
141
|
constructor({ onprogress, ...opts } = {}) {
|
|
140
142
|
super(opts)
|
|
@@ -147,6 +149,7 @@ export class ProgressStream extends Transform {
|
|
|
147
149
|
}
|
|
148
150
|
|
|
149
151
|
/**
|
|
152
|
+
* @override
|
|
150
153
|
* @param {Buffer | Uint8Array} chunk
|
|
151
154
|
* @param {Parameters<Transform['_transform']>[1]} encoding
|
|
152
155
|
* @param {Parameters<Transform['_transform']>[2]} callback
|