tileflow 0.1.0-alpha.13 → 0.1.0-alpha.14

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
@@ -53,7 +53,8 @@ workspaces are advanced repository orchestration, not a second application autho
53
53
 
54
54
  ## Map development tools
55
55
 
56
- Use `describe` for bounded language discovery, `inspect` for configuration and data, `capture`
56
+ Use `describe` for bounded language discovery (`describe themes --json` lists palette requirements
57
+ and optional time-of-day composition), `inspect` for configuration and data, `capture`
57
58
  for reproducible evidence, and `compare` for saved captures, semantic edits, styles, references or
58
59
  baselines. `preview` opens Map DevTools. These tools work in independent projects with
59
60
  custom maps and data; official maps are optional.
package/dist/index.js CHANGED
@@ -3922,7 +3922,7 @@ function isRecord(value) {
3922
3922
 
3923
3923
  // src/describe-command.ts
3924
3924
  function registerDescribeCommand(program2) {
3925
- program2.command("describe").description("Discover domains and query the generated authoring schema without loading a map").argument("[topic]", "domain name, expressions, or schema").option(
3925
+ program2.command("describe").description("Discover domains and query the generated authoring schema without loading a map").argument("[topic]", "domain name, themes, expressions, or schema").option(
3926
3926
  "--ref <pointer>",
3927
3927
  "exact local schema reference, such as #/$defs/TileflowRoadsModuleOptions"
3928
3928
  ).option("--path <pointer>", "JSON Pointer within the selected topic or schema", "").option("--limit <count>", "maximum returned tree entries (1\u2013500)", "80").option("--depth <count>", "maximum returned tree depth (1\u201312)", "4").option("--json", "print deterministic bounded JSON").action(
@@ -3933,7 +3933,7 @@ function registerDescribeCommand(program2) {
3933
3933
  const manifest = tileflowAuthoringManifest2;
3934
3934
  let value;
3935
3935
  let schemaRef;
3936
- if (options.ref || topic && topic !== "expressions") {
3936
+ if (options.ref || topic && topic !== "expressions" && topic !== "themes") {
3937
3937
  const schema = await loadPackagedSchema();
3938
3938
  const domain = manifest.domains.find((entry) => entry.name === topic);
3939
3939
  if (topic && topic !== "schema" && !domain)
@@ -3946,6 +3946,8 @@ function registerDescribeCommand(program2) {
3946
3946
  value = selectJsonPointer(schema, schemaRef.replace(/^#/u, ""));
3947
3947
  } else if (topic === "expressions") {
3948
3948
  value = manifest.expressions;
3949
+ } else if (topic === "themes") {
3950
+ value = manifest.themes;
3949
3951
  } else {
3950
3952
  value = {
3951
3953
  domains: manifest.domains.map(({ name, api, optionsSchemaRef }) => ({
@@ -3953,7 +3955,7 @@ function registerDescribeCommand(program2) {
3953
3955
  api,
3954
3956
  schemaRef: optionsSchemaRef
3955
3957
  })),
3956
- topics: ["expressions", "schema"],
3958
+ topics: ["expressions", "schema", "themes"],
3957
3959
  commands: ["describe", "inspect", "capture", "compare", "preview"]
3958
3960
  };
3959
3961
  }
@@ -4126,11 +4128,14 @@ function printFeatureInspectError(error) {
4126
4128
  }
4127
4129
 
4128
4130
  // src/hosted-client.ts
4129
- import { serializeTileflowFontBundleManifest } from "@tileflow/core";
4131
+ import {
4132
+ serializeTileflowFontBundleManifest,
4133
+ serializeTileflowModelBundleManifest
4134
+ } from "@tileflow/core";
4130
4135
 
4131
4136
  // src/hosted-response.ts
4132
4137
  import { z as z2 } from "zod";
4133
- import { tileflowIconPackageLimits } from "@tileflow/core";
4138
+ import { tileflowIconPackageLimits, tileflowModelBundleLimits } from "@tileflow/core";
4134
4139
  var maximumHostedResponseBytes = 1024 * 1024;
4135
4140
  var safeTextSchema = z2.string().min(1).max(300).refine((value) => !/[\p{Cc}]/u.test(value));
4136
4141
  var safeIdentifierSchema = z2.string().min(1).max(200).regex(/^[A-Za-z0-9][A-Za-z0-9._:-]*$/u);
@@ -4168,6 +4173,14 @@ var hostedFontBundleResponseSchema = z2.object({
4168
4173
  id: z2.string().regex(/^fnb_[A-Za-z0-9_-]{16,64}$/u),
4169
4174
  totalBytes: z2.number().int().positive().max(Number.MAX_SAFE_INTEGER)
4170
4175
  });
4176
+ var hostedModelBundleResponseSchema = z2.object({
4177
+ baseUrl: publicHttpUrlSchema,
4178
+ changed: z2.boolean().optional(),
4179
+ contentHash: z2.string().regex(/^[a-f0-9]{64}$/u),
4180
+ id: z2.string().regex(/^mdb_[A-Za-z0-9_-]{16,64}$/u),
4181
+ modelCount: z2.number().int().positive().max(tileflowModelBundleLimits.maxModelCount),
4182
+ totalBytes: z2.number().int().positive().max(tileflowModelBundleLimits.maxPackageBytes)
4183
+ });
4171
4184
  var hostedStatusStyleSchema = z2.object({
4172
4185
  environment: safeTextSchema,
4173
4186
  key: safeStorageKeySchema,
@@ -4520,6 +4533,42 @@ async function uploadHostedFontBundle(api, bundle, options = {}) {
4520
4533
  }
4521
4534
  return { ok: true, value };
4522
4535
  }
4536
+ async function uploadHostedModelBundle(api, bundle, options = {}) {
4537
+ const formData = new FormData();
4538
+ formData.append(
4539
+ "manifest",
4540
+ new Blob([serializeTileflowModelBundleManifest(bundle.manifest)], {
4541
+ type: "application/json"
4542
+ }),
4543
+ "manifest.json"
4544
+ );
4545
+ for (const file of bundle.files) {
4546
+ const bytes = new Uint8Array(file.source.byteLength);
4547
+ bytes.set(file.source);
4548
+ formData.append(
4549
+ `file:${encodeURIComponent(file.fileName)}`,
4550
+ new Blob([bytes.buffer], { type: file.contentType }),
4551
+ file.fileName
4552
+ );
4553
+ }
4554
+ const response = await requestHostedJson(
4555
+ api.apiUrl,
4556
+ `/v1/model-bundles/${encodeURIComponent(bundle.contentHash)}`,
4557
+ { body: formData, headers: authorizationHeaders(api.apiKey, api.mapId), method: "PUT" },
4558
+ options
4559
+ );
4560
+ if (!response.ok) return { ok: false, status: response.status };
4561
+ const value = parseResponse(
4562
+ response,
4563
+ hostedModelBundleResponseSchema,
4564
+ "Model bundle upload response"
4565
+ );
4566
+ const publicBaseUrl = new URL(value.baseUrl);
4567
+ if (value.contentHash !== bundle.contentHash || value.modelCount !== bundle.manifest.models.length || value.totalBytes !== bundle.manifest.models.reduce((total, model) => total + model.byteLength, 0) || publicBaseUrl.pathname !== `/model-bundles/${value.id}` || publicBaseUrl.search !== "") {
4568
+ throw new Error("Model bundle upload response did not confirm the submitted bundle.");
4569
+ }
4570
+ return { ok: true, value };
4571
+ }
4523
4572
  async function fetchHostedMapStatus(api, options = {}) {
4524
4573
  const response = await requestHostedJson(
4525
4574
  api.apiUrl,
@@ -4773,22 +4822,6 @@ function inspectTileflowHostedCompatibility(project, styles) {
4773
4822
  });
4774
4823
  }
4775
4824
  }
4776
- const own = /* @__PURE__ */ new Set();
4777
- for (const style of Object.values(themes)) {
4778
- const models = style?.metadata?.["tileflow:buildings"]?.models;
4779
- if (!Array.isArray(models)) continue;
4780
- for (const entry of models) {
4781
- const { model, url } = entry ?? {};
4782
- if (typeof url === "string" && url.includes(`models/${mapName}/`) && typeof model === "string")
4783
- own.add(model);
4784
- }
4785
- }
4786
- if (own.size > 0)
4787
- issues.push({
4788
- map: mapName,
4789
- message: `Hosted deploy does not take a map's own 3D building models yet. Map ${mapName} places ${[...own].sort(compareCodeUnits5).join(", ")}; keep the map local or remove them from buildings.threeDimensional.models.`,
4790
- path: `maps.${mapName}.modules.buildings.threeDimensional.models`
4791
- });
4792
4825
  }
4793
4826
  return issues;
4794
4827
  }
@@ -4797,17 +4830,36 @@ function cloneStyle(style) {
4797
4830
  }
4798
4831
 
4799
4832
  // src/model-assets.ts
4800
- import { prepareTileflowMapTreeModels } from "@tileflow/dev/icons";
4833
+ import {
4834
+ createTileflowModelBundle,
4835
+ prepareTileflowMapTreeModels
4836
+ } from "@tileflow/dev/icons";
4801
4837
  async function prepareModelAssets(project, options) {
4802
- const assets = {};
4838
+ const prepared = { assets: {}, bundles: {}, identities: {} };
4803
4839
  for (const [name, map] of Object.entries(project.maps)) {
4804
4840
  if (!map.models?.length) continue;
4805
- const prepared = await prepareTileflowMapTreeModels(map.models, {
4841
+ const models = await prepareTileflowMapTreeModels(map.models, {
4806
4842
  ...options,
4807
4843
  cwd: process.cwd(),
4808
4844
  configPath: `maps.${name}.models`
4809
4845
  });
4810
- assets[name] = { models: prepared.models };
4846
+ prepared.assets[name] = { models: models.models };
4847
+ const bundle = await createTileflowModelBundle(models.buildings);
4848
+ if (bundle) prepared.bundles[name] = bundle;
4849
+ if (models.identities.length > 0) prepared.identities[name] = models.identities;
4850
+ }
4851
+ return prepared;
4852
+ }
4853
+ function mapAssetsWithBuildingModels(prepared, baseOf) {
4854
+ const assets = { ...prepared.assets };
4855
+ for (const [mapName, bundle] of Object.entries(prepared.bundles)) {
4856
+ const base = baseOf(mapName, bundle).replace(/\/+$/u, "");
4857
+ assets[mapName] = {
4858
+ ...assets[mapName],
4859
+ buildingModels: Object.fromEntries(
4860
+ bundle.manifest.models.map((model) => [model.id, `${base}/${model.file}`])
4861
+ )
4862
+ };
4811
4863
  }
4812
4864
  return assets;
4813
4865
  }
@@ -4850,13 +4902,17 @@ async function prepareNativeHostedDeploy(options) {
4850
4902
  const iconPackage = binding ? compiledIcons.packages.find((entry) => entry.contentHash === binding.packageHash) : void 0;
4851
4903
  const sprite = binding ? `${apiOrigin}/sprites/preflight/${binding.packageHash}/sprite` : void 0;
4852
4904
  const modelAssets = await prepareModelAssets(project, { baseDirectory, target: "hosted" });
4905
+ if (modelAssets.bundles[mapName])
4906
+ throw failure(
4907
+ "A deployment with --with-native does not take a map's own 3D building models yet. Deploy without --with-native, or remove them from the map's models directories."
4908
+ );
4853
4909
  const mapAssets = {
4854
- ...modelAssets,
4910
+ ...modelAssets.assets,
4855
4911
  ...Object.fromEntries(
4856
4912
  compiledIcons.bindings.map((entry) => [
4857
4913
  entry.mapName,
4858
4914
  {
4859
- ...modelAssets[entry.mapName],
4915
+ ...modelAssets.assets[entry.mapName],
4860
4916
  icons: {
4861
4917
  ids: entry.iconIds,
4862
4918
  sprite: `${apiOrigin}/sprites/preflight/${entry.packageHash}/sprite`
@@ -9348,7 +9404,10 @@ program.command("validate").description("Validate a Tileflow config").option("-c
9348
9404
  const mapAssets = createCompiledMapAssets(
9349
9405
  compiledIcons,
9350
9406
  (binding) => options.target === "hosted" ? `${normalizeUrl(options.apiBaseUrl)}/sprites/preflight/${binding.packageHash}/sprite` : `/tileflow/icons/${binding.mapName}/sprite`,
9351
- modelAssets
9407
+ mapAssetsWithBuildingModels(
9408
+ modelAssets,
9409
+ (mapName) => options.target === "hosted" ? `${normalizeUrl(options.apiBaseUrl)}/model-bundles/preflight` : `/tileflow/models/${mapName}`
9410
+ )
9352
9411
  );
9353
9412
  failureDefaults = { code: "STYLE_INVALID", phase: "style-validation" };
9354
9413
  const compiledStyles = createTileflowStyles2(project, {
@@ -9824,7 +9883,7 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
9824
9883
  const preflightMapAssets = createCompiledMapAssets(
9825
9884
  compiledIcons,
9826
9885
  (binding) => `${apiUrl}/sprites/preflight/${binding.packageHash}/sprite`,
9827
- modelAssets
9886
+ mapAssetsWithBuildingModels(modelAssets, () => `${apiUrl}/model-bundles/preflight`)
9828
9887
  );
9829
9888
  const compiledPreflightStyles = createTileflowStyles2(deploymentProject, {
9830
9889
  apiBaseUrl: apiUrl,
@@ -9898,6 +9957,25 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
9898
9957
  );
9899
9958
  hostedSpriteByPackageHash.set(iconPackage.contentHash, uploaded.value.spriteUrl);
9900
9959
  }
9960
+ const hostedModelBaseByHash = /* @__PURE__ */ new Map();
9961
+ const uniqueModelBundles = [
9962
+ ...new Map(
9963
+ Object.values(modelAssets.bundles).map((bundle) => [bundle.contentHash, bundle])
9964
+ ).values()
9965
+ ].sort((left, right) => left.contentHash.localeCompare(right.contentHash));
9966
+ for (const bundle of uniqueModelBundles) {
9967
+ const uploaded = await uploadHostedModelBundle(api, bundle);
9968
+ if (!uploaded.ok) {
9969
+ logError(`Model bundle upload failed: ${uploaded.status}.`);
9970
+ process.exitCode = 1;
9971
+ return;
9972
+ }
9973
+ const action = uploaded.value.changed === false ? "Reused" : "Uploaded";
9974
+ logSuccess(
9975
+ `${action} 3D model bundle (${plural(bundle.manifest.models.length, "model")}, ${formatBytes3(uploaded.value.totalBytes)}, ${bundle.contentHash.slice(0, 12)}).`
9976
+ );
9977
+ hostedModelBaseByHash.set(bundle.contentHash, uploaded.value.baseUrl);
9978
+ }
9901
9979
  const hostedMapAssets = createCompiledMapAssets(
9902
9980
  compiledIcons,
9903
9981
  (binding) => {
@@ -9905,7 +9983,11 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
9905
9983
  if (!sprite) throw new Error(`Missing hosted sprite URL for map ${binding.mapName}`);
9906
9984
  return sprite;
9907
9985
  },
9908
- modelAssets
9986
+ mapAssetsWithBuildingModels(modelAssets, (mapName, bundle) => {
9987
+ const base = hostedModelBaseByHash.get(bundle.contentHash);
9988
+ if (!base) throw new Error(`Missing hosted model bundle URL for map ${mapName}.`);
9989
+ return base;
9990
+ })
9909
9991
  );
9910
9992
  const compiledHostedStyles = createTileflowStyles2(deploymentProject, {
9911
9993
  apiBaseUrl: api.apiUrl,
@@ -9996,7 +10078,12 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
9996
10078
  fileName: `icons/${mapName}/${file.fileName}`,
9997
10079
  source: file.source
9998
10080
  })),
9999
- ...hostedFonts.bundles[mapName]?.files ?? []
10081
+ ...hostedFonts.bundles[mapName]?.files ?? [],
10082
+ ...(modelAssets.bundles[mapName]?.files ?? []).map((file) => ({
10083
+ contentType: file.contentType,
10084
+ fileName: `models/${mapName}/${file.fileName}`,
10085
+ source: file.source
10086
+ }))
10000
10087
  ],
10001
10088
  lineage: deploymentProject.mapMetadata?.[mapName]?.lineage ?? [
10002
10089
  { id: map.id, mapVersion: map.version }
@@ -10005,7 +10092,8 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
10005
10092
  sourceAssets: {
10006
10093
  fonts: hostedFonts.sourceIdentities[mapName] ?? [],
10007
10094
  icons: compiledIcons.sourceIdentities[mapName] ?? [],
10008
- ...compiledIcons.compositions[mapName] ? { iconComposition: compiledIcons.compositions[mapName] } : {}
10095
+ ...compiledIcons.compositions[mapName] ? { iconComposition: compiledIcons.compositions[mapName] } : {},
10096
+ ...modelAssets.identities[mapName] ? { models: modelAssets.identities[mapName] } : {}
10009
10097
  },
10010
10098
  styles: styles[mapName]
10011
10099
  }
@@ -10018,11 +10106,13 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
10018
10106
  const iconBinding = bindingsByMap.get(mapName);
10019
10107
  const iconPackage = iconBinding ? packagesByHash.get(iconBinding.packageHash) : void 0;
10020
10108
  const fontBundle = hostedFonts.bundles[mapName];
10109
+ const modelBundle = modelAssets.bundles[mapName];
10021
10110
  return {
10022
10111
  iconBinding,
10023
10112
  iconComposition: compiledIcons.compositions[mapName],
10024
10113
  iconPackage,
10025
10114
  fontBundle,
10115
+ modelBundle,
10026
10116
  mapName,
10027
10117
  teamSources: hostedThemeFamilies[mapName].teamSources,
10028
10118
  buildManifest: {
@@ -10042,6 +10132,7 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
10042
10132
  iconComposition,
10043
10133
  iconPackage,
10044
10134
  mapName,
10135
+ modelBundle,
10045
10136
  teamSources,
10046
10137
  styles: themeStyles
10047
10138
  } = deployment;
@@ -10076,6 +10167,7 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
10076
10167
  } : {},
10077
10168
  ...iconComposition ? { iconComposition } : {},
10078
10169
  ...fontBundle ? { fontBundle: { contentHash: fontBundle.contentHash } } : {},
10170
+ ...modelBundle ? { modelBundle: { contentHash: modelBundle.contentHash } } : {},
10079
10171
  source
10080
10172
  },
10081
10173
  `Deploy response for ${mapName}`
@@ -184,8 +184,19 @@ Hosted deploy currently rejects local or package-owned web fonts before authenti
184
184
  writes. Use self-hosted delivery or an explicit public glyph provider. Enabling managed font bundles
185
185
  requires a separately deployed ownership, quota, retention, and immutable-delivery contract.
186
186
 
187
- For both managed resource kinds, `assetSetSha256` remains a separate per-map output identity. It
187
+ A map's own 3D building models (the building models of its `models` directories) deploy as one
188
+ model bundle per map. Deploy prepares them as `build` does, uploads the bundle with
189
+ `PUT /v1/model-bundles/:contentHash` after authentication and before the first style write, compiles
190
+ each placement's `url` at the bundle's hosted address and publishes the style with the bundle's
191
+ content hash. Byte-identical bundles are reused by content hash. A bundle holds at most 64 models,
192
+ 16 MiB a prepared file and 32 MiB together, and shares the organization's hosted-storage pool like
193
+ icon packages. Upload success must confirm the submitted `contentHash`, model count and byte total,
194
+ one opaque `mdb_<id>` and the query-free `/model-bundles/<id>` base URL. A deploy with
195
+ `--with-native` rejects a map's own building models, because MapLibre Native draws no models.
196
+
197
+ For every managed resource kind, `assetSetSha256` remains a separate per-map output identity. It
188
198
  hashes each generated file's exact portable name, media type, byte count, and SHA-256 using Core's
189
199
  asset-set v1 contract; it does not hash only the icon-package manifest. Hosted must
190
200
  confirm the immutable resource bytes, reconstruct those same per-file identities (including the
191
- `icons/<mapId>/` prefix), and reject the Style when the submitted build-manifest hash diverges.
201
+ `icons/<mapId>/` and `models/<mapId>/` prefixes), and reject the Style when the submitted
202
+ build-manifest hash diverges.
@@ -62,8 +62,8 @@ authoring:
62
62
  ```sh
63
63
  # Two maps or themes from one config.
64
64
  npx tileflow preview \
65
- --map harad --theme light \
66
- --against-map ferraris --against-theme light
65
+ --map baedeker --theme light \
66
+ --against-map map-1777 --against-theme light
67
67
 
68
68
  # Or compare against another config.
69
69
  npx tileflow preview \
@@ -25,6 +25,7 @@ Start with bounded queries, then request more detail at the returned JSON Pointe
25
25
  ```sh
26
26
  npx tileflow describe --json
27
27
  npx tileflow describe roads --path /properties/classes --json
28
+ npx tileflow describe themes --json
28
29
  npx tileflow inspect --domain roads --limit 80 --json
29
30
  npx tileflow inspect compiled --domain roads --theme dark --json
30
31
  npx tileflow compare configs --from-config before/tileflow.config.ts --to-config after/tileflow.config.ts --json
@@ -54,6 +55,12 @@ Inspection includes resolved inheritance, merge provenance, and the theme contra
54
55
  returns compilation diagnostics and a report, not the complete Style JSON. Add `--inspection` only
55
56
  when physical output provenance is needed; those layer IDs are not stable authoring targets.
56
57
 
58
+ `describe themes` lists the minimal palette roles and optional time-of-day behavior. Compiled
59
+ inspection includes `report.decisions` for resolved color roles and selected module defaults,
60
+ with the effective value, source, reason and override guidance. Query it with
61
+ `inspect compiled --path /report/decisions --limit 80 --json`. It describes semantic decisions,
62
+ not every final paint property; config inspection retains inheritance provenance separately.
63
+
57
64
  For a normal config, map selection is implicit. Paired `--from-config` and `--to-config` compare
58
65
  two ordinary configs. Workspace comparisons use their documented explicit map selectors; do not
59
66
  mix the paired-config and same-workspace forms.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tileflow",
3
- "version": "0.1.0-alpha.13",
3
+ "version": "0.1.0-alpha.14",
4
4
  "license": "Apache-2.0",
5
5
  "description": "CLI-only tools for Tileflow config, capture, visual testing, and deploy.",
6
6
  "type": "module",
@@ -39,9 +39,9 @@
39
39
  "@tileflow/capture": ">=0.1.0-alpha.31 <0.1.0-beta.0",
40
40
  "@tileflow/coordinates": ">=0.1.0-alpha.2 <0.1.0-beta.0",
41
41
  "@tileflow/coordinates-runtime": ">=0.1.0-alpha.5 <0.1.0-beta.0",
42
- "@tileflow/core": ">=0.1.0-alpha.35 <0.1.0-beta.0",
43
- "@tileflow/dev": ">=0.1.0-alpha.32 <0.1.0-beta.0",
44
- "@tileflow/maps": ">=0.1.0-alpha.9 <0.1.0-beta.0",
42
+ "@tileflow/core": ">=0.1.0-alpha.36 <0.1.0-beta.0",
43
+ "@tileflow/dev": ">=0.1.0-alpha.33 <0.1.0-beta.0",
44
+ "@tileflow/maps": ">=0.1.0-alpha.10 <0.1.0-beta.0",
45
45
  "commander": "^15.0.0",
46
46
  "hono": "4.13.7",
47
47
  "picocolors": "^1.1.1",