@tootallnate/ivfc 0.0.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/.turbo/turbo-build.log +4 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +138 -0
- package/dist/index.js.map +1 -0
- package/package.json +17 -0
- package/src/index.ts +192 -0
- package/tsconfig.json +14 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IVFC (Integrity Verification File Collection) hash tree builder.
|
|
3
|
+
*
|
|
4
|
+
* Used in Nintendo Switch NCA RomFS sections. Builds a 6-level SHA-256
|
|
5
|
+
* hash tree where each level is the hash table of the level below it.
|
|
6
|
+
*
|
|
7
|
+
* Level 6 = actual data (RomFS)
|
|
8
|
+
* Level 5 = SHA-256 hashes of level 6 blocks
|
|
9
|
+
* Level 4 = SHA-256 hashes of level 5 blocks
|
|
10
|
+
* ...
|
|
11
|
+
* Level 1 = SHA-256 hashes of level 2 blocks
|
|
12
|
+
* Master hash = SHA-256 of level 1
|
|
13
|
+
*
|
|
14
|
+
* Block size is 0x4000 (16KB) for all levels.
|
|
15
|
+
*
|
|
16
|
+
* Reference: hacbrewpack/ivfc.c, hacbrewpack/ivfc.h
|
|
17
|
+
*/
|
|
18
|
+
/** IVFC hash block size: 2^14 = 0x4000 = 16384 bytes */
|
|
19
|
+
export declare const IVFC_HASH_BLOCK_SIZE = 16384;
|
|
20
|
+
/** IVFC header size: 0xE0 bytes */
|
|
21
|
+
export declare const IVFC_HEADER_SIZE = 224;
|
|
22
|
+
/**
|
|
23
|
+
* Result of building an IVFC hash tree.
|
|
24
|
+
*/
|
|
25
|
+
export interface IvfcResult {
|
|
26
|
+
/** The IVFC header (0xE0 bytes) including the master hash */
|
|
27
|
+
header: ArrayBuffer;
|
|
28
|
+
/** Level data arrays, from level 1 (top) to level 5 (bottom).
|
|
29
|
+
* Level 6 is the original data and is not included here. */
|
|
30
|
+
levels: Uint8Array[];
|
|
31
|
+
/** Total size of all level data (levels 1-5) */
|
|
32
|
+
totalLevelSize: number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Build an IVFC hash tree from source data (typically a RomFS image).
|
|
36
|
+
*
|
|
37
|
+
* @param data - The source data (level 6). Must already be padded to IVFC_HASH_BLOCK_SIZE.
|
|
38
|
+
* @param crypto - Optional Crypto implementation
|
|
39
|
+
* @returns IVFC header and all intermediate level data
|
|
40
|
+
*/
|
|
41
|
+
export declare function build(data: Uint8Array, crypto?: Crypto): Promise<IvfcResult>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IVFC (Integrity Verification File Collection) hash tree builder.
|
|
3
|
+
*
|
|
4
|
+
* Used in Nintendo Switch NCA RomFS sections. Builds a 6-level SHA-256
|
|
5
|
+
* hash tree where each level is the hash table of the level below it.
|
|
6
|
+
*
|
|
7
|
+
* Level 6 = actual data (RomFS)
|
|
8
|
+
* Level 5 = SHA-256 hashes of level 6 blocks
|
|
9
|
+
* Level 4 = SHA-256 hashes of level 5 blocks
|
|
10
|
+
* ...
|
|
11
|
+
* Level 1 = SHA-256 hashes of level 2 blocks
|
|
12
|
+
* Master hash = SHA-256 of level 1
|
|
13
|
+
*
|
|
14
|
+
* Block size is 0x4000 (16KB) for all levels.
|
|
15
|
+
*
|
|
16
|
+
* Reference: hacbrewpack/ivfc.c, hacbrewpack/ivfc.h
|
|
17
|
+
*/
|
|
18
|
+
/** IVFC hash block size: 2^14 = 0x4000 = 16384 bytes */
|
|
19
|
+
export const IVFC_HASH_BLOCK_SIZE = 0x4000;
|
|
20
|
+
/** Number of IVFC levels (excluding the data level) */
|
|
21
|
+
const IVFC_NUM_LEVELS = 6;
|
|
22
|
+
/** IVFC magic: "IVFC" */
|
|
23
|
+
const IVFC_MAGIC = 0x43465649;
|
|
24
|
+
/** IVFC version identifier */
|
|
25
|
+
const IVFC_ID = 0x20000;
|
|
26
|
+
/** SHA-256 hash size */
|
|
27
|
+
const HASH_SIZE = 0x20;
|
|
28
|
+
/** IVFC header size: 0xE0 bytes */
|
|
29
|
+
export const IVFC_HEADER_SIZE = 0xe0;
|
|
30
|
+
/**
|
|
31
|
+
* Align a value up to the given alignment.
|
|
32
|
+
*/
|
|
33
|
+
function align(value, alignment) {
|
|
34
|
+
const mask = alignment - 1;
|
|
35
|
+
return (value + mask) & ~mask;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* SHA-256 hash a Uint8Array using Web Crypto.
|
|
39
|
+
*/
|
|
40
|
+
async function sha256(data, crypto = globalThis.crypto) {
|
|
41
|
+
const hash = await crypto.subtle.digest('SHA-256', data);
|
|
42
|
+
return new Uint8Array(hash);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Create an IVFC level: hash each block of the source data with SHA-256.
|
|
46
|
+
* Returns the hash data padded to the IVFC block size boundary.
|
|
47
|
+
*/
|
|
48
|
+
async function createLevel(sourceData, crypto = globalThis.crypto) {
|
|
49
|
+
const blockSize = IVFC_HASH_BLOCK_SIZE;
|
|
50
|
+
const sourceSize = sourceData.length;
|
|
51
|
+
// Calculate number of blocks and hash table size
|
|
52
|
+
const numBlocks = Math.ceil(sourceSize / blockSize);
|
|
53
|
+
const hashDataSize = numBlocks * HASH_SIZE;
|
|
54
|
+
// Pad to block size boundary
|
|
55
|
+
const paddedSize = align(hashDataSize, blockSize);
|
|
56
|
+
const hashes = new Uint8Array(paddedSize);
|
|
57
|
+
// Hash each block
|
|
58
|
+
const block = new Uint8Array(blockSize);
|
|
59
|
+
for (let i = 0; i < numBlocks; i++) {
|
|
60
|
+
const offset = i * blockSize;
|
|
61
|
+
const remaining = sourceSize - offset;
|
|
62
|
+
const readSize = Math.min(remaining, blockSize);
|
|
63
|
+
// Zero-fill the block buffer (important for the last partial block)
|
|
64
|
+
block.fill(0);
|
|
65
|
+
block.set(sourceData.subarray(offset, offset + readSize));
|
|
66
|
+
const hash = await sha256(block.subarray(0, readSize), crypto);
|
|
67
|
+
hashes.set(hash, i * HASH_SIZE);
|
|
68
|
+
}
|
|
69
|
+
return { hashes, hashDataSize };
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Build an IVFC hash tree from source data (typically a RomFS image).
|
|
73
|
+
*
|
|
74
|
+
* @param data - The source data (level 6). Must already be padded to IVFC_HASH_BLOCK_SIZE.
|
|
75
|
+
* @param crypto - Optional Crypto implementation
|
|
76
|
+
* @returns IVFC header and all intermediate level data
|
|
77
|
+
*/
|
|
78
|
+
export async function build(data, crypto = globalThis.crypto) {
|
|
79
|
+
// Build levels from bottom (6) to top (1)
|
|
80
|
+
// Level 6 = data, Level 5 = hashes of level 6, ..., Level 1 = hashes of level 2
|
|
81
|
+
const levelData = [];
|
|
82
|
+
const levelHashDataSizes = [];
|
|
83
|
+
const levelPaddedSizes = [];
|
|
84
|
+
let currentSource = data;
|
|
85
|
+
for (let level = 0; level < IVFC_NUM_LEVELS - 1; level++) {
|
|
86
|
+
const { hashes, hashDataSize } = await createLevel(currentSource, crypto);
|
|
87
|
+
levelData.unshift(hashes); // Prepend (we're building bottom-up but storing top-down)
|
|
88
|
+
levelHashDataSizes.unshift(hashDataSize);
|
|
89
|
+
levelPaddedSizes.unshift(hashes.length);
|
|
90
|
+
currentSource = hashes;
|
|
91
|
+
}
|
|
92
|
+
// Calculate master hash = SHA-256 of level 1 (the top level, which is levelData[0])
|
|
93
|
+
const masterHash = await sha256(levelData[0], crypto);
|
|
94
|
+
// Build the IVFC header (0xE0 bytes)
|
|
95
|
+
const header = new ArrayBuffer(IVFC_HEADER_SIZE);
|
|
96
|
+
const view = new DataView(header);
|
|
97
|
+
// IVFC header fields
|
|
98
|
+
view.setUint32(0x00, IVFC_MAGIC, true); // magic = "IVFC"
|
|
99
|
+
view.setUint32(0x04, IVFC_ID, true); // id = 0x20000
|
|
100
|
+
view.setUint32(0x08, HASH_SIZE, true); // master_hash_size = 0x20
|
|
101
|
+
view.setUint32(0x0c, IVFC_NUM_LEVELS + 1, true); // num_levels = 7 (6 hash levels + 1 data level)
|
|
102
|
+
// Level headers (6 entries, each 0x18 bytes, starting at offset 0x10)
|
|
103
|
+
// These describe levels 1-6 (level 6 = data itself)
|
|
104
|
+
// The logical_offset for each level is the cumulative offset of all previous levels
|
|
105
|
+
let logicalOffset = 0;
|
|
106
|
+
for (let i = 0; i < IVFC_NUM_LEVELS - 1; i++) {
|
|
107
|
+
const entryOffset = 0x10 + i * 0x18;
|
|
108
|
+
const dataSize = levelPaddedSizes[i];
|
|
109
|
+
// logical_offset (8 bytes)
|
|
110
|
+
view.setBigUint64(entryOffset + 0x00, BigInt(logicalOffset), true);
|
|
111
|
+
// hash_data_size (8 bytes)
|
|
112
|
+
view.setBigUint64(entryOffset + 0x08, BigInt(dataSize), true);
|
|
113
|
+
// block_size (4 bytes) — log2 of the block size
|
|
114
|
+
view.setUint32(entryOffset + 0x10, 0x0e, true); // 2^14 = 0x4000
|
|
115
|
+
// reserved (4 bytes) — already zero
|
|
116
|
+
logicalOffset += dataSize;
|
|
117
|
+
}
|
|
118
|
+
// Level 6 entry (the actual data)
|
|
119
|
+
{
|
|
120
|
+
const entryOffset = 0x10 + (IVFC_NUM_LEVELS - 1) * 0x18;
|
|
121
|
+
view.setBigUint64(entryOffset + 0x00, BigInt(logicalOffset), true);
|
|
122
|
+
view.setBigUint64(entryOffset + 0x08, BigInt(data.length), true);
|
|
123
|
+
view.setUint32(entryOffset + 0x10, 0x0e, true);
|
|
124
|
+
}
|
|
125
|
+
// Master hash at offset 0xC0 (0x20 bytes)
|
|
126
|
+
new Uint8Array(header, 0xc0, HASH_SIZE).set(masterHash);
|
|
127
|
+
// Calculate total level size
|
|
128
|
+
let totalLevelSize = 0;
|
|
129
|
+
for (const level of levelData) {
|
|
130
|
+
totalLevelSize += level.length;
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
header,
|
|
134
|
+
levels: levelData,
|
|
135
|
+
totalLevelSize,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tootallnate/ivfc",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "IVFC (Integrity Verification File Collection) hash tree builder for Nintendo Switch",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"test": "vitest run"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [],
|
|
12
|
+
"author": "Nathan Rajlich <n@n8.io>",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"typescript": "^5.3.3"
|
|
16
|
+
}
|
|
17
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IVFC (Integrity Verification File Collection) hash tree builder.
|
|
3
|
+
*
|
|
4
|
+
* Used in Nintendo Switch NCA RomFS sections. Builds a 6-level SHA-256
|
|
5
|
+
* hash tree where each level is the hash table of the level below it.
|
|
6
|
+
*
|
|
7
|
+
* Level 6 = actual data (RomFS)
|
|
8
|
+
* Level 5 = SHA-256 hashes of level 6 blocks
|
|
9
|
+
* Level 4 = SHA-256 hashes of level 5 blocks
|
|
10
|
+
* ...
|
|
11
|
+
* Level 1 = SHA-256 hashes of level 2 blocks
|
|
12
|
+
* Master hash = SHA-256 of level 1
|
|
13
|
+
*
|
|
14
|
+
* Block size is 0x4000 (16KB) for all levels.
|
|
15
|
+
*
|
|
16
|
+
* Reference: hacbrewpack/ivfc.c, hacbrewpack/ivfc.h
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** IVFC hash block size: 2^14 = 0x4000 = 16384 bytes */
|
|
20
|
+
export const IVFC_HASH_BLOCK_SIZE = 0x4000;
|
|
21
|
+
|
|
22
|
+
/** Number of IVFC levels (excluding the data level) */
|
|
23
|
+
const IVFC_NUM_LEVELS = 6;
|
|
24
|
+
|
|
25
|
+
/** IVFC magic: "IVFC" */
|
|
26
|
+
const IVFC_MAGIC = 0x43465649;
|
|
27
|
+
|
|
28
|
+
/** IVFC version identifier */
|
|
29
|
+
const IVFC_ID = 0x20000;
|
|
30
|
+
|
|
31
|
+
/** SHA-256 hash size */
|
|
32
|
+
const HASH_SIZE = 0x20;
|
|
33
|
+
|
|
34
|
+
/** IVFC header size: 0xE0 bytes */
|
|
35
|
+
export const IVFC_HEADER_SIZE = 0xe0;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Align a value up to the given alignment.
|
|
39
|
+
*/
|
|
40
|
+
function align(value: number, alignment: number): number {
|
|
41
|
+
const mask = alignment - 1;
|
|
42
|
+
return (value + mask) & ~mask;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* SHA-256 hash a Uint8Array using Web Crypto.
|
|
47
|
+
*/
|
|
48
|
+
async function sha256(
|
|
49
|
+
data: Uint8Array,
|
|
50
|
+
crypto: Crypto = globalThis.crypto
|
|
51
|
+
): Promise<Uint8Array> {
|
|
52
|
+
const hash = await crypto.subtle.digest('SHA-256', data);
|
|
53
|
+
return new Uint8Array(hash);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Create an IVFC level: hash each block of the source data with SHA-256.
|
|
58
|
+
* Returns the hash data padded to the IVFC block size boundary.
|
|
59
|
+
*/
|
|
60
|
+
async function createLevel(
|
|
61
|
+
sourceData: Uint8Array,
|
|
62
|
+
crypto: Crypto = globalThis.crypto
|
|
63
|
+
): Promise<{ hashes: Uint8Array; hashDataSize: number }> {
|
|
64
|
+
const blockSize = IVFC_HASH_BLOCK_SIZE;
|
|
65
|
+
const sourceSize = sourceData.length;
|
|
66
|
+
|
|
67
|
+
// Calculate number of blocks and hash table size
|
|
68
|
+
const numBlocks = Math.ceil(sourceSize / blockSize);
|
|
69
|
+
const hashDataSize = numBlocks * HASH_SIZE;
|
|
70
|
+
|
|
71
|
+
// Pad to block size boundary
|
|
72
|
+
const paddedSize = align(hashDataSize, blockSize);
|
|
73
|
+
const hashes = new Uint8Array(paddedSize);
|
|
74
|
+
|
|
75
|
+
// Hash each block
|
|
76
|
+
const block = new Uint8Array(blockSize);
|
|
77
|
+
for (let i = 0; i < numBlocks; i++) {
|
|
78
|
+
const offset = i * blockSize;
|
|
79
|
+
const remaining = sourceSize - offset;
|
|
80
|
+
const readSize = Math.min(remaining, blockSize);
|
|
81
|
+
|
|
82
|
+
// Zero-fill the block buffer (important for the last partial block)
|
|
83
|
+
block.fill(0);
|
|
84
|
+
block.set(sourceData.subarray(offset, offset + readSize));
|
|
85
|
+
|
|
86
|
+
const hash = await sha256(block.subarray(0, readSize), crypto);
|
|
87
|
+
hashes.set(hash, i * HASH_SIZE);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return { hashes, hashDataSize };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Result of building an IVFC hash tree.
|
|
95
|
+
*/
|
|
96
|
+
export interface IvfcResult {
|
|
97
|
+
/** The IVFC header (0xE0 bytes) including the master hash */
|
|
98
|
+
header: ArrayBuffer;
|
|
99
|
+
|
|
100
|
+
/** Level data arrays, from level 1 (top) to level 5 (bottom).
|
|
101
|
+
* Level 6 is the original data and is not included here. */
|
|
102
|
+
levels: Uint8Array[];
|
|
103
|
+
|
|
104
|
+
/** Total size of all level data (levels 1-5) */
|
|
105
|
+
totalLevelSize: number;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Build an IVFC hash tree from source data (typically a RomFS image).
|
|
110
|
+
*
|
|
111
|
+
* @param data - The source data (level 6). Must already be padded to IVFC_HASH_BLOCK_SIZE.
|
|
112
|
+
* @param crypto - Optional Crypto implementation
|
|
113
|
+
* @returns IVFC header and all intermediate level data
|
|
114
|
+
*/
|
|
115
|
+
export async function build(
|
|
116
|
+
data: Uint8Array,
|
|
117
|
+
crypto: Crypto = globalThis.crypto
|
|
118
|
+
): Promise<IvfcResult> {
|
|
119
|
+
// Build levels from bottom (6) to top (1)
|
|
120
|
+
// Level 6 = data, Level 5 = hashes of level 6, ..., Level 1 = hashes of level 2
|
|
121
|
+
const levelData: Uint8Array[] = [];
|
|
122
|
+
const levelHashDataSizes: number[] = [];
|
|
123
|
+
const levelPaddedSizes: number[] = [];
|
|
124
|
+
|
|
125
|
+
let currentSource = data;
|
|
126
|
+
|
|
127
|
+
for (let level = 0; level < IVFC_NUM_LEVELS - 1; level++) {
|
|
128
|
+
const { hashes, hashDataSize } = await createLevel(
|
|
129
|
+
currentSource,
|
|
130
|
+
crypto
|
|
131
|
+
);
|
|
132
|
+
levelData.unshift(hashes); // Prepend (we're building bottom-up but storing top-down)
|
|
133
|
+
levelHashDataSizes.unshift(hashDataSize);
|
|
134
|
+
levelPaddedSizes.unshift(hashes.length);
|
|
135
|
+
currentSource = hashes;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Calculate master hash = SHA-256 of level 1 (the top level, which is levelData[0])
|
|
139
|
+
const masterHash = await sha256(levelData[0], crypto);
|
|
140
|
+
|
|
141
|
+
// Build the IVFC header (0xE0 bytes)
|
|
142
|
+
const header = new ArrayBuffer(IVFC_HEADER_SIZE);
|
|
143
|
+
const view = new DataView(header);
|
|
144
|
+
|
|
145
|
+
// IVFC header fields
|
|
146
|
+
view.setUint32(0x00, IVFC_MAGIC, true); // magic = "IVFC"
|
|
147
|
+
view.setUint32(0x04, IVFC_ID, true); // id = 0x20000
|
|
148
|
+
view.setUint32(0x08, HASH_SIZE, true); // master_hash_size = 0x20
|
|
149
|
+
view.setUint32(0x0c, IVFC_NUM_LEVELS + 1, true); // num_levels = 7 (6 hash levels + 1 data level)
|
|
150
|
+
|
|
151
|
+
// Level headers (6 entries, each 0x18 bytes, starting at offset 0x10)
|
|
152
|
+
// These describe levels 1-6 (level 6 = data itself)
|
|
153
|
+
// The logical_offset for each level is the cumulative offset of all previous levels
|
|
154
|
+
let logicalOffset = 0;
|
|
155
|
+
for (let i = 0; i < IVFC_NUM_LEVELS - 1; i++) {
|
|
156
|
+
const entryOffset = 0x10 + i * 0x18;
|
|
157
|
+
const dataSize = levelPaddedSizes[i];
|
|
158
|
+
|
|
159
|
+
// logical_offset (8 bytes)
|
|
160
|
+
view.setBigUint64(entryOffset + 0x00, BigInt(logicalOffset), true);
|
|
161
|
+
// hash_data_size (8 bytes)
|
|
162
|
+
view.setBigUint64(entryOffset + 0x08, BigInt(dataSize), true);
|
|
163
|
+
// block_size (4 bytes) — log2 of the block size
|
|
164
|
+
view.setUint32(entryOffset + 0x10, 0x0e, true); // 2^14 = 0x4000
|
|
165
|
+
// reserved (4 bytes) — already zero
|
|
166
|
+
|
|
167
|
+
logicalOffset += dataSize;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Level 6 entry (the actual data)
|
|
171
|
+
{
|
|
172
|
+
const entryOffset = 0x10 + (IVFC_NUM_LEVELS - 1) * 0x18;
|
|
173
|
+
view.setBigUint64(entryOffset + 0x00, BigInt(logicalOffset), true);
|
|
174
|
+
view.setBigUint64(entryOffset + 0x08, BigInt(data.length), true);
|
|
175
|
+
view.setUint32(entryOffset + 0x10, 0x0e, true);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Master hash at offset 0xC0 (0x20 bytes)
|
|
179
|
+
new Uint8Array(header, 0xc0, HASH_SIZE).set(masterHash);
|
|
180
|
+
|
|
181
|
+
// Calculate total level size
|
|
182
|
+
let totalLevelSize = 0;
|
|
183
|
+
for (const level of levelData) {
|
|
184
|
+
totalLevelSize += level.length;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
header,
|
|
189
|
+
levels: levelData,
|
|
190
|
+
totalLevelSize,
|
|
191
|
+
};
|
|
192
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es2020",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"sourceMap": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"skipLibCheck": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*.ts"]
|
|
14
|
+
}
|