@glivion/square-screen-js-sdk 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 (44) hide show
  1. package/.github/workflows/build-js-sdk.yml +70 -0
  2. package/README.md +463 -0
  3. package/eslint.config.js +3 -0
  4. package/examples/react-app/README.md +73 -0
  5. package/examples/react-app/eslint.config.js +22 -0
  6. package/examples/react-app/index.html +13 -0
  7. package/examples/react-app/package-lock.json +2239 -0
  8. package/examples/react-app/package.json +31 -0
  9. package/examples/react-app/public/favicon.svg +1 -0
  10. package/examples/react-app/public/icons.svg +24 -0
  11. package/examples/react-app/src/App.css +184 -0
  12. package/examples/react-app/src/App.tsx +157 -0
  13. package/examples/react-app/src/EmergencyTicker.tsx +25 -0
  14. package/examples/react-app/src/HeadlessExample.tsx +66 -0
  15. package/examples/react-app/src/RendererExample.tsx +70 -0
  16. package/examples/react-app/src/assets/hero.png +0 -0
  17. package/examples/react-app/src/assets/react.svg +1 -0
  18. package/examples/react-app/src/assets/vite.svg +1 -0
  19. package/examples/react-app/src/index.css +183 -0
  20. package/examples/react-app/src/main.tsx +10 -0
  21. package/examples/react-app/src/mockNetworkDataSource.ts +116 -0
  22. package/examples/react-app/src/usePlayer.ts +71 -0
  23. package/examples/react-app/tsconfig.app.json +25 -0
  24. package/examples/react-app/tsconfig.json +7 -0
  25. package/examples/react-app/tsconfig.node.json +24 -0
  26. package/examples/react-app/vite.config.ts +7 -0
  27. package/examples/react-app/yarn.lock +1089 -0
  28. package/package.json +49 -0
  29. package/src/__tests__/cache/SquareScreenCache.test.ts +375 -0
  30. package/src/__tests__/network/NetworkClient.test.ts +217 -0
  31. package/src/__tests__/network/mappers.test.ts +163 -0
  32. package/src/__tests__/player/SquareScreenPlayer.test.ts +840 -0
  33. package/src/cache/SquareScreenCache.ts +154 -0
  34. package/src/constants.ts +9 -0
  35. package/src/core/types.ts +251 -0
  36. package/src/env.d.ts +4 -0
  37. package/src/index.ts +34 -0
  38. package/src/network/NetworkClient.ts +234 -0
  39. package/src/network/apiTypes.ts +89 -0
  40. package/src/network/mappers.ts +106 -0
  41. package/src/player/SquareScreenPlayer.ts +414 -0
  42. package/src/renderer/SquareScreenRenderer.ts +282 -0
  43. package/tsconfig.json +12 -0
  44. package/tsdown.config.ts +23 -0
@@ -0,0 +1,70 @@
1
+ name: Build JS SDK
2
+
3
+ on:
4
+ push:
5
+ branches: [ develop ]
6
+ pull_request:
7
+ branches: [ develop ]
8
+
9
+ jobs:
10
+ lint:
11
+ name: Lint
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+
16
+ - uses: actions/setup-node@v4
17
+ with:
18
+ node-version: 20
19
+ cache: yarn
20
+
21
+ - name: Install dependencies
22
+ run: yarn install --frozen-lockfile
23
+
24
+ - name: ESLint
25
+ run: yarn lint
26
+
27
+ - name: Type check
28
+ run: yarn typecheck
29
+
30
+ test:
31
+ name: Test
32
+ needs: lint
33
+ runs-on: ubuntu-latest
34
+ steps:
35
+ - uses: actions/checkout@v4
36
+
37
+ - uses: actions/setup-node@v4
38
+ with:
39
+ node-version: 20
40
+ cache: yarn
41
+
42
+ - name: Install dependencies
43
+ run: yarn install --frozen-lockfile
44
+
45
+ - name: Run tests
46
+ run: yarn test
47
+
48
+ build:
49
+ name: Build
50
+ needs: test
51
+ runs-on: ubuntu-latest
52
+ steps:
53
+ - uses: actions/checkout@v4
54
+
55
+ - uses: actions/setup-node@v4
56
+ with:
57
+ node-version: 20
58
+ cache: yarn
59
+
60
+ - name: Install dependencies
61
+ run: yarn install --frozen-lockfile
62
+
63
+ - name: Build
64
+ run: yarn build
65
+
66
+ - name: Upload dist artifact
67
+ uses: actions/upload-artifact@v4
68
+ with:
69
+ name: dist
70
+ path: dist/
package/README.md ADDED
@@ -0,0 +1,463 @@
1
+ # SquareScreen JS SDK
2
+
3
+ A framework-agnostic JavaScript/TypeScript SDK for building SquareScreen digital signage players. It handles playlist fetching, local media caching, item advancement, background preloading, polling, heartbeats, and emergency alerts — all without touching the DOM.
4
+
5
+ ---
6
+
7
+ ## Table of contents
8
+
9
+ - [Installation](#installation)
10
+ - [Quick start](#quick-start)
11
+ - [Architecture](#architecture)
12
+ - [Player](#player)
13
+ - [Configuration](#configuration)
14
+ - [Events](#events)
15
+ - [Methods](#methods)
16
+ - [Renderer (optional)](#renderer-optional)
17
+ - [Renderer configuration](#renderer-configuration)
18
+ - [Cache layer](#cache-layer)
19
+ - [Custom cache provider](#custom-cache-provider)
20
+ - [Framework integrations](#framework-integrations)
21
+ - [React](#react)
22
+ - [Vanilla JS](#vanilla-js)
23
+ - [Mocking for development](#mocking-for-development)
24
+ - [Example app](#example-app)
25
+
26
+ ---
27
+
28
+ ## Installation
29
+
30
+ ```bash
31
+ npm install square-screen-js-sdk
32
+ # or
33
+ yarn add square-screen-js-sdk
34
+ ```
35
+
36
+ ---
37
+
38
+ ## Quick start
39
+
40
+ ```ts
41
+ import { SquareScreenPlayer, SquareScreenRenderer } from "square-screen-js-sdk";
42
+
43
+ const player = new SquareScreenPlayer({
44
+ deviceId: "your-device-uuid",
45
+ deviceToken: "your-device-token",
46
+ version: "1.0.0",
47
+ });
48
+
49
+ // Optional: attach the built-in renderer to a container element
50
+ const renderer = new SquareScreenRenderer(
51
+ document.getElementById("screen"),
52
+ player,
53
+ { defaultTransition: "fade", transitionDuration: 500 },
54
+ );
55
+ renderer.mount();
56
+
57
+ // Listen to player events
58
+ player.addEventListener("itemchange", ({ detail }) => {
59
+ console.log("Now playing:", detail.item.name, `(${detail.index + 1}/${detail.total})`);
60
+ });
61
+
62
+ player.addEventListener("emergencyalert", ({ detail }) => {
63
+ if (detail.alert) console.warn("Emergency:", detail.alert.title);
64
+ });
65
+
66
+ player.addEventListener("statuschange", ({ detail }) => {
67
+ console.log("Status:", detail.status); // "connecting" | "online" | "offline"
68
+ });
69
+
70
+ await player.start();
71
+
72
+ // Tear down
73
+ player.stop();
74
+ renderer.unmount();
75
+ ```
76
+
77
+ ---
78
+
79
+ ## Architecture
80
+
81
+ The SDK is split into three independent layers:
82
+
83
+ ```
84
+ ┌─────────────────────────────────────────┐
85
+ │ Your application │
86
+ └────────────────┬────────────────────────┘
87
+ │ events / state
88
+ ┌────────────────▼────────────────────────┐
89
+ │ SquareScreenPlayer │ headless — no DOM
90
+ │ · fetches & caches playlists │
91
+ │ · advances items by duration │
92
+ │ · background media preloading │
93
+ │ · polls for updates & emergency alerts │
94
+ │ · heartbeat │
95
+ └──────┬──────────────────┬───────────────┘
96
+ │ │
97
+ ┌──────▼──────┐ ┌────────▼──────────────┐
98
+ │ NetworkClient│ │ SquareScreenCache │
99
+ │ (fetch/API)│ │ IndexedDB + Cache API│
100
+ └─────────────┘ └───────────────────────┘
101
+ ```
102
+
103
+ The optional `SquareScreenRenderer` sits above your application and listens to `itemchange` events to manage `<img>` / `<video>` elements and CSS transitions. You can ignore it entirely and render with any framework.
104
+
105
+ ---
106
+
107
+ ## Player
108
+
109
+ ### Configuration
110
+
111
+ All REST traffic uses the production API root bundled with the package (`SQUARESCREEN_API_BASE_URL`, currently `https://api.squarescreen.io/api/v1`). You cannot point the default client at another host; pass a custom `networkDataSource` only for tests, staging gateways, or non-standard deployments.
112
+
113
+ ```ts
114
+ interface SquareScreenPlayerConfig {
115
+ /** Device UUID */
116
+ deviceId: string;
117
+ /** Device secret token — never expose this in client-side logs */
118
+ deviceToken: string;
119
+ /** SDK version string sent on every heartbeat */
120
+ version: string;
121
+ /** How long a cached playlist stays fresh in ms. Default: 5 minutes */
122
+ ttl?: number;
123
+ /** How often to poll for playlist updates in ms. Default: 30 seconds */
124
+ pollInterval?: number;
125
+ /** How often to poll for emergency alerts in ms. Default: 15 seconds */
126
+ emergencyPollInterval?: number;
127
+ /** How often to send a heartbeat in ms. Default: 60 seconds */
128
+ heartbeatInterval?: number;
129
+ /**
130
+ * Override the network layer with a custom implementation — useful for
131
+ * testing or mocking without a real API. When provided, deviceId and
132
+ * deviceToken are ignored for network calls.
133
+ */
134
+ networkDataSource?: NetworkDataSource;
135
+ /**
136
+ * Override the cache layer with a custom implementation. When omitted the
137
+ * default SquareScreenCache (IndexedDB + Cache API) is used.
138
+ */
139
+ cacheProvider?: SquareScreenCacheProvider;
140
+ }
141
+ ```
142
+
143
+ > **Note:** The constructor throws if `deviceId` or `deviceToken` is missing when no `networkDataSource` override is provided. With a mock or custom network layer, those credentials are not validated.
144
+
145
+ ### Events
146
+
147
+ Use `addEventListener` / `removeEventListener` exactly like any DOM `EventTarget`. TypeScript users get full type inference on `event.detail`.
148
+
149
+ | Event | Detail | When it fires |
150
+ |---|---|---|
151
+ | `itemchange` | `{ item: PlaylistItem, index: number, total: number }` | Current item advances |
152
+ | `statuschange` | `{ status: DeviceStatus }` | Connectivity state changes (`"connecting"` → `"online"` / `"offline"`) |
153
+ | `emergencyalert` | `{ alert: EmergencyAlert \| null }` | Alert becomes active, changes, or is cleared |
154
+ | `playlistupdate` | `{ playlist: Playlist }` | Playlist is refreshed from the network or cache |
155
+
156
+ ```ts
157
+ player.addEventListener("itemchange", ({ detail }) => {
158
+ const { item, index, total } = detail;
159
+ // item.url is a blob: URL if the media is cached locally,
160
+ // or the original HTTPS URL on first play (cached in the background).
161
+ // item.type is "image" | "video"
162
+ // item.duration is how long this item plays in seconds
163
+ // item.transition is "fade" | "slide" | "none"
164
+ });
165
+ ```
166
+
167
+ ### Methods
168
+
169
+ | Method | Description |
170
+ |---|---|
171
+ | `start()` | Fetches the playlist, begins playback, starts polling and heartbeat. Returns a `Promise` that resolves once the first load attempt completes. |
172
+ | `stop()` | Clears all timers and internal state. Call this when tearing down the player. |
173
+ | `refreshPlaylist()` | Manually trigger a playlist refresh outside the normal poll cycle. |
174
+ | `currentItem` | Getter — returns the currently playing `PlaylistItem`, or `null`. |
175
+
176
+ ---
177
+
178
+ ## Renderer (optional)
179
+
180
+ `SquareScreenRenderer` is a vanilla JS DOM renderer that wires directly to a player instance. It is entirely optional — you can build your own rendering layer using the player's events.
181
+
182
+ ```ts
183
+ import { SquareScreenRenderer } from "square-screen-js-sdk";
184
+
185
+ const renderer = new SquareScreenRenderer(containerElement, player, options);
186
+ renderer.mount(); // inject DOM and start listening
187
+ renderer.unmount(); // remove DOM and stop listening
188
+ ```
189
+
190
+ The renderer maintains two absolutely-positioned slot elements inside the container and alternates between them on each item change, enabling smooth cross-slot transitions.
191
+
192
+ **What it handles:**
193
+ - Creates and manages `<img>` and `<video>` elements
194
+ - Waits for video `canplay` before transitioning (with a configurable timeout to prevent blocking on slow networks)
195
+ - Sets `autoplay`, `muted`, `playsInline`, and `loop` on videos (required for browser autoplay policies and iOS)
196
+ - Full-screen emergency alert overlay
197
+
198
+ **What it does not handle:**
199
+ - Emergency alert UI beyond the built-in overlay — listen to `"emergencyalert"` if you need custom presentation
200
+
201
+ ### Renderer configuration
202
+
203
+ ```ts
204
+ interface SquareScreenRendererConfig {
205
+ /** Transition to use when an item has no transition set. Default: "none" */
206
+ defaultTransition?: "fade" | "slide" | "none";
207
+ /** Transition animation duration in ms. Default: 500 */
208
+ transitionDuration?: number;
209
+ /**
210
+ * Maximum ms to wait for a video to reach canplay before transitioning anyway.
211
+ * Prevents long stalls when media loads from a slow network on the first cycle.
212
+ * Default: 3000. Set to 0 to transition immediately without waiting.
213
+ */
214
+ canPlayTimeout?: number;
215
+ }
216
+ ```
217
+
218
+ ---
219
+
220
+ ## Cache layer
221
+
222
+ The cache layer is used internally by the player and requires no direct interaction in most cases.
223
+
224
+ **How it works:**
225
+
226
+ - **Playlist metadata** is stored in IndexedDB (via the `idb` library) with a per-record TTL. Each playlist is stored as a single document keyed by UUID; items are stored inline.
227
+ - **Media blobs** are stored in the browser's Cache API, keyed by the original media URL. This is the same storage used by service workers, making future SW integration straightforward.
228
+ - Once a blob is retrieved from the Cache API it is wrapped in a `URL.createObjectURL` handle that is kept in an in-memory map and reused on subsequent calls. All handles are revoked when `clear()` is called.
229
+
230
+ **Media URL lifecycle:**
231
+
232
+ On the **first cycle**, if a media file hasn't been downloaded yet, the player dispatches `itemchange` with the raw HTTPS URL immediately (no blocking). The file is downloaded in the background and stored in the Cache API. On every subsequent cycle the blob URL is used — transitions are instant.
233
+
234
+ **Preloading:**
235
+
236
+ Set `strategy.preloadCount` on the playlist to control how many upcoming items are pre-downloaded while the current item plays.
237
+
238
+ ```
239
+ preloadCount: 1 → always 1 item ahead
240
+ preloadCount: 3 → 3 items ahead
241
+ preloadCount: 0 → disabled
242
+ preloadCount: (absent) → download entire playlist up front
243
+ ```
244
+
245
+ **Offline fallback:**
246
+
247
+ If the network is unavailable on start-up and the player has never loaded a playlist in the current session, it falls back to the last-known playlist UUID (stored in `localStorage`) and loads it from IndexedDB with `allowStale: true` — serving it even if its TTL has expired.
248
+
249
+ ### Custom cache provider
250
+
251
+ Implement `SquareScreenCacheProvider` and pass it via `cacheProvider` to replace the default storage entirely:
252
+
253
+ ```ts
254
+ import type { SquareScreenCacheProvider, Playlist } from "square-screen-js-sdk";
255
+
256
+ const myCache: SquareScreenCacheProvider = {
257
+ async getPlaylist(uuid, allowStale) { /* ... */ return null; },
258
+ async savePlaylist(playlist, ttlMs) { /* ... */ },
259
+ async getMediaUrl(url) { /* ... */ return null; },
260
+ async saveMedia(url, blob) { /* ... */ return URL.createObjectURL(blob); },
261
+ async clear() { /* ... */ },
262
+ };
263
+
264
+ const player = new SquareScreenPlayer({ ..., cacheProvider: myCache });
265
+ ```
266
+
267
+ ---
268
+
269
+ ## Framework integrations
270
+
271
+ ### React
272
+
273
+ Store named listener references so cleanup can call `removeEventListener` precisely — required for React StrictMode, which mounts effects twice.
274
+
275
+ ```tsx
276
+ import { useEffect, useRef, useState } from "react";
277
+ import {
278
+ SquareScreenPlayer,
279
+ SquareScreenRenderer,
280
+ type SquareScreenPlayerConfig,
281
+ type DeviceStatus,
282
+ type EmergencyAlert,
283
+ type PlaylistItem,
284
+ } from "square-screen-js-sdk";
285
+
286
+ function usePlayer(config: SquareScreenPlayerConfig | null) {
287
+ const playerRef = useRef<SquareScreenPlayer | null>(null);
288
+ const [state, setState] = useState({
289
+ status: "connecting" as DeviceStatus,
290
+ currentItem: null as PlaylistItem | null,
291
+ currentIndex: 0,
292
+ total: 0,
293
+ alert: null as EmergencyAlert | null,
294
+ });
295
+
296
+ useEffect(() => {
297
+ if (!config) return;
298
+ const player = new SquareScreenPlayer(config);
299
+ playerRef.current = player;
300
+
301
+ const onStatus: Parameters<typeof player.addEventListener<"statuschange">>[1] =
302
+ (e) => setState((s) => ({ ...s, status: e.detail.status }));
303
+
304
+ const onItem: Parameters<typeof player.addEventListener<"itemchange">>[1] =
305
+ (e) => setState((s) => ({
306
+ ...s,
307
+ currentItem: e.detail.item,
308
+ currentIndex: e.detail.index,
309
+ total: e.detail.total,
310
+ }));
311
+
312
+ const onAlert: Parameters<typeof player.addEventListener<"emergencyalert">>[1] =
313
+ (e) => setState((s) => ({ ...s, alert: e.detail.alert }));
314
+
315
+ player.addEventListener("statuschange", onStatus);
316
+ player.addEventListener("itemchange", onItem);
317
+ player.addEventListener("emergencyalert", onAlert);
318
+ player.start();
319
+
320
+ return () => {
321
+ player.removeEventListener("statuschange", onStatus);
322
+ player.removeEventListener("itemchange", onItem);
323
+ player.removeEventListener("emergencyalert", onAlert);
324
+ player.stop();
325
+ playerRef.current = null;
326
+ };
327
+ }, [config]);
328
+
329
+ return { player: playerRef.current, state };
330
+ }
331
+
332
+ // Renderer approach — SDK handles the DOM
333
+ function RendererScreen({ config }: { config: SquareScreenPlayerConfig }) {
334
+ const { player } = usePlayer(config);
335
+ const containerRef = useRef<HTMLDivElement>(null);
336
+
337
+ useEffect(() => {
338
+ if (!player || !containerRef.current) return;
339
+ const renderer = new SquareScreenRenderer(containerRef.current, player, {
340
+ defaultTransition: "fade",
341
+ transitionDuration: 500,
342
+ });
343
+ renderer.mount();
344
+ return () => renderer.unmount();
345
+ }, [player]);
346
+
347
+ return <div ref={containerRef} style={{ width: "100%", height: "100%" }} />;
348
+ }
349
+
350
+ // Headless approach — you control the DOM
351
+ function HeadlessScreen({ config }: { config: SquareScreenPlayerConfig }) {
352
+ const { state } = usePlayer(config);
353
+ const { currentItem } = state;
354
+ if (!currentItem) return null;
355
+ return currentItem.type === "video"
356
+ ? <video key={currentItem.uuid} src={currentItem.url} autoPlay muted playsInline loop />
357
+ : <img key={currentItem.uuid} src={currentItem.url} alt={currentItem.name} />;
358
+ }
359
+ ```
360
+
361
+ ### Vanilla JS
362
+
363
+ ```js
364
+ import { SquareScreenPlayer, SquareScreenRenderer } from "square-screen-js-sdk";
365
+
366
+ const player = new SquareScreenPlayer({
367
+ deviceId: "device-uuid",
368
+ deviceToken: "device-token",
369
+ version: "1.0.0",
370
+ pollInterval: 30_000,
371
+ });
372
+
373
+ const renderer = new SquareScreenRenderer(
374
+ document.getElementById("screen"),
375
+ player,
376
+ { defaultTransition: "slide", transitionDuration: 400 },
377
+ );
378
+ renderer.mount();
379
+
380
+ // Handle emergency alerts yourself
381
+ player.addEventListener("emergencyalert", ({ detail }) => {
382
+ const ticker = document.getElementById("emergency-ticker");
383
+ if (detail.alert) {
384
+ ticker.textContent = `${detail.alert.title}: ${detail.alert.message}`;
385
+ ticker.hidden = false;
386
+ } else {
387
+ ticker.hidden = true;
388
+ }
389
+ });
390
+
391
+ await player.start();
392
+
393
+ // Tear down when done
394
+ player.stop();
395
+ renderer.unmount();
396
+ ```
397
+
398
+ ---
399
+
400
+ ## Mocking for development
401
+
402
+ Pass a `networkDataSource` to bypass the real API entirely:
403
+
404
+ ```ts
405
+ import type { NetworkDataSource } from "square-screen-js-sdk";
406
+
407
+ const mockDataSource: NetworkDataSource = {
408
+ fetchPlaylist: async () => ({
409
+ success: true,
410
+ data: {
411
+ uuid: "playlist-001",
412
+ cachedAt: Date.now(),
413
+ strategy: { loop: true, shuffle: false, preloadCount: 2 },
414
+ items: [
415
+ {
416
+ uuid: "item-001",
417
+ name: "Demo image",
418
+ type: "image",
419
+ url: "https://fastly.picsum.photos/id/10/2500/1667.jpg",
420
+ duration: 5,
421
+ transition: "fade",
422
+ },
423
+ {
424
+ uuid: "item-002",
425
+ name: "Demo video",
426
+ type: "video",
427
+ url: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
428
+ duration: 10,
429
+ transition: "slide",
430
+ },
431
+ ],
432
+ },
433
+ }),
434
+ fetchVideoPlaylist: async () => ({ success: false, error: { kind: "network", code: 0, message: "mock" } }),
435
+ fetchImagePlaylist: async () => ({ success: false, error: { kind: "network", code: 0, message: "mock" } }),
436
+ checkForEmergencyAlert: async () => ({ success: true, data: null }),
437
+ healthCheck: async () => ({ success: true, data: { received: true } }),
438
+ reportPlaybackEvent: async () => ({ success: true, data: { recorded: true } }),
439
+ };
440
+
441
+ const player = new SquareScreenPlayer({
442
+ deviceId: "",
443
+ deviceToken: "",
444
+ version: "1.0.0",
445
+ networkDataSource: mockDataSource,
446
+ });
447
+ ```
448
+
449
+ ---
450
+
451
+ ## Example app
452
+
453
+ A React example app lives in [`examples/react-app`](./examples/react-app). It demonstrates both the renderer and headless approaches side by side, with a built-in mock data source and an emergency alert toggle.
454
+
455
+ ```bash
456
+ cd examples/react-app
457
+ npm install
458
+ npm run dev
459
+ ```
460
+
461
+ The **Renderer** tab uses `SquareScreenRenderer` attached to a div — transitions and media lifecycle are fully managed by the SDK.
462
+
463
+ The **Headless** tab renders directly with React JSX, using `key={item.uuid}` on media elements to force remount when the item changes.
@@ -0,0 +1,3 @@
1
+ import tseslint from 'typescript-eslint';
2
+
3
+ export default tseslint.config(tseslint.configs.recommended);
@@ -0,0 +1,73 @@
1
+ # React + TypeScript + Vite
2
+
3
+ This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4
+
5
+ Currently, two official plugins are available:
6
+
7
+ - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
8
+ - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
9
+
10
+ ## React Compiler
11
+
12
+ The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
13
+
14
+ ## Expanding the ESLint configuration
15
+
16
+ If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
17
+
18
+ ```js
19
+ export default defineConfig([
20
+ globalIgnores(['dist']),
21
+ {
22
+ files: ['**/*.{ts,tsx}'],
23
+ extends: [
24
+ // Other configs...
25
+
26
+ // Remove tseslint.configs.recommended and replace with this
27
+ tseslint.configs.recommendedTypeChecked,
28
+ // Alternatively, use this for stricter rules
29
+ tseslint.configs.strictTypeChecked,
30
+ // Optionally, add this for stylistic rules
31
+ tseslint.configs.stylisticTypeChecked,
32
+
33
+ // Other configs...
34
+ ],
35
+ languageOptions: {
36
+ parserOptions: {
37
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
38
+ tsconfigRootDir: import.meta.dirname,
39
+ },
40
+ // other options...
41
+ },
42
+ },
43
+ ])
44
+ ```
45
+
46
+ You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
47
+
48
+ ```js
49
+ // eslint.config.js
50
+ import reactX from 'eslint-plugin-react-x'
51
+ import reactDom from 'eslint-plugin-react-dom'
52
+
53
+ export default defineConfig([
54
+ globalIgnores(['dist']),
55
+ {
56
+ files: ['**/*.{ts,tsx}'],
57
+ extends: [
58
+ // Other configs...
59
+ // Enable lint rules for React
60
+ reactX.configs['recommended-typescript'],
61
+ // Enable lint rules for React DOM
62
+ reactDom.configs.recommended,
63
+ ],
64
+ languageOptions: {
65
+ parserOptions: {
66
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
67
+ tsconfigRootDir: import.meta.dirname,
68
+ },
69
+ // other options...
70
+ },
71
+ },
72
+ ])
73
+ ```
@@ -0,0 +1,22 @@
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import tseslint from 'typescript-eslint'
6
+ import { defineConfig, globalIgnores } from 'eslint/config'
7
+
8
+ export default defineConfig([
9
+ globalIgnores(['dist']),
10
+ {
11
+ files: ['**/*.{ts,tsx}'],
12
+ extends: [
13
+ js.configs.recommended,
14
+ tseslint.configs.recommended,
15
+ reactHooks.configs.flat.recommended,
16
+ reactRefresh.configs.vite,
17
+ ],
18
+ languageOptions: {
19
+ globals: globals.browser,
20
+ },
21
+ },
22
+ ])
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>react-app</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
+ </html>