@scarlett-player/playlist 1.8.0 → 1.9.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/README.md +126 -0
- package/dist/index.cjs +30 -15
- package/dist/index.js +32 -15
- package/package.json +6 -6
package/README.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# @scarlett-player/playlist
|
|
2
|
+
|
|
3
|
+
Playlist plugin for [Scarlett Player](https://scarlettplayer.com). Queue management (add, insert, remove, move, clear), shuffle with a Fisher-Yates order, repeat modes, auto-advance when a track ends, optional localStorage persistence, and control-bar buttons for previous, next and a queue panel when `@scarlett-player/ui` is installed.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @scarlett-player/playlist @scarlett-player/core
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`@scarlett-player/core` is a required peer dependency. `@scarlett-player/ui` is an optional peer: the control-bar controls register only when it is present, and everything else works without it.
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
This matches the audio example in the root README.
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { createPlayer } from '@scarlett-player/core';
|
|
19
|
+
import { createNativePlugin } from '@scarlett-player/native';
|
|
20
|
+
import { createAudioUIPlugin } from '@scarlett-player/audio-ui';
|
|
21
|
+
import { createPlaylistPlugin, type IPlaylistPlugin } from '@scarlett-player/playlist';
|
|
22
|
+
import { createMediaSessionPlugin } from '@scarlett-player/media-session';
|
|
23
|
+
|
|
24
|
+
const player = await createPlayer({
|
|
25
|
+
container: document.getElementById('audio-player'),
|
|
26
|
+
plugins: [
|
|
27
|
+
createNativePlugin(),
|
|
28
|
+
createAudioUIPlugin({ layout: 'full' }),
|
|
29
|
+
createPlaylistPlugin({
|
|
30
|
+
tracks: [
|
|
31
|
+
{ id: '1', src: '/track1.mp3', title: 'Track 1', artist: 'Artist', artwork: '/art1.jpg' },
|
|
32
|
+
{ id: '2', src: '/track2.mp3', title: 'Track 2', artist: 'Artist', artwork: '/art2.jpg' },
|
|
33
|
+
],
|
|
34
|
+
}),
|
|
35
|
+
createMediaSessionPlugin(),
|
|
36
|
+
],
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Start the first track through the playlist, not through `src` or
|
|
40
|
+
// player.load(). The playlist owns the current index: play() sets it, writes
|
|
41
|
+
// the track's title and artwork into state, and emits `media:load-request`,
|
|
42
|
+
// which the player loads and plays. Loading a source behind the playlist's
|
|
43
|
+
// back leaves its index pointing at nothing, so next/previous and
|
|
44
|
+
// auto-advance start from the wrong place.
|
|
45
|
+
const playlist = player.getPlugin<IPlaylistPlugin>('playlist');
|
|
46
|
+
playlist?.play(0);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
If you do pass `src` to `createPlayer()` for the first track, set `initialIndex: 0` so the playlist knows which track is already loaded.
|
|
50
|
+
|
|
51
|
+
## Configuration
|
|
52
|
+
|
|
53
|
+
| Option | Type | Default | Description |
|
|
54
|
+
|---|---|---|---|
|
|
55
|
+
| `tracks` | `PlaylistTrack[]` | `[]` | Initial queue. Tracks without an `id` get a generated one |
|
|
56
|
+
| `autoAdvance` | `boolean` | `true` | Select the next track on `playback:ended` |
|
|
57
|
+
| `advanceDelay` | `number` | `0` | Milliseconds to wait before auto-advancing |
|
|
58
|
+
| `autoLoad` | `boolean` | `true` | Emit `media:load-request` with `autoplay: true` whenever the current track changes, so the player loads it. Set to `false` to load from `playlist:change` yourself |
|
|
59
|
+
| `shuffle` | `boolean` | `false` | Initial shuffle state |
|
|
60
|
+
| `repeat` | `'none' \| 'one' \| 'all'` | `'none'` | Initial repeat mode |
|
|
61
|
+
| `initialIndex` | `number` | `-1` | Index of the track already loaded by the player. `-1` means no track is active. Out-of-range values fall back to `-1` |
|
|
62
|
+
| `persist` | `boolean` | `false` | Save tracks, index, shuffle and repeat to localStorage and restore them on init |
|
|
63
|
+
| `persistKey` | `string` | `'scarlett-playlist'` | localStorage key used by `persist` |
|
|
64
|
+
| `preloadNext` | `boolean` | `true` | Accepted and stored, but no code path currently reads it |
|
|
65
|
+
|
|
66
|
+
A `PlaylistTrack` is `{ id, src, title?, artist?, album?, artwork?, duration?, type?, mimeType?, metadata? }` plus any extra properties you want to carry. `type` defaults to `'audio'` in player state when omitted.
|
|
67
|
+
|
|
68
|
+
## Plugin API
|
|
69
|
+
|
|
70
|
+
Registered under the id `playlist`; retrieve it with `player.getPlugin<IPlaylistPlugin>('playlist')`.
|
|
71
|
+
|
|
72
|
+
| Method | Description |
|
|
73
|
+
|---|---|
|
|
74
|
+
| `add(track \| track[])` | Append to the queue |
|
|
75
|
+
| `insert(index, track)` | Insert at a position (clamped to the queue length) |
|
|
76
|
+
| `remove(idOrIndex)` | Remove by id or index. Removing the current track selects the track that slides into its slot |
|
|
77
|
+
| `move(fromIndex, toIndex)` | Reorder |
|
|
78
|
+
| `clear()` | Empty the queue and reset the index to `-1` |
|
|
79
|
+
| `play(idOrIndex?)` | Select a track. With no argument, resumes the current track or starts the first |
|
|
80
|
+
| `next()` / `previous()` | Step through the queue honouring shuffle and repeat. `previous()` restarts the current track when more than 3 seconds in |
|
|
81
|
+
| `toggleShuffle()` / `setShuffle(enabled)` | Shuffle control. The current track stays first in the new order |
|
|
82
|
+
| `cycleRepeat()` / `setRepeat(mode)` | Repeat control. `cycleRepeat()` goes none, all, one |
|
|
83
|
+
| `getState()` | `{ tracks, currentIndex, currentTrack, shuffle, repeat, shuffleOrder, hasNext, hasPrevious }` |
|
|
84
|
+
| `getTracks()` / `getCurrentTrack()` / `getTrack(id)` | Read the queue |
|
|
85
|
+
|
|
86
|
+
With the player focused, the `N` and `P` keys call `next()` and `previous()`. Keystrokes inside inputs or with a modifier held are left alone.
|
|
87
|
+
|
|
88
|
+
## Events
|
|
89
|
+
|
|
90
|
+
All payload types live in `PlayerEventMap` in `@scarlett-player/core`.
|
|
91
|
+
|
|
92
|
+
| Event | Payload | When |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| `playlist:change` | `{ track, index }` | The current track changed. `track` is `null` after `clear()` |
|
|
95
|
+
| `playlist:add` | `{ track, index }` | `add()` (once per track) or `insert()` |
|
|
96
|
+
| `playlist:remove` | `{ track, index }` | `remove()` |
|
|
97
|
+
| `playlist:reorder` | `{ tracks }` | `move()` |
|
|
98
|
+
| `playlist:clear` | `void` | `clear()` |
|
|
99
|
+
| `playlist:shuffle` | `{ enabled }` | Shuffle changed |
|
|
100
|
+
| `playlist:repeat` | `{ mode }` | Repeat mode changed |
|
|
101
|
+
| `playlist:ended` | `void` | The last track ended with nothing to advance to |
|
|
102
|
+
|
|
103
|
+
On every track change the plugin also writes `title`, `poster` (from `artwork`) and `mediaType` into player state, clearing title and poster when the track has none so a previous track's values never leak.
|
|
104
|
+
|
|
105
|
+
## Control-bar controls
|
|
106
|
+
|
|
107
|
+
When `@scarlett-player/ui` is installed, three controls are registered. Nothing is placed until you list them in the UI layout:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
uiPlugin({ controls: ['playlist-previous', 'play', 'playlist-next', 'progress', 'spacer', 'playlist', 'fullscreen'] })
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
| Control id | Renders |
|
|
114
|
+
|---|---|
|
|
115
|
+
| `playlist-previous` / `playlist-next` | Skip buttons, disabled at the ends of the queue |
|
|
116
|
+
| `playlist` | A button that opens the queue as a list |
|
|
117
|
+
|
|
118
|
+
All three hide themselves when the queue has one track or fewer. The classes `PlaylistSkipButton`, `PlaylistPanel` and the `PLAYLIST_ICONS` map are exported for custom layouts.
|
|
119
|
+
|
|
120
|
+
## Styling
|
|
121
|
+
|
|
122
|
+
A stylesheet is injected once per document (style element id `sp-playlist-styles`). Override these classes to restyle: `sp-playlist-skip` (with `--previous` / `--next` and the `[disabled]` state), `sp-playlist`, `sp-playlist--open`, `sp-playlist__button`, `sp-playlist__panel`, `sp-playlist__item`, `sp-playlist__item--active`, `sp-playlist__position`, `sp-playlist__text`, `sp-playlist__title` and `sp-playlist__artist`. No CSS custom properties are used.
|
|
123
|
+
|
|
124
|
+
## License
|
|
125
|
+
|
|
126
|
+
MIT
|
package/dist/index.cjs
CHANGED
|
@@ -37,6 +37,7 @@ __export(index_exports, {
|
|
|
37
37
|
default: () => index_default
|
|
38
38
|
});
|
|
39
39
|
module.exports = __toCommonJS(index_exports);
|
|
40
|
+
var import_core2 = require("@scarlett-player/core");
|
|
40
41
|
|
|
41
42
|
// src/controls.ts
|
|
42
43
|
var PLAYLIST_ICONS = {
|
|
@@ -197,6 +198,7 @@ var PlaylistPanel = class {
|
|
|
197
198
|
};
|
|
198
199
|
|
|
199
200
|
// src/styles.ts
|
|
201
|
+
var import_core = require("@scarlett-player/core");
|
|
200
202
|
var STYLE_ID = "sp-playlist-styles";
|
|
201
203
|
var styles = `
|
|
202
204
|
.sp-playlist-skip[disabled] {
|
|
@@ -298,18 +300,11 @@ var styles = `
|
|
|
298
300
|
}
|
|
299
301
|
`;
|
|
300
302
|
function injectStyles() {
|
|
301
|
-
|
|
302
|
-
return null;
|
|
303
|
-
}
|
|
304
|
-
const el = document.createElement("style");
|
|
305
|
-
el.id = STYLE_ID;
|
|
306
|
-
el.textContent = styles;
|
|
307
|
-
document.head.appendChild(el);
|
|
308
|
-
return el;
|
|
303
|
+
return (0, import_core.injectSharedStyles)(STYLE_ID, styles);
|
|
309
304
|
}
|
|
310
305
|
|
|
311
306
|
// src/version.ts
|
|
312
|
-
var PKG_VERSION = true ? "1.
|
|
307
|
+
var PKG_VERSION = true ? "1.9.0" : "0.0.0-dev";
|
|
313
308
|
|
|
314
309
|
// src/index.ts
|
|
315
310
|
var DEFAULT_CONFIG = {
|
|
@@ -336,6 +331,9 @@ function shuffleArray(array) {
|
|
|
336
331
|
function createPlaylistPlugin(config) {
|
|
337
332
|
const mergedConfig = { ...DEFAULT_CONFIG, ...config };
|
|
338
333
|
let api = null;
|
|
334
|
+
let releaseStyles = null;
|
|
335
|
+
let releaseControls = null;
|
|
336
|
+
let lifecycle = 0;
|
|
339
337
|
let tracks = mergedConfig.tracks || [];
|
|
340
338
|
let currentIndex = mergedConfig.initialIndex ?? -1;
|
|
341
339
|
if (currentIndex < -1 || currentIndex >= tracks.length) {
|
|
@@ -458,7 +456,7 @@ function createPlaylistPlugin(config) {
|
|
|
458
456
|
}
|
|
459
457
|
const track = tracks[index];
|
|
460
458
|
currentIndex = index;
|
|
461
|
-
api?.logger.info("Track changed", { index, title: track.title, src: track.src });
|
|
459
|
+
api?.logger.info("Track changed", { index, title: track.title, src: (0, import_core2.sanitizeUrl)(track.src) });
|
|
462
460
|
api?.setState("title", track.title || "");
|
|
463
461
|
api?.setState("poster", track.artwork || "");
|
|
464
462
|
api?.setState("mediaType", track.type || "audio");
|
|
@@ -474,6 +472,7 @@ function createPlaylistPlugin(config) {
|
|
|
474
472
|
type: "feature",
|
|
475
473
|
description: "Playlist management with shuffle, repeat, and gapless playback",
|
|
476
474
|
async init(pluginApi) {
|
|
475
|
+
const generation = ++lifecycle;
|
|
477
476
|
api = pluginApi;
|
|
478
477
|
api.logger.info("Playlist plugin initialized");
|
|
479
478
|
loadPersistedPlaylist();
|
|
@@ -499,20 +498,31 @@ function createPlaylistPlugin(config) {
|
|
|
499
498
|
api?.emit("playlist:ended", void 0);
|
|
500
499
|
}
|
|
501
500
|
});
|
|
502
|
-
injectStyles();
|
|
503
|
-
|
|
501
|
+
releaseStyles = injectStyles();
|
|
502
|
+
const owner = api.container;
|
|
503
|
+
void import("@scarlett-player/ui").then(({ registerControl, unregisterControl }) => {
|
|
504
|
+
if (generation !== lifecycle) return;
|
|
504
505
|
const self = plugin;
|
|
505
506
|
registerControl(
|
|
506
507
|
"playlist-previous",
|
|
507
|
-
() => new PlaylistSkipButton(self, "previous")
|
|
508
|
+
() => new PlaylistSkipButton(self, "previous"),
|
|
509
|
+
{ owner }
|
|
508
510
|
);
|
|
509
|
-
registerControl("playlist-next", () => new PlaylistSkipButton(self, "next")
|
|
511
|
+
registerControl("playlist-next", () => new PlaylistSkipButton(self, "next"), {
|
|
512
|
+
owner
|
|
513
|
+
});
|
|
510
514
|
registerControl(
|
|
511
515
|
"playlist",
|
|
512
516
|
() => new PlaylistPanel(self, {
|
|
513
517
|
onSelect: (index) => self.play(index)
|
|
514
|
-
})
|
|
518
|
+
}),
|
|
519
|
+
{ owner }
|
|
515
520
|
);
|
|
521
|
+
releaseControls = () => {
|
|
522
|
+
unregisterControl("playlist-previous", { owner });
|
|
523
|
+
unregisterControl("playlist-next", { owner });
|
|
524
|
+
unregisterControl("playlist", { owner });
|
|
525
|
+
};
|
|
516
526
|
}).catch(() => {
|
|
517
527
|
api?.logger.debug("@scarlett-player/ui not present, playlist controls not registered");
|
|
518
528
|
});
|
|
@@ -543,8 +553,13 @@ function createPlaylistPlugin(config) {
|
|
|
543
553
|
});
|
|
544
554
|
},
|
|
545
555
|
async destroy() {
|
|
556
|
+
lifecycle++;
|
|
546
557
|
api?.logger.info("Playlist plugin destroying");
|
|
547
558
|
persistPlaylist();
|
|
559
|
+
releaseStyles?.();
|
|
560
|
+
releaseStyles = null;
|
|
561
|
+
releaseControls?.();
|
|
562
|
+
releaseControls = null;
|
|
548
563
|
api = null;
|
|
549
564
|
},
|
|
550
565
|
add(trackOrTracks) {
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { sanitizeUrl } from "@scarlett-player/core";
|
|
3
|
+
|
|
1
4
|
// src/controls.ts
|
|
2
5
|
var PLAYLIST_ICONS = {
|
|
3
6
|
previous: '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false"><path d="M6 6h2v12H6V6zm3.5 6L18 6v12l-8.5-6z"/></svg>',
|
|
@@ -157,6 +160,7 @@ var PlaylistPanel = class {
|
|
|
157
160
|
};
|
|
158
161
|
|
|
159
162
|
// src/styles.ts
|
|
163
|
+
import { injectSharedStyles } from "@scarlett-player/core";
|
|
160
164
|
var STYLE_ID = "sp-playlist-styles";
|
|
161
165
|
var styles = `
|
|
162
166
|
.sp-playlist-skip[disabled] {
|
|
@@ -258,18 +262,11 @@ var styles = `
|
|
|
258
262
|
}
|
|
259
263
|
`;
|
|
260
264
|
function injectStyles() {
|
|
261
|
-
|
|
262
|
-
return null;
|
|
263
|
-
}
|
|
264
|
-
const el = document.createElement("style");
|
|
265
|
-
el.id = STYLE_ID;
|
|
266
|
-
el.textContent = styles;
|
|
267
|
-
document.head.appendChild(el);
|
|
268
|
-
return el;
|
|
265
|
+
return injectSharedStyles(STYLE_ID, styles);
|
|
269
266
|
}
|
|
270
267
|
|
|
271
268
|
// src/version.ts
|
|
272
|
-
var PKG_VERSION = true ? "1.
|
|
269
|
+
var PKG_VERSION = true ? "1.9.0" : "0.0.0-dev";
|
|
273
270
|
|
|
274
271
|
// src/index.ts
|
|
275
272
|
var DEFAULT_CONFIG = {
|
|
@@ -296,6 +293,9 @@ function shuffleArray(array) {
|
|
|
296
293
|
function createPlaylistPlugin(config) {
|
|
297
294
|
const mergedConfig = { ...DEFAULT_CONFIG, ...config };
|
|
298
295
|
let api = null;
|
|
296
|
+
let releaseStyles = null;
|
|
297
|
+
let releaseControls = null;
|
|
298
|
+
let lifecycle = 0;
|
|
299
299
|
let tracks = mergedConfig.tracks || [];
|
|
300
300
|
let currentIndex = mergedConfig.initialIndex ?? -1;
|
|
301
301
|
if (currentIndex < -1 || currentIndex >= tracks.length) {
|
|
@@ -418,7 +418,7 @@ function createPlaylistPlugin(config) {
|
|
|
418
418
|
}
|
|
419
419
|
const track = tracks[index];
|
|
420
420
|
currentIndex = index;
|
|
421
|
-
api?.logger.info("Track changed", { index, title: track.title, src: track.src });
|
|
421
|
+
api?.logger.info("Track changed", { index, title: track.title, src: sanitizeUrl(track.src) });
|
|
422
422
|
api?.setState("title", track.title || "");
|
|
423
423
|
api?.setState("poster", track.artwork || "");
|
|
424
424
|
api?.setState("mediaType", track.type || "audio");
|
|
@@ -434,6 +434,7 @@ function createPlaylistPlugin(config) {
|
|
|
434
434
|
type: "feature",
|
|
435
435
|
description: "Playlist management with shuffle, repeat, and gapless playback",
|
|
436
436
|
async init(pluginApi) {
|
|
437
|
+
const generation = ++lifecycle;
|
|
437
438
|
api = pluginApi;
|
|
438
439
|
api.logger.info("Playlist plugin initialized");
|
|
439
440
|
loadPersistedPlaylist();
|
|
@@ -459,20 +460,31 @@ function createPlaylistPlugin(config) {
|
|
|
459
460
|
api?.emit("playlist:ended", void 0);
|
|
460
461
|
}
|
|
461
462
|
});
|
|
462
|
-
injectStyles();
|
|
463
|
-
|
|
463
|
+
releaseStyles = injectStyles();
|
|
464
|
+
const owner = api.container;
|
|
465
|
+
void import("@scarlett-player/ui").then(({ registerControl, unregisterControl }) => {
|
|
466
|
+
if (generation !== lifecycle) return;
|
|
464
467
|
const self = plugin;
|
|
465
468
|
registerControl(
|
|
466
469
|
"playlist-previous",
|
|
467
|
-
() => new PlaylistSkipButton(self, "previous")
|
|
470
|
+
() => new PlaylistSkipButton(self, "previous"),
|
|
471
|
+
{ owner }
|
|
468
472
|
);
|
|
469
|
-
registerControl("playlist-next", () => new PlaylistSkipButton(self, "next")
|
|
473
|
+
registerControl("playlist-next", () => new PlaylistSkipButton(self, "next"), {
|
|
474
|
+
owner
|
|
475
|
+
});
|
|
470
476
|
registerControl(
|
|
471
477
|
"playlist",
|
|
472
478
|
() => new PlaylistPanel(self, {
|
|
473
479
|
onSelect: (index) => self.play(index)
|
|
474
|
-
})
|
|
480
|
+
}),
|
|
481
|
+
{ owner }
|
|
475
482
|
);
|
|
483
|
+
releaseControls = () => {
|
|
484
|
+
unregisterControl("playlist-previous", { owner });
|
|
485
|
+
unregisterControl("playlist-next", { owner });
|
|
486
|
+
unregisterControl("playlist", { owner });
|
|
487
|
+
};
|
|
476
488
|
}).catch(() => {
|
|
477
489
|
api?.logger.debug("@scarlett-player/ui not present, playlist controls not registered");
|
|
478
490
|
});
|
|
@@ -503,8 +515,13 @@ function createPlaylistPlugin(config) {
|
|
|
503
515
|
});
|
|
504
516
|
},
|
|
505
517
|
async destroy() {
|
|
518
|
+
lifecycle++;
|
|
506
519
|
api?.logger.info("Playlist plugin destroying");
|
|
507
520
|
persistPlaylist();
|
|
521
|
+
releaseStyles?.();
|
|
522
|
+
releaseStyles = null;
|
|
523
|
+
releaseControls?.();
|
|
524
|
+
releaseControls = null;
|
|
508
525
|
api = null;
|
|
509
526
|
},
|
|
510
527
|
add(trackOrTracks) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scarlett-player/playlist",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "Playlist Plugin for Scarlett Player - Queue management, shuffle, repeat, and gapless playback",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -22,16 +22,16 @@
|
|
|
22
22
|
"dist"
|
|
23
23
|
],
|
|
24
24
|
"peerDependencies": {
|
|
25
|
-
"@scarlett-player/core": "^1.
|
|
26
|
-
"@scarlett-player/ui": "^1.
|
|
25
|
+
"@scarlett-player/core": "^1.8.0",
|
|
26
|
+
"@scarlett-player/ui": "^1.8.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"tsup": "^8.0.0",
|
|
30
30
|
"typescript": "^5.3.0",
|
|
31
31
|
"vitest": "^1.6.0",
|
|
32
32
|
"jsdom": "^24.0.0",
|
|
33
|
-
"@scarlett-player/core": "1.
|
|
34
|
-
"@scarlett-player/ui": "1.
|
|
33
|
+
"@scarlett-player/core": "1.9.0",
|
|
34
|
+
"@scarlett-player/ui": "1.9.0"
|
|
35
35
|
},
|
|
36
36
|
"keywords": [
|
|
37
37
|
"video",
|
|
@@ -65,6 +65,6 @@
|
|
|
65
65
|
"dev": "tsup --watch",
|
|
66
66
|
"test": "vitest --run",
|
|
67
67
|
"test:watch": "vitest",
|
|
68
|
-
"typecheck": "tsc --noEmit"
|
|
68
|
+
"typecheck": "tsc --noEmit -p tsconfig.typecheck.json"
|
|
69
69
|
}
|
|
70
70
|
}
|