Skip to content

Commit ef31350

Browse files
committed
chore: debug and test env vars
1 parent 10611ac commit ef31350

5 files changed

Lines changed: 191 additions & 31 deletions

File tree

AGENTS.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Tileserver
2+
3+
Vector tile server wrapping `tileserver-gl-light`. Pulls tilesets (mbtiles) and styles (maplibre) from a `data-fair/registry` instance at boot, then serves tiles, styles, fonts, and sprites over HTTP.
4+
5+
## Stack
6+
7+
- **Runtime**: Node.js with TypeScript (ts extensions, no build step — run directly via `node --experimental-strip-types`)
8+
- **Framework**: Express 5
9+
- **Tile engine**: `tileserver-gl-light` (started on ephemeral port, inner server closed, express app reused as middleware)
10+
- **Config validation**: Zod (`src/config.ts` parses `process.env`)
11+
- **Tests**: Playwright Test (not browser tests — API + unit tests using playwright's runner)
12+
- **Lint**: ESLint via neostandard
13+
- **Commits**: Conventional commits enforced by commitlint + husky
14+
15+
## Project layout
16+
17+
```
18+
src/
19+
index.ts # Entrypoint — starts server, handles signals
20+
config.ts # Zod env parsing, exports typed config singleton
21+
log.ts # Minimal leveled logger (trace/debug/info/warn/error)
22+
server.ts # HTTP server lifecycle (start/stop)
23+
app.ts # Express app — helmet, health endpoint, tileserver-gl middleware
24+
build-config.ts # Fetches artefacts from registry, applies include/exclude/aliases, writes tileserver-gl config.json
25+
registry-client.ts # Axios wrapper for registry API
26+
style-normalize.ts # Rewrites style.json (sources, glyphs, sprites) for local serving
27+
tests/
28+
*.api.spec.ts # Integration tests (hit running tileserver)
29+
*.unit.spec.ts # Unit tests (no server needed)
30+
support/
31+
global-setup.ts # Spawns mock registry + tileserver process in tmpdir
32+
global-teardown.ts
33+
mock-registry.ts # Express mock of the registry API
34+
fixtures.ts # Builds test mbtiles and style tarballs
35+
fixtures/ # Static fixture data (style JSON, etc.)
36+
```
37+
38+
## Key commands
39+
40+
```bash
41+
npm run dev # Start dev server with nodemon
42+
npm run dev-deps # Start docker dependencies
43+
npm test # Run all tests (playwright test)
44+
npm run lint # ESLint
45+
npm run check-types # tsc --noEmit
46+
npm run quality # lint + types + tests
47+
```
48+
49+
## Environment variables
50+
51+
Core: `REGISTRY_URL`, `REGISTRY_SECRET`, `DATA_DIR`, `FONTS_DIR`, `PORT`, `LOG_LEVEL`.
52+
53+
Filtering & aliases (comma-separated):
54+
- `TILESET_INCLUDE` / `TILESET_EXCLUDE` — whitelist/blacklist tileset IDs
55+
- `TILESET_ALIASES``source:alias` pairs to remap tileset serving keys
56+
- `STYLE_INCLUDE` / `STYLE_EXCLUDE` / `STYLE_ALIASES` — same for styles
57+
58+
## Testing
59+
60+
Tests use a real tileserver process spawned against a mock registry (see `tests/support/global-setup.ts`). The mock serves fixture mbtiles and style tarballs. Test logs go to `dev/logs/test-tileserver.log`.
61+
62+
To add env-var-dependent tests (include/exclude/aliases), pass the relevant env vars in the `global-setup.ts` spawn call.
63+
64+
## Imports
65+
66+
Uses Node.js subpath imports (`#config`, `#log`) defined in `package.json` `"imports"` field.

src/build-config.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import config from '#config'
66
import log from '#log'
77
import { listArtefacts, type Artefact } from './registry-client.ts'
88
import { normalizeStyle } from './style-normalize.ts'
9+
import { filterArtefacts } from './config-parse.ts'
910

1011
export interface TileserverConfig {
1112
options: {
@@ -24,13 +25,6 @@ export interface TileserverConfig {
2425
data: Record<string, { mbtiles: string }>
2526
}
2627

27-
const filterArtefacts = (artefacts: Artefact[], include: string[], exclude: string[]): Artefact[] => {
28-
let filtered = artefacts
29-
if (include.length) filtered = filtered.filter(a => include.includes(a._id))
30-
if (exclude.length) filtered = filtered.filter(a => !exclude.includes(a._id))
31-
return filtered
32-
}
33-
3428
const stylePackageName = (a: Artefact): string => {
3529
const name = a.name.replace(/^@[^/]+\//, '')
3630
return name.replace(/[^a-z0-9_-]/gi, '-')
@@ -57,9 +51,13 @@ export const buildTileserverConfig = async (): Promise<TileserverConfig> => {
5751
const styles = await listArtefacts({ category: 'maplibre-style', format: 'file' })
5852
log.info(`found ${styles.length} styles`)
5953

54+
if (config.tilesetInclude.length) log.info(`TILESET_INCLUDE: ${config.tilesetInclude.join(', ')}`)
55+
if (config.tilesetExclude.length) log.info(`TILESET_EXCLUDE: ${config.tilesetExclude.join(', ')}`)
56+
if (Object.keys(config.tilesetAliases).length) log.info(`TILESET_ALIASES: ${Object.entries(config.tilesetAliases).map(([s, a]) => `${s}:${a}`).join(', ')}`)
57+
6058
const filteredTilesets = filterArtefacts(tilesets, config.tilesetInclude, config.tilesetExclude)
6159
if (filteredTilesets.length !== tilesets.length) {
62-
log.info(`filtered to ${filteredTilesets.length} tilesets`)
60+
log.info(`filtered to ${filteredTilesets.length} tilesets: ${filteredTilesets.map(t => t._id).join(', ')}`)
6361
}
6462

6563
const data: TileserverConfig['data'] = {}
@@ -79,9 +77,13 @@ export const buildTileserverConfig = async (): Promise<TileserverConfig> => {
7977
tilesetIds.add(dataKey)
8078
}
8179

80+
if (config.styleInclude.length) log.info(`STYLE_INCLUDE: ${config.styleInclude.join(', ')}`)
81+
if (config.styleExclude.length) log.info(`STYLE_EXCLUDE: ${config.styleExclude.join(', ')}`)
82+
if (Object.keys(config.styleAliases).length) log.info(`STYLE_ALIASES: ${Object.entries(config.styleAliases).map(([s, a]) => `${s}:${a}`).join(', ')}`)
83+
8284
const filteredStyles = filterArtefacts(styles, config.styleInclude, config.styleExclude)
8385
if (filteredStyles.length !== styles.length) {
84-
log.info(`filtered to ${filteredStyles.length} styles`)
86+
log.info(`filtered to ${filteredStyles.length} styles: ${filteredStyles.map(s => s._id).join(', ')}`)
8587
}
8688

8789
const stylesCfg: TileserverConfig['styles'] = {}

src/config-parse.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export const parseList = (v: string): string[] =>
2+
v ? v.split(',').map(s => s.trim()).filter(Boolean) : []
3+
4+
export const parseAliases = (v: string): Record<string, string> => {
5+
if (!v) return {}
6+
const map: Record<string, string> = {}
7+
for (const pair of v.split(',')) {
8+
const [source, alias] = pair.split(':').map(s => s.trim())
9+
if (source && alias) map[source] = alias
10+
}
11+
return map
12+
}
13+
14+
export const filterArtefacts = <T extends { _id: string }>(artefacts: T[], include: string[], exclude: string[]): T[] => {
15+
let filtered = artefacts
16+
if (include.length) filtered = filtered.filter(a => include.includes(a._id))
17+
if (exclude.length) filtered = filtered.filter(a => !exclude.includes(a._id))
18+
return filtered
19+
}

src/config.ts

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { z } from 'zod'
2+
import { parseList, parseAliases } from './config-parse.ts'
23

34
const EnvSchema = z.object({
45
REGISTRY_URL: z.string().url(),
@@ -9,28 +10,12 @@ const EnvSchema = z.object({
910
LOG_LEVEL: z.enum(['trace', 'debug', 'info', 'warn', 'error']).default('info'),
1011
OBSERVER_ACTIVE: z.enum(['true', 'false']).default('true').transform(v => v === 'true'),
1112
OBSERVER_PORT: z.coerce.number().int().positive().default(9090),
12-
TILESET_INCLUDE: z.string().default('').transform(v => v ? v.split(',').map(s => s.trim()).filter(Boolean) : []),
13-
TILESET_EXCLUDE: z.string().default('').transform(v => v ? v.split(',').map(s => s.trim()).filter(Boolean) : []),
14-
TILESET_ALIASES: z.string().default('').transform(v => {
15-
if (!v) return {} as Record<string, string>
16-
const map: Record<string, string> = {}
17-
for (const pair of v.split(',')) {
18-
const [source, alias] = pair.split(':').map(s => s.trim())
19-
if (source && alias) map[source] = alias
20-
}
21-
return map
22-
}),
23-
STYLE_INCLUDE: z.string().default('').transform(v => v ? v.split(',').map(s => s.trim()).filter(Boolean) : []),
24-
STYLE_EXCLUDE: z.string().default('').transform(v => v ? v.split(',').map(s => s.trim()).filter(Boolean) : []),
25-
STYLE_ALIASES: z.string().default('').transform(v => {
26-
if (!v) return {} as Record<string, string>
27-
const map: Record<string, string> = {}
28-
for (const pair of v.split(',')) {
29-
const [source, alias] = pair.split(':').map(s => s.trim())
30-
if (source && alias) map[source] = alias
31-
}
32-
return map
33-
})
13+
TILESET_INCLUDE: z.string().default('').transform(parseList),
14+
TILESET_EXCLUDE: z.string().default('').transform(parseList),
15+
TILESET_ALIASES: z.string().default('').transform(parseAliases),
16+
STYLE_INCLUDE: z.string().default('').transform(parseList),
17+
STYLE_EXCLUDE: z.string().default('').transform(parseList),
18+
STYLE_ALIASES: z.string().default('').transform(parseAliases)
3419
})
3520

3621
const parsed = EnvSchema.safeParse(process.env)

tests/filter-aliases.unit.spec.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { test, expect } from '@playwright/test'
2+
import { parseList, parseAliases, filterArtefacts } from '../src/config-parse.ts'
3+
4+
const artefact = (id: string) => ({ _id: id, name: id, category: 'tileset', format: 'file' })
5+
6+
test.describe('parseList', () => {
7+
test('returns empty array for empty string', () => {
8+
expect(parseList('')).toEqual([])
9+
})
10+
11+
test('splits comma-separated values', () => {
12+
expect(parseList('a,b,c')).toEqual(['a', 'b', 'c'])
13+
})
14+
15+
test('trims whitespace', () => {
16+
expect(parseList(' a , b , c ')).toEqual(['a', 'b', 'c'])
17+
})
18+
19+
test('filters out empty segments', () => {
20+
expect(parseList('a,,b,')).toEqual(['a', 'b'])
21+
})
22+
})
23+
24+
test.describe('parseAliases', () => {
25+
test('returns empty object for empty string', () => {
26+
expect(parseAliases('')).toEqual({})
27+
})
28+
29+
test('parses source:alias pairs', () => {
30+
expect(parseAliases('world:france')).toEqual({ world: 'france' })
31+
})
32+
33+
test('parses multiple pairs', () => {
34+
expect(parseAliases('world:france,contours-2026:contours')).toEqual({
35+
world: 'france',
36+
'contours-2026': 'contours'
37+
})
38+
})
39+
40+
test('trims whitespace around keys and values', () => {
41+
expect(parseAliases(' world : france , foo : bar ')).toEqual({
42+
world: 'france',
43+
foo: 'bar'
44+
})
45+
})
46+
47+
test('skips malformed entries without colon', () => {
48+
expect(parseAliases('good:pair,nocolon,also:fine')).toEqual({
49+
good: 'pair',
50+
also: 'fine'
51+
})
52+
})
53+
54+
test('skips entries with empty source or alias', () => {
55+
expect(parseAliases(':alias,source:,ok:ok')).toEqual({ ok: 'ok' })
56+
})
57+
})
58+
59+
test.describe('filterArtefacts', () => {
60+
const all = [artefact('a'), artefact('b'), artefact('c')]
61+
62+
test('returns all when include and exclude are empty', () => {
63+
expect(filterArtefacts(all, [], [])).toEqual(all)
64+
})
65+
66+
test('include keeps only matching artefacts', () => {
67+
const result = filterArtefacts(all, ['a', 'c'], [])
68+
expect(result.map(a => a._id)).toEqual(['a', 'c'])
69+
})
70+
71+
test('exclude removes matching artefacts', () => {
72+
const result = filterArtefacts(all, [], ['b'])
73+
expect(result.map(a => a._id)).toEqual(['a', 'c'])
74+
})
75+
76+
test('include and exclude combine (include first, then exclude)', () => {
77+
const result = filterArtefacts(all, ['a', 'b'], ['b'])
78+
expect(result.map(a => a._id)).toEqual(['a'])
79+
})
80+
81+
test('include with no matches returns empty', () => {
82+
expect(filterArtefacts(all, ['x'], [])).toEqual([])
83+
})
84+
85+
test('exclude with no matches returns all', () => {
86+
expect(filterArtefacts(all, [], ['x'])).toEqual(all)
87+
})
88+
})

0 commit comments

Comments
 (0)