@freqhole/playlistz 0.0.1
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/config.json +11 -0
- package/.changeset/nice-wolves-thank.md +5 -0
- package/.freqhole-versions.json +4 -0
- package/.github/copilot-instructions.md +201 -0
- package/.github/workflows/changesets.yml +50 -0
- package/.github/workflows/npm-publish.yml +124 -0
- package/.github/workflows/pr-checks.yml +103 -0
- package/README.md +30 -0
- package/build-component.js +141 -0
- package/build-zip-bundle-lib.js +44 -0
- package/config/playwright.config.ts +47 -0
- package/config/vite.config.ts +44 -0
- package/config/vitest.config.ts +39 -0
- package/dist/assets/automerge_wasm_bg-Cik4BF9l.wasm +0 -0
- package/dist/assets/index-CbOXzGiA.js +216 -0
- package/dist/assets/index-CbOXzGiA.js.map +1 -0
- package/dist/assets/index-TvJ6RFpy.css +1 -0
- package/dist/assets/midden-DceCrT_L.js +2 -0
- package/dist/assets/midden-DceCrT_L.js.map +1 -0
- package/dist/assets/midden_bg-BLhfGIU-.wasm +0 -0
- package/dist/index.html +55 -0
- package/dist/sw.js +134 -0
- package/docs/AUTOMERGE_P2P_PLAN.md +233 -0
- package/docs/COLLABORATIVE_SHARING_PLAN.md +188 -0
- package/docs/E2E_TESTID_PLAN.md +234 -0
- package/docs/IROH_P2P_PLAN.md +302 -0
- package/docs/ROADMAP.md +695 -0
- package/docs/TODO.md +167 -0
- package/docs/bundle-embedding-plan.md +134 -0
- package/docs/standalone-refactor.md +184 -0
- package/e2e/all-playlists.spec.ts +220 -0
- package/e2e/audio-player.spec.ts +226 -0
- package/e2e/collaborative-features.spec.ts +229 -0
- package/e2e/contexts.ts +238 -0
- package/e2e/edit-panel.spec.ts +87 -0
- package/e2e/fixtures/bare-glitch-1s.m4a +0 -0
- package/e2e/fixtures/bare-glitch-1s.mp3 +0 -0
- package/e2e/fixtures/bare-glitch-1s.ogg +0 -0
- package/e2e/fixtures/chord-stack-3s.wav +0 -0
- package/e2e/fixtures/cover-anim.gif +0 -0
- package/e2e/fixtures/cover-blue.png +0 -0
- package/e2e/fixtures/cover-checkers.png +0 -0
- package/e2e/fixtures/cover-gradient.jpg +0 -0
- package/e2e/fixtures/cover-mono.gif +0 -0
- package/e2e/fixtures/cover-noise.png +0 -0
- package/e2e/fixtures/cover-plasma.webp +0 -0
- package/e2e/fixtures/cover-portrait.jpg +0 -0
- package/e2e/fixtures/cover-red.png +0 -0
- package/e2e/fixtures/cover-thumb.jpg +0 -0
- package/e2e/fixtures/cover-wide.webp +0 -0
- package/e2e/fixtures/generate.mjs +257 -0
- package/e2e/fixtures/long-drone-90s.mp3 +0 -0
- package/e2e/fixtures/noisy-binaural-8s.mp3 +0 -0
- package/e2e/fixtures/tagged-a3-4s.m4a +0 -0
- package/e2e/fixtures/tagged-a3-4s.mp3 +0 -0
- package/e2e/fixtures/tagged-a3-4s.ogg +0 -0
- package/e2e/fixtures/tagged-c5-3s.m4a +0 -0
- package/e2e/fixtures/tagged-c5-3s.mp3 +0 -0
- package/e2e/fixtures/tagged-c5-3s.ogg +0 -0
- package/e2e/fixtures/tagged-f4-6s.m4a +0 -0
- package/e2e/fixtures/tagged-f4-6s.mp3 +0 -0
- package/e2e/fixtures/tagged-f4-6s.ogg +0 -0
- package/e2e/fixtures/tone-220hz-10s.wav +0 -0
- package/e2e/fixtures/tone-440hz-2s.wav +0 -0
- package/e2e/fixtures/tone-880hz-5s.wav +0 -0
- package/e2e/fixtures/tone-stereo-3s.wav +0 -0
- package/e2e/fixtures/user-provided/README.md +1 -0
- package/e2e/helpers/app.ts +143 -0
- package/e2e/helpers/hooks.ts +133 -0
- package/e2e/helpers/index.ts +12 -0
- package/e2e/helpers/media.ts +125 -0
- package/e2e/helpers.ts +10 -0
- package/e2e/p2p-collaboration.spec.ts +356 -0
- package/e2e/p2p-multi-peer.spec.ts +723 -0
- package/e2e/p2p-states.spec.ts +302 -0
- package/e2e/playback.spec.ts +56 -0
- package/e2e/playlist-crud.spec.ts +126 -0
- package/e2e/share-link-autoplay.spec.ts +129 -0
- package/e2e/sharing-access.spec.ts +205 -0
- package/e2e/sharing.spec.ts +195 -0
- package/e2e/song-cache-state.spec.ts +202 -0
- package/e2e/zip-bundle.spec.ts +855 -0
- package/eslint.config.js +114 -0
- package/index.html +54 -0
- package/package.json +119 -0
- package/public/sw.js +134 -0
- package/scripts/use-local.mjs +37 -0
- package/scripts/use-published.mjs +37 -0
- package/src/App.tsx +9 -0
- package/src/cli/check.ts +164 -0
- package/src/cli/generate.ts +184 -0
- package/src/cli/http.ts +88 -0
- package/src/cli/index.ts +65 -0
- package/src/cli/init.ts +18 -0
- package/src/components/AllPlaylistsPanel.tsx +713 -0
- package/src/components/AudioPlayer.tsx +122 -0
- package/src/components/MarqueeText.tsx +101 -0
- package/src/components/PlaylistCoverModal.tsx +519 -0
- package/src/components/PlaylistEditPanel.tsx +803 -0
- package/src/components/PlaylistSharePanel.tsx +1020 -0
- package/src/components/ShareLinkKnockPanel.tsx +144 -0
- package/src/components/SharePanel.tsx +584 -0
- package/src/components/SongEditModal.tsx +453 -0
- package/src/components/SongEditPanel.tsx +578 -0
- package/src/components/SongRow.tsx +689 -0
- package/src/components/index.tsx +494 -0
- package/src/components/playlist/index.tsx +1203 -0
- package/src/context/PlaylistzContext.tsx +74 -0
- package/src/dev-hooks.ts +35 -0
- package/src/hooks/createDocIndexQuery.ts +53 -0
- package/src/hooks/createDocStore.test.ts +303 -0
- package/src/hooks/createDocStore.ts +90 -0
- package/src/hooks/useDragAndDrop.test.ts +474 -0
- package/src/hooks/useDragAndDrop.ts +400 -0
- package/src/hooks/useImageModal.test.ts +174 -0
- package/src/hooks/useImageModal.ts +201 -0
- package/src/hooks/usePlaylistManager.test.ts +453 -0
- package/src/hooks/usePlaylistManager.ts +685 -0
- package/src/hooks/usePlaylistsQuery.test.tsx +120 -0
- package/src/hooks/usePlaylistsQuery.ts +44 -0
- package/src/hooks/useSongState.test.ts +236 -0
- package/src/hooks/useSongState.ts +114 -0
- package/src/hooks/useUIState.ts +71 -0
- package/src/index.tsx +18 -0
- package/src/services/audioService.dev.ts +22 -0
- package/src/services/audioService.test.ts +1226 -0
- package/src/services/audioService.ts +1395 -0
- package/src/services/automergeRepo.test.ts +269 -0
- package/src/services/automergeRepo.ts +226 -0
- package/src/services/blobTransferService.dev.ts +119 -0
- package/src/services/blobTransferService.test.ts +441 -0
- package/src/services/blobTransferService.ts +702 -0
- package/src/services/docIndexService.test.ts +179 -0
- package/src/services/docIndexService.ts +118 -0
- package/src/services/fileProcessingService.test.ts +554 -0
- package/src/services/fileProcessingService.ts +239 -0
- package/src/services/imageService.test.ts +701 -0
- package/src/services/imageService.ts +365 -0
- package/src/services/indexedDBService.integration.test.ts +104 -0
- package/src/services/indexedDBService.test.ts +202 -0
- package/src/services/indexedDBService.ts +436 -0
- package/src/services/offlineService.test.ts +661 -0
- package/src/services/offlineService.ts +382 -0
- package/src/services/p2pService.test.ts +305 -0
- package/src/services/p2pService.ts +344 -0
- package/src/services/playlistDocService.test.ts +448 -0
- package/src/services/playlistDocService.ts +707 -0
- package/src/services/playlistDownloadService.test.ts +674 -0
- package/src/services/playlistDownloadService.ts +389 -0
- package/src/services/sharingService.test.ts +812 -0
- package/src/services/sharingService.ts +1073 -0
- package/src/services/sharingState.ts +161 -0
- package/src/services/songReactivity.test.ts +620 -0
- package/src/services/songReactivity.ts +145 -0
- package/src/services/standaloneService.test.ts +1025 -0
- package/src/services/standaloneService.ts +588 -0
- package/src/services/streamingAudioService.test.ts +275 -0
- package/src/services/streamingAudioService.ts +166 -0
- package/src/styles.css +428 -0
- package/src/test-setup.ts +547 -0
- package/src/types/global.d.ts +40 -0
- package/src/types/playlist.ts +99 -0
- package/src/utils/hashUtils.ts +41 -0
- package/src/utils/log.ts +97 -0
- package/src/utils/m3u.test.ts +172 -0
- package/src/utils/m3u.ts +136 -0
- package/src/utils/mockData.ts +166 -0
- package/src/utils/standaloneTemplates.test.ts +175 -0
- package/src/utils/standaloneTemplates.ts +83 -0
- package/src/utils/swTemplate.ts +84 -0
- package/src/utils/timeUtils.ts +166 -0
- package/src/utils/typeGuards.ts +171 -0
- package/src/web-component.tsx +98 -0
- package/src/zip-bundle/index.ts +7 -0
- package/src/zip-bundle/m3u.ts +45 -0
- package/src/zip-bundle/types.ts +50 -0
- package/src/zip-bundle/utils.ts +33 -0
- package/src/zip-bundle/zipBuilder.ts +309 -0
- package/tailwind.config.js +55 -0
- package/tsconfig.json +43 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
|
|
3
|
+
"changelog": "@changesets/cli/changelog",
|
|
4
|
+
"commit": false,
|
|
5
|
+
"fixed": [],
|
|
6
|
+
"linked": [],
|
|
7
|
+
"access": "public",
|
|
8
|
+
"baseBranch": "main",
|
|
9
|
+
"updateInternalDependencies": "patch",
|
|
10
|
+
"ignore": []
|
|
11
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# freqhole-playlistz Development Guide
|
|
2
|
+
|
|
3
|
+
## Project Overview
|
|
4
|
+
|
|
5
|
+
freqhole-playlistz is a music playlist management tool built on top of the main freqhole project. It provides a standalone web interface for creating, editing, and sharing playlists.
|
|
6
|
+
|
|
7
|
+
## Code Style
|
|
8
|
+
|
|
9
|
+
### Lowercase Prose Preference
|
|
10
|
+
|
|
11
|
+
Write comments, documentation, and user-facing messages in lowercase conversational style.
|
|
12
|
+
|
|
13
|
+
**Keep uppercase for:**
|
|
14
|
+
|
|
15
|
+
- Acronyms: API, HTTP, JSON, SQL, CRUD, REST, CLI
|
|
16
|
+
- Proper nouns: Rust, TypeScript, GitHub, SQLite, PostgreSQL
|
|
17
|
+
- Code identifiers: function names, type names, constants
|
|
18
|
+
- Special markers: TODO, FIXME, NOTE, WARNING
|
|
19
|
+
|
|
20
|
+
**Use lowercase for:**
|
|
21
|
+
|
|
22
|
+
- Regular comments explaining logic
|
|
23
|
+
- Documentation/docstrings
|
|
24
|
+
- Error messages and user-facing strings
|
|
25
|
+
- Log messages
|
|
26
|
+
|
|
27
|
+
**Examples:**
|
|
28
|
+
|
|
29
|
+
```rust
|
|
30
|
+
// ✅ GOOD
|
|
31
|
+
// extract album metadata from file tags
|
|
32
|
+
let metadata = parse_tags(&file)?;
|
|
33
|
+
|
|
34
|
+
return Err(GrimoireError::NotFound("playlist not found".to_string()));
|
|
35
|
+
|
|
36
|
+
// TODO: add support for batch operations
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```rust
|
|
40
|
+
// ❌ AVOID
|
|
41
|
+
// Extract Album Metadata From File Tags
|
|
42
|
+
let metadata = parse_tags(&file)?;
|
|
43
|
+
|
|
44
|
+
return Err(GrimoireError::NotFound("Playlist Not Found".to_string()));
|
|
45
|
+
|
|
46
|
+
// Todo: Add Support For Batch Operations
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### No Emojis in Code
|
|
50
|
+
|
|
51
|
+
Avoid emojis in comments, error messages, or any code. Use them only in markdown documentation if appropriate.
|
|
52
|
+
|
|
53
|
+
## Conventions
|
|
54
|
+
|
|
55
|
+
- **Naming**: Use `snake_case` for Rust and TypeScript (tho `camelCase` is used)
|
|
56
|
+
- **Documentation**: AI-generated docs live in `docs/`, with `docs/INDEX.md` as the entry point
|
|
57
|
+
|
|
58
|
+
## E2E Testing Conventions
|
|
59
|
+
|
|
60
|
+
### Selectors - prefer `data-testid`
|
|
61
|
+
|
|
62
|
+
Use `data-testid` attributes as the primary selector for interactive elements and structural containers. This decouples tests from copy, tooltips, and class names.
|
|
63
|
+
|
|
64
|
+
**Add `data-testid` to:**
|
|
65
|
+
|
|
66
|
+
- Icon-only buttons with no visible text (hamburger, edit, share, close, remove)
|
|
67
|
+
- Panel/container roots used for scoping child locators
|
|
68
|
+
- App-state sentinels (e.g. the visually-hidden `<h1>` that signals the app has loaded)
|
|
69
|
+
|
|
70
|
+
**Naming scheme:**
|
|
71
|
+
|
|
72
|
+
| Type | Pattern | Examples |
|
|
73
|
+
| ----------------- | --------------------- | -------------------------------------------------------------- |
|
|
74
|
+
| panels / drawers | `[name]-panel` | `all-playlists-panel`, `share-panel`, `edit-panel` |
|
|
75
|
+
| icon buttons | `btn-[action]` | `btn-edit-playlist`, `btn-share-playlist`, `btn-all-playlists` |
|
|
76
|
+
| close buttons | `btn-close-panel` | single stable testid; only one panel open at a time |
|
|
77
|
+
| row-level buttons | `btn-[action]-song` | `btn-edit-song`, `btn-remove-song` |
|
|
78
|
+
| content cells | `[component]-[field]` | `song-duration`, `song-count` |
|
|
79
|
+
| app sentinel | `app-ready` | signals initial load is done |
|
|
80
|
+
|
|
81
|
+
**When `getByText` / `getByRole` is still fine:**
|
|
82
|
+
|
|
83
|
+
- Asserting that specific _content_ is visible: `expect(page.getByText("song-00")).toBeVisible()`
|
|
84
|
+
|
|
85
|
+
**Always use `data-testid` for:**
|
|
86
|
+
|
|
87
|
+
- Buttons (even ones with visible text labels) - copy changes; testids don't
|
|
88
|
+
- Form inputs - use testid, not placeholder text
|
|
89
|
+
|
|
90
|
+
**Avoid:**
|
|
91
|
+
|
|
92
|
+
- `getByTitle(...)` to click or wait on elements - `title` is a tooltip for UX, not a test hook; it can collide when shared and changes with UI state
|
|
93
|
+
- `.first()` on a click target - this means the selector is ambiguous; fix it with a scoped container or a testid instead
|
|
94
|
+
- `page.getByRole("heading", { name: "playlistz" })` as the app-ready sentinel - use `getByTestId("app-ready")`
|
|
95
|
+
|
|
96
|
+
### ARIA attributes for state
|
|
97
|
+
|
|
98
|
+
Use ARIA attributes to express interactive state on elements. This is good accessibility practice and produces stable, semantic selectors in tests - no asserting on border colors, class names, or theme tokens to detect selected/active state.
|
|
99
|
+
|
|
100
|
+
**Preferred attributes and when to use them:**
|
|
101
|
+
|
|
102
|
+
| Attribute | Element | When |
|
|
103
|
+
| --------------- | ------------------------------- | --------------------------------------------------------- |
|
|
104
|
+
| `aria-pressed` | toggle buttons | button is in an "on" state (e.g. mode active, panel open) |
|
|
105
|
+
| `aria-selected` | tab-like buttons, playlist rows | item is the current selection |
|
|
106
|
+
| `aria-current` | nav-style items | the currently active page/view/step |
|
|
107
|
+
| `aria-expanded` | buttons that open panels | panel is open |
|
|
108
|
+
| `aria-checked` | custom checkboxes / toggles | item is checked |
|
|
109
|
+
| `aria-busy` | containers loading data | async operation in progress |
|
|
110
|
+
| `aria-disabled` | buttons that are disabled | disabled state (use alongside `disabled` attr) |
|
|
111
|
+
|
|
112
|
+
**Examples in source:**
|
|
113
|
+
|
|
114
|
+
```tsx
|
|
115
|
+
// mode toggle button - aria-pressed reflects active state
|
|
116
|
+
<button
|
|
117
|
+
data-testid="btn-mode-public"
|
|
118
|
+
aria-pressed={settings().mode === "public"}
|
|
119
|
+
onClick={() => void handleSaveSettings({ mode: "public" })}
|
|
120
|
+
>
|
|
121
|
+
anyone (public)
|
|
122
|
+
</button>
|
|
123
|
+
|
|
124
|
+
// hamburger that opens a panel - aria-expanded reflects open state
|
|
125
|
+
<button
|
|
126
|
+
data-testid="btn-all-playlists"
|
|
127
|
+
aria-expanded={showAllPlaylists()}
|
|
128
|
+
>...</button>
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Examples in tests - assert state without touching styles:**
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
// good: semantic, stable
|
|
135
|
+
await expect(page.getByTestId("btn-mode-public")).toHaveAttribute(
|
|
136
|
+
"aria-pressed",
|
|
137
|
+
"true"
|
|
138
|
+
);
|
|
139
|
+
await expect(page.getByTestId("btn-all-playlists")).toHaveAttribute(
|
|
140
|
+
"aria-expanded",
|
|
141
|
+
"true"
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
// bad: couples the test to the current theme/design
|
|
145
|
+
await expect(page.getByTestId("btn-mode-public")).toHaveClass(
|
|
146
|
+
/border-magenta-500/
|
|
147
|
+
);
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Avoid:**
|
|
151
|
+
|
|
152
|
+
- Asserting on CSS classes or inline styles to detect active/selected state
|
|
153
|
+
- Using `aria-label` as a test hook when a `data-testid` would be cleaner (aria-label is for screen reader copy, which can change)
|
|
154
|
+
|
|
155
|
+
### Logging for E2E debugging
|
|
156
|
+
|
|
157
|
+
Use the `log` utility (`src/utils/log.ts`) - never raw `console.log` in source files.
|
|
158
|
+
|
|
159
|
+
**Levels** (lowest to highest): `trace` < `debug` < `info` < `warn` < `error`
|
|
160
|
+
|
|
161
|
+
- `trace` - call-by-call service internals (off by default, even in dev)
|
|
162
|
+
- `debug` - normal dev noise (on in dev, off in prod)
|
|
163
|
+
- `warn` / `error` - always on
|
|
164
|
+
|
|
165
|
+
**Tags** use dotted namespaces: `"automerge.repo"`, `"playlist.sync"`, `"idb.docindex"`.
|
|
166
|
+
|
|
167
|
+
To turn on trace logging for a specific area during e2e debugging, set `localStorage` before reload:
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
// in browser devtools or a page.evaluate():
|
|
171
|
+
localStorage.setItem("logLevel", "trace");
|
|
172
|
+
localStorage.setItem("logFilter", "automerge.repo,playlist.doc");
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Or via env at test time:
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
VITE_LOG_LEVEL=trace VITE_LOG_FILTER=playlist npm run test:e2e
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Keep `console.log` / `console.warn` out of committed source - use `log.trace` for traces you want to keep around but silent by default.
|
|
182
|
+
|
|
183
|
+
### Timeouts
|
|
184
|
+
|
|
185
|
+
Lean on the global Playwright timeouts configured in `config/playwright.config.ts`:
|
|
186
|
+
|
|
187
|
+
- `timeout` - per-test timeout (default 30s; slow tests call `test.setTimeout()`)
|
|
188
|
+
- `actionTimeout` - single interaction (click, fill, etc.)
|
|
189
|
+
- `navigationTimeout` - `goto` / `waitForURL`
|
|
190
|
+
- `expect.timeout` - default assertion timeout for `expect(...).toBeVisible()` etc.
|
|
191
|
+
|
|
192
|
+
**Only add inline `{ timeout: N }` when:**
|
|
193
|
+
|
|
194
|
+
- The wait is genuinely longer than the global default (e.g. waiting 15s for IDB file processing to complete)
|
|
195
|
+
- The wait is genuinely shorter than the global default and you want a faster failure signal (e.g. `{ timeout: 1000 }` on a "must not be visible" assertion)
|
|
196
|
+
|
|
197
|
+
**Avoid:**
|
|
198
|
+
|
|
199
|
+
- Duplicating the global default: `{ timeout: 5000 }` when the global expect timeout is already 5000
|
|
200
|
+
- Sprinkling `waitForTimeout(300)` as a "let things settle" patch - prefer waiting for a specific element state instead
|
|
201
|
+
- Overly generous timeouts that mask real bugs (a test that "passes" in 28s is hiding something)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# on every push to main, run the changesets action:
|
|
2
|
+
# - changesets pending -> open/update the "Version Packages" PR on
|
|
3
|
+
# changeset-release/main (runs `npm run version` = changeset version).
|
|
4
|
+
# - no changesets pending (version PR just merged + tag pushed) ->
|
|
5
|
+
# npm-publish.yml handles the actual registry publish on the tag event.
|
|
6
|
+
name: changesets
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
push:
|
|
10
|
+
branches: [main]
|
|
11
|
+
|
|
12
|
+
concurrency:
|
|
13
|
+
group: changesets-${{ github.ref }}
|
|
14
|
+
cancel-in-progress: false
|
|
15
|
+
|
|
16
|
+
permissions:
|
|
17
|
+
contents: write
|
|
18
|
+
pull-requests: write
|
|
19
|
+
|
|
20
|
+
jobs:
|
|
21
|
+
version:
|
|
22
|
+
name: open / update version PR
|
|
23
|
+
runs-on: ubuntu-24.04
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v4
|
|
26
|
+
with:
|
|
27
|
+
fetch-depth: 0
|
|
28
|
+
|
|
29
|
+
- name: setup node
|
|
30
|
+
uses: actions/setup-node@v4
|
|
31
|
+
with:
|
|
32
|
+
node-version: 24
|
|
33
|
+
|
|
34
|
+
- name: install deps
|
|
35
|
+
run: npm ci
|
|
36
|
+
|
|
37
|
+
- name: configure git identity
|
|
38
|
+
run: |
|
|
39
|
+
git config user.name "github-actions[bot]"
|
|
40
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
41
|
+
|
|
42
|
+
- name: changesets - open/update version PR
|
|
43
|
+
uses: changesets/action@v1
|
|
44
|
+
with:
|
|
45
|
+
version: npm run version
|
|
46
|
+
# no publish here - npm-publish.yml handles it on the version tag
|
|
47
|
+
commit: "chore: version packages"
|
|
48
|
+
title: "chore: version packages"
|
|
49
|
+
env:
|
|
50
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# publish @freqhole/playlistz to npm when a version tag is pushed.
|
|
2
|
+
# the version tag is created by the "Version Packages" PR merge
|
|
3
|
+
# (changesets/action creates it via the version PR).
|
|
4
|
+
#
|
|
5
|
+
# after publishing, opens a PR in tomb/ to bump @freqhole/playlistz
|
|
6
|
+
# to the new version so consumers stay up-to-date.
|
|
7
|
+
name: npm publish
|
|
8
|
+
|
|
9
|
+
on:
|
|
10
|
+
push:
|
|
11
|
+
tags:
|
|
12
|
+
- "v*.*.*"
|
|
13
|
+
workflow_dispatch:
|
|
14
|
+
|
|
15
|
+
permissions:
|
|
16
|
+
contents: read
|
|
17
|
+
id-token: write
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
publish:
|
|
21
|
+
name: publish @freqhole/playlistz
|
|
22
|
+
runs-on: ubuntu-24.04
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v4
|
|
25
|
+
|
|
26
|
+
- name: setup node
|
|
27
|
+
uses: actions/setup-node@v4
|
|
28
|
+
with:
|
|
29
|
+
node-version: 24
|
|
30
|
+
registry-url: "https://registry.npmjs.org"
|
|
31
|
+
|
|
32
|
+
- name: install deps
|
|
33
|
+
run: npm ci
|
|
34
|
+
|
|
35
|
+
- name: build
|
|
36
|
+
run: npm run build
|
|
37
|
+
|
|
38
|
+
- name: verify package version matches tag
|
|
39
|
+
if: github.ref_type == 'tag'
|
|
40
|
+
run: |
|
|
41
|
+
PKG_VERSION=$(node -p "require('./package.json').version")
|
|
42
|
+
TAG_VERSION=${GITHUB_REF_NAME#v}
|
|
43
|
+
if [ "$PKG_VERSION" != "$TAG_VERSION" ]; then
|
|
44
|
+
echo "package version $PKG_VERSION does not match tag $TAG_VERSION"
|
|
45
|
+
exit 1
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
- name: skip if already published
|
|
49
|
+
id: exists
|
|
50
|
+
run: |
|
|
51
|
+
PKG_NAME=$(node -p "require('./package.json').name")
|
|
52
|
+
PKG_VERSION=$(node -p "require('./package.json').version")
|
|
53
|
+
if npm view "$PKG_NAME@$PKG_VERSION" version >/dev/null 2>&1; then
|
|
54
|
+
echo "published=true" >> "$GITHUB_OUTPUT"
|
|
55
|
+
else
|
|
56
|
+
echo "published=false" >> "$GITHUB_OUTPUT"
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
- name: publish
|
|
60
|
+
if: steps.exists.outputs.published == 'false'
|
|
61
|
+
run: npm publish --access public --provenance
|
|
62
|
+
env:
|
|
63
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
64
|
+
|
|
65
|
+
# ---- bump @freqhole/playlistz in tomb/ ----
|
|
66
|
+
bump-tomb:
|
|
67
|
+
name: bump @freqhole/playlistz in tomb
|
|
68
|
+
needs: publish
|
|
69
|
+
runs-on: ubuntu-24.04
|
|
70
|
+
steps:
|
|
71
|
+
- name: checkout tomb
|
|
72
|
+
uses: actions/checkout@v4
|
|
73
|
+
with:
|
|
74
|
+
repository: freqhole/tomb
|
|
75
|
+
token: ${{ secrets.TOMB_PAT }}
|
|
76
|
+
fetch-depth: 1
|
|
77
|
+
|
|
78
|
+
- name: setup node
|
|
79
|
+
uses: actions/setup-node@v4
|
|
80
|
+
with:
|
|
81
|
+
node-version: 24
|
|
82
|
+
|
|
83
|
+
- name: get new version
|
|
84
|
+
id: ver
|
|
85
|
+
run: |
|
|
86
|
+
TAG=${GITHUB_REF_NAME:-$(curl -s https://registry.npmjs.org/@freqhole/playlistz/latest | node -p "JSON.parse(require('fs').readFileSync('/dev/stdin','utf-8')).version")}
|
|
87
|
+
echo "version=${TAG#v}" >> "$GITHUB_OUTPUT"
|
|
88
|
+
|
|
89
|
+
- name: update @freqhole/playlistz in tomb/client/spume/package.json
|
|
90
|
+
run: |
|
|
91
|
+
VERSION=${{ steps.ver.outputs.version }}
|
|
92
|
+
node -e "
|
|
93
|
+
const fs = require('fs');
|
|
94
|
+
const path = 'client/spume/package.json';
|
|
95
|
+
const pkg = JSON.parse(fs.readFileSync(path, 'utf-8'));
|
|
96
|
+
if (pkg.dependencies?.['@freqhole/playlistz']) {
|
|
97
|
+
pkg.dependencies['@freqhole/playlistz'] = VERSION;
|
|
98
|
+
}
|
|
99
|
+
if (pkg.devDependencies?.['@freqhole/playlistz']) {
|
|
100
|
+
pkg.devDependencies['@freqhole/playlistz'] = VERSION;
|
|
101
|
+
}
|
|
102
|
+
fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + '\n');
|
|
103
|
+
"
|
|
104
|
+
|
|
105
|
+
- name: configure git
|
|
106
|
+
run: |
|
|
107
|
+
git config user.name "github-actions[bot]"
|
|
108
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
109
|
+
|
|
110
|
+
- name: open PR in tomb
|
|
111
|
+
run: |
|
|
112
|
+
VERSION=${{ steps.ver.outputs.version }}
|
|
113
|
+
BRANCH="chore/bump-playlistz-${VERSION}"
|
|
114
|
+
git checkout -b "$BRANCH"
|
|
115
|
+
git add client/spume/package.json
|
|
116
|
+
git commit -m "chore: bump @freqhole/playlistz to ${VERSION}"
|
|
117
|
+
git push origin "$BRANCH"
|
|
118
|
+
gh pr create \
|
|
119
|
+
--title "chore: bump @freqhole/playlistz to ${VERSION}" \
|
|
120
|
+
--body "Automated bump of @freqhole/playlistz to ${VERSION} after npm publish." \
|
|
121
|
+
--base main \
|
|
122
|
+
--head "$BRANCH"
|
|
123
|
+
env:
|
|
124
|
+
GH_TOKEN: ${{ secrets.TOMB_PAT }}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# fast checks on every pull request to main.
|
|
2
|
+
# skips the "Version Packages" changeset PR (it only bumps versions and changelogs).
|
|
3
|
+
name: pr checks
|
|
4
|
+
|
|
5
|
+
on:
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
concurrency:
|
|
10
|
+
group: pr-checks-${{ github.ref }}
|
|
11
|
+
cancel-in-progress: true
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
pull-requests: write
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
# ---- changeset presence check ----
|
|
19
|
+
changeset:
|
|
20
|
+
name: changeset required
|
|
21
|
+
if: github.head_ref != 'changeset-release/main'
|
|
22
|
+
runs-on: ubuntu-24.04
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v4
|
|
25
|
+
with:
|
|
26
|
+
fetch-depth: 0
|
|
27
|
+
|
|
28
|
+
- name: setup node
|
|
29
|
+
uses: actions/setup-node@v4
|
|
30
|
+
with:
|
|
31
|
+
node-version: 24
|
|
32
|
+
|
|
33
|
+
- name: install deps
|
|
34
|
+
run: npm ci
|
|
35
|
+
|
|
36
|
+
- name: check for changeset
|
|
37
|
+
run: npx changeset status --since=origin/main
|
|
38
|
+
|
|
39
|
+
# ---- type-check + lint ----
|
|
40
|
+
lint:
|
|
41
|
+
name: type-check + lint
|
|
42
|
+
if: github.head_ref != 'changeset-release/main'
|
|
43
|
+
runs-on: ubuntu-24.04
|
|
44
|
+
steps:
|
|
45
|
+
- uses: actions/checkout@v4
|
|
46
|
+
|
|
47
|
+
- name: setup node
|
|
48
|
+
uses: actions/setup-node@v4
|
|
49
|
+
with:
|
|
50
|
+
node-version: 24
|
|
51
|
+
|
|
52
|
+
- name: install deps
|
|
53
|
+
run: npm ci
|
|
54
|
+
|
|
55
|
+
- name: type-check
|
|
56
|
+
run: npm run typecheck
|
|
57
|
+
|
|
58
|
+
- name: lint
|
|
59
|
+
run: npm run lint
|
|
60
|
+
|
|
61
|
+
# ---- unit tests ----
|
|
62
|
+
test:
|
|
63
|
+
name: unit tests
|
|
64
|
+
if: github.head_ref != 'changeset-release/main'
|
|
65
|
+
runs-on: ubuntu-24.04
|
|
66
|
+
steps:
|
|
67
|
+
- uses: actions/checkout@v4
|
|
68
|
+
|
|
69
|
+
- name: setup node
|
|
70
|
+
uses: actions/setup-node@v4
|
|
71
|
+
with:
|
|
72
|
+
node-version: 24
|
|
73
|
+
|
|
74
|
+
- name: install deps
|
|
75
|
+
run: npm ci
|
|
76
|
+
|
|
77
|
+
- name: test
|
|
78
|
+
run: npm test
|
|
79
|
+
|
|
80
|
+
# ---- e2e tests ----
|
|
81
|
+
e2e:
|
|
82
|
+
name: e2e tests
|
|
83
|
+
if: github.head_ref != 'changeset-release/main'
|
|
84
|
+
runs-on: ubuntu-24.04
|
|
85
|
+
steps:
|
|
86
|
+
- uses: actions/checkout@v4
|
|
87
|
+
|
|
88
|
+
- name: setup node
|
|
89
|
+
uses: actions/setup-node@v4
|
|
90
|
+
with:
|
|
91
|
+
node-version: 24
|
|
92
|
+
|
|
93
|
+
- name: install deps
|
|
94
|
+
run: npm ci
|
|
95
|
+
|
|
96
|
+
- name: install playwright browsers
|
|
97
|
+
run: npx playwright install --with-deps chromium
|
|
98
|
+
|
|
99
|
+
- name: build standalone bundle (needed by e2e tests)
|
|
100
|
+
run: npm run build:standalone
|
|
101
|
+
|
|
102
|
+
- name: e2e tests
|
|
103
|
+
run: npm run test:e2e
|
package/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# P L A Y L I S T Z
|
|
2
|
+
|
|
3
|
+
make some music playlistz in yr browser!
|
|
4
|
+
|
|
5
|
+
## devel
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
|
|
9
|
+
# do this once
|
|
10
|
+
npm install
|
|
11
|
+
|
|
12
|
+
# run hot-reloading dev server
|
|
13
|
+
npm run dev
|
|
14
|
+
|
|
15
|
+
# build standalone .html page (+ sw.js)
|
|
16
|
+
npm run build:standalone
|
|
17
|
+
|
|
18
|
+
# build web-component
|
|
19
|
+
npm run build
|
|
20
|
+
|
|
21
|
+
# run way-too-mocked up testz
|
|
22
|
+
npm test
|
|
23
|
+
|
|
24
|
+
# or
|
|
25
|
+
npm run test:watch
|
|
26
|
+
npm run test:coverage
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
made with 💖 in NYC
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* global console, process */
|
|
3
|
+
import { build } from "vite";
|
|
4
|
+
import solid from "vite-plugin-solid";
|
|
5
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
6
|
+
import wasm from "vite-plugin-wasm";
|
|
7
|
+
import topLevelAwait from "vite-plugin-top-level-await";
|
|
8
|
+
import { transform, build as esbuild } from "esbuild";
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
import path from "path";
|
|
11
|
+
import { fileURLToPath } from "url";
|
|
12
|
+
|
|
13
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const skipClear = process.argv.includes("--no-clear");
|
|
15
|
+
|
|
16
|
+
function readServiceWorker() {
|
|
17
|
+
const swPath = path.resolve("public/sw.js");
|
|
18
|
+
return fs.existsSync(swPath) ? fs.readFileSync(swPath, "utf-8") : null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function generateIndexHtml() {
|
|
22
|
+
// this is the dev server / standalone shell - no playlistz.js here.
|
|
23
|
+
// zip bundles get their own index.html (with playlistz.js) from standaloneTemplates.ts.
|
|
24
|
+
return `<!DOCTYPE html>
|
|
25
|
+
<html lang="en">
|
|
26
|
+
<head>
|
|
27
|
+
<meta charset="UTF-8" />
|
|
28
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
29
|
+
<title>playlistz</title>
|
|
30
|
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
31
|
+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
32
|
+
<meta name="mobile-web-app-capable" content="yes">
|
|
33
|
+
<meta name="theme-color" content="#000000">
|
|
34
|
+
</head>
|
|
35
|
+
<body>
|
|
36
|
+
<freqhole-playlistz></freqhole-playlistz>
|
|
37
|
+
<script src="freqhole-playlistz.js" defer></script>
|
|
38
|
+
</body>
|
|
39
|
+
</html>
|
|
40
|
+
`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function buildStandalone() {
|
|
44
|
+
const distDir = path.resolve("dist");
|
|
45
|
+
if (!skipClear) {
|
|
46
|
+
if (fs.existsSync(distDir)) fs.rmSync(distDir, { recursive: true, force: true });
|
|
47
|
+
}
|
|
48
|
+
fs.mkdirSync(distDir, { recursive: true });
|
|
49
|
+
|
|
50
|
+
const indexHtml = generateIndexHtml();
|
|
51
|
+
const swJs = readServiceWorker() ?? "";
|
|
52
|
+
|
|
53
|
+
// ---- browser bundle ----
|
|
54
|
+
console.log("building freqhole-playlistz.js...");
|
|
55
|
+
let browserCode = "";
|
|
56
|
+
let cssCode = "";
|
|
57
|
+
|
|
58
|
+
await build({
|
|
59
|
+
configFile: false,
|
|
60
|
+
plugins: [
|
|
61
|
+
wasm(), topLevelAwait(), solid({ typescript: true, jsx: "preserve" }), tailwindcss(),
|
|
62
|
+
{
|
|
63
|
+
name: "capture-browser-bundle",
|
|
64
|
+
enforce: "post",
|
|
65
|
+
async generateBundle(_, bundle) {
|
|
66
|
+
const jsChunk = Object.values(bundle).find(
|
|
67
|
+
(f) => f.type === "chunk" && typeof f.code === "string",
|
|
68
|
+
);
|
|
69
|
+
const cssAsset = Object.values(bundle).find(
|
|
70
|
+
(f) => f.type === "asset" && String(f.fileName).endsWith(".css"),
|
|
71
|
+
);
|
|
72
|
+
if (!jsChunk) { console.error("no js chunk found"); return; }
|
|
73
|
+
for (const [fileName, file] of Object.entries(bundle)) {
|
|
74
|
+
if (file.type === "asset" && fileName.endsWith(".wasm")) {
|
|
75
|
+
const b64 = Buffer.from(file.source).toString("base64");
|
|
76
|
+
const dataUri = `data:application/wasm;base64,${b64}`;
|
|
77
|
+
jsChunk.code = jsChunk.code.split(`/${fileName}`).join(dataUri);
|
|
78
|
+
delete bundle[fileName];
|
|
79
|
+
console.log(` inlined wasm: ${fileName} (${(b64.length / 1024 / 1024).toFixed(1)} mb)`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
cssCode = cssAsset ? String(cssAsset.source) : "";
|
|
83
|
+
browserCode = jsChunk.code;
|
|
84
|
+
for (const key of Object.keys(bundle)) delete bundle[key];
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
build: {
|
|
89
|
+
outDir: "dist", target: "esnext", minify: false, sourcemap: false, emptyOutDir: false,
|
|
90
|
+
rollupOptions: {
|
|
91
|
+
input: "./src/web-component.tsx",
|
|
92
|
+
output: {
|
|
93
|
+
format: "es", entryFileNames: "playlistz-entry.js",
|
|
94
|
+
chunkFileNames: "playlistz-[hash].js", assetFileNames: "playlistz.[ext]",
|
|
95
|
+
inlineDynamicImports: true,
|
|
96
|
+
},
|
|
97
|
+
external: ["@freqhole/midden"],
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
let cssInjector = "";
|
|
103
|
+
if (cssCode) {
|
|
104
|
+
const cssMinified = (await transform(cssCode, { loader: "css", minify: true })).code.replace(/\n/g, "");
|
|
105
|
+
cssInjector = `(()=>{const s=document.createElement('style');s.textContent=${JSON.stringify(cssMinified)};document.head.appendChild(s);})();\n`;
|
|
106
|
+
}
|
|
107
|
+
const { code: browserMinified } = await transform(cssInjector + browserCode, {
|
|
108
|
+
minify: true, sourcemap: false, format: "iife", target: "esnext",
|
|
109
|
+
});
|
|
110
|
+
fs.writeFileSync(path.resolve("dist/freqhole-playlistz.js"), browserMinified, "utf-8");
|
|
111
|
+
console.log(`generated: freqhole-playlistz.js (${(browserMinified.length / 1024 / 1024).toFixed(2)} mb)`);
|
|
112
|
+
|
|
113
|
+
// ---- cli bundle ----
|
|
114
|
+
console.log("building freqhole-playlistz.cli.mjs...");
|
|
115
|
+
const cliOut = path.resolve("dist/freqhole-playlistz.cli.mjs");
|
|
116
|
+
const cliEntry = path.resolve(__dirname, "src/cli/index.ts");
|
|
117
|
+
await esbuild({
|
|
118
|
+
entryPoints: [cliEntry],
|
|
119
|
+
bundle: true, platform: "node", format: "esm", outfile: cliOut,
|
|
120
|
+
minify: true, sourcemap: false,
|
|
121
|
+
banner: { js: "#!/usr/bin/env node" },
|
|
122
|
+
define: { "import.meta.url": '"file://"' },
|
|
123
|
+
});
|
|
124
|
+
let cliCode = fs.readFileSync(cliOut, "utf-8");
|
|
125
|
+
cliCode = cliCode.replace('"__INDEX_HTML__"', JSON.stringify(indexHtml));
|
|
126
|
+
cliCode = cliCode.replace('"__SW_JS__"', JSON.stringify(swJs));
|
|
127
|
+
fs.writeFileSync(cliOut, cliCode, "utf-8");
|
|
128
|
+
fs.chmodSync(cliOut, 0o755);
|
|
129
|
+
console.log("generated: freqhole-playlistz.cli.mjs");
|
|
130
|
+
|
|
131
|
+
// ---- static assets ----
|
|
132
|
+
fs.writeFileSync(path.resolve("dist/index.html"), indexHtml, "utf-8");
|
|
133
|
+
if (swJs) fs.writeFileSync(path.resolve("dist/sw.js"), swJs, "utf-8");
|
|
134
|
+
console.log("generated: index.html, sw.js");
|
|
135
|
+
|
|
136
|
+
console.log("\nbuild completed!");
|
|
137
|
+
console.log(" browser: dist/freqhole-playlistz.js");
|
|
138
|
+
console.log(" cli: dist/freqhole-playlistz.cli.mjs");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
buildStandalone().catch((err) => { console.error("build failed:", err); process.exit(1); });
|