chart-preview 1.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/.changeset/README.md +8 -0
- package/.changeset/config.json +11 -0
- package/.github/workflows/release.yml +38 -0
- package/.vscode/extensions.json +6 -0
- package/.vscode/settings.json +8 -0
- package/README.md +541 -0
- package/dev/index.html +299 -0
- package/dev/main.ts +525 -0
- package/dev/tsconfig.json +16 -0
- package/dist/index.d.ts +496 -0
- package/dist/index.js +2158 -0
- package/dist/index.mjs +2125 -0
- package/package.json +57 -0
- package/src/ChartPreview.ts +1346 -0
- package/src/ChartPreviewPlayer.ts +1163 -0
- package/src/SharedAudioContext.ts +83 -0
- package/src/SngLoader.ts +330 -0
- package/src/index.ts +37 -0
- package/tsconfig.json +26 -0
- package/vite.config.ts +18 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared AudioContext manager for multi-instance support.
|
|
3
|
+
*
|
|
4
|
+
* Browsers limit the number of AudioContexts that can be created (typically ~6).
|
|
5
|
+
* By sharing a single AudioContext across all ChartPreview instances, we can
|
|
6
|
+
* support many more simultaneous players.
|
|
7
|
+
*
|
|
8
|
+
* Each player instance gets its own GainNode connected to the shared context,
|
|
9
|
+
* allowing independent volume control while sharing the same audio destination.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13
|
+
const AudioContextClass =
|
|
14
|
+
typeof window !== "undefined"
|
|
15
|
+
? window.AudioContext || (window as any).webkitAudioContext
|
|
16
|
+
: null;
|
|
17
|
+
|
|
18
|
+
let sharedContext: AudioContext | null = null;
|
|
19
|
+
let activeInstanceCount = 0;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Gets or creates the shared AudioContext.
|
|
23
|
+
* The context is lazily created on first use.
|
|
24
|
+
*
|
|
25
|
+
* @returns The shared AudioContext, or null if AudioContext is not supported
|
|
26
|
+
*/
|
|
27
|
+
export function getSharedAudioContext(): AudioContext | null {
|
|
28
|
+
if (!AudioContextClass) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!sharedContext || sharedContext.state === "closed") {
|
|
33
|
+
sharedContext = new AudioContextClass();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return sharedContext;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Registers a new instance that will use the shared AudioContext.
|
|
41
|
+
* Call this when a player starts using the context.
|
|
42
|
+
*/
|
|
43
|
+
export function registerInstance(): void {
|
|
44
|
+
activeInstanceCount++;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Unregisters an instance from the shared AudioContext.
|
|
49
|
+
* When all instances are unregistered, the context can be closed.
|
|
50
|
+
*
|
|
51
|
+
* Note: We don't actually close the context here because:
|
|
52
|
+
* 1. Creating a new context is expensive
|
|
53
|
+
* 2. The user might create new players soon
|
|
54
|
+
* 3. The context uses minimal resources when not playing
|
|
55
|
+
*/
|
|
56
|
+
export function unregisterInstance(): void {
|
|
57
|
+
activeInstanceCount = Math.max(0, activeInstanceCount - 1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Gets the current number of active instances using the shared context.
|
|
62
|
+
*/
|
|
63
|
+
export function getActiveInstanceCount(): number {
|
|
64
|
+
return activeInstanceCount;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Checks if AudioContext is supported in the current environment.
|
|
69
|
+
*/
|
|
70
|
+
export function isAudioContextSupported(): boolean {
|
|
71
|
+
return AudioContextClass !== null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Resumes the shared AudioContext if it's suspended.
|
|
76
|
+
* This is needed because browsers require user interaction before playing audio.
|
|
77
|
+
*/
|
|
78
|
+
export async function resumeSharedContext(): Promise<void> {
|
|
79
|
+
const ctx = getSharedAudioContext();
|
|
80
|
+
if (ctx && ctx.state === "suspended") {
|
|
81
|
+
await ctx.resume();
|
|
82
|
+
}
|
|
83
|
+
}
|
package/src/SngLoader.ts
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { SngStream } from "parse-sng";
|
|
2
|
+
import {
|
|
3
|
+
scanChartFolder,
|
|
4
|
+
parseChartFile,
|
|
5
|
+
getInstrumentType,
|
|
6
|
+
type Difficulty,
|
|
7
|
+
type Instrument,
|
|
8
|
+
type ScannedChart,
|
|
9
|
+
} from "scan-chart";
|
|
10
|
+
import { ChartPreview, type ParsedChart } from "./ChartPreview";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Audio file extensions supported by the chart preview.
|
|
14
|
+
*/
|
|
15
|
+
const AUDIO_EXTENSIONS = [".ogg", ".mp3", ".wav", ".opus"];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Audio file names to exclude from playback (preview stems, crowd noise).
|
|
19
|
+
*/
|
|
20
|
+
const EXCLUDED_AUDIO_NAMES = ["preview", "crowd"];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Configuration for loading a chart from a URL.
|
|
24
|
+
*/
|
|
25
|
+
export interface LoadFromUrlConfig {
|
|
26
|
+
/** URL to the .sng file */
|
|
27
|
+
url: string;
|
|
28
|
+
/** The instrument to display */
|
|
29
|
+
instrument: Instrument;
|
|
30
|
+
/** The difficulty level to display */
|
|
31
|
+
difficulty: Difficulty;
|
|
32
|
+
/** Initial seek position as a percentage (0-1). Defaults to 0. */
|
|
33
|
+
initialSeekPercent?: number;
|
|
34
|
+
/** AbortSignal to cancel the fetch operation */
|
|
35
|
+
signal?: AbortSignal;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Configuration for loading a chart from raw .sng file data.
|
|
40
|
+
*/
|
|
41
|
+
export interface LoadFromSngFileConfig {
|
|
42
|
+
/** Raw .sng file data */
|
|
43
|
+
sngFile: Uint8Array;
|
|
44
|
+
/** The instrument to display */
|
|
45
|
+
instrument: Instrument;
|
|
46
|
+
/** The difficulty level to display */
|
|
47
|
+
difficulty: Difficulty;
|
|
48
|
+
/** Initial seek position as a percentage (0-1). Defaults to 0. */
|
|
49
|
+
initialSeekPercent?: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Configuration for loading a chart from individual chart and audio files.
|
|
54
|
+
*/
|
|
55
|
+
export interface LoadFromChartFilesConfig {
|
|
56
|
+
/** Array of files with their names and data */
|
|
57
|
+
files: { fileName: string; data: Uint8Array }[];
|
|
58
|
+
/** The instrument to display */
|
|
59
|
+
instrument: Instrument;
|
|
60
|
+
/** The difficulty level to display */
|
|
61
|
+
difficulty: Difficulty;
|
|
62
|
+
/** Initial seek position as a percentage (0-1). Defaults to 0. */
|
|
63
|
+
initialSeekPercent?: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Result of preparing chart data for playback.
|
|
68
|
+
*/
|
|
69
|
+
export interface PreparedChartData {
|
|
70
|
+
parsedChart: ParsedChart;
|
|
71
|
+
textures: Awaited<ReturnType<typeof ChartPreview.loadTextures>>;
|
|
72
|
+
audioFiles: Uint8Array[];
|
|
73
|
+
instrument: Instrument;
|
|
74
|
+
difficulty: Difficulty;
|
|
75
|
+
startDelayMs: number;
|
|
76
|
+
audioLengthMs: number;
|
|
77
|
+
initialSeekPercent: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Extracts files from a .sng archive.
|
|
82
|
+
*
|
|
83
|
+
* @param data - Raw .sng file data
|
|
84
|
+
* @returns Array of extracted files with their names and data
|
|
85
|
+
*/
|
|
86
|
+
export async function extractSngFile(
|
|
87
|
+
data: Uint8Array
|
|
88
|
+
): Promise<{ fileName: string; data: Uint8Array }[]> {
|
|
89
|
+
return new Promise((resolve, reject) => {
|
|
90
|
+
const files: { fileName: string; data: Uint8Array }[] = [];
|
|
91
|
+
|
|
92
|
+
// Convert Uint8Array to ReadableStream
|
|
93
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
94
|
+
start(controller) {
|
|
95
|
+
controller.enqueue(data);
|
|
96
|
+
controller.close();
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const sngStream = new SngStream(stream, { generateSongIni: true });
|
|
101
|
+
|
|
102
|
+
sngStream.on("error", (error) => {
|
|
103
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
sngStream.on("file", async (fileName, fileStream, nextFile) => {
|
|
107
|
+
try {
|
|
108
|
+
// Read the entire file stream into a Uint8Array
|
|
109
|
+
const chunks: Uint8Array[] = [];
|
|
110
|
+
const reader = fileStream.getReader();
|
|
111
|
+
|
|
112
|
+
while (true) {
|
|
113
|
+
const { done, value } = await reader.read();
|
|
114
|
+
if (done) break;
|
|
115
|
+
chunks.push(value);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Combine chunks into a single Uint8Array
|
|
119
|
+
const totalLength = chunks.reduce(
|
|
120
|
+
(sum, chunk) => sum + chunk.length,
|
|
121
|
+
0
|
|
122
|
+
);
|
|
123
|
+
const fileData = new Uint8Array(totalLength);
|
|
124
|
+
let offset = 0;
|
|
125
|
+
for (const chunk of chunks) {
|
|
126
|
+
fileData.set(chunk, offset);
|
|
127
|
+
offset += chunk.length;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
files.push({ fileName, data: fileData });
|
|
131
|
+
|
|
132
|
+
if (nextFile) {
|
|
133
|
+
nextFile();
|
|
134
|
+
} else {
|
|
135
|
+
resolve(files);
|
|
136
|
+
}
|
|
137
|
+
} catch (error) {
|
|
138
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
sngStream.start();
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Finds the chart file (.chart or .mid) from an array of files.
|
|
148
|
+
* Prefers .mid over .chart when both exist.
|
|
149
|
+
*
|
|
150
|
+
* @param files - Array of files to search
|
|
151
|
+
* @returns The chart file, or null if not found
|
|
152
|
+
*/
|
|
153
|
+
export function findChartFile(
|
|
154
|
+
files: { fileName: string; data: Uint8Array }[]
|
|
155
|
+
): { fileName: string; data: Uint8Array; format: "chart" | "mid" } | null {
|
|
156
|
+
// Look for .mid first (preferred)
|
|
157
|
+
const midFile = files.find((f) => f.fileName.toLowerCase().endsWith(".mid"));
|
|
158
|
+
if (midFile) {
|
|
159
|
+
return { ...midFile, format: "mid" };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Fall back to .chart
|
|
163
|
+
const chartFile = files.find((f) =>
|
|
164
|
+
f.fileName.toLowerCase().endsWith(".chart")
|
|
165
|
+
);
|
|
166
|
+
if (chartFile) {
|
|
167
|
+
return { ...chartFile, format: "chart" };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Finds audio files from an array of files, excluding preview and crowd stems.
|
|
175
|
+
*
|
|
176
|
+
* @param files - Array of files to search
|
|
177
|
+
* @returns Array of audio file data
|
|
178
|
+
*/
|
|
179
|
+
export function findAudioFiles(
|
|
180
|
+
files: { fileName: string; data: Uint8Array }[]
|
|
181
|
+
): Uint8Array[] {
|
|
182
|
+
return files
|
|
183
|
+
.filter((file) => {
|
|
184
|
+
const ext = file.fileName
|
|
185
|
+
.toLowerCase()
|
|
186
|
+
.slice(file.fileName.lastIndexOf("."));
|
|
187
|
+
if (!AUDIO_EXTENSIONS.includes(ext)) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Exclude preview and crowd audio files
|
|
192
|
+
const baseName =
|
|
193
|
+
file.fileName
|
|
194
|
+
.toLowerCase()
|
|
195
|
+
.replace(/\.[^/.]+$/, "")
|
|
196
|
+
.split("/")
|
|
197
|
+
.pop() || "";
|
|
198
|
+
return !EXCLUDED_AUDIO_NAMES.some((excluded) =>
|
|
199
|
+
baseName.includes(excluded)
|
|
200
|
+
);
|
|
201
|
+
})
|
|
202
|
+
.map((file) => file.data);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Checks if a filename is a video file (to be excluded from .sng extraction).
|
|
207
|
+
*/
|
|
208
|
+
export function isVideoFile(fileName: string): boolean {
|
|
209
|
+
const videoExtensions = [".mp4", ".avi", ".webm", ".ogv", ".mpeg"];
|
|
210
|
+
const ext = fileName.toLowerCase().slice(fileName.lastIndexOf("."));
|
|
211
|
+
return videoExtensions.includes(ext);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Prepares chart data for playback by extracting, parsing, and loading all resources.
|
|
216
|
+
*
|
|
217
|
+
* @param files - Array of extracted files
|
|
218
|
+
* @param instrument - The instrument to display
|
|
219
|
+
* @param difficulty - The difficulty level to display
|
|
220
|
+
* @param initialSeekPercent - Initial seek position (0-1)
|
|
221
|
+
* @returns Prepared chart data ready for the player
|
|
222
|
+
*/
|
|
223
|
+
export async function prepareChartData(
|
|
224
|
+
files: { fileName: string; data: Uint8Array }[],
|
|
225
|
+
instrument: Instrument,
|
|
226
|
+
difficulty: Difficulty,
|
|
227
|
+
initialSeekPercent = 0
|
|
228
|
+
): Promise<PreparedChartData> {
|
|
229
|
+
// Filter out video files
|
|
230
|
+
const filteredFiles = files.filter((f) => !isVideoFile(f.fileName));
|
|
231
|
+
|
|
232
|
+
// Scan the chart folder to get metadata and modifiers
|
|
233
|
+
const scannedChart: ScannedChart = scanChartFolder(filteredFiles);
|
|
234
|
+
|
|
235
|
+
if (!scannedChart.playable || !scannedChart.notesData) {
|
|
236
|
+
throw new Error("This chart has no playable tracks.");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Find the chart file
|
|
240
|
+
const chartFile = findChartFile(filteredFiles);
|
|
241
|
+
if (!chartFile) {
|
|
242
|
+
throw new Error("No .chart or .mid file found in the chart.");
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Build chart modifiers from scanned chart
|
|
246
|
+
const modifiers = {
|
|
247
|
+
song_length: scannedChart.song_length || 0,
|
|
248
|
+
hopo_frequency: scannedChart.hopo_frequency || 0,
|
|
249
|
+
eighthnote_hopo: scannedChart.eighthnote_hopo || false,
|
|
250
|
+
multiplier_note: scannedChart.multiplier_note || 0,
|
|
251
|
+
sustain_cutoff_threshold: scannedChart.sustain_cutoff_threshold ?? -1,
|
|
252
|
+
chord_snap_threshold: scannedChart.chord_snap_threshold || 0,
|
|
253
|
+
five_lane_drums: scannedChart.five_lane_drums || false,
|
|
254
|
+
pro_drums: scannedChart.pro_drums || false,
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// Parse the chart
|
|
258
|
+
const parsedChart = parseChartFile(
|
|
259
|
+
chartFile.data,
|
|
260
|
+
chartFile.format,
|
|
261
|
+
modifiers
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
// Verify the requested track exists
|
|
265
|
+
const track = parsedChart.trackData.find(
|
|
266
|
+
(t) => t.instrument === instrument && t.difficulty === difficulty
|
|
267
|
+
);
|
|
268
|
+
if (!track) {
|
|
269
|
+
throw new Error(
|
|
270
|
+
`No ${difficulty} ${instrument} track found in this chart.`
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Find audio files
|
|
275
|
+
const audioFiles = findAudioFiles(filteredFiles);
|
|
276
|
+
|
|
277
|
+
// Load textures for the instrument type
|
|
278
|
+
const instrumentType = getInstrumentType(instrument);
|
|
279
|
+
const textures = await ChartPreview.loadTextures(instrumentType);
|
|
280
|
+
|
|
281
|
+
// Calculate audio length from the last note or from metadata
|
|
282
|
+
const lastNote = parsedChart.trackData
|
|
283
|
+
.flatMap((t) => t.noteEventGroups.flat())
|
|
284
|
+
.reduce((max, note) => Math.max(max, note.msTime + note.msLength), 0);
|
|
285
|
+
const audioLengthMs = scannedChart.song_length || lastNote + 5000;
|
|
286
|
+
|
|
287
|
+
// Calculate start delay from chart metadata
|
|
288
|
+
const startDelayMs =
|
|
289
|
+
(scannedChart.delay || 0) + (scannedChart.chart_offset || 0) * 1000;
|
|
290
|
+
|
|
291
|
+
// Calculate initial seek position from preview_start_time if not explicitly provided
|
|
292
|
+
const seekPercent =
|
|
293
|
+
initialSeekPercent > 0
|
|
294
|
+
? initialSeekPercent
|
|
295
|
+
: scannedChart.preview_start_time
|
|
296
|
+
? scannedChart.preview_start_time / audioLengthMs
|
|
297
|
+
: 0;
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
parsedChart,
|
|
301
|
+
textures,
|
|
302
|
+
audioFiles,
|
|
303
|
+
instrument,
|
|
304
|
+
difficulty,
|
|
305
|
+
startDelayMs,
|
|
306
|
+
audioLengthMs,
|
|
307
|
+
initialSeekPercent: seekPercent,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Fetches a .sng file from a URL.
|
|
313
|
+
*
|
|
314
|
+
* @param url - URL to fetch the .sng file from
|
|
315
|
+
* @param signal - Optional AbortSignal to cancel the fetch
|
|
316
|
+
* @returns Raw .sng file data
|
|
317
|
+
*/
|
|
318
|
+
export async function fetchSngFile(
|
|
319
|
+
url: string,
|
|
320
|
+
signal?: AbortSignal
|
|
321
|
+
): Promise<Uint8Array> {
|
|
322
|
+
const response = await fetch(url, { signal });
|
|
323
|
+
if (!response.ok) {
|
|
324
|
+
throw new Error(
|
|
325
|
+
`Failed to fetch chart: ${response.status} ${response.statusText}`
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
329
|
+
return new Uint8Array(arrayBuffer);
|
|
330
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Core classes
|
|
2
|
+
export {
|
|
3
|
+
ChartPreview,
|
|
4
|
+
type ChartPreviewConfig,
|
|
5
|
+
type ChartPreviewEvents,
|
|
6
|
+
type ParsedChart,
|
|
7
|
+
} from "./ChartPreview";
|
|
8
|
+
|
|
9
|
+
// Web Component player
|
|
10
|
+
export {
|
|
11
|
+
ChartPreviewPlayer,
|
|
12
|
+
type ChartPreviewPlayerConfig,
|
|
13
|
+
type PlayerState,
|
|
14
|
+
} from "./ChartPreviewPlayer";
|
|
15
|
+
|
|
16
|
+
// Loader utilities and types (for advanced use cases)
|
|
17
|
+
export {
|
|
18
|
+
extractSngFile,
|
|
19
|
+
fetchSngFile,
|
|
20
|
+
prepareChartData,
|
|
21
|
+
findChartFile,
|
|
22
|
+
findAudioFiles,
|
|
23
|
+
isVideoFile,
|
|
24
|
+
type LoadFromUrlConfig,
|
|
25
|
+
type LoadFromSngFileConfig,
|
|
26
|
+
type LoadFromChartFilesConfig,
|
|
27
|
+
type PreparedChartData,
|
|
28
|
+
} from "./SngLoader";
|
|
29
|
+
|
|
30
|
+
// Re-export commonly used types from scan-chart for convenience
|
|
31
|
+
export {
|
|
32
|
+
type Difficulty,
|
|
33
|
+
type Instrument,
|
|
34
|
+
type InstrumentType,
|
|
35
|
+
getInstrumentType,
|
|
36
|
+
instrumentTypes,
|
|
37
|
+
} from "scan-chart";
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"emitDeclarationOnly": false,
|
|
9
|
+
"allowSyntheticDefaultImports": true,
|
|
10
|
+
"sourceMap": true,
|
|
11
|
+
"outDir": "./dist",
|
|
12
|
+
"baseUrl": "./",
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"strictNullChecks": true,
|
|
15
|
+
"noImplicitAny": true,
|
|
16
|
+
"downlevelIteration": true,
|
|
17
|
+
"strictBindCallApply": true,
|
|
18
|
+
"forceConsistentCasingInFileNames": true,
|
|
19
|
+
"noFallthroughCasesInSwitch": true,
|
|
20
|
+
"noErrorTruncation": true,
|
|
21
|
+
"noEmit": true,
|
|
22
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"]
|
|
23
|
+
},
|
|
24
|
+
"include": ["src/**/*"],
|
|
25
|
+
"exclude": ["node_modules", "dist", "dev"]
|
|
26
|
+
}
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineConfig } from "vite";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
|
|
7
|
+
export default defineConfig({
|
|
8
|
+
root: "dev",
|
|
9
|
+
server: {
|
|
10
|
+
port: 3000,
|
|
11
|
+
open: true,
|
|
12
|
+
},
|
|
13
|
+
resolve: {
|
|
14
|
+
alias: {
|
|
15
|
+
"chart-preview": path.resolve(__dirname, "src/index.ts"),
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
});
|