@brandlab-digital/conformance 3.4.0 → 3.6.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/README.md +14 -0
- package/dist/area-map-cli.d.ts +73 -0
- package/dist/area-map-cli.d.ts.map +1 -0
- package/dist/area-map-cli.js +245 -0
- package/dist/area-map-cli.js.map +1 -0
- package/dist/current-release.js +5 -5
- package/dist/current-release.js.map +1 -1
- package/dist/engineering.d.ts.map +1 -1
- package/dist/engineering.js +3 -1
- package/dist/engineering.js.map +1 -1
- package/dist/estate.d.ts +2 -0
- package/dist/estate.d.ts.map +1 -1
- package/dist/estate.js +34 -2
- package/dist/estate.js.map +1 -1
- package/dist/page-standard.d.ts +23 -0
- package/dist/page-standard.d.ts.map +1 -1
- package/dist/page-standard.js +63 -0
- package/dist/page-standard.js.map +1 -1
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
1
|
# `@brandlab-digital/conformance`
|
|
2
2
|
|
|
3
3
|
Proprietary software © Brandlab Digital Marketing. All rights reserved. Published for Brandlab client sites.
|
|
4
|
+
|
|
5
|
+
## Service-area maps (from 3.6.0)
|
|
6
|
+
|
|
7
|
+
- `brandlab-area-map places --out src/data/locations/siteOwnedBoundaries.json Young@2594 Cowra Cootamundra`
|
|
8
|
+
writes a site's own towns (outside the Sydney and Central Coast registries)
|
|
9
|
+
and the land around them in the shared area-map format, from the NSW Spatial
|
|
10
|
+
Services suburb layer (name, and postcode where it repeats; registry places
|
|
11
|
+
are refused). Options: `--margin-km` (land around the towns, default 15) and
|
|
12
|
+
`--tolerance` (metres, default 20).
|
|
13
|
+
- The estate journey fails a service-area map (`<figure data-area-map>`) without
|
|
14
|
+
a caption carrying the CC BY boundary credit, with a link that is a tab stop
|
|
15
|
+
or does not go to an indexable catalogue page, with more than
|
|
16
|
+
`areaMapMaxBytes` inline (default 60 000), or whose `<use>` base-layer file
|
|
17
|
+
does not answer 200 as `image/svg+xml` with the groups it uses.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** The shared boundary document format (`@brandlab-digital/site-core/area-map`), as far as this command reads it. */
|
|
3
|
+
export interface AreaMapBoundaryDocument {
|
|
4
|
+
kind: "brandlab-area-map-boundaries";
|
|
5
|
+
version: string;
|
|
6
|
+
generated: string;
|
|
7
|
+
source: {
|
|
8
|
+
title: string;
|
|
9
|
+
url: string;
|
|
10
|
+
licence: string;
|
|
11
|
+
read: string;
|
|
12
|
+
method: string;
|
|
13
|
+
};
|
|
14
|
+
attribution: string;
|
|
15
|
+
toleranceMetres: number;
|
|
16
|
+
transform: {
|
|
17
|
+
scale: readonly [number, number];
|
|
18
|
+
translate: readonly [number, number];
|
|
19
|
+
};
|
|
20
|
+
arcs: readonly string[];
|
|
21
|
+
shapes: Readonly<Record<string, readonly (readonly number[])[]>>;
|
|
22
|
+
}
|
|
23
|
+
interface RegistryLike {
|
|
24
|
+
regions: readonly {
|
|
25
|
+
slug: string;
|
|
26
|
+
}[];
|
|
27
|
+
subRegions: readonly {
|
|
28
|
+
slug: string;
|
|
29
|
+
}[];
|
|
30
|
+
suburbs: readonly {
|
|
31
|
+
slug: string;
|
|
32
|
+
}[];
|
|
33
|
+
nonSuburbLocalities: readonly {
|
|
34
|
+
slug: string;
|
|
35
|
+
}[];
|
|
36
|
+
aliases: Readonly<Record<string, string>>;
|
|
37
|
+
}
|
|
38
|
+
export declare const NSW_SUBURB_LAYER = "https://portal.spatial.nsw.gov.au/server/rest/services/NSW_Administrative_Boundaries_Theme_multiCRS/FeatureServer/2";
|
|
39
|
+
export interface PlaceRequest {
|
|
40
|
+
name: string;
|
|
41
|
+
slug: string;
|
|
42
|
+
postcode?: string;
|
|
43
|
+
}
|
|
44
|
+
type Fetcher = (url: string, init: {
|
|
45
|
+
method: "POST";
|
|
46
|
+
body: URLSearchParams;
|
|
47
|
+
}) => Promise<{
|
|
48
|
+
ok: boolean;
|
|
49
|
+
status: number;
|
|
50
|
+
json(): Promise<unknown>;
|
|
51
|
+
}>;
|
|
52
|
+
/** Parses `Name[@postcode][=slug]`. */
|
|
53
|
+
export declare function parsePlaceRequest(value: string): PlaceRequest;
|
|
54
|
+
/** A requested town that clashes with the shared registries, which already carry its boundary. */
|
|
55
|
+
export declare function registryClashes(places: readonly PlaceRequest[], registries: readonly RegistryLike[]): string[];
|
|
56
|
+
export interface PlaceBoundaryOptions {
|
|
57
|
+
places: readonly PlaceRequest[];
|
|
58
|
+
marginKm?: number;
|
|
59
|
+
toleranceMetres?: number;
|
|
60
|
+
fetcher?: Fetcher;
|
|
61
|
+
today?: string;
|
|
62
|
+
}
|
|
63
|
+
/** Reads the towns and the land around them and encodes them as a site boundary document. */
|
|
64
|
+
export declare function buildPlaceBoundaries(options: PlaceBoundaryOptions): Promise<AreaMapBoundaryDocument>;
|
|
65
|
+
/** One arc and one shape per line, so a refresh diffs readably. */
|
|
66
|
+
export declare function formatBoundaryDocument(document: AreaMapBoundaryDocument): string;
|
|
67
|
+
interface CliIo {
|
|
68
|
+
stdout: (value: string) => void;
|
|
69
|
+
stderr: (value: string) => void;
|
|
70
|
+
}
|
|
71
|
+
export declare function runAreaMapCli(argv: readonly string[], io: CliIo, fetcher?: Fetcher): Promise<number>;
|
|
72
|
+
export {};
|
|
73
|
+
//# sourceMappingURL=area-map-cli.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"area-map-cli.d.ts","sourceRoot":"","sources":["../src/area-map-cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* brandlab-area-map (3.6.0): writes a site's own area-map boundaries, for towns
|
|
4
|
+
* outside the shared Sydney and Central Coast registries, in the shared format
|
|
5
|
+
* (`@brandlab-digital/site-core/area-map`, `siteOwnedBoundaries`).
|
|
6
|
+
*
|
|
7
|
+
* npx brandlab-area-map places --out src/data/locations/siteOwnedBoundaries.json Young@2594 Cowra@2794 Cootamundra@2590
|
|
8
|
+
*
|
|
9
|
+
* Each town is read from the NSW Spatial Services suburb layer by its official
|
|
10
|
+
* name (and postcode where a name repeats); a town without exactly one match
|
|
11
|
+
* fails. The localities around the towns (their box plus a margin) become the
|
|
12
|
+
* map's land, so coasts and gaps draw correctly. Shared borders stay shared,
|
|
13
|
+
* every arc is simplified on its own (Douglas-Peucker, 20 m) and the result is
|
|
14
|
+
* credited to NSW Spatial Services under CC BY 4.0.
|
|
15
|
+
*/
|
|
16
|
+
import { writeFileSync, realpathSync } from "node:fs";
|
|
17
|
+
import { resolve } from "node:path";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
/*
|
|
20
|
+
* site-core is read at run time, like the geography in page-standard.ts, so the
|
|
21
|
+
* conformance package compiles without a site-core build (the operations
|
|
22
|
+
* adapter compiles it on its own).
|
|
23
|
+
*/
|
|
24
|
+
const AREA_MAP_MODULE = "@brandlab-digital/site-core/area-map";
|
|
25
|
+
const AREA_MAP_ENCODE_MODULE = "@brandlab-digital/site-core/area-map/encode";
|
|
26
|
+
const GEOGRAPHY_MODULE = "@brandlab-digital/site-core/geography";
|
|
27
|
+
async function loadSiteCore() {
|
|
28
|
+
const [areaMap, encode, geography] = await Promise.all([import(AREA_MAP_MODULE), import(AREA_MAP_ENCODE_MODULE), import(GEOGRAPHY_MODULE)]);
|
|
29
|
+
if (typeof encode.encodeAreaMapBoundaries !== "function" || typeof areaMap.AREA_MAP_ATTRIBUTION !== "string") {
|
|
30
|
+
throw new Error("@brandlab-digital/site-core has no area-map entry: install site-core 3.6.0 or later with the conformance package.");
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
attribution: areaMap.AREA_MAP_ATTRIBUTION,
|
|
34
|
+
encode: encode.encodeAreaMapBoundaries,
|
|
35
|
+
registries: [geography.sydneyGeography, geography.centralCoastGeography],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** A place name as a registry-style slug: lowercase, hyphenated. */
|
|
39
|
+
function toLocationSlug(name) {
|
|
40
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
41
|
+
}
|
|
42
|
+
export const NSW_SUBURB_LAYER = "https://portal.spatial.nsw.gov.au/server/rest/services/NSW_Administrative_Boundaries_Theme_multiCRS/FeatureServer/2";
|
|
43
|
+
const HELP = `Brandlab area-map boundaries for a site's own towns
|
|
44
|
+
|
|
45
|
+
Usage:
|
|
46
|
+
brandlab-area-map places --out <file.json> [--margin-km <km>] [--tolerance <metres>] <Name[@postcode][=slug]>...
|
|
47
|
+
|
|
48
|
+
Reads each NSW town from the NSW Spatial Services suburb layer by its official
|
|
49
|
+
name (and postcode where the name repeats) and writes its boundary, with the
|
|
50
|
+
land around it, in the shared area-map format. The slug defaults to the name in
|
|
51
|
+
kebab-case and must match the town's slug in src/data/locations/siteOwnedPlaces.ts.
|
|
52
|
+
A registry suburb or alias cannot be a site-owned town.`;
|
|
53
|
+
/** Parses `Name[@postcode][=slug]`. */
|
|
54
|
+
export function parsePlaceRequest(value) {
|
|
55
|
+
const match = /^([^@=]+?)(?:@(\d{4}))?(?:=([a-z0-9]+(?:-[a-z0-9]+)*))?$/.exec(value.trim());
|
|
56
|
+
if (!match)
|
|
57
|
+
throw new Error(`"${value}" is not Name[@postcode][=slug].`);
|
|
58
|
+
const name = match[1].trim();
|
|
59
|
+
return { name, slug: match[3] ?? toLocationSlug(name), ...(match[2] ? { postcode: match[2] } : {}) };
|
|
60
|
+
}
|
|
61
|
+
/** A requested town that clashes with the shared registries, which already carry its boundary. */
|
|
62
|
+
export function registryClashes(places, registries) {
|
|
63
|
+
const problems = [];
|
|
64
|
+
for (const registry of registries) {
|
|
65
|
+
const slugs = new Set([...registry.regions, ...registry.subRegions, ...registry.suburbs, ...registry.nonSuburbLocalities].map((entry) => entry.slug));
|
|
66
|
+
for (const place of places) {
|
|
67
|
+
if (slugs.has(place.slug) || Object.hasOwn(registry.aliases, place.slug)) {
|
|
68
|
+
problems.push(`${place.slug}: already in the shared registry; its boundary is in @brandlab-digital/site-core/area-map`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return [...new Set(problems)];
|
|
73
|
+
}
|
|
74
|
+
async function query(fetcher, body) {
|
|
75
|
+
const response = await fetcher(`${NSW_SUBURB_LAYER}/query`, { method: "POST", body: new URLSearchParams({ ...body, f: "json" }) });
|
|
76
|
+
if (!response.ok)
|
|
77
|
+
throw new Error(`The NSW suburb layer answered ${response.status}.`);
|
|
78
|
+
const page = (await response.json());
|
|
79
|
+
if (page.error)
|
|
80
|
+
throw new Error(`The NSW suburb layer refused the query: ${JSON.stringify(page.error)}`);
|
|
81
|
+
return { features: page.features ?? [], exceededTransferLimit: page.exceededTransferLimit };
|
|
82
|
+
}
|
|
83
|
+
/** Reads the towns and the land around them and encodes them as a site boundary document. */
|
|
84
|
+
export async function buildPlaceBoundaries(options) {
|
|
85
|
+
const fetcher = options.fetcher ?? fetch;
|
|
86
|
+
const places = options.places;
|
|
87
|
+
if (places.length === 0)
|
|
88
|
+
throw new Error("Name at least one town.");
|
|
89
|
+
const repeated = places.map((place) => place.slug).filter((slug, index, all) => all.indexOf(slug) !== index);
|
|
90
|
+
if (repeated.length)
|
|
91
|
+
throw new Error(`Listed more than once: ${[...new Set(repeated)].join(", ")}.`);
|
|
92
|
+
const siteCore = await loadSiteCore();
|
|
93
|
+
const clashes = registryClashes(places, siteCore.registries);
|
|
94
|
+
if (clashes.length)
|
|
95
|
+
throw new Error(clashes.join("\n"));
|
|
96
|
+
const names = [...new Set(places.map((place) => place.name.toUpperCase()))];
|
|
97
|
+
const { features } = await query(fetcher, {
|
|
98
|
+
where: `suburbname IN (${names.map((name) => `'${name.replace(/'/g, "''")}'`).join(",")})`,
|
|
99
|
+
outFields: "suburbname,postcode,OBJECTID",
|
|
100
|
+
returnGeometry: "true",
|
|
101
|
+
outSR: "4326",
|
|
102
|
+
});
|
|
103
|
+
const problems = [];
|
|
104
|
+
const matched = new Map();
|
|
105
|
+
for (const place of places) {
|
|
106
|
+
const candidates = features.filter((feature) => String(feature.attributes.suburbname ?? "").trim().toUpperCase() === place.name.toUpperCase() && feature.geometry?.rings?.length);
|
|
107
|
+
const found = candidates.length === 1 && !place.postcode ? candidates : candidates.filter((feature) => String(feature.attributes.postcode ?? "") === String(place.postcode ?? ""));
|
|
108
|
+
if (found.length !== 1) {
|
|
109
|
+
const ambiguous = candidates.length > 1 && !place.postcode;
|
|
110
|
+
problems.push(`${place.name}${place.postcode ? ` ${place.postcode}` : ""}: ${ambiguous ? candidates.length : found.length} matching NSW localities${ambiguous ? " (add @postcode)" : ""}`);
|
|
111
|
+
}
|
|
112
|
+
else
|
|
113
|
+
matched.set(place.slug, found[0]);
|
|
114
|
+
}
|
|
115
|
+
if (problems.length)
|
|
116
|
+
throw new Error(`Towns could not be matched:\n- ${problems.join("\n- ")}`);
|
|
117
|
+
let [west, south, east, north] = [180, 90, -180, -90];
|
|
118
|
+
for (const feature of matched.values()) {
|
|
119
|
+
for (const ring of feature.geometry.rings) {
|
|
120
|
+
for (const [longitude, latitude] of ring) {
|
|
121
|
+
west = Math.min(west, longitude);
|
|
122
|
+
east = Math.max(east, longitude);
|
|
123
|
+
south = Math.min(south, latitude);
|
|
124
|
+
north = Math.max(north, latitude);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const marginKm = options.marginKm ?? 15;
|
|
129
|
+
const marginY = marginKm / 110.574;
|
|
130
|
+
const marginX = marginKm / (111.32 * Math.cos((((north + south) / 2) * Math.PI) / 180));
|
|
131
|
+
const box = [west - marginX, south - marginY, east + marginX, north + marginY].map((value) => Math.round(value * 1000) / 1000);
|
|
132
|
+
const context = [];
|
|
133
|
+
for (let offset = 0;; offset += 500) {
|
|
134
|
+
const page = await query(fetcher, {
|
|
135
|
+
where: "1=1",
|
|
136
|
+
geometry: box.join(","),
|
|
137
|
+
geometryType: "esriGeometryEnvelope",
|
|
138
|
+
inSR: "4326",
|
|
139
|
+
spatialRel: "esriSpatialRelIntersects",
|
|
140
|
+
outFields: "suburbname,postcode,OBJECTID",
|
|
141
|
+
returnGeometry: "true",
|
|
142
|
+
outSR: "4326",
|
|
143
|
+
orderByFields: "OBJECTID",
|
|
144
|
+
resultOffset: String(offset),
|
|
145
|
+
resultRecordCount: "500",
|
|
146
|
+
});
|
|
147
|
+
context.push(...page.features);
|
|
148
|
+
if (!page.exceededTransferLimit && page.features.length < 500)
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
const townIds = new Set([...matched.values()].map((feature) => feature.attributes.OBJECTID));
|
|
152
|
+
const surrounding = context
|
|
153
|
+
.filter((feature) => !townIds.has(feature.attributes.OBJECTID) && feature.geometry?.rings?.length)
|
|
154
|
+
.sort((a, b) => a.attributes.OBJECTID - b.attributes.OBJECTID);
|
|
155
|
+
const featureList = [
|
|
156
|
+
...[...matched].sort(([a], [b]) => a.localeCompare(b)).map(([slug, feature]) => ({ key: `place:${slug}`, rings: feature.geometry.rings })),
|
|
157
|
+
...surrounding.map((feature) => ({ key: `context:${feature.attributes.OBJECTID}`, rings: feature.geometry.rings, write: false })),
|
|
158
|
+
];
|
|
159
|
+
const read = options.today ?? new Date().toISOString().slice(0, 10);
|
|
160
|
+
const toleranceMetres = options.toleranceMetres ?? 20;
|
|
161
|
+
return siteCore.encode({
|
|
162
|
+
features: featureList,
|
|
163
|
+
groups: { land: featureList.map((feature) => feature.key) },
|
|
164
|
+
toleranceMetres,
|
|
165
|
+
version: "1.0.0",
|
|
166
|
+
generated: read,
|
|
167
|
+
source: {
|
|
168
|
+
title: "NSW Administrative Boundaries Theme - GDA2020 Suburb layer (NSW Spatial Services)",
|
|
169
|
+
url: NSW_SUBURB_LAYER,
|
|
170
|
+
licence: "CC BY 4.0. © State of New South Wales (Spatial Services, a business unit of the Department of Customer Service NSW). For current information go to spatial.nsw.gov.au.",
|
|
171
|
+
read,
|
|
172
|
+
method: `Towns matched by official name (and postcode where a name repeats); the localities within ${marginKm} km of them as land; one topology with shared borders as single arcs; each arc simplified on its own (Douglas-Peucker, ${toleranceMetres} m); quantised to 0.00001 degrees. Written by brandlab-area-map places.`,
|
|
173
|
+
},
|
|
174
|
+
attribution: siteCore.attribution,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
/** One arc and one shape per line, so a refresh diffs readably. */
|
|
178
|
+
export function formatBoundaryDocument(document) {
|
|
179
|
+
const { arcs, shapes, ...head } = document;
|
|
180
|
+
const lines = Object.entries(head).map(([key, value]) => ` ${JSON.stringify(key)}: ${JSON.stringify(value)},`);
|
|
181
|
+
lines.push(` "arcs": [\n${arcs.map((arc) => ` ${JSON.stringify(arc)}`).join(",\n")}\n ],`);
|
|
182
|
+
lines.push(` "shapes": {\n${Object.entries(shapes).map(([key, rings]) => ` ${JSON.stringify(key)}: ${JSON.stringify(rings)}`).join(",\n")}\n }`);
|
|
183
|
+
return `{\n${lines.join("\n")}\n}\n`;
|
|
184
|
+
}
|
|
185
|
+
export async function runAreaMapCli(argv, io, fetcher) {
|
|
186
|
+
const [command, ...rest] = argv;
|
|
187
|
+
if (!command || command === "--help" || command === "-h") {
|
|
188
|
+
io.stdout(`${HELP}\n`);
|
|
189
|
+
return command ? 0 : 1;
|
|
190
|
+
}
|
|
191
|
+
if (command !== "places") {
|
|
192
|
+
io.stderr(`Unknown command "${command}".\n\n${HELP}\n`);
|
|
193
|
+
return 1;
|
|
194
|
+
}
|
|
195
|
+
let out;
|
|
196
|
+
let marginKm;
|
|
197
|
+
let toleranceMetres;
|
|
198
|
+
const requests = [];
|
|
199
|
+
try {
|
|
200
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
201
|
+
const argument = rest[index];
|
|
202
|
+
if (argument === "--out")
|
|
203
|
+
out = rest[++index];
|
|
204
|
+
else if (argument === "--margin-km")
|
|
205
|
+
marginKm = Number(rest[++index]);
|
|
206
|
+
else if (argument === "--tolerance")
|
|
207
|
+
toleranceMetres = Number(rest[++index]);
|
|
208
|
+
else if (argument.startsWith("--"))
|
|
209
|
+
throw new Error(`Unknown option ${argument}.`);
|
|
210
|
+
else
|
|
211
|
+
requests.push(parsePlaceRequest(argument));
|
|
212
|
+
}
|
|
213
|
+
if (!out)
|
|
214
|
+
throw new Error("Name the output file with --out (for example src/data/locations/siteOwnedBoundaries.json).");
|
|
215
|
+
if (marginKm !== undefined && !(marginKm >= 0 && marginKm <= 200))
|
|
216
|
+
throw new Error("--margin-km must be from 0 to 200.");
|
|
217
|
+
if (toleranceMetres !== undefined && !(toleranceMetres > 0 && toleranceMetres <= 500))
|
|
218
|
+
throw new Error("--tolerance must be above 0 and at most 500 metres.");
|
|
219
|
+
const document = await buildPlaceBoundaries({ places: requests, marginKm, toleranceMetres, fetcher });
|
|
220
|
+
const text = formatBoundaryDocument(document);
|
|
221
|
+
writeFileSync(resolve(out), text);
|
|
222
|
+
io.stdout(`Wrote ${requests.length} town boundar${requests.length === 1 ? "y" : "ies"} and the land around them (${Math.round(text.length / 1024)} KB) to ${out}.\n`);
|
|
223
|
+
return 0;
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
io.stderr(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
227
|
+
return 1;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function isEntrypoint(moduleUrl, argvPath) {
|
|
231
|
+
if (!argvPath)
|
|
232
|
+
return false;
|
|
233
|
+
try {
|
|
234
|
+
return realpathSync(fileURLToPath(moduleUrl)) === realpathSync(resolve(argvPath));
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (isEntrypoint(import.meta.url, process.argv[1])) {
|
|
241
|
+
runAreaMapCli(process.argv.slice(2), { stdout: (value) => process.stdout.write(value), stderr: (value) => process.stderr.write(value) }).then((code) => {
|
|
242
|
+
process.exitCode = code;
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
//# sourceMappingURL=area-map-cli.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"area-map-cli.js","sourceRoot":"","sources":["../src/area-map-cli.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\n/**\n * brandlab-area-map (3.6.0): writes a site's own area-map boundaries, for towns\n * outside the shared Sydney and Central Coast registries, in the shared format\n * (`@brandlab-digital/site-core/area-map`, `siteOwnedBoundaries`).\n *\n * npx brandlab-area-map places --out src/data/locations/siteOwnedBoundaries.json Young@2594 Cowra@2794 Cootamundra@2590\n *\n * Each town is read from the NSW Spatial Services suburb layer by its official\n * name (and postcode where a name repeats); a town without exactly one match\n * fails. The localities around the towns (their box plus a margin) become the\n * map's land, so coasts and gaps draw correctly. Shared borders stay shared,\n * every arc is simplified on its own (Douglas-Peucker, 20 m) and the result is\n * credited to NSW Spatial Services under CC BY 4.0.\n */\nimport { writeFileSync, realpathSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\n/*\n * site-core is read at run time, like the geography in page-standard.ts, so the\n * conformance package compiles without a site-core build (the operations\n * adapter compiles it on its own).\n */\nconst AREA_MAP_MODULE: string = \"@brandlab-digital/site-core/area-map\";\nconst AREA_MAP_ENCODE_MODULE: string = \"@brandlab-digital/site-core/area-map/encode\";\nconst GEOGRAPHY_MODULE: string = \"@brandlab-digital/site-core/geography\";\n\n/** The shared boundary document format (`@brandlab-digital/site-core/area-map`), as far as this command reads it. */\nexport interface AreaMapBoundaryDocument {\n kind: \"brandlab-area-map-boundaries\";\n version: string;\n generated: string;\n source: { title: string; url: string; licence: string; read: string; method: string };\n attribution: string;\n toleranceMetres: number;\n transform: { scale: readonly [number, number]; translate: readonly [number, number] };\n arcs: readonly string[];\n shapes: Readonly<Record<string, readonly (readonly number[])[]>>;\n}\n\ninterface RegistryLike {\n regions: readonly { slug: string }[];\n subRegions: readonly { slug: string }[];\n suburbs: readonly { slug: string }[];\n nonSuburbLocalities: readonly { slug: string }[];\n aliases: Readonly<Record<string, string>>;\n}\n\ninterface SiteCoreAreaMap {\n attribution: string;\n encode: (options: {\n features: readonly { key: string; rings: readonly (readonly (readonly [number, number])[])[]; write?: boolean }[];\n groups?: Readonly<Record<string, readonly string[]>>;\n toleranceMetres: number;\n version: string;\n generated: string;\n source: AreaMapBoundaryDocument[\"source\"];\n attribution: string;\n }) => AreaMapBoundaryDocument;\n registries: readonly RegistryLike[];\n}\n\nasync function loadSiteCore(): Promise<SiteCoreAreaMap> {\n const [areaMap, encode, geography] = await Promise.all([import(AREA_MAP_MODULE), import(AREA_MAP_ENCODE_MODULE), import(GEOGRAPHY_MODULE)]);\n if (typeof encode.encodeAreaMapBoundaries !== \"function\" || typeof areaMap.AREA_MAP_ATTRIBUTION !== \"string\") {\n throw new Error(\"@brandlab-digital/site-core has no area-map entry: install site-core 3.6.0 or later with the conformance package.\");\n }\n return {\n attribution: areaMap.AREA_MAP_ATTRIBUTION,\n encode: encode.encodeAreaMapBoundaries,\n registries: [geography.sydneyGeography, geography.centralCoastGeography],\n };\n}\n\n/** A place name as a registry-style slug: lowercase, hyphenated. */\nfunction toLocationSlug(name: string): string {\n return name.toLowerCase().replace(/[^a-z0-9]+/g, \"-\").replace(/(^-|-$)/g, \"\");\n}\n\nexport const NSW_SUBURB_LAYER =\n \"https://portal.spatial.nsw.gov.au/server/rest/services/NSW_Administrative_Boundaries_Theme_multiCRS/FeatureServer/2\";\n\nexport interface PlaceRequest {\n name: string;\n slug: string;\n postcode?: string;\n}\n\ninterface LayerFeature {\n attributes: { suburbname?: string; postcode?: number | string | null; OBJECTID: number };\n geometry?: { rings?: [number, number][][] };\n}\n\ntype Fetcher = (url: string, init: { method: \"POST\"; body: URLSearchParams }) => Promise<{ ok: boolean; status: number; json(): Promise<unknown> }>;\n\nconst HELP = `Brandlab area-map boundaries for a site's own towns\n\nUsage:\n brandlab-area-map places --out <file.json> [--margin-km <km>] [--tolerance <metres>] <Name[@postcode][=slug]>...\n\nReads each NSW town from the NSW Spatial Services suburb layer by its official\nname (and postcode where the name repeats) and writes its boundary, with the\nland around it, in the shared area-map format. The slug defaults to the name in\nkebab-case and must match the town's slug in src/data/locations/siteOwnedPlaces.ts.\nA registry suburb or alias cannot be a site-owned town.`;\n\n/** Parses `Name[@postcode][=slug]`. */\nexport function parsePlaceRequest(value: string): PlaceRequest {\n const match = /^([^@=]+?)(?:@(\\d{4}))?(?:=([a-z0-9]+(?:-[a-z0-9]+)*))?$/.exec(value.trim());\n if (!match) throw new Error(`\"${value}\" is not Name[@postcode][=slug].`);\n const name = match[1]!.trim();\n return { name, slug: match[3] ?? toLocationSlug(name), ...(match[2] ? { postcode: match[2] } : {}) };\n}\n\n/** A requested town that clashes with the shared registries, which already carry its boundary. */\nexport function registryClashes(places: readonly PlaceRequest[], registries: readonly RegistryLike[]): string[] {\n const problems: string[] = [];\n for (const registry of registries) {\n const slugs = new Set([...registry.regions, ...registry.subRegions, ...registry.suburbs, ...registry.nonSuburbLocalities].map((entry) => entry.slug));\n for (const place of places) {\n if (slugs.has(place.slug) || Object.hasOwn(registry.aliases, place.slug)) {\n problems.push(`${place.slug}: already in the shared registry; its boundary is in @brandlab-digital/site-core/area-map`);\n }\n }\n }\n return [...new Set(problems)];\n}\n\nasync function query(fetcher: Fetcher, body: Record<string, string>): Promise<{ features: LayerFeature[]; exceededTransferLimit?: boolean }> {\n const response = await fetcher(`${NSW_SUBURB_LAYER}/query`, { method: \"POST\", body: new URLSearchParams({ ...body, f: \"json\" }) });\n if (!response.ok) throw new Error(`The NSW suburb layer answered ${response.status}.`);\n const page = (await response.json()) as { features?: LayerFeature[]; exceededTransferLimit?: boolean; error?: unknown };\n if (page.error) throw new Error(`The NSW suburb layer refused the query: ${JSON.stringify(page.error)}`);\n return { features: page.features ?? [], exceededTransferLimit: page.exceededTransferLimit };\n}\n\nexport interface PlaceBoundaryOptions {\n places: readonly PlaceRequest[];\n marginKm?: number;\n toleranceMetres?: number;\n fetcher?: Fetcher;\n today?: string;\n}\n\n/** Reads the towns and the land around them and encodes them as a site boundary document. */\nexport async function buildPlaceBoundaries(options: PlaceBoundaryOptions): Promise<AreaMapBoundaryDocument> {\n const fetcher = options.fetcher ?? (fetch as unknown as Fetcher);\n const places = options.places;\n if (places.length === 0) throw new Error(\"Name at least one town.\");\n const repeated = places.map((place) => place.slug).filter((slug, index, all) => all.indexOf(slug) !== index);\n if (repeated.length) throw new Error(`Listed more than once: ${[...new Set(repeated)].join(\", \")}.`);\n const siteCore = await loadSiteCore();\n const clashes = registryClashes(places, siteCore.registries);\n if (clashes.length) throw new Error(clashes.join(\"\\n\"));\n const names = [...new Set(places.map((place) => place.name.toUpperCase()))];\n const { features } = await query(fetcher, {\n where: `suburbname IN (${names.map((name) => `'${name.replace(/'/g, \"''\")}'`).join(\",\")})`,\n outFields: \"suburbname,postcode,OBJECTID\",\n returnGeometry: \"true\",\n outSR: \"4326\",\n });\n const problems: string[] = [];\n const matched = new Map<string, LayerFeature>();\n for (const place of places) {\n const candidates = features.filter((feature) => String(feature.attributes.suburbname ?? \"\").trim().toUpperCase() === place.name.toUpperCase() && feature.geometry?.rings?.length);\n const found = candidates.length === 1 && !place.postcode ? candidates : candidates.filter((feature) => String(feature.attributes.postcode ?? \"\") === String(place.postcode ?? \"\"));\n if (found.length !== 1) {\n const ambiguous = candidates.length > 1 && !place.postcode;\n problems.push(`${place.name}${place.postcode ? ` ${place.postcode}` : \"\"}: ${ambiguous ? candidates.length : found.length} matching NSW localities${ambiguous ? \" (add @postcode)\" : \"\"}`);\n } else matched.set(place.slug, found[0]!);\n }\n if (problems.length) throw new Error(`Towns could not be matched:\\n- ${problems.join(\"\\n- \")}`);\n\n let [west, south, east, north] = [180, 90, -180, -90];\n for (const feature of matched.values()) {\n for (const ring of feature.geometry!.rings!) {\n for (const [longitude, latitude] of ring) {\n west = Math.min(west, longitude);\n east = Math.max(east, longitude);\n south = Math.min(south, latitude);\n north = Math.max(north, latitude);\n }\n }\n }\n const marginKm = options.marginKm ?? 15;\n const marginY = marginKm / 110.574;\n const marginX = marginKm / (111.32 * Math.cos((((north + south) / 2) * Math.PI) / 180));\n const box = [west - marginX, south - marginY, east + marginX, north + marginY].map((value) => Math.round(value * 1000) / 1000);\n const context: LayerFeature[] = [];\n for (let offset = 0; ; offset += 500) {\n const page = await query(fetcher, {\n where: \"1=1\",\n geometry: box.join(\",\"),\n geometryType: \"esriGeometryEnvelope\",\n inSR: \"4326\",\n spatialRel: \"esriSpatialRelIntersects\",\n outFields: \"suburbname,postcode,OBJECTID\",\n returnGeometry: \"true\",\n outSR: \"4326\",\n orderByFields: \"OBJECTID\",\n resultOffset: String(offset),\n resultRecordCount: \"500\",\n });\n context.push(...page.features);\n if (!page.exceededTransferLimit && page.features.length < 500) break;\n }\n const townIds = new Set([...matched.values()].map((feature) => feature.attributes.OBJECTID));\n const surrounding = context\n .filter((feature) => !townIds.has(feature.attributes.OBJECTID) && feature.geometry?.rings?.length)\n .sort((a, b) => a.attributes.OBJECTID - b.attributes.OBJECTID);\n const featureList = [\n ...[...matched].sort(([a], [b]) => a.localeCompare(b)).map(([slug, feature]) => ({ key: `place:${slug}`, rings: feature.geometry!.rings! })),\n ...surrounding.map((feature) => ({ key: `context:${feature.attributes.OBJECTID}`, rings: feature.geometry!.rings!, write: false })),\n ];\n const read = options.today ?? new Date().toISOString().slice(0, 10);\n const toleranceMetres = options.toleranceMetres ?? 20;\n return siteCore.encode({\n features: featureList,\n groups: { land: featureList.map((feature) => feature.key) },\n toleranceMetres,\n version: \"1.0.0\",\n generated: read,\n source: {\n title: \"NSW Administrative Boundaries Theme - GDA2020 Suburb layer (NSW Spatial Services)\",\n url: NSW_SUBURB_LAYER,\n licence:\n \"CC BY 4.0. © State of New South Wales (Spatial Services, a business unit of the Department of Customer Service NSW). For current information go to spatial.nsw.gov.au.\",\n read,\n method: `Towns matched by official name (and postcode where a name repeats); the localities within ${marginKm} km of them as land; one topology with shared borders as single arcs; each arc simplified on its own (Douglas-Peucker, ${toleranceMetres} m); quantised to 0.00001 degrees. Written by brandlab-area-map places.`,\n },\n attribution: siteCore.attribution,\n });\n}\n\n/** One arc and one shape per line, so a refresh diffs readably. */\nexport function formatBoundaryDocument(document: AreaMapBoundaryDocument): string {\n const { arcs, shapes, ...head } = document;\n const lines = Object.entries(head).map(([key, value]) => ` ${JSON.stringify(key)}: ${JSON.stringify(value)},`);\n lines.push(` \"arcs\": [\\n${arcs.map((arc) => ` ${JSON.stringify(arc)}`).join(\",\\n\")}\\n ],`);\n lines.push(` \"shapes\": {\\n${Object.entries(shapes).map(([key, rings]) => ` ${JSON.stringify(key)}: ${JSON.stringify(rings)}`).join(\",\\n\")}\\n }`);\n return `{\\n${lines.join(\"\\n\")}\\n}\\n`;\n}\n\ninterface CliIo {\n stdout: (value: string) => void;\n stderr: (value: string) => void;\n}\n\nexport async function runAreaMapCli(argv: readonly string[], io: CliIo, fetcher?: Fetcher): Promise<number> {\n const [command, ...rest] = argv;\n if (!command || command === \"--help\" || command === \"-h\") {\n io.stdout(`${HELP}\\n`);\n return command ? 0 : 1;\n }\n if (command !== \"places\") {\n io.stderr(`Unknown command \"${command}\".\\n\\n${HELP}\\n`);\n return 1;\n }\n let out: string | undefined;\n let marginKm: number | undefined;\n let toleranceMetres: number | undefined;\n const requests: PlaceRequest[] = [];\n try {\n for (let index = 0; index < rest.length; index += 1) {\n const argument = rest[index]!;\n if (argument === \"--out\") out = rest[++index];\n else if (argument === \"--margin-km\") marginKm = Number(rest[++index]);\n else if (argument === \"--tolerance\") toleranceMetres = Number(rest[++index]);\n else if (argument.startsWith(\"--\")) throw new Error(`Unknown option ${argument}.`);\n else requests.push(parsePlaceRequest(argument));\n }\n if (!out) throw new Error(\"Name the output file with --out (for example src/data/locations/siteOwnedBoundaries.json).\");\n if (marginKm !== undefined && !(marginKm >= 0 && marginKm <= 200)) throw new Error(\"--margin-km must be from 0 to 200.\");\n if (toleranceMetres !== undefined && !(toleranceMetres > 0 && toleranceMetres <= 500)) throw new Error(\"--tolerance must be above 0 and at most 500 metres.\");\n const document = await buildPlaceBoundaries({ places: requests, marginKm, toleranceMetres, fetcher });\n const text = formatBoundaryDocument(document);\n writeFileSync(resolve(out), text);\n io.stdout(`Wrote ${requests.length} town boundar${requests.length === 1 ? \"y\" : \"ies\"} and the land around them (${Math.round(text.length / 1024)} KB) to ${out}.\\n`);\n return 0;\n } catch (error) {\n io.stderr(`${error instanceof Error ? error.message : String(error)}\\n`);\n return 1;\n }\n}\n\nfunction isEntrypoint(moduleUrl: string, argvPath: string | undefined): boolean {\n if (!argvPath) return false;\n try {\n return realpathSync(fileURLToPath(moduleUrl)) === realpathSync(resolve(argvPath));\n } catch {\n return false;\n }\n}\n\nif (isEntrypoint(import.meta.url, process.argv[1])) {\n runAreaMapCli(process.argv.slice(2), { stdout: (value) => process.stdout.write(value), stderr: (value) => process.stderr.write(value) }).then((code) => {\n process.exitCode = code;\n });\n}\n"]}
|
package/dist/current-release.js
CHANGED
|
@@ -23,19 +23,19 @@ export const BRANDLAB_CURRENT_RUNTIME_RELEASE = Object.freeze({
|
|
|
23
23
|
"kind": "brandlab-runtime-release-contract",
|
|
24
24
|
"packages": {
|
|
25
25
|
"@brandlab-digital/conformance": {
|
|
26
|
-
"version": "3.
|
|
26
|
+
"version": "3.6.0"
|
|
27
27
|
},
|
|
28
28
|
"@brandlab-digital/eslint-plugin": {
|
|
29
|
-
"version": "3.
|
|
29
|
+
"version": "3.6.0"
|
|
30
30
|
},
|
|
31
31
|
"@brandlab-digital/site-core": {
|
|
32
|
-
"version": "3.
|
|
32
|
+
"version": "3.6.0"
|
|
33
33
|
},
|
|
34
34
|
"@brandlab-digital/site-next": {
|
|
35
|
-
"version": "3.
|
|
35
|
+
"version": "3.6.0"
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
|
-
"releaseVersion": "3.
|
|
38
|
+
"releaseVersion": "3.6.0",
|
|
39
39
|
"runtime": {
|
|
40
40
|
"bundler": "turbopack",
|
|
41
41
|
"cacheComponents": false,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"current-release.js","sourceRoot":"","sources":["../src/current-release.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,wEAAwE;AACxE,OAAO,EAEL,sCAAsC,GACvC,MAAM,uBAAuB,CAAC;AAE/B,MAAM,CAAC,MAAM,gCAAgC,GAAG,MAAM,CAAC,MAAM,CAC7D;IACE,cAAc,EAAE;QACd,uCAAuC;QACvC,8BAA8B;QAC9B,yBAAyB;QACzB,6BAA6B;QAC7B,0BAA0B;QAC1B,4BAA4B;QAC5B,kCAAkC;QAClC,wCAAwC;QACxC,yBAAyB;KAC1B;IACD,WAAW,EAAE;QACX,WAAW,EAAE,cAAc;QAC3B,cAAc,EAAE,iBAAiB;QACjC,eAAe,EAAE,mBAAmB;QACpC,SAAS,EAAE,YAAY;QACvB,KAAK,EAAE,QAAQ;KAChB;IACD,MAAM,EAAE,mCAAmC;IAC3C,UAAU,EAAE;QACV,+BAA+B,EAAE;YAC/B,SAAS,EAAE,OAAO;SACnB;QACD,iCAAiC,EAAE;YACjC,SAAS,EAAE,OAAO;SACnB;QACD,6BAA6B,EAAE;YAC7B,SAAS,EAAE,OAAO;SACnB;QACD,6BAA6B,EAAE;YAC7B,SAAS,EAAE,OAAO;SACnB;KACF;IACD,gBAAgB,EAAE,OAAO;IACzB,SAAS,EAAE;QACT,SAAS,EAAE,WAAW;QACtB,iBAAiB,EAAE,KAAK;QACxB,MAAM,EAAE,QAAQ;QAChB,mBAAmB,EAAE,OAAO;QAC5B,MAAM,EAAE,MAAM;QACd,gBAAgB,EAAE,aAAa;QAC/B,OAAO,EAAE,QAAQ;QACjB,UAAU,EAAE,QAAQ;QACpB,QAAQ,EAAE,KAAK;QACf,OAAO,EAAE,QAAQ;QACjB,YAAY,EAAE,OAAO;KACtB;IACD,eAAe,EAAE,sCAAsC;CACxD,CAC6C,CAAC;AAE/C,sCAAsC,CAAC,gCAAgC,CAAC,CAAC","sourcesContent":["// Generated by standards/scripts/generate-conformance-current-release.mjs.\n// Each exact package release verifies its own current runtime contract.\nimport {\n type BrandlabRuntimeReleaseContract,\n validateBrandlabRuntimeReleaseContract,\n} from \"./release-manifest.js\";\n\nexport const BRANDLAB_CURRENT_RUNTIME_RELEASE = Object.freeze(\n{\n \"capabilities\": [\n \"typed-project-business-and-route-data\",\n \"essential-search-foundations\",\n \"shared-contact-delivery\",\n \"privacy-safe-lead-analytics\",\n \"security-patched-runtime\",\n \"live-google-review-summary\",\n \"high-signal-project-verification\",\n \"packed-positive-and-negative-consumers\",\n \"changesets-oidc-release\"\n ],\n \"contracts\": {\n \"analytics\": \"analytics-v4\",\n \"architecture\": \"architecture-v7\",\n \"configuration\": \"project-config-v7\",\n \"contact\": \"contact-v4\",\n \"seo\": \"seo-v4\"\n },\n \"kind\": \"brandlab-runtime-release-contract\",\n \"packages\": {\n \"@brandlab-digital/conformance\": {\n \"version\": \"3.
|
|
1
|
+
{"version":3,"file":"current-release.js","sourceRoot":"","sources":["../src/current-release.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,wEAAwE;AACxE,OAAO,EAEL,sCAAsC,GACvC,MAAM,uBAAuB,CAAC;AAE/B,MAAM,CAAC,MAAM,gCAAgC,GAAG,MAAM,CAAC,MAAM,CAC7D;IACE,cAAc,EAAE;QACd,uCAAuC;QACvC,8BAA8B;QAC9B,yBAAyB;QACzB,6BAA6B;QAC7B,0BAA0B;QAC1B,4BAA4B;QAC5B,kCAAkC;QAClC,wCAAwC;QACxC,yBAAyB;KAC1B;IACD,WAAW,EAAE;QACX,WAAW,EAAE,cAAc;QAC3B,cAAc,EAAE,iBAAiB;QACjC,eAAe,EAAE,mBAAmB;QACpC,SAAS,EAAE,YAAY;QACvB,KAAK,EAAE,QAAQ;KAChB;IACD,MAAM,EAAE,mCAAmC;IAC3C,UAAU,EAAE;QACV,+BAA+B,EAAE;YAC/B,SAAS,EAAE,OAAO;SACnB;QACD,iCAAiC,EAAE;YACjC,SAAS,EAAE,OAAO;SACnB;QACD,6BAA6B,EAAE;YAC7B,SAAS,EAAE,OAAO;SACnB;QACD,6BAA6B,EAAE;YAC7B,SAAS,EAAE,OAAO;SACnB;KACF;IACD,gBAAgB,EAAE,OAAO;IACzB,SAAS,EAAE;QACT,SAAS,EAAE,WAAW;QACtB,iBAAiB,EAAE,KAAK;QACxB,MAAM,EAAE,QAAQ;QAChB,mBAAmB,EAAE,OAAO;QAC5B,MAAM,EAAE,MAAM;QACd,gBAAgB,EAAE,aAAa;QAC/B,OAAO,EAAE,QAAQ;QACjB,UAAU,EAAE,QAAQ;QACpB,QAAQ,EAAE,KAAK;QACf,OAAO,EAAE,QAAQ;QACjB,YAAY,EAAE,OAAO;KACtB;IACD,eAAe,EAAE,sCAAsC;CACxD,CAC6C,CAAC;AAE/C,sCAAsC,CAAC,gCAAgC,CAAC,CAAC","sourcesContent":["// Generated by standards/scripts/generate-conformance-current-release.mjs.\n// Each exact package release verifies its own current runtime contract.\nimport {\n type BrandlabRuntimeReleaseContract,\n validateBrandlabRuntimeReleaseContract,\n} from \"./release-manifest.js\";\n\nexport const BRANDLAB_CURRENT_RUNTIME_RELEASE = Object.freeze(\n{\n \"capabilities\": [\n \"typed-project-business-and-route-data\",\n \"essential-search-foundations\",\n \"shared-contact-delivery\",\n \"privacy-safe-lead-analytics\",\n \"security-patched-runtime\",\n \"live-google-review-summary\",\n \"high-signal-project-verification\",\n \"packed-positive-and-negative-consumers\",\n \"changesets-oidc-release\"\n ],\n \"contracts\": {\n \"analytics\": \"analytics-v4\",\n \"architecture\": \"architecture-v7\",\n \"configuration\": \"project-config-v7\",\n \"contact\": \"contact-v4\",\n \"seo\": \"seo-v4\"\n },\n \"kind\": \"brandlab-runtime-release-contract\",\n \"packages\": {\n \"@brandlab-digital/conformance\": {\n \"version\": \"3.6.0\"\n },\n \"@brandlab-digital/eslint-plugin\": {\n \"version\": \"3.6.0\"\n },\n \"@brandlab-digital/site-core\": {\n \"version\": \"3.6.0\"\n },\n \"@brandlab-digital/site-next\": {\n \"version\": \"3.6.0\"\n }\n },\n \"releaseVersion\": \"3.6.0\",\n \"runtime\": {\n \"bundler\": \"turbopack\",\n \"cacheComponents\": false,\n \"next\": \"16.3.8\",\n \"nextVersionPolicy\": \"exact\",\n \"node\": \"24.x\",\n \"packageManager\": \"npm@11.14.1\",\n \"react\": \"19.2.8\",\n \"reactDom\": \"19.2.8\",\n \"router\": \"app\",\n \"sharp\": \"0.35.5\",\n \"typescript\": \"6.0.3\"\n },\n \"schemaVersion\": \"brandlab.runtime-release-contract.v1\"\n},\n) as unknown as BrandlabRuntimeReleaseContract;\n\nvalidateBrandlabRuntimeReleaseContract(BRANDLAB_CURRENT_RUNTIME_RELEASE);\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engineering.d.ts","sourceRoot":"","sources":["../src/engineering.ts"],"names":[],"mappings":"AAmBA,eAAO,MAAM,yBAAyB,sGAAuG,CAAC;AAE9I,oFAAoF;AACpF,eAAO,MAAM,+BAA+B,4IAC+F,CAAC;AAE5I,iFAAiF;AACjF,eAAO,MAAM,yBAAyB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CASrE,CAAC;AAEH,MAAM,MAAM,0BAA0B,GAAG,SAAS,GAAG,MAAM,CAAC;AAC5D,MAAM,WAAW,kBAAkB;IAAG,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,0BAA0B,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE;AACxI,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,QAAQ,EAAE,kBAAkB,EAAE,CAAC;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,mBAAmB,EAAE,SAAS,MAAM,EAAE,CAAC;CACxC;AAqBD,yEAAyE;AACzE,eAAO,MAAM,wBAAwB,SAAS,CAAC;AAC/C;;;;;GAKG;AACH,eAAO,MAAM,8BAA8B,iOAC6K,CAAC;
|
|
1
|
+
{"version":3,"file":"engineering.d.ts","sourceRoot":"","sources":["../src/engineering.ts"],"names":[],"mappings":"AAmBA,eAAO,MAAM,yBAAyB,sGAAuG,CAAC;AAE9I,oFAAoF;AACpF,eAAO,MAAM,+BAA+B,4IAC+F,CAAC;AAE5I,iFAAiF;AACjF,eAAO,MAAM,yBAAyB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CASrE,CAAC;AAEH,MAAM,MAAM,0BAA0B,GAAG,SAAS,GAAG,MAAM,CAAC;AAC5D,MAAM,WAAW,kBAAkB;IAAG,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,0BAA0B,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE;AACxI,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,QAAQ,EAAE,kBAAkB,EAAE,CAAC;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,mBAAmB,EAAE,SAAS,MAAM,EAAE,CAAC;CACxC;AAqBD,yEAAyE;AACzE,eAAO,MAAM,wBAAwB,SAAS,CAAC;AAC/C;;;;;GAKG;AACH,eAAO,MAAM,8BAA8B,iOAC6K,CAAC;AAqBzN,iGAAiG;AACjG,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,CAyBxE;AAwCD,uDAAuD;AACvD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAE3D;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,MAAM,EAAE,GAAG,IAAI,CA2B5G"}
|
package/dist/engineering.js
CHANGED
|
@@ -48,12 +48,14 @@ const BUSINESS_FACT_CALLS = /\b(?:defineBrandlabBusiness|verifiedFact|unknownFac
|
|
|
48
48
|
const GEOGRAPHY_ENTRY = '@brandlab-digital/site-core/geography';
|
|
49
49
|
/**
|
|
50
50
|
* Package entries a browser bundle cannot resolve from 3.3.3 (their exports map `browser` to null): the geography
|
|
51
|
-
* registries and website-location policies, which the location estate reads, and the server halves of contact and reviews
|
|
51
|
+
* registries and website-location policies, which the location estate reads, and the server halves of contact and reviews
|
|
52
|
+
* (3.5.0 adds the Business Profile reviews feed client).
|
|
52
53
|
*/
|
|
53
54
|
const SERVER_PACKAGE_ENTRIES = new Map([
|
|
54
55
|
[GEOGRAPHY_ENTRY, 'The Brandlab geography registry and website-location policy (server data, about 190 KB)'],
|
|
55
56
|
['@brandlab-digital/site-next/contact/server', 'The contact route runtime'],
|
|
56
57
|
['@brandlab-digital/site-next/google-reviews/server', 'The Google reviews route runtime'],
|
|
58
|
+
['@brandlab-digital/site-next/google-business-reviews/server', 'The Business Profile reviews feed client (it reads the feed token)'],
|
|
57
59
|
]);
|
|
58
60
|
/** Opt-in structural check. Static success must never masquerade as whole-project acceptance. */
|
|
59
61
|
export function checkEngineeringStructure(root) {
|