@tileflow/dev 0.1.0-alpha.7 → 0.1.0-alpha.8

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 CHANGED
@@ -2,12 +2,23 @@
2
2
 
3
3
  Node utilities used by the Tileflow CLI and build integrations.
4
4
 
5
+ Most apps should use `@tileflow/cli` or `@tileflow/vite` directly. Use this package when you are
6
+ building a custom dev server, bundler plugin, or deployment adapter that needs to load and compile a
7
+ `tileflow.config.ts`.
8
+
5
9
  ```ts
6
- import { createTileflowBuildArtifacts } from "@tileflow/dev";
10
+ import {createTileflowBuildArtifacts} from '@tileflow/dev';
7
11
 
8
12
  const artifacts = await createTileflowBuildArtifacts({
9
- config: "tileflow.config.ts",
13
+ config: 'tileflow.config.ts',
10
14
  });
11
15
  ```
12
16
 
17
+ If a map uses `icons: './icons/brand'`, build artifacts include the generated
18
+ MapLibre `sprite.json`, `sprite.png`, `sprite@2x.json`, and `sprite@2x.png`
19
+ assets alongside the styles.
20
+
21
+ This package is public, but it is an integration layer. Map styling primitives live in
22
+ `@tileflow/core`; React rendering lives in `@tileflow/react`.
23
+
13
24
  Docs: https://tileflow.dev/docs
package/dist/index.d.ts CHANGED
@@ -1,5 +1,11 @@
1
1
  import { TileflowManifest, MapLibreStyle, ValidationMessage, TileflowProjectConfig, TileflowStyleOptions } from '@tileflow/core';
2
2
 
3
+ type TileflowBuildAsset = {
4
+ contentType: string;
5
+ fileName: string;
6
+ source: string | Uint8Array;
7
+ };
8
+
3
9
  declare const defaultTileflowApiUrl = "https://api.tileflow.dev";
4
10
  declare const defaultTileflowConfigPath = "tileflow.config.ts";
5
11
  declare const defaultTileflowManifestPath = "public/tileflow/manifest.json";
@@ -21,14 +27,17 @@ type TileflowDevRequestHandlerOptions = {
21
27
  tileBaseUrl?: string;
22
28
  };
23
29
  type TileflowBuildArtifactsOptions = {
30
+ assetBaseUrl?: string;
24
31
  config?: string;
25
32
  cwd?: string;
26
33
  styleBaseUrl?: string;
27
34
  tileBaseUrl?: string;
28
35
  };
29
36
  type TileflowBuildArtifacts = {
37
+ assets: TileflowBuildAsset[];
30
38
  manifest: TileflowManifest;
31
39
  styles: Record<string, MapLibreStyle>;
40
+ watchPaths: string[];
32
41
  };
33
42
  type WriteTileflowBuildArtifactsOptions = TileflowBuildArtifactsOptions & {
34
43
  outDir: string;
@@ -47,8 +56,9 @@ declare function getTileflowStyleMapName(path: string): string;
47
56
  declare function createTileflowManifest(project: TileflowProjectConfig, options?: TileflowManifestOptions): TileflowManifest;
48
57
  declare function createTileflowStyle(project: TileflowProjectConfig, mapName: string, options?: TileflowStyleOptions): MapLibreStyle;
49
58
  declare function createTileflowBuildArtifacts(options?: TileflowBuildArtifactsOptions): Promise<TileflowBuildArtifacts>;
59
+ declare function getTileflowWatchPaths(options?: Pick<TileflowBuildArtifactsOptions, 'config' | 'cwd'>): Promise<string[]>;
50
60
  declare function writeTileflowBuildArtifacts(options: WriteTileflowBuildArtifactsOptions): Promise<TileflowBuildArtifacts>;
51
61
  declare function createTileflowDevRequestHandler(options?: TileflowDevRequestHandlerOptions): (request: Request) => Promise<Response>;
52
62
  declare function normalizeTileflowBasePath(value?: string): string;
53
63
 
54
- export { type LoadTileflowConfigOptions, type TileflowBuildArtifacts, type TileflowBuildArtifactsOptions, type TileflowDevRequestHandlerOptions, type TileflowManifestOptions, TileflowValidationError, type WriteTileflowBuildArtifactsOptions, asTileflowProjectConfig, assertValidTileflowConfig, createTileflowBuildArtifacts, createTileflowDevRequestHandler, createTileflowManifest, createTileflowStyle, defaultTileflowApiUrl, defaultTileflowConfigPath, defaultTileflowManifestPath, defaultTileflowTileset, getFirstTileflowMapName, getTileflowMapNames, getTileflowStyleMapName, loadTileflowConfig, loadValidTileflowConfig, normalizeTileflowBasePath, writeTileflowBuildArtifacts };
64
+ export { type LoadTileflowConfigOptions, type TileflowBuildArtifacts, type TileflowBuildArtifactsOptions, type TileflowDevRequestHandlerOptions, type TileflowManifestOptions, TileflowValidationError, type WriteTileflowBuildArtifactsOptions, asTileflowProjectConfig, assertValidTileflowConfig, createTileflowBuildArtifacts, createTileflowDevRequestHandler, createTileflowManifest, createTileflowStyle, defaultTileflowApiUrl, defaultTileflowConfigPath, defaultTileflowManifestPath, defaultTileflowTileset, getFirstTileflowMapName, getTileflowMapNames, getTileflowStyleMapName, getTileflowWatchPaths, loadTileflowConfig, loadValidTileflowConfig, normalizeTileflowBasePath, writeTileflowBuildArtifacts };
package/dist/index.js CHANGED
@@ -1,13 +1,196 @@
1
1
  // src/index.ts
2
+ import { createJiti } from "jiti";
3
+ import { existsSync as existsSync2 } from "fs";
4
+ import { mkdir, writeFile } from "fs/promises";
5
+ import { dirname, resolve as resolve2 } from "path";
2
6
  import {
3
7
  createManifest,
4
8
  createStyleFromProject,
5
9
  validateConfig
6
10
  } from "@tileflow/core";
11
+
12
+ // src/icons.ts
7
13
  import { existsSync } from "fs";
8
- import { mkdir, writeFile } from "fs/promises";
9
- import { resolve } from "path";
10
- import { createJiti } from "jiti";
14
+ import { readdir } from "fs/promises";
15
+ import { extname, resolve } from "path";
16
+ var iconFileExtensions = /* @__PURE__ */ new Set([".svg", ".png", ".jpg", ".jpeg", ".webp"]);
17
+ var iconSpriteSize = 24;
18
+ async function prepareTileflowProjectIcons(project, options) {
19
+ const nextProject = cloneProject(project);
20
+ const localSources = getLocalIconSources(project, options.cwd);
21
+ for (const localSource of localSources) {
22
+ const mapConfig = nextProject.maps[localSource.mapName];
23
+ if (!mapConfig) {
24
+ continue;
25
+ }
26
+ const iconSet = resolveIconSet(mapConfig.icons, nextProject.icons);
27
+ if (!iconSet) {
28
+ continue;
29
+ }
30
+ const spriteUrl = joinUrl(options.assetBaseUrl, `icons/${localSource.mapName}/sprite`);
31
+ mapConfig.icons = {
32
+ ...iconSet,
33
+ sprite: spriteUrl
34
+ };
35
+ }
36
+ if (localSources.length === 0) {
37
+ return {
38
+ assets: [],
39
+ project: nextProject,
40
+ watchPaths: []
41
+ };
42
+ }
43
+ const assets = (await Promise.all(
44
+ localSources.map(async (source) => buildSpriteAssets(source.sourceDir, source.mapName))
45
+ )).flat();
46
+ return {
47
+ assets,
48
+ project: nextProject,
49
+ watchPaths: localSources.map((source) => source.sourceDir)
50
+ };
51
+ }
52
+ function getTileflowIconWatchPaths(project, cwd) {
53
+ return uniqueStrings(getLocalIconSources(project, cwd).map((source) => source.sourceDir));
54
+ }
55
+ function getLocalIconSources(project, cwd) {
56
+ return Object.entries(project.maps).flatMap(([mapName, mapConfig]) => {
57
+ const iconSet = resolveIconSet(mapConfig.icons, project.icons);
58
+ const sourceDir = resolveLocalIconSource(iconSet, cwd);
59
+ return sourceDir ? [{ mapName, sourceDir }] : [];
60
+ });
61
+ }
62
+ function resolveLocalIconSource(iconSet, cwd) {
63
+ if (!iconSet?.source) {
64
+ return null;
65
+ }
66
+ return resolve(cwd, iconSet.source);
67
+ }
68
+ function resolveIconSet(icons, iconSets, path = []) {
69
+ if (!icons) {
70
+ return void 0;
71
+ }
72
+ if (typeof icons === "string") {
73
+ const referenced = iconSets?.[icons];
74
+ if (referenced) {
75
+ if (path.includes(icons)) {
76
+ throw new Error(`Circular Tileflow icon set extends: ${[...path, icons].join(" -> ")}`);
77
+ }
78
+ return resolveIconSet(referenced, iconSets, [...path, icons]);
79
+ }
80
+ return isRemoteSpriteReference(icons) ? { sprite: icons } : { source: icons };
81
+ }
82
+ const extended = icons.extends ? resolveIconSet(icons.extends, iconSets, path) : void 0;
83
+ return {
84
+ ...extended,
85
+ ...icons,
86
+ mapping: {
87
+ ...extended?.mapping,
88
+ ...icons.mapping
89
+ }
90
+ };
91
+ }
92
+ async function buildSpriteAssets(sourceDir, mapName) {
93
+ const icons = await readIconFiles(sourceDir);
94
+ if (icons.length === 0) {
95
+ throw new Error(`No SVG or PNG icons found in ${sourceDir}.`);
96
+ }
97
+ const [sprite, sprite2x] = await Promise.all([
98
+ buildSprite(sourceDir, icons, 1),
99
+ buildSprite(sourceDir, icons, 2)
100
+ ]);
101
+ return [
102
+ jsonAsset(`icons/${mapName}/sprite.json`, sprite.index),
103
+ binaryAsset(`icons/${mapName}/sprite.png`, sprite.image, "image/png"),
104
+ jsonAsset(`icons/${mapName}/sprite@2x.json`, sprite2x.index),
105
+ binaryAsset(`icons/${mapName}/sprite@2x.png`, sprite2x.image, "image/png")
106
+ ];
107
+ }
108
+ async function readIconFiles(sourceDir) {
109
+ if (!existsSync(sourceDir)) {
110
+ throw new Error(`Icon source not found: ${sourceDir}`);
111
+ }
112
+ const entries = await readdir(sourceDir, { withFileTypes: true });
113
+ return entries.filter((entry) => entry.isFile() && iconFileExtensions.has(extname(entry.name).toLowerCase())).map((entry) => ({
114
+ name: entry.name.slice(0, -extname(entry.name).length),
115
+ path: resolve(sourceDir, entry.name)
116
+ })).sort((left, right) => left.name.localeCompare(right.name));
117
+ }
118
+ async function buildSprite(sourceDir, icons, pixelRatio) {
119
+ const sharp = (await import("sharp")).default;
120
+ const cellSize = iconSpriteSize * pixelRatio;
121
+ const columns = Math.ceil(Math.sqrt(icons.length));
122
+ const rows = Math.ceil(icons.length / columns);
123
+ const width = columns * cellSize;
124
+ const height = rows * cellSize;
125
+ const index = {};
126
+ const composite = await Promise.all(
127
+ icons.map(async (icon, indexNumber) => {
128
+ const column = indexNumber % columns;
129
+ const row = Math.floor(indexNumber / columns);
130
+ const left = column * cellSize;
131
+ const top = row * cellSize;
132
+ index[icon.name] = {
133
+ height: cellSize,
134
+ pixelRatio,
135
+ width: cellSize,
136
+ x: left,
137
+ y: top
138
+ };
139
+ return {
140
+ input: await sharp(icon.path, { density: 72 * pixelRatio }).resize(cellSize, cellSize, {
141
+ background: { alpha: 0, b: 0, g: 0, r: 0 },
142
+ fit: "contain"
143
+ }).png().toBuffer(),
144
+ left,
145
+ top
146
+ };
147
+ })
148
+ );
149
+ const image = await sharp({
150
+ create: {
151
+ background: { alpha: 0, b: 0, g: 0, r: 0 },
152
+ channels: 4,
153
+ height,
154
+ width
155
+ }
156
+ }).composite(composite).png().toBuffer();
157
+ return {
158
+ image,
159
+ index,
160
+ sourceDir
161
+ };
162
+ }
163
+ function jsonAsset(fileName, value) {
164
+ return {
165
+ contentType: "application/json; charset=utf-8",
166
+ fileName,
167
+ source: `${JSON.stringify(value, null, 2)}
168
+ `
169
+ };
170
+ }
171
+ function binaryAsset(fileName, source, contentType) {
172
+ return {
173
+ contentType,
174
+ fileName,
175
+ source
176
+ };
177
+ }
178
+ function cloneProject(project) {
179
+ return JSON.parse(JSON.stringify(project));
180
+ }
181
+ function isRemoteSpriteReference(value) {
182
+ return value.startsWith("http://") || value.startsWith("https://") || value.startsWith("/") || value.startsWith("data:");
183
+ }
184
+ function joinUrl(base, path) {
185
+ const trimmedBase = base.replace(/\/+$/g, "");
186
+ const trimmedPath = path.replace(/^\/+/g, "");
187
+ return trimmedBase ? `${trimmedBase}/${trimmedPath}` : trimmedPath;
188
+ }
189
+ function uniqueStrings(values) {
190
+ return [...new Set(values)];
191
+ }
192
+
193
+ // src/index.ts
11
194
  var defaultTileflowApiUrl = "https://api.tileflow.dev";
12
195
  var defaultTileflowConfigPath = "tileflow.config.ts";
13
196
  var defaultTileflowManifestPath = "public/tileflow/manifest.json";
@@ -22,8 +205,8 @@ var TileflowValidationError = class extends Error {
22
205
  };
23
206
  async function loadTileflowConfig(configPath = defaultTileflowConfigPath, options = {}) {
24
207
  const cwd = options.cwd ?? process.cwd();
25
- const resolvedPath = resolve(cwd, configPath);
26
- if (!existsSync(resolvedPath)) {
208
+ const resolvedPath = resolve2(cwd, configPath);
209
+ if (!existsSync2(resolvedPath)) {
27
210
  throw new Error(`Config not found: ${configPath}`);
28
211
  }
29
212
  const jiti = createJiti(import.meta.url, {
@@ -81,44 +264,60 @@ function createTileflowStyle(project, mapName, options = {}) {
81
264
  return createStyleFromProject(project, mapName, options);
82
265
  }
83
266
  async function createTileflowBuildArtifacts(options = {}) {
84
- const project = await loadValidTileflowConfig(
85
- options.config ?? defaultTileflowConfigPath,
86
- {
87
- cwd: options.cwd,
88
- fresh: true
89
- }
90
- );
267
+ const project = await loadValidTileflowConfig(options.config ?? defaultTileflowConfigPath, {
268
+ cwd: options.cwd,
269
+ fresh: true
270
+ });
271
+ const prepared = await prepareTileflowProjectIcons(project, {
272
+ assetBaseUrl: resolveAssetBaseUrl(options),
273
+ cwd: options.cwd ?? process.cwd()
274
+ });
91
275
  const styleOptions = { tileBaseUrl: options.tileBaseUrl };
92
276
  const styles = Object.fromEntries(
93
- getTileflowMapNames(project).map((mapName) => [
277
+ getTileflowMapNames(prepared.project).map((mapName) => [
94
278
  mapName,
95
- createStyleFromProject(project, mapName, styleOptions)
279
+ createStyleFromProject(prepared.project, mapName, styleOptions)
96
280
  ])
97
281
  );
98
282
  return {
99
- manifest: createManifest(project, {
283
+ assets: prepared.assets,
284
+ manifest: createManifest(prepared.project, {
100
285
  styleBaseUrl: options.styleBaseUrl,
101
286
  tileBaseUrl: options.tileBaseUrl
102
287
  }),
103
- styles
288
+ styles,
289
+ watchPaths: prepared.watchPaths
104
290
  };
105
291
  }
292
+ async function getTileflowWatchPaths(options = {}) {
293
+ const cwd = options.cwd ?? process.cwd();
294
+ const project = await loadValidTileflowConfig(options.config ?? defaultTileflowConfigPath, {
295
+ cwd,
296
+ fresh: true
297
+ });
298
+ return getTileflowIconWatchPaths(project, cwd);
299
+ }
106
300
  async function writeTileflowBuildArtifacts(options) {
107
301
  const cwd = options.cwd ?? process.cwd();
108
- const outDir = resolve(cwd, options.outDir);
109
- const stylesDir = resolve(outDir, "styles");
302
+ const outDir = resolve2(cwd, options.outDir);
303
+ const stylesDir = resolve2(outDir, "styles");
110
304
  const artifacts = await createTileflowBuildArtifacts(options);
111
305
  await mkdir(stylesDir, { recursive: true });
112
306
  for (const [mapName, style] of Object.entries(artifacts.styles)) {
113
307
  await writeFile(
114
- resolve(stylesDir, `${mapName}.json`),
308
+ resolve2(stylesDir, `${mapName}.json`),
115
309
  `${JSON.stringify(style, null, 2)}
116
310
  `,
117
311
  "utf8"
118
312
  );
119
313
  }
314
+ for (const asset of artifacts.assets) {
315
+ const outputPath = resolve2(outDir, asset.fileName);
316
+ await mkdir(dirname(outputPath), { recursive: true });
317
+ await writeFile(outputPath, asset.source);
318
+ }
120
319
  await writeFile(
121
- resolve(outDir, "manifest.json"),
320
+ resolve2(outDir, "manifest.json"),
122
321
  `${JSON.stringify(artifacts.manifest, null, 2)}
123
322
  `,
124
323
  "utf8"
@@ -136,6 +335,16 @@ function createTileflowDevRequestHandler(options = {}) {
136
335
  }
137
336
  const url = new URL(request.url);
138
337
  const path = stripTileflowBasePath(url.pathname, basePath);
338
+ const loadPreparedProject = async () => {
339
+ const project = await loadValidTileflowConfig(configPath, {
340
+ cwd,
341
+ fresh: true
342
+ });
343
+ return prepareTileflowProjectIcons(project, {
344
+ assetBaseUrl: `${url.origin}${basePath}`,
345
+ cwd
346
+ });
347
+ };
139
348
  if (path === null) {
140
349
  return jsonResponse({ error: "Not found" }, 404);
141
350
  }
@@ -145,9 +354,7 @@ function createTileflowDevRequestHandler(options = {}) {
145
354
  cwd,
146
355
  fresh: true
147
356
  });
148
- return htmlResponse(
149
- previewHtml(getFirstTileflowMapName(project), basePath)
150
- );
357
+ return htmlResponse(previewHtml(getFirstTileflowMapName(project), basePath));
151
358
  }
152
359
  if (path === "/manifest.json") {
153
360
  const project = await loadValidTileflowConfig(configPath, {
@@ -163,31 +370,34 @@ function createTileflowDevRequestHandler(options = {}) {
163
370
  );
164
371
  }
165
372
  if (path === "/style.json") {
166
- const project = await loadValidTileflowConfig(configPath, {
167
- cwd,
168
- fresh: true
169
- });
373
+ const prepared = await loadPreparedProject();
170
374
  return jsonResponse(
171
- createStyleFromProject(project, getFirstTileflowMapName(project), {
375
+ createStyleFromProject(prepared.project, getFirstTileflowMapName(prepared.project), {
172
376
  tileBaseUrl
173
377
  })
174
378
  );
175
379
  }
176
380
  if (path.startsWith("/styles/")) {
177
- const project = await loadValidTileflowConfig(configPath, {
178
- cwd,
179
- fresh: true
180
- });
381
+ const prepared = await loadPreparedProject();
181
382
  const mapName = getTileflowStyleMapName(path);
182
- if (!project.maps[mapName]) {
383
+ if (!prepared.project.maps[mapName]) {
183
384
  return jsonResponse({ error: `Unknown map: ${mapName}` }, 404);
184
385
  }
185
386
  return jsonResponse(
186
- createStyleFromProject(project, mapName, {
387
+ createStyleFromProject(prepared.project, mapName, {
187
388
  tileBaseUrl
188
389
  })
189
390
  );
190
391
  }
392
+ if (path.startsWith("/icons/")) {
393
+ const prepared = await loadPreparedProject();
394
+ const assetName = path.replace(/^\/+/, "");
395
+ const asset = prepared.assets.find((candidate) => candidate.fileName === assetName);
396
+ if (!asset) {
397
+ return jsonResponse({ error: `Unknown icon asset: ${assetName}` }, 404);
398
+ }
399
+ return assetResponse(asset);
400
+ }
191
401
  return jsonResponse({ error: "Not found" }, 404);
192
402
  } catch (error) {
193
403
  options.onError?.(error);
@@ -245,6 +455,28 @@ function htmlResponse(body, status = 200) {
245
455
  status
246
456
  });
247
457
  }
458
+ function assetResponse(asset) {
459
+ const body = typeof asset.source === "string" ? asset.source : Buffer.from(asset.source);
460
+ return new Response(body, {
461
+ headers: {
462
+ "Access-Control-Allow-Origin": "*",
463
+ "Cache-Control": "no-store",
464
+ "Content-Type": asset.contentType
465
+ }
466
+ });
467
+ }
468
+ function resolveAssetBaseUrl(options) {
469
+ if (options.assetBaseUrl !== void 0) {
470
+ return options.assetBaseUrl;
471
+ }
472
+ if (options.styleBaseUrl && !isRelativePublicUrl(options.styleBaseUrl)) {
473
+ return options.styleBaseUrl;
474
+ }
475
+ return "..";
476
+ }
477
+ function isRelativePublicUrl(value) {
478
+ return !(value.startsWith("http://") || value.startsWith("https://") || value.startsWith("/") || value.startsWith("data:"));
479
+ }
248
480
  function previewHtml(mapName, basePath) {
249
481
  const styleUrl = `${basePath}/styles/${mapName}.json`;
250
482
  return `<!doctype html>
@@ -307,6 +539,7 @@ export {
307
539
  getFirstTileflowMapName,
308
540
  getTileflowMapNames,
309
541
  getTileflowStyleMapName,
542
+ getTileflowWatchPaths,
310
543
  loadTileflowConfig,
311
544
  loadValidTileflowConfig,
312
545
  normalizeTileflowBasePath,
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "@tileflow/dev",
3
- "version": "0.1.0-alpha.7",
3
+ "version": "0.1.0-alpha.8",
4
4
  "description": "Node development utilities for Tileflow configs and builds.",
5
5
  "type": "module",
6
+ "sideEffects": false,
6
7
  "homepage": "https://tileflow.dev",
7
8
  "repository": {
8
9
  "type": "git",
@@ -36,7 +37,8 @@
36
37
  },
37
38
  "dependencies": {
38
39
  "jiti": "^2.7.0",
39
- "@tileflow/core": "0.1.0-alpha.7"
40
+ "sharp": "^0.34.5",
41
+ "@tileflow/core": "0.1.0-alpha.8"
40
42
  },
41
43
  "scripts": {
42
44
  "build": "tsup src/index.ts --format esm --dts --clean --platform node",