@mapsight/traffic-style 5.0.0 → 5.0.1

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/meta.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mapsight/traffic-style",
3
- "version": "5.0.0",
3
+ "version": "5.0.1",
4
4
  "copyright": "Open Mapsight",
5
5
  "defaultIcon": "ort",
6
6
  "icons": [
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mapsight/traffic-style",
3
3
  "description": "Mapsight Traffic Style",
4
- "version": "5.0.0",
4
+ "version": "5.0.1",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "traffic-icon-sprite": "scripts/traffic-icon-sprite.ts",
@@ -47,7 +47,7 @@
47
47
  "build:styles": "vector-style-compiler src/scss/default.scss --output tmp/mapsight-vector-styles --name default && cp tmp/mapsight-vector-styles/default.css dist",
48
48
  "clean": "rimraf tmp/* dist/*",
49
49
  "clean-build": "run-s clean build",
50
- "copy": "run-p copy:src copy:misc",
50
+ "copy": "run-p copy:src copy:misc copy:scripts",
51
51
  "copy:misc": "cp package.json README.md *.scss dist/",
52
52
  "copy:scripts": "mkdirp dist; cp -r scripts dist/",
53
53
  "copy:src": "mkdirp dist/src; cp -r src/scss dist/src/",
@@ -0,0 +1,141 @@
1
+ import {readFile, readdir} from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ const colors = {
5
+ red: (str: string) => `\x1b[31m${str}\x1b[0m`,
6
+ yellow: (str: string) => `\x1b[33m${str}\x1b[0m`,
7
+ };
8
+
9
+ interface Icon {
10
+ fileName: string;
11
+ name: string;
12
+ variant: string;
13
+ suffix: string;
14
+ }
15
+
16
+ interface MetaIcon {
17
+ label?: {
18
+ de: string;
19
+ en: string;
20
+ };
21
+ aliases?: string[];
22
+ groups?: string[];
23
+ }
24
+
25
+ interface MetaData {
26
+ icons?: Record<string, MetaIcon>;
27
+ copyright?: string;
28
+ defaultIcon?: string;
29
+ }
30
+
31
+ interface PackageJson {
32
+ name: string;
33
+ version: string;
34
+ }
35
+
36
+ interface ResultIcon extends MetaIcon {
37
+ id: string;
38
+ variants: string[];
39
+ }
40
+
41
+ interface Result {
42
+ name: string;
43
+ version: string;
44
+ copyright: string;
45
+ defaultIcon?: string;
46
+ icons: ResultIcon[];
47
+ }
48
+
49
+ function parseIconName(fileName: string): Icon {
50
+ const parts = fileName.split(".");
51
+ if (parts.length < 2) {
52
+ throw new Error("Invalid file name: no extension");
53
+ }
54
+ const main = parts[0]!;
55
+ const suffix = parts[1]!;
56
+ const nameParts = main.split("-");
57
+ if (nameParts.length < 1) {
58
+ throw new Error("Invalid file name: no parts");
59
+ }
60
+ const variant = nameParts.pop()!;
61
+ const name = nameParts.join("-");
62
+
63
+ return {
64
+ fileName,
65
+ name,
66
+ variant,
67
+ suffix,
68
+ };
69
+ }
70
+
71
+ async function main() {
72
+ const argv = process.argv;
73
+ const [, , metaPath, srcPath] = argv;
74
+
75
+ if (!metaPath) {
76
+ throw new Error("argument metaPath missing");
77
+ }
78
+ if (!srcPath) {
79
+ throw new Error("argument srcPath missing");
80
+ }
81
+
82
+ const metaDataContent = await readFile(path.resolve(metaPath), "utf8");
83
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
84
+ const metaData: MetaData = JSON.parse(metaDataContent);
85
+
86
+ const packageJsonContent = await readFile("package.json", "utf8");
87
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
88
+ const packageJson: PackageJson = JSON.parse(packageJsonContent);
89
+
90
+ if (!metaData.icons) {
91
+ console.error(
92
+ colors.red("ERROR: No Icons field found in src/meta.json!"),
93
+ );
94
+ }
95
+
96
+ const result: Result = {
97
+ name: packageJson.name,
98
+ version: packageJson.version,
99
+ copyright: metaData.copyright || "Open Mapsight",
100
+ defaultIcon: metaData.defaultIcon,
101
+ icons: [],
102
+ };
103
+
104
+ const files = await readdir(srcPath);
105
+ const icons = files
106
+ .filter((fileName) => /(\.webp|\.png|\.svg)$/.test(fileName))
107
+ .map((iconFile) => parseIconName(iconFile));
108
+ const variants = [...new Set(icons.map(({variant}) => variant))];
109
+ const iconGroups = icons.reduce(
110
+ (groups, icon) => ({
111
+ ...groups,
112
+ [icon.name]: {
113
+ ...(groups[icon.name] || {}),
114
+ [icon.variant]: icon,
115
+ },
116
+ }),
117
+ {} as Record<string, Record<string, Icon>>,
118
+ );
119
+
120
+ Object.keys(iconGroups).forEach((name) => {
121
+ if (metaData.icons && !metaData.icons[name]) {
122
+ console.warn(
123
+ colors.yellow(
124
+ 'WARNING: Icon "' +
125
+ name +
126
+ '" is not specified in src/meta.json!',
127
+ ),
128
+ );
129
+ }
130
+
131
+ result.icons.push({
132
+ ...((metaData.icons && metaData.icons[name]) || {}),
133
+ id: name,
134
+ variants: variants,
135
+ });
136
+ });
137
+
138
+ console.log(JSON.stringify(result, null, 4));
139
+ }
140
+
141
+ main().catch(console.error);
@@ -0,0 +1,110 @@
1
+ import {readFile, readdir} from "fs/promises";
2
+
3
+ import table from "markdown-table";
4
+
5
+ interface Icon {
6
+ fileName: string;
7
+ name: string;
8
+ variant: string;
9
+ suffix: string;
10
+ }
11
+
12
+ interface MetaIcon {
13
+ label: {
14
+ de: string;
15
+ en: string;
16
+ };
17
+ aliases?: string[];
18
+ groups?: string[];
19
+ }
20
+
21
+ interface MetaData {
22
+ icons: Record<string, MetaIcon>;
23
+ }
24
+
25
+ function parseIconName(fileName: string): Icon {
26
+ const parts = fileName.split(".");
27
+ if (parts.length < 2) {
28
+ throw new Error("Invalid file name: no extension");
29
+ }
30
+ const [main = "", suffix = ""] = parts;
31
+ const nameParts = main.split("-");
32
+ if (nameParts.length < 1) {
33
+ throw new Error("Invalid file name: no variant");
34
+ }
35
+ const variant = nameParts.pop()!;
36
+ const name = nameParts.join("-");
37
+
38
+ return {
39
+ fileName,
40
+ name,
41
+ variant,
42
+ suffix,
43
+ };
44
+ }
45
+
46
+ function iconGroupsToRows(
47
+ variants: string[],
48
+ iconGroups: Record<string, Record<string, Icon>>,
49
+ imgPath: string,
50
+ metaData: MetaData,
51
+ ): string[][] {
52
+ return Object.keys(iconGroups).map((name) => [
53
+ name,
54
+ metaData.icons[name]?.label.de || "",
55
+ metaData.icons[name]?.label.en || "",
56
+ ...variants.map((variant) => {
57
+ const icon = iconGroups[name]?.[variant];
58
+
59
+ return icon
60
+ ? `![${icon.fileName}](${imgPath + icon.fileName})`
61
+ : "-/-";
62
+ }),
63
+ metaData.icons[name]?.aliases
64
+ ? JSON.stringify(metaData.icons[name].aliases)
65
+ : "",
66
+ metaData.icons[name]?.groups
67
+ ? JSON.stringify(metaData.icons[name].groups)
68
+ : "",
69
+ ]);
70
+ }
71
+
72
+ async function main() {
73
+ const argv = process.argv;
74
+ const [, , srcPath, imgPath] = argv;
75
+ if (!srcPath) {
76
+ throw new Error("argument srcPath missing");
77
+ }
78
+ if (!imgPath) {
79
+ throw new Error("argument imgPath missing");
80
+ }
81
+
82
+ const metaDataContent = await readFile("src/meta.json", "utf8");
83
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
84
+ const metaData: MetaData = JSON.parse(metaDataContent);
85
+
86
+ const files = await readdir(srcPath);
87
+ const icons = files
88
+ .filter((fileName) => /(\.webp|\.png|\.svg)$/.test(fileName))
89
+ .map((iconFile) => parseIconName(iconFile));
90
+ const variants = [...new Set(icons.map(({variant}) => variant))];
91
+ const iconGroups = icons.reduce(
92
+ (groups, icon) => ({
93
+ ...groups,
94
+ [icon.name]: {
95
+ ...(groups[icon.name] || {}),
96
+ [icon.variant]: icon,
97
+ },
98
+ }),
99
+ {} as Record<string, Record<string, Icon>>,
100
+ );
101
+
102
+ const iconTable = [
103
+ ["Name", "Label (de)", "Label (en)", ...variants, "Aliases", "Groups"],
104
+ ...iconGroupsToRows(variants, iconGroups, imgPath, metaData),
105
+ ];
106
+
107
+ console.log(table(iconTable));
108
+ }
109
+
110
+ main().catch(console.error);
@@ -0,0 +1,40 @@
1
+ import {z} from "zod/v4";
2
+
3
+ export type IconVariant = z.infer<typeof IconVariantSchema>;
4
+ export const IconVariantSchema = z.stringFormat(
5
+ "mapsight-icon-variant",
6
+ /[a-z]+/,
7
+ );
8
+
9
+ export type IconId = z.infer<typeof IconIdSchema>;
10
+ export const IconIdSchema = z.stringFormat("mapsight-icon-id", /[0-9a-z-]+/);
11
+
12
+ export type LangCode = z.infer<typeof LangCodeSchema>;
13
+ export const LangCodeSchema = z.stringFormat("mapsight-lang-code", /[a-z]{2}/);
14
+
15
+ export type IconGroupName = z.infer<typeof IconGroupNameSchema>;
16
+ export const IconGroupNameSchema = z.stringFormat(
17
+ "mapsight-icon-group",
18
+ /[0-9a-z-]+/,
19
+ );
20
+
21
+ export type IconMeta = z.infer<typeof IconMetaSchema>;
22
+ export const IconMetaSchema = z.object({
23
+ // The label object has dynamic keys for languages (e.g., "de", "en", "en_US")
24
+ // and string values for the text.
25
+ id: IconIdSchema,
26
+ label: z.record(LangCodeSchema, z.string()).optional(),
27
+ aliases: z.array(IconIdSchema).optional(),
28
+ groups: z.array(IconGroupNameSchema).optional(),
29
+ // The fallback field is optional.
30
+ fallback: IconIdSchema.optional(),
31
+ });
32
+
33
+ export type MetaData = z.infer<typeof DistMetaDataSchema>;
34
+ export const DistMetaDataSchema = z.object({
35
+ name: z.string(),
36
+ version: z.string(),
37
+ copyright: z.string(),
38
+ defaultIcon: IconIdSchema,
39
+ icons: z.array(IconMetaSchema),
40
+ });
@@ -0,0 +1,234 @@
1
+ import {createHash} from "node:crypto";
2
+ import {mkdir, readFile, rm, stat, unlink, writeFile} from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import {fileURLToPath} from "node:url";
6
+
7
+ import {afterEach, beforeEach, describe, expect, it, vi} from "vitest";
8
+
9
+ import {main} from "./optimize-icons";
10
+
11
+ describe("optimize-icons.ts", () => {
12
+ let tmpDir: string;
13
+ let srcDir: string;
14
+ let destDir: string;
15
+
16
+ beforeEach(async () => {
17
+ tmpDir = path.join(
18
+ os.tmpdir(),
19
+ `optimize-icons-test-${Math.random().toString(36).slice(2)}`,
20
+ );
21
+ srcDir = path.join(tmpDir, "src");
22
+ destDir = path.join(tmpDir, "dest");
23
+ await mkdir(srcDir, {recursive: true});
24
+ await mkdir(destDir, {recursive: true});
25
+
26
+ // Mock process.exitCode to avoid affecting the test runner
27
+ vi.stubGlobal("process", {
28
+ ...process,
29
+ exitCode: 0,
30
+ });
31
+ });
32
+
33
+ afterEach(async () => {
34
+ if (srcDir && destDir) {
35
+ try {
36
+ const manifestPath = getManifestPath(srcDir, destDir);
37
+ await unlink(manifestPath);
38
+ } catch {
39
+ // ignore if manifest doesn't exist
40
+ }
41
+ }
42
+ if (tmpDir) {
43
+ await rm(tmpDir, {recursive: true, force: true});
44
+ }
45
+ vi.unstubAllGlobals();
46
+ });
47
+
48
+ async function runScript(args: Record<string, string | boolean>) {
49
+ const argv = ["node", "optimize-icons.ts"];
50
+ for (const [key, value] of Object.entries(args)) {
51
+ if (typeof value === "boolean") {
52
+ if (value) argv.push(`--${key}`);
53
+ } else {
54
+ argv.push(`--${key}`, String(value));
55
+ }
56
+ }
57
+
58
+ vi.stubGlobal("process", {
59
+ ...process,
60
+ argv,
61
+ });
62
+
63
+ await main();
64
+ }
65
+
66
+ function getManifestPath(src: string, dest: string) {
67
+ const scriptPath = fileURLToPath(import.meta.url);
68
+ const packageRoot = path.resolve(path.dirname(scriptPath), "..");
69
+ const hash = createHash("sha256")
70
+ .update(`${path.resolve(src)}:${path.resolve(dest)}`)
71
+ .digest("hex")
72
+ .slice(0, 16);
73
+ return path.join(
74
+ packageRoot,
75
+ "tmp",
76
+ `.optimize-icons-manifest-${hash}.json`,
77
+ );
78
+ }
79
+
80
+ it("should process an SVG file and produce SVG, PNG, and WebP outputs", async () => {
81
+ const svgContent = `<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><rect width="100" height="100" fill="red"/></svg>`;
82
+ await writeFile(path.join(srcDir, "test.svg"), svgContent);
83
+
84
+ await runScript({
85
+ src: srcDir,
86
+ dest: destDir,
87
+ });
88
+
89
+ expect(await stat(path.join(destDir, "test.svg"))).toBeDefined();
90
+ expect(await stat(path.join(destDir, "test.png"))).toBeDefined();
91
+ expect(await stat(path.join(destDir, "test.webp"))).toBeDefined();
92
+
93
+ const manifestPath = getManifestPath(srcDir, destDir);
94
+ const manifestRaw = await readFile(manifestPath, "utf8");
95
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
96
+ const outputs = JSON.parse(manifestRaw).files["test.svg"].outputs as
97
+ | string[]
98
+ | undefined;
99
+ expect(outputs).toBeDefined();
100
+ expect(outputs).toContain("test.svg");
101
+ expect(outputs).toContain("test.png");
102
+ expect(outputs).toContain("test.webp");
103
+ });
104
+
105
+ it("should process a PNG file and produce PNG and WebP outputs", async () => {
106
+ // Create a tiny valid PNG using sharp if possible, or just a placeholder if we skip actual image check
107
+ // Since we have sharp available in the environment, let's use it if we can, but simpler is to use a minimal PNG buffer.
108
+ const sharp = (await import("sharp")).default;
109
+ const pngBuffer = await sharp({
110
+ create: {
111
+ width: 10,
112
+ height: 10,
113
+ channels: 4,
114
+ background: {r: 255, g: 0, b: 0, alpha: 0.5},
115
+ },
116
+ })
117
+ .png()
118
+ .toBuffer();
119
+
120
+ await writeFile(path.join(srcDir, "test.png"), pngBuffer);
121
+
122
+ await runScript({
123
+ src: srcDir,
124
+ dest: destDir,
125
+ });
126
+
127
+ expect(await stat(path.join(destDir, "test.png"))).toBeDefined();
128
+ expect(await stat(path.join(destDir, "test.webp"))).toBeDefined();
129
+ await expect(
130
+ async () => await stat(path.join(destDir, "test.svg")),
131
+ ).rejects.toThrow();
132
+ });
133
+
134
+ it("should scale images when --scale is provided", async () => {
135
+ const svgContent = `<svg width="10" height="10" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><rect width="100" height="100" fill="red"/></svg>`;
136
+ await writeFile(path.join(srcDir, "scale-test.svg"), svgContent);
137
+
138
+ await runScript({
139
+ src: srcDir,
140
+ dest: destDir,
141
+ scale: "2",
142
+ });
143
+
144
+ const sharp = (await import("sharp")).default;
145
+ const metadata = await sharp(
146
+ path.join(destDir, "scale-test.png"),
147
+ ).metadata();
148
+ expect(metadata.width).toBe(20);
149
+ expect(metadata.height).toBe(20);
150
+ });
151
+
152
+ it("should skip processing if files haven't changed", async () => {
153
+ const svgContent = `<svg width="10" height="10" xmlns="http://www.w3.org/2000/svg"><rect width="10" height="10" fill="red"/></svg>`;
154
+ const testFile = path.join(srcDir, "skip-test.svg");
155
+ await writeFile(testFile, svgContent);
156
+
157
+ // First run
158
+ await runScript({
159
+ src: srcDir,
160
+ dest: destDir,
161
+ verbose: true,
162
+ });
163
+
164
+ const destFile = path.join(destDir, "skip-test.png");
165
+ const firstStat = await stat(destFile);
166
+
167
+ // Second run
168
+ await runScript({
169
+ src: srcDir,
170
+ dest: destDir,
171
+ verbose: true,
172
+ });
173
+
174
+ const secondStat = await stat(destFile);
175
+ expect(secondStat.mtimeMs).toBe(firstStat.mtimeMs);
176
+ });
177
+
178
+ it("should only produce specified targets when --target is provided", async () => {
179
+ const svgContent = `<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg"><rect width="100" height="100" fill="red"/></svg>`;
180
+ await writeFile(path.join(srcDir, "target-test.svg"), svgContent);
181
+
182
+ await runScript({
183
+ src: srcDir,
184
+ dest: destDir,
185
+ target: "png",
186
+ });
187
+
188
+ expect(await stat(path.join(destDir, "target-test.png"))).toBeDefined();
189
+ await expect(
190
+ async () => await stat(path.join(destDir, "target-test.svg")),
191
+ ).rejects.toThrow();
192
+ await expect(
193
+ async () => await stat(path.join(destDir, "target-test.webp")),
194
+ ).rejects.toThrow();
195
+
196
+ const manifestPath = getManifestPath(srcDir, destDir);
197
+ const manifestRaw = await readFile(manifestPath, "utf8");
198
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
199
+ const outputs = JSON.parse(manifestRaw).files["target-test.svg"]
200
+ .outputs as string[];
201
+ expect(outputs).toEqual(["target-test.png"]);
202
+ });
203
+
204
+ it("should produce multiple specified targets", async () => {
205
+ const svgContent = `<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg"><rect width="100" height="100" fill="red"/></svg>`;
206
+ await writeFile(path.join(srcDir, "multi-target.svg"), svgContent);
207
+
208
+ await runScript({
209
+ src: srcDir,
210
+ dest: destDir,
211
+ target: "svg,webp",
212
+ });
213
+
214
+ expect(
215
+ await stat(path.join(destDir, "multi-target.svg")),
216
+ ).toBeDefined();
217
+ expect(
218
+ await stat(path.join(destDir, "multi-target.webp")),
219
+ ).toBeDefined();
220
+ await expect(
221
+ async () => await stat(path.join(destDir, "multi-target.png")),
222
+ ).rejects.toThrow();
223
+ });
224
+
225
+ it("should fail if an invalid target is provided", async () => {
226
+ await expect(
227
+ runScript({
228
+ src: srcDir,
229
+ dest: destDir,
230
+ target: "invalid",
231
+ }),
232
+ ).rejects.toThrow("Invalid target format: invalid");
233
+ });
234
+ });