@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.
Files changed (180) hide show
  1. package/.changeset/config.json +11 -0
  2. package/.changeset/nice-wolves-thank.md +5 -0
  3. package/.freqhole-versions.json +4 -0
  4. package/.github/copilot-instructions.md +201 -0
  5. package/.github/workflows/changesets.yml +50 -0
  6. package/.github/workflows/npm-publish.yml +124 -0
  7. package/.github/workflows/pr-checks.yml +103 -0
  8. package/README.md +30 -0
  9. package/build-component.js +141 -0
  10. package/build-zip-bundle-lib.js +44 -0
  11. package/config/playwright.config.ts +47 -0
  12. package/config/vite.config.ts +44 -0
  13. package/config/vitest.config.ts +39 -0
  14. package/dist/assets/automerge_wasm_bg-Cik4BF9l.wasm +0 -0
  15. package/dist/assets/index-CbOXzGiA.js +216 -0
  16. package/dist/assets/index-CbOXzGiA.js.map +1 -0
  17. package/dist/assets/index-TvJ6RFpy.css +1 -0
  18. package/dist/assets/midden-DceCrT_L.js +2 -0
  19. package/dist/assets/midden-DceCrT_L.js.map +1 -0
  20. package/dist/assets/midden_bg-BLhfGIU-.wasm +0 -0
  21. package/dist/index.html +55 -0
  22. package/dist/sw.js +134 -0
  23. package/docs/AUTOMERGE_P2P_PLAN.md +233 -0
  24. package/docs/COLLABORATIVE_SHARING_PLAN.md +188 -0
  25. package/docs/E2E_TESTID_PLAN.md +234 -0
  26. package/docs/IROH_P2P_PLAN.md +302 -0
  27. package/docs/ROADMAP.md +695 -0
  28. package/docs/TODO.md +167 -0
  29. package/docs/bundle-embedding-plan.md +134 -0
  30. package/docs/standalone-refactor.md +184 -0
  31. package/e2e/all-playlists.spec.ts +220 -0
  32. package/e2e/audio-player.spec.ts +226 -0
  33. package/e2e/collaborative-features.spec.ts +229 -0
  34. package/e2e/contexts.ts +238 -0
  35. package/e2e/edit-panel.spec.ts +87 -0
  36. package/e2e/fixtures/bare-glitch-1s.m4a +0 -0
  37. package/e2e/fixtures/bare-glitch-1s.mp3 +0 -0
  38. package/e2e/fixtures/bare-glitch-1s.ogg +0 -0
  39. package/e2e/fixtures/chord-stack-3s.wav +0 -0
  40. package/e2e/fixtures/cover-anim.gif +0 -0
  41. package/e2e/fixtures/cover-blue.png +0 -0
  42. package/e2e/fixtures/cover-checkers.png +0 -0
  43. package/e2e/fixtures/cover-gradient.jpg +0 -0
  44. package/e2e/fixtures/cover-mono.gif +0 -0
  45. package/e2e/fixtures/cover-noise.png +0 -0
  46. package/e2e/fixtures/cover-plasma.webp +0 -0
  47. package/e2e/fixtures/cover-portrait.jpg +0 -0
  48. package/e2e/fixtures/cover-red.png +0 -0
  49. package/e2e/fixtures/cover-thumb.jpg +0 -0
  50. package/e2e/fixtures/cover-wide.webp +0 -0
  51. package/e2e/fixtures/generate.mjs +257 -0
  52. package/e2e/fixtures/long-drone-90s.mp3 +0 -0
  53. package/e2e/fixtures/noisy-binaural-8s.mp3 +0 -0
  54. package/e2e/fixtures/tagged-a3-4s.m4a +0 -0
  55. package/e2e/fixtures/tagged-a3-4s.mp3 +0 -0
  56. package/e2e/fixtures/tagged-a3-4s.ogg +0 -0
  57. package/e2e/fixtures/tagged-c5-3s.m4a +0 -0
  58. package/e2e/fixtures/tagged-c5-3s.mp3 +0 -0
  59. package/e2e/fixtures/tagged-c5-3s.ogg +0 -0
  60. package/e2e/fixtures/tagged-f4-6s.m4a +0 -0
  61. package/e2e/fixtures/tagged-f4-6s.mp3 +0 -0
  62. package/e2e/fixtures/tagged-f4-6s.ogg +0 -0
  63. package/e2e/fixtures/tone-220hz-10s.wav +0 -0
  64. package/e2e/fixtures/tone-440hz-2s.wav +0 -0
  65. package/e2e/fixtures/tone-880hz-5s.wav +0 -0
  66. package/e2e/fixtures/tone-stereo-3s.wav +0 -0
  67. package/e2e/fixtures/user-provided/README.md +1 -0
  68. package/e2e/helpers/app.ts +143 -0
  69. package/e2e/helpers/hooks.ts +133 -0
  70. package/e2e/helpers/index.ts +12 -0
  71. package/e2e/helpers/media.ts +125 -0
  72. package/e2e/helpers.ts +10 -0
  73. package/e2e/p2p-collaboration.spec.ts +356 -0
  74. package/e2e/p2p-multi-peer.spec.ts +723 -0
  75. package/e2e/p2p-states.spec.ts +302 -0
  76. package/e2e/playback.spec.ts +56 -0
  77. package/e2e/playlist-crud.spec.ts +126 -0
  78. package/e2e/share-link-autoplay.spec.ts +129 -0
  79. package/e2e/sharing-access.spec.ts +205 -0
  80. package/e2e/sharing.spec.ts +195 -0
  81. package/e2e/song-cache-state.spec.ts +202 -0
  82. package/e2e/zip-bundle.spec.ts +855 -0
  83. package/eslint.config.js +114 -0
  84. package/index.html +54 -0
  85. package/package.json +119 -0
  86. package/public/sw.js +134 -0
  87. package/scripts/use-local.mjs +37 -0
  88. package/scripts/use-published.mjs +37 -0
  89. package/src/App.tsx +9 -0
  90. package/src/cli/check.ts +164 -0
  91. package/src/cli/generate.ts +184 -0
  92. package/src/cli/http.ts +88 -0
  93. package/src/cli/index.ts +65 -0
  94. package/src/cli/init.ts +18 -0
  95. package/src/components/AllPlaylistsPanel.tsx +713 -0
  96. package/src/components/AudioPlayer.tsx +122 -0
  97. package/src/components/MarqueeText.tsx +101 -0
  98. package/src/components/PlaylistCoverModal.tsx +519 -0
  99. package/src/components/PlaylistEditPanel.tsx +803 -0
  100. package/src/components/PlaylistSharePanel.tsx +1020 -0
  101. package/src/components/ShareLinkKnockPanel.tsx +144 -0
  102. package/src/components/SharePanel.tsx +584 -0
  103. package/src/components/SongEditModal.tsx +453 -0
  104. package/src/components/SongEditPanel.tsx +578 -0
  105. package/src/components/SongRow.tsx +689 -0
  106. package/src/components/index.tsx +494 -0
  107. package/src/components/playlist/index.tsx +1203 -0
  108. package/src/context/PlaylistzContext.tsx +74 -0
  109. package/src/dev-hooks.ts +35 -0
  110. package/src/hooks/createDocIndexQuery.ts +53 -0
  111. package/src/hooks/createDocStore.test.ts +303 -0
  112. package/src/hooks/createDocStore.ts +90 -0
  113. package/src/hooks/useDragAndDrop.test.ts +474 -0
  114. package/src/hooks/useDragAndDrop.ts +400 -0
  115. package/src/hooks/useImageModal.test.ts +174 -0
  116. package/src/hooks/useImageModal.ts +201 -0
  117. package/src/hooks/usePlaylistManager.test.ts +453 -0
  118. package/src/hooks/usePlaylistManager.ts +685 -0
  119. package/src/hooks/usePlaylistsQuery.test.tsx +120 -0
  120. package/src/hooks/usePlaylistsQuery.ts +44 -0
  121. package/src/hooks/useSongState.test.ts +236 -0
  122. package/src/hooks/useSongState.ts +114 -0
  123. package/src/hooks/useUIState.ts +71 -0
  124. package/src/index.tsx +18 -0
  125. package/src/services/audioService.dev.ts +22 -0
  126. package/src/services/audioService.test.ts +1226 -0
  127. package/src/services/audioService.ts +1395 -0
  128. package/src/services/automergeRepo.test.ts +269 -0
  129. package/src/services/automergeRepo.ts +226 -0
  130. package/src/services/blobTransferService.dev.ts +119 -0
  131. package/src/services/blobTransferService.test.ts +441 -0
  132. package/src/services/blobTransferService.ts +702 -0
  133. package/src/services/docIndexService.test.ts +179 -0
  134. package/src/services/docIndexService.ts +118 -0
  135. package/src/services/fileProcessingService.test.ts +554 -0
  136. package/src/services/fileProcessingService.ts +239 -0
  137. package/src/services/imageService.test.ts +701 -0
  138. package/src/services/imageService.ts +365 -0
  139. package/src/services/indexedDBService.integration.test.ts +104 -0
  140. package/src/services/indexedDBService.test.ts +202 -0
  141. package/src/services/indexedDBService.ts +436 -0
  142. package/src/services/offlineService.test.ts +661 -0
  143. package/src/services/offlineService.ts +382 -0
  144. package/src/services/p2pService.test.ts +305 -0
  145. package/src/services/p2pService.ts +344 -0
  146. package/src/services/playlistDocService.test.ts +448 -0
  147. package/src/services/playlistDocService.ts +707 -0
  148. package/src/services/playlistDownloadService.test.ts +674 -0
  149. package/src/services/playlistDownloadService.ts +389 -0
  150. package/src/services/sharingService.test.ts +812 -0
  151. package/src/services/sharingService.ts +1073 -0
  152. package/src/services/sharingState.ts +161 -0
  153. package/src/services/songReactivity.test.ts +620 -0
  154. package/src/services/songReactivity.ts +145 -0
  155. package/src/services/standaloneService.test.ts +1025 -0
  156. package/src/services/standaloneService.ts +588 -0
  157. package/src/services/streamingAudioService.test.ts +275 -0
  158. package/src/services/streamingAudioService.ts +166 -0
  159. package/src/styles.css +428 -0
  160. package/src/test-setup.ts +547 -0
  161. package/src/types/global.d.ts +40 -0
  162. package/src/types/playlist.ts +99 -0
  163. package/src/utils/hashUtils.ts +41 -0
  164. package/src/utils/log.ts +97 -0
  165. package/src/utils/m3u.test.ts +172 -0
  166. package/src/utils/m3u.ts +136 -0
  167. package/src/utils/mockData.ts +166 -0
  168. package/src/utils/standaloneTemplates.test.ts +175 -0
  169. package/src/utils/standaloneTemplates.ts +83 -0
  170. package/src/utils/swTemplate.ts +84 -0
  171. package/src/utils/timeUtils.ts +166 -0
  172. package/src/utils/typeGuards.ts +171 -0
  173. package/src/web-component.tsx +98 -0
  174. package/src/zip-bundle/index.ts +7 -0
  175. package/src/zip-bundle/m3u.ts +45 -0
  176. package/src/zip-bundle/types.ts +50 -0
  177. package/src/zip-bundle/utils.ts +33 -0
  178. package/src/zip-bundle/zipBuilder.ts +309 -0
  179. package/tailwind.config.js +55 -0
  180. 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,5 @@
1
+ ---
2
+ "@freqhole/playlistz": patch
3
+ ---
4
+
5
+ zomg, hello world! listen to more playlistz! 💖
@@ -0,0 +1,4 @@
1
+ {
2
+ "@freqhole/api-client": "0.1.31",
3
+ "@freqhole/midden": "0.1.31"
4
+ }
@@ -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); });