@osmix/geoparquet 0.1.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +47 -0
  3. package/dist/src/from-geoparquet.d.ts +68 -0
  4. package/dist/src/from-geoparquet.d.ts.map +1 -0
  5. package/dist/src/from-geoparquet.js +455 -0
  6. package/dist/src/from-geoparquet.js.map +1 -0
  7. package/dist/src/index.d.ts +27 -0
  8. package/dist/src/index.d.ts.map +1 -0
  9. package/dist/src/index.js +27 -0
  10. package/dist/src/index.js.map +1 -0
  11. package/dist/src/types.d.ts +47 -0
  12. package/dist/src/types.d.ts.map +1 -0
  13. package/dist/src/types.js +6 -0
  14. package/dist/src/types.js.map +1 -0
  15. package/dist/src/wkb.d.ts +22 -0
  16. package/dist/src/wkb.d.ts.map +1 -0
  17. package/dist/src/wkb.js +181 -0
  18. package/dist/src/wkb.js.map +1 -0
  19. package/dist/test/from-geoparquet.test.d.ts +2 -0
  20. package/dist/test/from-geoparquet.test.d.ts.map +1 -0
  21. package/dist/test/from-geoparquet.test.js +445 -0
  22. package/dist/test/from-geoparquet.test.js.map +1 -0
  23. package/dist/test/monaco-parquet.test.d.ts +2 -0
  24. package/dist/test/monaco-parquet.test.d.ts.map +1 -0
  25. package/dist/test/monaco-parquet.test.js +200 -0
  26. package/dist/test/monaco-parquet.test.js.map +1 -0
  27. package/dist/test/wkb.test.d.ts +2 -0
  28. package/dist/test/wkb.test.d.ts.map +1 -0
  29. package/dist/test/wkb.test.js +234 -0
  30. package/dist/test/wkb.test.js.map +1 -0
  31. package/package.json +53 -0
  32. package/src/from-geoparquet.ts +565 -0
  33. package/src/index.ts +27 -0
  34. package/src/types.ts +51 -0
  35. package/src/wkb.ts +218 -0
  36. package/test/download-monaco-highways.sh +40 -0
  37. package/test/from-geoparquet.test.ts +520 -0
  38. package/test/monaco-parquet.test.ts +249 -0
  39. package/test/wkb.test.ts +296 -0
  40. package/tsconfig.json +9 -0
@@ -0,0 +1,249 @@
1
+ import { describe, expect, it } from "bun:test"
2
+ import { getFixturePath } from "@osmix/shared/test/fixtures"
3
+ import { fromGeoParquet, GeoParquetOsmBuilder } from "../src"
4
+
5
+ /**
6
+ * Integration tests using the Monaco highways GeoParquet fixture.
7
+ *
8
+ * The fixture was downloaded from LayerCake (https://openstreetmap.us/our-work/layercake/)
9
+ * using DuckDB with a bounding box filter for Monaco. The fixture has a flattened
10
+ * structure where each tag column is separate (highway, name, surface, etc.) rather
11
+ * than nested in a tags struct.
12
+ *
13
+ * Note: hyparquet automatically parses GeoParquet WKB geometry into GeoJSON objects.
14
+ *
15
+ * Monaco bounding box:
16
+ * - xmin: 7.409205, xmax: 7.448637
17
+ * - ymin: 43.72335, ymax: 43.75169
18
+ */
19
+
20
+ describe("@osmix/geoparquet: Monaco highways fixture", () => {
21
+ const fixtureFile = () => Bun.file(getFixturePath("monaco.parquet"))
22
+ const getOsm = async () => fromGeoParquet(await fixtureFile().arrayBuffer())
23
+
24
+ it("should load the monaco.parquet fixture", async () => {
25
+ const file = fixtureFile()
26
+ expect(await file.exists()).toBe(true)
27
+
28
+ const size = file.size
29
+ expect(size).toBeGreaterThan(0)
30
+ expect(size).toBeLessThan(1_000_000) // Should be a small fixture
31
+ })
32
+
33
+ it("should read parquet rows with correct structure", async () => {
34
+ const builder = new GeoParquetOsmBuilder({ id: "monaco" }, { rowEnd: 5 })
35
+ const rows = await builder.readParquetRows(
36
+ await fixtureFile().arrayBuffer(),
37
+ )
38
+
39
+ expect(rows.length).toBe(5)
40
+
41
+ // Verify row structure
42
+ const firstRow = rows[0]!
43
+ expect(firstRow["id"]).toBeDefined()
44
+ expect(firstRow["geometry"]).toBeDefined()
45
+ // expect(firstRow.osm_type).toBeDefined()
46
+
47
+ // hyparquet parses GeoParquet WKB into GeoJSON automatically
48
+ expect(firstRow["geometry"].type).toBeDefined()
49
+ // Verify it's a geometry with coordinates (not a GeometryCollection)
50
+ expect(["Point", "LineString", "Polygon", "MultiPolygon"]).toContain(
51
+ firstRow["geometry"].type,
52
+ )
53
+ })
54
+
55
+ it("should convert highway features to OSM entities", async () => {
56
+ const osm = await getOsm()
57
+
58
+ // Should have created nodes and ways from the highway features
59
+ expect(osm.nodes.size).toBeGreaterThan(0)
60
+ expect(osm.ways.size).toBeGreaterThan(0)
61
+
62
+ // Monaco is small, so we expect reasonable numbers
63
+ // The highway extract should have a few hundred to a few thousand ways
64
+ expect(osm.ways.size).toBeGreaterThan(100)
65
+ expect(osm.ways.size).toBeLessThan(10_000)
66
+ })
67
+
68
+ it("should preserve highway tags from GeoParquet columns", async () => {
69
+ const osm = await getOsm()
70
+
71
+ // Find ways with highway tags
72
+ let highwayCount = 0
73
+ for (let i = 0; i < osm.ways.size; i++) {
74
+ const way = osm.ways.getByIndex(i)
75
+ if (way?.tags?.["highway"]) {
76
+ highwayCount++
77
+ }
78
+ }
79
+
80
+ // Most features should have highway tags since this is the highways layer
81
+ expect(highwayCount).toBeGreaterThan(0)
82
+ })
83
+
84
+ it("should have valid node coordinates within Monaco bounds", async () => {
85
+ const osm = await getOsm()
86
+
87
+ // Monaco bounding box (slightly expanded for node positions)
88
+ const minLon = 7.4
89
+ const maxLon = 7.45
90
+ const minLat = 43.72
91
+ const maxLat = 43.76
92
+
93
+ // Check that all nodes are within Monaco bounds
94
+ let validNodes = 0
95
+ for (let i = 0; i < osm.nodes.size; i++) {
96
+ const node = osm.nodes.getByIndex(i)
97
+ if (node) {
98
+ expect(node.lon).toBeGreaterThanOrEqual(minLon)
99
+ expect(node.lon).toBeLessThanOrEqual(maxLon)
100
+ expect(node.lat).toBeGreaterThanOrEqual(minLat)
101
+ expect(node.lat).toBeLessThanOrEqual(maxLat)
102
+ validNodes++
103
+ }
104
+ }
105
+
106
+ expect(validNodes).toBeGreaterThan(0)
107
+ })
108
+
109
+ it("should build spatial indexes after loading", async () => {
110
+ const osm = await getOsm()
111
+
112
+ // Indexes should be built
113
+ expect(osm.isReady()).toBe(true)
114
+ expect(osm.nodes.isReady()).toBe(true)
115
+ expect(osm.ways.isReady()).toBe(true)
116
+ })
117
+
118
+ it("should handle ways with multiple nodes", async () => {
119
+ const osm = await getOsm()
120
+ // Count ways with enough nodes
121
+ let validWays = 0
122
+ for (let i = 0; i < osm.ways.size; i++) {
123
+ const way = osm.ways.getByIndex(i)
124
+ if (way && way.refs.length >= 2) {
125
+ validWays++
126
+ // All node refs should resolve to actual nodes
127
+ for (const ref of way.refs) {
128
+ const node = osm.nodes.getById(ref)
129
+ expect(node).toBeDefined()
130
+ }
131
+ }
132
+ }
133
+
134
+ // Most ways should have at least 2 nodes (minimum for a LineString)
135
+ expect(validWays).toBeGreaterThan(0)
136
+ })
137
+
138
+ it("should handle maxRows option to limit features", async () => {
139
+ const builder = new GeoParquetOsmBuilder({ id: "monaco" }, { rowEnd: 10 })
140
+ const rows = await builder.readParquetRows(
141
+ await fixtureFile().arrayBuffer(),
142
+ )
143
+ builder.processGeoParquetRows(rows)
144
+ const osm = builder.buildOsm()
145
+
146
+ // Should only have processed 10 features
147
+ expect(rows.length).toBe(10)
148
+ // Note: Some rows may be Point geometries (crossings) which don't create ways
149
+ expect(osm.nodes.size + osm.ways.size).toBeGreaterThan(0)
150
+ })
151
+
152
+ it("should find common highway types in Monaco", async () => {
153
+ const osm = await getOsm()
154
+
155
+ const highwayTypes = new Map<string, number>()
156
+
157
+ for (let i = 0; i < osm.ways.size; i++) {
158
+ const way = osm.ways.getByIndex(i)
159
+ const highway = way?.tags?.["highway"]
160
+ if (highway && typeof highway === "string") {
161
+ highwayTypes.set(highway, (highwayTypes.get(highway) ?? 0) + 1)
162
+ }
163
+ }
164
+
165
+ // Monaco should have some common highway types
166
+ const allTypes = Array.from(highwayTypes.keys())
167
+ expect(allTypes.length).toBeGreaterThan(0)
168
+
169
+ // Common types that should exist in Monaco's dense urban area
170
+ const commonTypes = ["residential", "footway", "service", "path", "steps"]
171
+ const foundCommonTypes = commonTypes.filter((t) => highwayTypes.has(t))
172
+
173
+ // At least some common types should be present
174
+ expect(foundCommonTypes.length).toBeGreaterThan(0)
175
+ })
176
+
177
+ it("should handle surface and other highway attributes", async () => {
178
+ const osm = await getOsm()
179
+
180
+ // Check that some ways have additional attributes beyond just highway
181
+ let waysWithSurface = 0
182
+ let waysWithName = 0
183
+
184
+ for (let i = 0; i < osm.ways.size; i++) {
185
+ const way = osm.ways.getByIndex(i)
186
+ if (way?.tags) {
187
+ if (way.tags["surface"]) waysWithSurface++
188
+ if (way.tags["name"]) waysWithName++
189
+ }
190
+ }
191
+
192
+ // Some highways should have surface or name attributes
193
+ // (not all will, so we just check that at least some do)
194
+ expect(waysWithSurface + waysWithName).toBeGreaterThan(0)
195
+ })
196
+
197
+ it("should reuse nodes for connected highways", async () => {
198
+ const osm = await getOsm()
199
+
200
+ // Count how many ways reference the same nodes
201
+ const nodeRefCounts = new Map<number, number>()
202
+
203
+ for (let i = 0; i < osm.ways.size; i++) {
204
+ const way = osm.ways.getByIndex(i)
205
+ if (way) {
206
+ for (const ref of way.refs) {
207
+ nodeRefCounts.set(ref, (nodeRefCounts.get(ref) ?? 0) + 1)
208
+ }
209
+ }
210
+ }
211
+
212
+ // Some nodes should be shared between multiple ways (intersections)
213
+ const sharedNodes = Array.from(nodeRefCounts.values()).filter((c) => c > 1)
214
+ expect(sharedNodes.length).toBeGreaterThan(0)
215
+ })
216
+
217
+ it("should handle Point geometries (crossings)", async () => {
218
+ const osm = await getOsm()
219
+
220
+ // Find nodes with crossing tags (Point features become Nodes with tags)
221
+ let crossingCount = 0
222
+ for (let i = 0; i < osm.nodes.size; i++) {
223
+ const node = osm.nodes.getByIndex(i)
224
+ if (node?.tags?.["highway"] === "crossing") {
225
+ crossingCount++
226
+ }
227
+ }
228
+
229
+ // Monaco should have some pedestrian crossings
230
+ expect(crossingCount).toBeGreaterThan(0)
231
+ })
232
+
233
+ it("should count geometry types in the fixture", async () => {
234
+ const builder = new GeoParquetOsmBuilder({ id: "monaco" })
235
+ const rows = await builder.readParquetRows(
236
+ await fixtureFile().arrayBuffer(),
237
+ )
238
+
239
+ const typeCounts = new Map<string, number>()
240
+ for (const row of rows) {
241
+ const type = row["geometry"]?.type ?? "unknown"
242
+ typeCounts.set(type, (typeCounts.get(type) ?? 0) + 1)
243
+ }
244
+
245
+ // Highways layer should have mostly LineStrings (roads) and some Points (crossings)
246
+ expect(typeCounts.get("LineString") ?? 0).toBeGreaterThan(0)
247
+ expect(typeCounts.get("Point") ?? 0).toBeGreaterThan(0)
248
+ })
249
+ })
@@ -0,0 +1,296 @@
1
+ import { describe, expect, it } from "bun:test"
2
+ import { parseWkb } from "../src/wkb"
3
+
4
+ /**
5
+ * Helper to create a WKB Point with properly encoded float64 coordinates.
6
+ */
7
+ function createPointWkb(
8
+ lon: number,
9
+ lat: number,
10
+ littleEndian = true,
11
+ ): Uint8Array {
12
+ const buffer = new ArrayBuffer(21)
13
+ const view = new DataView(buffer)
14
+ let offset = 0
15
+
16
+ view.setUint8(offset, littleEndian ? 1 : 0)
17
+ offset += 1
18
+ view.setUint32(offset, 1, littleEndian) // Point type
19
+ offset += 4
20
+ view.setFloat64(offset, lon, littleEndian)
21
+ offset += 8
22
+ view.setFloat64(offset, lat, littleEndian)
23
+
24
+ return new Uint8Array(buffer)
25
+ }
26
+
27
+ describe("@osmix/geoparquet: WKB Parser", () => {
28
+ it("should parse a Point geometry (little endian)", () => {
29
+ const wkb = createPointWkb(-122.4194, 37.7749, true)
30
+
31
+ const geometry = parseWkb(wkb)
32
+
33
+ expect(geometry.type).toBe("Point")
34
+ if (geometry.type === "Point") {
35
+ expect(geometry.coordinates[0]).toBeCloseTo(-122.4194, 4)
36
+ expect(geometry.coordinates[1]).toBeCloseTo(37.7749, 4)
37
+ }
38
+ })
39
+
40
+ it("should parse a Point geometry (big endian)", () => {
41
+ const wkb = createPointWkb(-122.4194, 37.7749, false)
42
+
43
+ const geometry = parseWkb(wkb)
44
+
45
+ expect(geometry.type).toBe("Point")
46
+ if (geometry.type === "Point") {
47
+ expect(geometry.coordinates[0]).toBeCloseTo(-122.4194, 4)
48
+ expect(geometry.coordinates[1]).toBeCloseTo(37.7749, 4)
49
+ }
50
+ })
51
+
52
+ it("should parse a LineString geometry", () => {
53
+ // WKB LineString with 3 points
54
+ const buffer = new ArrayBuffer(1 + 4 + 4 + 3 * 16)
55
+ const view = new DataView(buffer)
56
+ let offset = 0
57
+
58
+ // Byte order: little endian
59
+ view.setUint8(offset, 1)
60
+ offset += 1
61
+
62
+ // Geometry type: LineString (2)
63
+ view.setUint32(offset, 2, true)
64
+ offset += 4
65
+
66
+ // Number of points: 3
67
+ view.setUint32(offset, 3, true)
68
+ offset += 4
69
+
70
+ // Point 1: -122.4194, 37.7749
71
+ view.setFloat64(offset, -122.4194, true)
72
+ offset += 8
73
+ view.setFloat64(offset, 37.7749, true)
74
+ offset += 8
75
+
76
+ // Point 2: -122.4094, 37.7849
77
+ view.setFloat64(offset, -122.4094, true)
78
+ offset += 8
79
+ view.setFloat64(offset, 37.7849, true)
80
+ offset += 8
81
+
82
+ // Point 3: -122.3994, 37.7949
83
+ view.setFloat64(offset, -122.3994, true)
84
+ offset += 8
85
+ view.setFloat64(offset, 37.7949, true)
86
+
87
+ const wkb = new Uint8Array(buffer)
88
+ const geometry = parseWkb(wkb)
89
+
90
+ expect(geometry.type).toBe("LineString")
91
+ if (geometry.type === "LineString") {
92
+ expect(geometry.coordinates).toHaveLength(3)
93
+ expect(geometry.coordinates[0]?.[0]).toBeCloseTo(-122.4194, 4)
94
+ expect(geometry.coordinates[0]?.[1]).toBeCloseTo(37.7749, 4)
95
+ expect(geometry.coordinates[1]?.[0]).toBeCloseTo(-122.4094, 4)
96
+ expect(geometry.coordinates[1]?.[1]).toBeCloseTo(37.7849, 4)
97
+ expect(geometry.coordinates[2]?.[0]).toBeCloseTo(-122.3994, 4)
98
+ expect(geometry.coordinates[2]?.[1]).toBeCloseTo(37.7949, 4)
99
+ }
100
+ })
101
+
102
+ it("should parse a Polygon geometry", () => {
103
+ // WKB Polygon with 1 ring of 5 points (closed)
104
+ const buffer = new ArrayBuffer(1 + 4 + 4 + 4 + 5 * 16)
105
+ const view = new DataView(buffer)
106
+ let offset = 0
107
+
108
+ // Byte order: little endian
109
+ view.setUint8(offset, 1)
110
+ offset += 1
111
+
112
+ // Geometry type: Polygon (3)
113
+ view.setUint32(offset, 3, true)
114
+ offset += 4
115
+
116
+ // Number of rings: 1
117
+ view.setUint32(offset, 1, true)
118
+ offset += 4
119
+
120
+ // Number of points in ring: 5
121
+ view.setUint32(offset, 5, true)
122
+ offset += 4
123
+
124
+ // Points (closed ring)
125
+ const coords = [
126
+ [-122.4194, 37.7749],
127
+ [-122.4094, 37.7749],
128
+ [-122.4094, 37.7849],
129
+ [-122.4194, 37.7849],
130
+ [-122.4194, 37.7749], // closed
131
+ ]
132
+
133
+ for (const [lon, lat] of coords) {
134
+ view.setFloat64(offset, lon!, true)
135
+ offset += 8
136
+ view.setFloat64(offset, lat!, true)
137
+ offset += 8
138
+ }
139
+
140
+ const wkb = new Uint8Array(buffer)
141
+ const geometry = parseWkb(wkb)
142
+
143
+ expect(geometry.type).toBe("Polygon")
144
+ if (geometry.type === "Polygon") {
145
+ expect(geometry.coordinates).toHaveLength(1)
146
+ expect(geometry.coordinates[0]).toHaveLength(5)
147
+ expect(geometry.coordinates[0]?.[0]?.[0]).toBeCloseTo(-122.4194, 4)
148
+ expect(geometry.coordinates[0]?.[0]?.[1]).toBeCloseTo(37.7749, 4)
149
+ }
150
+ })
151
+
152
+ it("should parse a MultiPolygon geometry", () => {
153
+ // Helper to create polygon WKB
154
+ const createPolygonWkb = (
155
+ coords: [number, number][][],
156
+ ): [ArrayBuffer, number] => {
157
+ const totalPoints = coords.reduce((sum, ring) => sum + ring.length, 0)
158
+ const size = 1 + 4 + 4 + coords.length * 4 + totalPoints * 16
159
+ const buffer = new ArrayBuffer(size)
160
+ const view = new DataView(buffer)
161
+ let offset = 0
162
+
163
+ // Byte order
164
+ view.setUint8(offset, 1)
165
+ offset += 1
166
+
167
+ // Polygon type
168
+ view.setUint32(offset, 3, true)
169
+ offset += 4
170
+
171
+ // Number of rings
172
+ view.setUint32(offset, coords.length, true)
173
+ offset += 4
174
+
175
+ for (const ring of coords) {
176
+ view.setUint32(offset, ring.length, true)
177
+ offset += 4
178
+ for (const [lon, lat] of ring) {
179
+ view.setFloat64(offset, lon, true)
180
+ offset += 8
181
+ view.setFloat64(offset, lat, true)
182
+ offset += 8
183
+ }
184
+ }
185
+
186
+ return [buffer, size]
187
+ }
188
+
189
+ // Create MultiPolygon with 2 polygons
190
+ const poly1Coords: [number, number][][] = [
191
+ [
192
+ [-122.4194, 37.7749],
193
+ [-122.4094, 37.7749],
194
+ [-122.4094, 37.7849],
195
+ [-122.4194, 37.7849],
196
+ [-122.4194, 37.7749],
197
+ ],
198
+ ]
199
+
200
+ const poly2Coords: [number, number][][] = [
201
+ [
202
+ [-122.3994, 37.7649],
203
+ [-122.3894, 37.7649],
204
+ [-122.3894, 37.7749],
205
+ [-122.3994, 37.7749],
206
+ [-122.3994, 37.7649],
207
+ ],
208
+ ]
209
+
210
+ const [poly1Buffer, poly1Size] = createPolygonWkb(poly1Coords)
211
+ const [poly2Buffer, poly2Size] = createPolygonWkb(poly2Coords)
212
+
213
+ // MultiPolygon header + 2 polygons
214
+ const multiBuffer = new ArrayBuffer(1 + 4 + 4 + poly1Size + poly2Size)
215
+ const multiView = new DataView(multiBuffer)
216
+ let offset = 0
217
+
218
+ // Byte order
219
+ multiView.setUint8(offset, 1)
220
+ offset += 1
221
+
222
+ // MultiPolygon type (6)
223
+ multiView.setUint32(offset, 6, true)
224
+ offset += 4
225
+
226
+ // Number of geometries
227
+ multiView.setUint32(offset, 2, true)
228
+ offset += 4
229
+
230
+ // Copy polygon 1
231
+ new Uint8Array(multiBuffer, offset, poly1Size).set(
232
+ new Uint8Array(poly1Buffer),
233
+ )
234
+ offset += poly1Size
235
+
236
+ // Copy polygon 2
237
+ new Uint8Array(multiBuffer, offset, poly2Size).set(
238
+ new Uint8Array(poly2Buffer),
239
+ )
240
+
241
+ const wkb = new Uint8Array(multiBuffer)
242
+ const geometry = parseWkb(wkb)
243
+
244
+ expect(geometry.type).toBe("MultiPolygon")
245
+ if (geometry.type === "MultiPolygon") {
246
+ expect(geometry.coordinates).toHaveLength(2)
247
+ expect(geometry.coordinates[0]).toHaveLength(1)
248
+ expect(geometry.coordinates[1]).toHaveLength(1)
249
+ }
250
+ })
251
+
252
+ it("should handle EWKB with SRID flag", () => {
253
+ // EWKB Point with SRID (SRID flag: 0x20000000)
254
+ const buffer = new ArrayBuffer(1 + 4 + 4 + 16)
255
+ const view = new DataView(buffer)
256
+ let offset = 0
257
+
258
+ // Byte order: little endian
259
+ view.setUint8(offset, 1)
260
+ offset += 1
261
+
262
+ // Geometry type: Point (1) with SRID flag (0x20000001)
263
+ view.setUint32(offset, 0x20000001, true)
264
+ offset += 4
265
+
266
+ // SRID: 4326
267
+ view.setUint32(offset, 4326, true)
268
+ offset += 4
269
+
270
+ // Point: -122.4194, 37.7749
271
+ view.setFloat64(offset, -122.4194, true)
272
+ offset += 8
273
+ view.setFloat64(offset, 37.7749, true)
274
+
275
+ const wkb = new Uint8Array(buffer)
276
+ const geometry = parseWkb(wkb)
277
+
278
+ expect(geometry.type).toBe("Point")
279
+ if (geometry.type === "Point") {
280
+ expect(geometry.coordinates[0]).toBeCloseTo(-122.4194, 4)
281
+ expect(geometry.coordinates[1]).toBeCloseTo(37.7749, 4)
282
+ }
283
+ })
284
+
285
+ it("should throw on unsupported geometry type", () => {
286
+ const wkb = new Uint8Array([
287
+ 0x01, // little endian
288
+ 0x64,
289
+ 0x00,
290
+ 0x00,
291
+ 0x00, // Invalid type (100)
292
+ ])
293
+
294
+ expect(() => parseWkb(wkb)).toThrow("Unsupported WKB geometry type: 100")
295
+ })
296
+ })
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
3
+ "extends": "@osmix/shared/tsconfig/base.json",
4
+ "exclude": ["node_modules", "dist"],
5
+ "include": ["src", "test"],
6
+ "compilerOptions": {
7
+ "outDir": "./dist"
8
+ }
9
+ }