fit-file-parser 2.0.3 → 2.0.5
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/.editorconfig +3 -0
- package/.github/workflows/ci.yml +12 -1
- package/.vscode/extensions.json +6 -0
- package/.vscode/launch.json +27 -0
- package/.vscode/tasks.json +34 -0
- package/README.md +46 -0
- package/codegen/codegen.ts +9 -0
- package/dist/binary.d.ts +3 -3
- package/dist/binary.js +1 -1
- package/dist/fit-parser.d.ts +2 -2
- package/dist/fit-parser.js +17 -19
- package/dist/fit.d.ts +4 -19
- package/dist/fit.js +0 -1
- package/dist/fit_types.d.ts +974 -0
- package/dist/fit_types.js +8 -0
- package/dist/helper.d.ts +1 -1
- package/dist/messages.d.ts +3 -2
- package/dist/type_generator.d.ts +26 -0
- package/dist/type_generator.js +243 -0
- package/eslint.config.js +8 -0
- package/package.json +13 -3
- package/tsconfig.json +2 -1
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* this file is auto generated using src/type_generator.ts
|
|
4
|
+
* it parses the big FIT definition object from src/fit.js into usable typescript types
|
|
5
|
+
* do not edit this file directly, instead edit the generator and
|
|
6
|
+
* regenerate it with "npm run codegen"
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
package/dist/helper.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type { ParsedLap, ParsedSession } from './
|
|
1
|
+
import type { ParsedLap, ParsedSession } from './fit_types.js';
|
|
2
2
|
export declare function mapDataIntoLap(inputLaps: ParsedLap[], lapKey: 'records' | 'lengths', data: any[]): ParsedLap[];
|
|
3
3
|
export declare function mapDataIntoSession(inputSessions: ParsedSession[], laps: ParsedLap[]): ParsedSession[];
|
package/dist/messages.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { MessageObject } from './fit.js';
|
|
2
|
+
import type { MesgNum } from './fit_types.js';
|
|
2
3
|
export declare function getFitMessage(messageNum: number): {
|
|
3
|
-
name:
|
|
4
|
+
name: MesgNum | '';
|
|
4
5
|
getAttributes: (fieldNum: number) => MessageObject;
|
|
5
6
|
};
|
|
6
7
|
export declare function getFitMessageBaseType(inp: any): any;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { PropertySignature, Statement, TypeNode } from 'typescript';
|
|
2
|
+
import type { Message, MessageObject } from './fit.js';
|
|
3
|
+
import ts from 'typescript';
|
|
4
|
+
type SnakeToCamelCase<S extends string> = S extends `${infer T}_${infer U}` ? `${T}${Capitalize<SnakeToCamelCase<U>>}` : S;
|
|
5
|
+
export declare function line(): ts.Node;
|
|
6
|
+
export declare function comment(contents: string): ts.Node[];
|
|
7
|
+
export declare function header(): ts.Node;
|
|
8
|
+
export declare function capitalize<T extends string>(s: T): string;
|
|
9
|
+
export declare function snakeToCamel<T extends string>(s: T): SnakeToCamelCase<Capitalize<T>>;
|
|
10
|
+
export declare function unicodeToChar(text: string): string;
|
|
11
|
+
export declare function generateProperty(name: string, type: TypeNode, optional?: boolean): PropertySignature;
|
|
12
|
+
export declare function generateArrayProperty(name: string, type: TypeNode, optional?: boolean): PropertySignature;
|
|
13
|
+
export declare function generateTypeFromField(def: MessageObject): TypeNode;
|
|
14
|
+
export declare function generateTypes(types: {
|
|
15
|
+
[typeName: string]: Record<number, string | number>;
|
|
16
|
+
}): Statement[];
|
|
17
|
+
export declare function generateOptions(options: {
|
|
18
|
+
[typeName: string]: Record<number, string | number>;
|
|
19
|
+
}): ts.Node[];
|
|
20
|
+
export declare function generateUtilities(): Statement[];
|
|
21
|
+
export declare function generateFitType(): Statement;
|
|
22
|
+
export declare function generateMessages(messages: {
|
|
23
|
+
[messageId: number]: Message;
|
|
24
|
+
}): Statement[];
|
|
25
|
+
export declare function main(): string;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import { FIT } from './fit.js';
|
|
3
|
+
export function line() {
|
|
4
|
+
return ts.factory.createIdentifier('\n');
|
|
5
|
+
}
|
|
6
|
+
export function comment(contents) {
|
|
7
|
+
return [
|
|
8
|
+
ts.factory.createIdentifier('\n'),
|
|
9
|
+
ts.factory.createIdentifier(`// ${contents}`),
|
|
10
|
+
ts.factory.createIdentifier('\n'),
|
|
11
|
+
];
|
|
12
|
+
}
|
|
13
|
+
export function header() {
|
|
14
|
+
return ts.factory.createNodeArray([ts.factory.createJSDocComment(`
|
|
15
|
+
this file is auto generated using src/type_generator.ts
|
|
16
|
+
it parses the big FIT definition object from src/fit.js into usable typescript types
|
|
17
|
+
do not edit this file directly, instead edit the generator and
|
|
18
|
+
regenerate it with "npm run codegen"
|
|
19
|
+
`)])[0];
|
|
20
|
+
}
|
|
21
|
+
export function capitalize(s) {
|
|
22
|
+
if (s.length === 0) {
|
|
23
|
+
return s;
|
|
24
|
+
}
|
|
25
|
+
return (s[0].toUpperCase() + s.slice(1));
|
|
26
|
+
}
|
|
27
|
+
export function snakeToCamel(s) {
|
|
28
|
+
return s.split('_').map(part => capitalize(part)).join('');
|
|
29
|
+
}
|
|
30
|
+
export function unicodeToChar(text) {
|
|
31
|
+
return text.replace(/\\u[\dA-F]{4}/gi, (match) => {
|
|
32
|
+
return String.fromCharCode(Number.parseInt(match.replace(/\\u/g, ''), 16));
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
export function generateProperty(name, type, optional = true) {
|
|
36
|
+
return ts.factory.createPropertySignature(undefined, ts.factory.createIdentifier(name), optional ? ts.factory.createToken(ts.SyntaxKind.QuestionToken) : undefined, type);
|
|
37
|
+
}
|
|
38
|
+
export function generateArrayProperty(name, type, optional = true) {
|
|
39
|
+
return generateProperty(name, ts.factory.createArrayTypeNode(type), optional);
|
|
40
|
+
}
|
|
41
|
+
export function generateTypeFromField(def) {
|
|
42
|
+
switch (def.type) {
|
|
43
|
+
case 'byte_array':
|
|
44
|
+
return ts.factory.createArrayTypeNode(ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword));
|
|
45
|
+
case 'bool':
|
|
46
|
+
return ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword);
|
|
47
|
+
case 'date_time':
|
|
48
|
+
case 'string':
|
|
49
|
+
return ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
|
|
50
|
+
case 'uint32':
|
|
51
|
+
case 'uint64':
|
|
52
|
+
case 'uint16':
|
|
53
|
+
case 'uint8':
|
|
54
|
+
case 'int32':
|
|
55
|
+
case 'int64':
|
|
56
|
+
case 'int16':
|
|
57
|
+
case 'int8':
|
|
58
|
+
case 'sint32':
|
|
59
|
+
case 'sint16':
|
|
60
|
+
case 'sint8':
|
|
61
|
+
case 'float32':
|
|
62
|
+
case 'float64':
|
|
63
|
+
case 'uint32z':
|
|
64
|
+
case 'uint64z':
|
|
65
|
+
case 'uint16z':
|
|
66
|
+
case 'uint8z':
|
|
67
|
+
case 'localtime_into_day':
|
|
68
|
+
case 'byte':
|
|
69
|
+
case 'device_index':
|
|
70
|
+
return ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword);
|
|
71
|
+
default:
|
|
72
|
+
return ts.factory.createTypeReferenceNode(snakeToCamel(def.type));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export function generateTypes(types) {
|
|
76
|
+
const nodes = [];
|
|
77
|
+
const typeNames = Object.keys(types);
|
|
78
|
+
typeNames.forEach((name) => {
|
|
79
|
+
if (name === 'message_index') {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const type = types[name];
|
|
83
|
+
const names = Object.values(type);
|
|
84
|
+
const typeAlias = ts.factory.createTypeAliasDeclaration([
|
|
85
|
+
ts.factory.createModifier(ts.SyntaxKind.ExportKeyword),
|
|
86
|
+
], snakeToCamel(name), undefined, ts.factory.createUnionTypeNode([...names.map(n => ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(String(n)))), ...(name === 'mesg_num'
|
|
87
|
+
// TODO those are somehow missing in the FIT.types.mesg_num definition
|
|
88
|
+
// especially 'tank_update' (319 in messages) is mapped to 'tank_pressure' a bug?
|
|
89
|
+
? [
|
|
90
|
+
ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral('o_hr_settings')),
|
|
91
|
+
ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral('tank_update')),
|
|
92
|
+
ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral('definition')),
|
|
93
|
+
]
|
|
94
|
+
: [])]));
|
|
95
|
+
nodes.push(typeAlias);
|
|
96
|
+
});
|
|
97
|
+
return nodes;
|
|
98
|
+
}
|
|
99
|
+
export function generateOptions(options) {
|
|
100
|
+
const nodes = [];
|
|
101
|
+
const optionNames = Object.keys(options);
|
|
102
|
+
// generate type aliases for each option (all unit values)
|
|
103
|
+
optionNames.forEach((name) => {
|
|
104
|
+
const option = options[name];
|
|
105
|
+
const names = Object.keys(option);
|
|
106
|
+
const unit = ts.factory.createTypeAliasDeclaration([
|
|
107
|
+
ts.factory.createModifier(ts.SyntaxKind.ExportKeyword),
|
|
108
|
+
], capitalize(name), undefined, ts.factory.createUnionTypeNode(names.map(n => ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(unicodeToChar(n))))));
|
|
109
|
+
nodes.push(unit);
|
|
110
|
+
});
|
|
111
|
+
nodes.push(line());
|
|
112
|
+
// generate FitOptions interface which contains all options and their respective types (from above)
|
|
113
|
+
const fitOptions = ts.factory.createInterfaceDeclaration([
|
|
114
|
+
ts.factory.createModifier(ts.SyntaxKind.ExportKeyword),
|
|
115
|
+
], 'FitOptions', undefined, undefined, [
|
|
116
|
+
...optionNames.map(name => generateProperty(name, ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('Unit'), [ts.factory.createTypeReferenceNode(ts.factory.createIdentifier(capitalize(name)))]), false)),
|
|
117
|
+
]);
|
|
118
|
+
nodes.push(fitOptions);
|
|
119
|
+
return nodes;
|
|
120
|
+
}
|
|
121
|
+
export function generateUtilities() {
|
|
122
|
+
const nodes = [];
|
|
123
|
+
const unitType = ts.factory.createTypeAliasDeclaration([ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], 'Unit', [ts.factory.createTypeParameterDeclaration(undefined, 'T', ts.factory.createTypeReferenceNode('string'))], ts.factory.createTypeReferenceNode('Record', [
|
|
124
|
+
ts.factory.createTypeReferenceNode('T'),
|
|
125
|
+
ts.factory.createTypeLiteralNode([
|
|
126
|
+
generateProperty('multiplier', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
|
|
127
|
+
generateProperty('offset', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
|
|
128
|
+
]),
|
|
129
|
+
]));
|
|
130
|
+
nodes.push(unitType);
|
|
131
|
+
const messageIndex = ts.factory.createInterfaceDeclaration([ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], 'MessageIndex', undefined, undefined, [
|
|
132
|
+
generateProperty('0', ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword), false),
|
|
133
|
+
generateProperty('value', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
|
|
134
|
+
generateProperty('reserved', ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword), false),
|
|
135
|
+
generateProperty('selected', ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword), false),
|
|
136
|
+
]);
|
|
137
|
+
nodes.push(messageIndex);
|
|
138
|
+
return nodes;
|
|
139
|
+
}
|
|
140
|
+
function generateAdditionalFields(msg) {
|
|
141
|
+
if (msg.name === 'lap') {
|
|
142
|
+
return [
|
|
143
|
+
generateArrayProperty('records', ts.factory.createTypeReferenceNode(snakeToCamel('parsed_record'))),
|
|
144
|
+
generateArrayProperty('lengths', ts.factory.createTypeReferenceNode(snakeToCamel('parsed_length'))),
|
|
145
|
+
];
|
|
146
|
+
}
|
|
147
|
+
if (msg.name === 'session') {
|
|
148
|
+
return [
|
|
149
|
+
generateArrayProperty('laps', ts.factory.createTypeReferenceNode(snakeToCamel('parsed_lap'))),
|
|
150
|
+
];
|
|
151
|
+
}
|
|
152
|
+
if (msg.name === 'activity') {
|
|
153
|
+
const props = {
|
|
154
|
+
sessions: 'parsed_session',
|
|
155
|
+
events: 'parsed_event',
|
|
156
|
+
hrv: 'parsed_hrv',
|
|
157
|
+
device_infos: 'parsed_device_info',
|
|
158
|
+
developer_data_ids: 'parsed_developer_data_id',
|
|
159
|
+
field_descriptions: 'parsed_field_description',
|
|
160
|
+
sports: 'parsed_sport',
|
|
161
|
+
};
|
|
162
|
+
return Object.keys(props).map(prop => generateArrayProperty(prop, ts.factory.createTypeReferenceNode(snakeToCamel(props[prop]))));
|
|
163
|
+
}
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
export function generateFitType() {
|
|
167
|
+
const collectionProperties = {
|
|
168
|
+
laps: 'parsed_lap',
|
|
169
|
+
records: 'parsed_record',
|
|
170
|
+
sessions: 'parsed_session',
|
|
171
|
+
lengths: 'parsed_length',
|
|
172
|
+
events: 'parsed_event',
|
|
173
|
+
device_infos: 'parsed_device_info',
|
|
174
|
+
developer_data_ids: 'parsed_developer_data_id',
|
|
175
|
+
field_descriptions: 'parsed_field_description',
|
|
176
|
+
hrv: 'parsed_hrv',
|
|
177
|
+
hr_zone: 'parsed_hr_zone',
|
|
178
|
+
power_zone: 'parsed_power_zone',
|
|
179
|
+
dive_gases: 'parsed_dive_gas',
|
|
180
|
+
course_points: 'parsed_course_point',
|
|
181
|
+
sports: 'parsed_sport',
|
|
182
|
+
monitors: 'parsed_monitoring',
|
|
183
|
+
stress: 'parsed_stress_level',
|
|
184
|
+
file_ids: 'parsed_file_id',
|
|
185
|
+
monitor_info: 'parsed_monitoring_info',
|
|
186
|
+
definitions: 'unknown',
|
|
187
|
+
tank_updates: 'parsed_tank_update',
|
|
188
|
+
tank_summaries: 'parsed_tank_summary',
|
|
189
|
+
};
|
|
190
|
+
const referenceProperties = {
|
|
191
|
+
file_creator: 'parsed_file_creator',
|
|
192
|
+
device_settings: 'parsed_device_settings',
|
|
193
|
+
dive_summary: '?parsed_dive_summary',
|
|
194
|
+
dive_settings: '?parsed_dive_settings',
|
|
195
|
+
software: 'parsed_software',
|
|
196
|
+
user_profile: 'parsed_user_profile',
|
|
197
|
+
activity: 'parsed_activity',
|
|
198
|
+
zones_target: '?parsed_zones_target',
|
|
199
|
+
};
|
|
200
|
+
return ts.factory.createInterfaceDeclaration([ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], 'ParsedFit', undefined, undefined, [
|
|
201
|
+
generateProperty('protocolVersion', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword)),
|
|
202
|
+
generateProperty('profileVersion', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword)),
|
|
203
|
+
...Object.keys(referenceProperties).map(prop => generateProperty(prop, ts.factory.createTypeReferenceNode(snakeToCamel(referenceProperties[prop].replace('?', ''))), referenceProperties[prop].startsWith('?'))),
|
|
204
|
+
...Object.keys(collectionProperties).map(prop => generateArrayProperty(prop, collectionProperties[prop] === 'unknown'
|
|
205
|
+
? ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword)
|
|
206
|
+
: ts.factory.createTypeReferenceNode(snakeToCamel(collectionProperties[prop])))),
|
|
207
|
+
]);
|
|
208
|
+
}
|
|
209
|
+
export function generateMessages(messages) {
|
|
210
|
+
const nodes = [];
|
|
211
|
+
Object.keys(messages).forEach((name) => {
|
|
212
|
+
const msg = FIT.messages[Number(name)];
|
|
213
|
+
const messageType = ts.factory.createInterfaceDeclaration([
|
|
214
|
+
ts.factory.createModifier(ts.SyntaxKind.ExportKeyword),
|
|
215
|
+
], snakeToCamel(`parsed_${msg.name}`), undefined, undefined, [
|
|
216
|
+
...Object.keys(msg).filter(n => n !== 'name').map((id) => {
|
|
217
|
+
const def = msg[Number(id)];
|
|
218
|
+
return generateProperty(def.field, generateTypeFromField(def), !['start_time', 'timestamp'].includes(def.field));
|
|
219
|
+
}),
|
|
220
|
+
...generateAdditionalFields(msg),
|
|
221
|
+
]);
|
|
222
|
+
nodes.push(messageType);
|
|
223
|
+
});
|
|
224
|
+
return nodes;
|
|
225
|
+
}
|
|
226
|
+
export function main() {
|
|
227
|
+
const sourceFile = ts.createSourceFile('', '', ts.ScriptTarget.Latest);
|
|
228
|
+
const nodes = [];
|
|
229
|
+
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
|
|
230
|
+
nodes.push(header());
|
|
231
|
+
nodes.push(...comment('utility types used internally'));
|
|
232
|
+
nodes.push(...generateUtilities());
|
|
233
|
+
nodes.push(...comment('parsed from Fit.types'));
|
|
234
|
+
nodes.push(...generateTypes(FIT.types));
|
|
235
|
+
nodes.push(...comment('parsed from Fit.options'));
|
|
236
|
+
nodes.push(...generateOptions(FIT.options));
|
|
237
|
+
nodes.push(...comment('parsed from Fit.messages'));
|
|
238
|
+
nodes.push(...generateMessages(FIT.messages));
|
|
239
|
+
nodes.push(...comment('the returned type after parsing a .fit file'));
|
|
240
|
+
nodes.push(generateFitType());
|
|
241
|
+
const nodesArray = ts.factory.createNodeArray(nodes);
|
|
242
|
+
return printer.printList(ts.ListFormat.MultiLine, nodesArray, sourceFile);
|
|
243
|
+
}
|
package/eslint.config.js
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fit-file-parser",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "2.0.
|
|
4
|
+
"version": "2.0.5",
|
|
5
5
|
"private": false,
|
|
6
6
|
"description": "Parse your .FIT files easily, directly from JS (Garmin, Polar, Suunto)",
|
|
7
7
|
"author": {
|
|
@@ -35,7 +35,9 @@
|
|
|
35
35
|
"exports": {
|
|
36
36
|
".": {
|
|
37
37
|
"types": "./dist/fit-parser.d.ts",
|
|
38
|
-
"import": "./dist/fit-parser.js"
|
|
38
|
+
"import": "./dist/fit-parser.js",
|
|
39
|
+
"require": "./dist/fit-parser.js",
|
|
40
|
+
"default": "./dist/fit-parser.js"
|
|
39
41
|
}
|
|
40
42
|
},
|
|
41
43
|
"main": "dist/fit-parser.js",
|
|
@@ -46,6 +48,8 @@
|
|
|
46
48
|
}
|
|
47
49
|
],
|
|
48
50
|
"scripts": {
|
|
51
|
+
"dev": "tsx --watch --watch-path=src/type_generator.ts --watch-path=src/fit.ts codegen/codegen.ts",
|
|
52
|
+
"codegen": "tsx codegen/codegen.ts",
|
|
49
53
|
"example-output": "node examples/parse.js examples/example.fit > examples/output-example.json",
|
|
50
54
|
"example-output-tank": "node examples/parse.js examples/example-diving.fit > examples/output-diving.json",
|
|
51
55
|
"example-output-summary-first": "node examples/parse.js examples/triathlon_summary_first.fit > examples/output_summary_first.json",
|
|
@@ -59,11 +63,17 @@
|
|
|
59
63
|
"build": "tsc --noCheck"
|
|
60
64
|
},
|
|
61
65
|
"test": "vitest",
|
|
66
|
+
"dependencies": {
|
|
67
|
+
"buffer": "^6.0.3"
|
|
68
|
+
},
|
|
62
69
|
"devDependencies": {
|
|
63
70
|
"@antfu/eslint-config": "^6.2.0",
|
|
64
71
|
"@types/node": "^24.10.1",
|
|
72
|
+
"@vitest/coverage-v8": "^4.0.13",
|
|
65
73
|
"eslint": "^9.10.0",
|
|
74
|
+
"eslint-plugin-format": "^1.1.0",
|
|
75
|
+
"tsx": "^4.21.0",
|
|
66
76
|
"typescript": "^5.9.3",
|
|
67
77
|
"vitest": "^4.0.13"
|
|
68
78
|
}
|
|
69
|
-
}
|
|
79
|
+
}
|