Skip to content

Commit 9c8bc1d

Browse files
committed
Deploy walkthrough to GitHub Pages
- Add `--base-path=/prefix` flag on `generate` + `serve`. Rewrites root-relative asset URLs (/walkthrough.css -> /<prefix>/walkthrough.css) and the Val-Town-shaped part links (/<slug>/part/<n> -> /<prefix>/walkthrough-part-<n>.html) so the output works under a subdirectory host. No-op when omitted (local dev, Val Town). - Service worker derives its precache base from `self.location.pathname`, so it's base-path-agnostic without a build-time substitution. - `.github/workflows/pages.yml` runs on push to main touching walkthrough sources: reuses SocketDev/socket-registry's setup-and-install composite action for toolchain parity, inits the meander submodule explicitly (the fleet checkout action doesn't do submodules), generates with --minify --base-path=/socket-packageurl-js, uploads the walkthrough/ dir as the Pages artifact, deploys via actions/deploy-pages. - Bump `packageManager` to pnpm@11.0.0-rc.5 per the fleet-wide bump. Site will land at https://socketdev.github.io/socket-packageurl-js/ once Pages is enabled on the repo + the workflow runs from main.
1 parent aea9807 commit 9c8bc1d

3 files changed

Lines changed: 208 additions & 14 deletions

File tree

.github/workflows/pages.yml

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
name: 📘 Walkthrough (GH Pages)
2+
3+
# Deploys the walkthrough pilot to https://socketdev.github.io/socket-packageurl-js/
4+
# Trigger: push to main that touches walkthrough sources (plus manual).
5+
#
6+
# Uses the SocketDev/socket-registry shared composite action for
7+
# checkout + Node/pnpm setup + install, so toolchain versions stay in
8+
# sync with the rest of the fleet. The build/deploy steps are custom
9+
# because there isn't a shared Pages workflow yet (worth PR'ing to
10+
# socket-registry once we've shipped this and validated the pattern).
11+
#
12+
# Build model:
13+
# 1. Shared setup-and-install → checkout w/ submodules, pnpm install.
14+
# 2. `pnpm walkthrough --minify --base-path=/socket-packageurl-js generate walkthrough.json`
15+
# The --base-path flag rewrites root-relative asset URLs and Val-Town
16+
# part links (/<slug>/part/<n>) into prefixed flat HTML paths so the
17+
# output works at socketdev.github.io/socket-packageurl-js/. --minify
18+
# runs esbuild on JS + lightningcss on CSS (~49% on-disk reduction).
19+
# 3. Upload `walkthrough/` as the Pages artifact.
20+
# 4. Deploy via actions/deploy-pages.
21+
22+
on:
23+
push:
24+
branches: [main]
25+
paths:
26+
- 'walkthrough.json'
27+
- 'walkthrough-comments.js'
28+
- 'walkthrough-drag.js'
29+
- 'walkthrough-overrides.css'
30+
- 'walkthrough-sw.js'
31+
- 'scripts/walkthrough.mts'
32+
- 'src/**'
33+
- 'assets/favicon/**'
34+
- '.github/workflows/pages.yml'
35+
- 'package.json'
36+
- 'pnpm-lock.yaml'
37+
workflow_dispatch:
38+
39+
permissions:
40+
contents: read
41+
pages: write
42+
id-token: write
43+
44+
# Queue Pages deploys serially — don't cancel a running deploy with a
45+
# newer commit (`cancel-in-progress: false`), that can leave the site
46+
# in a half-deployed state.
47+
concurrency:
48+
group: pages
49+
cancel-in-progress: false
50+
51+
jobs:
52+
build:
53+
name: Build walkthrough
54+
runs-on: ubuntu-latest
55+
steps:
56+
- name: Setup and install (checkout + Node + pnpm + install)
57+
uses: SocketDev/socket-registry/.github/actions/setup-and-install@d7b5d15a9be8973a5715459d12b9a033603a5ff5 # main
58+
with:
59+
checkout-fetch-depth: '1'
60+
61+
# The fleet's shared checkout action doesn't init submodules.
62+
# We need upstream/meander because the generate step shells out
63+
# to its CLI. Init non-recursively at the pinned commit the
64+
# superrepo points at — no re-resolving from meander upstream.
65+
- name: Init meander submodule
66+
run: git submodule update --init --depth=1 upstream/meander
67+
68+
- name: Generate walkthrough (minified, base-path=/socket-packageurl-js)
69+
run: >
70+
pnpm walkthrough
71+
--minify
72+
--base-path=/socket-packageurl-js
73+
generate walkthrough.json
74+
75+
- name: Upload Pages artifact
76+
uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3
77+
with:
78+
path: walkthrough
79+
80+
deploy:
81+
name: Deploy to Pages
82+
needs: build
83+
runs-on: ubuntu-latest
84+
environment:
85+
name: github-pages
86+
url: ${{ steps.deploy.outputs.page_url }}
87+
steps:
88+
- name: Deploy
89+
id: deploy
90+
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4

scripts/walkthrough.mts

Lines changed: 105 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -81,14 +81,82 @@ function ensureMeander(refresh: boolean): void {
8181
}
8282
}
8383

84+
/**
85+
* Normalize a base-path argument — enforce a single leading `/`, strip
86+
* trailing `/`. Empty input means "served at the origin root" (no
87+
* rewrite needed). Input of just `/` also normalizes to empty.
88+
*/
89+
function normalizeBasePath(raw: string): string {
90+
let p = raw.trim()
91+
if (!p || p === '/') {
92+
return ''
93+
}
94+
if (!p.startsWith('/')) {
95+
p = '/' + p
96+
}
97+
if (p.endsWith('/')) {
98+
p = p.slice(0, -1)
99+
}
100+
return p
101+
}
102+
103+
/**
104+
* Rewrite a generated HTML file for hosting under `basePath`. Two
105+
* categories of URL get prefixed:
106+
*
107+
* 1. Root-relative asset paths meander or our post-processor emits
108+
* (/walkthrough.css, /walkthrough-drag.js, /favicon.ico, etc.).
109+
* 2. Val-Town-shaped part links (/<slug>/part/<n>) — these don't
110+
* exist as files; rewrite to the real flat HTML name
111+
* (walkthrough-part-<n>.html) and prefix with basePath.
112+
*
113+
* We do a narrowly-scoped regex pass rather than a full HTML parse:
114+
* the set of URL attributes we emit is small and known, and we don't
115+
* want to touch hrefs in the prose (external links, anchor jumps).
116+
*/
117+
function applyBasePath(html: string, basePath: string, slug: string): string {
118+
if (!basePath) {
119+
return html
120+
}
121+
// 1. Flat part link — rewrite first so step 2 doesn't double-prefix.
122+
// Matches href="/<slug>/part/<n>" and rewrites to
123+
// href="<basePath>/walkthrough-part-<n>.html".
124+
const partLink = new RegExp(`(href=")/${slug}/part/(\\d+)/?(")`, 'g')
125+
let out = html.replace(
126+
partLink,
127+
(_m, pre, n, post) => `${pre}${basePath}/walkthrough-part-${n}.html${post}`,
128+
)
129+
// 2. Root-relative asset URLs. Match href="/..." and src="/..." and
130+
// ServiceWorker-style register('/...') — but skip:
131+
// - protocol-qualified URLs (https://, data:, mailto:, etc.)
132+
// - anchor/hash links ("#...")
133+
// - URLs that already begin with the basePath (idempotent)
134+
// - the part-link pattern above (already rewritten in step 1)
135+
const assetAttr = /(\s(?:href|src)=")(\/[^"]*)(")/g
136+
out = out.replace(assetAttr, (_m, pre, url, post) => {
137+
if (url.startsWith(basePath + '/') || url === basePath) {
138+
return `${pre}${url}${post}`
139+
}
140+
return `${pre}${basePath}${url}${post}`
141+
})
142+
// 3. SW register — also prefix. Matches .register('/walkthrough-sw.js').
143+
out = out.replace(/\.register\('(\/[^']+)'/g, (_m, url) =>
144+
url.startsWith(basePath + '/')
145+
? `.register('${url}'`
146+
: `.register('${basePath}${url}'`,
147+
)
148+
return out
149+
}
150+
84151
async function generate(
85152
refresh: boolean,
86153
minify: boolean,
154+
basePath: string,
87155
rest: readonly string[],
88156
): Promise<void> {
89157
if (rest.length === 0) {
90158
console.error(
91-
'Usage: pnpm walkthrough [--refresh] [--minify] generate <walkthrough.json>',
159+
'Usage: pnpm walkthrough [--refresh] [--minify] [--base-path=/prefix] generate <walkthrough.json>',
92160
)
93161
process.exit(1)
94162
}
@@ -150,10 +218,12 @@ async function generate(
150218
const configPath = rest[0]
151219
const walkthroughConfig = configPath
152220
? (JSON.parse(readFileSync(path.resolve(configPath), 'utf8')) as {
221+
slug?: string
153222
commentBackend?: string
154223
})
155224
: {}
156225
const commentBackend = walkthroughConfig.commentBackend || ''
226+
const slug = walkthroughConfig.slug || ''
157227
if (commentBackend && existsSync(commentsSrc)) {
158228
copyFileSync(
159229
commentsSrc,
@@ -259,6 +329,13 @@ async function generate(
259329
html = html.replace('</body>', ` ${footerTag}\n</body>`)
260330
}
261331

332+
// Base-path rewrite — last step so every injected tag above gets
333+
// prefixed in one pass. No-op when --base-path is empty (local dev,
334+
// Val Town hosting, etc.).
335+
if (basePath && slug) {
336+
html = applyBasePath(html, basePath, slug)
337+
}
338+
262339
writeFileSync(htmlPath, html)
263340
}
264341

@@ -381,7 +458,7 @@ function routeToFile(slug: string, urlPath: string): string | undefined {
381458
return urlPath.replace(/^\//, '')
382459
}
383460

384-
function serve(args: readonly string[]): void {
461+
function serve(basePath: string, args: readonly string[]): void {
385462
const portArg = args.find(a => a.startsWith('--port='))
386463
const port = portArg ? Number(portArg.slice('--port='.length)) : 8080
387464

@@ -396,7 +473,16 @@ function serve(args: readonly string[]): void {
396473

397474
const server = createServer((req, res) => {
398475
const rawUrl = (req.url ?? '/').split('?')[0]!.split('#')[0]!
399-
const decoded = decodeURIComponent(rawUrl)
476+
let decoded = decodeURIComponent(rawUrl)
477+
// Strip the base-path prefix so `routeToFile` sees the shape it
478+
// expects. Mirrors the generate-side `--base-path` rewrite so
479+
// `pnpm walkthrough --base-path=/X serve` + a build with the
480+
// same flag round-trips correctly.
481+
if (basePath && decoded.startsWith(basePath + '/')) {
482+
decoded = decoded.slice(basePath.length)
483+
} else if (basePath && decoded === basePath) {
484+
decoded = '/'
485+
}
400486
const relative = routeToFile(slug, decoded)
401487
if (relative === undefined) {
402488
res.writeHead(400).end('bad request')
@@ -451,15 +537,27 @@ function main(): void {
451537
const args = process.argv.slice(2)
452538
const refresh = args.includes('--refresh')
453539
const minify = args.includes('--minify')
454-
const rest = args.filter(a => a !== '--refresh' && a !== '--minify')
540+
// Parse --base-path=<p>. When set, every root-relative asset URL
541+
// and Val-Town-shaped part link is rewritten so the output works
542+
// under a subdirectory host (e.g. GitHub Pages /<repo>/). Leading
543+
// slash enforced, trailing slash stripped so path joins are clean.
544+
const basePathArg = args.find(a => a.startsWith('--base-path='))
545+
const basePath = basePathArg
546+
? normalizeBasePath(basePathArg.slice('--base-path='.length))
547+
: ''
548+
const rest = args.filter(
549+
a => a !== '--refresh' && a !== '--minify' && !a.startsWith('--base-path='),
550+
)
455551
const command = rest[0]
456552

457553
switch (command) {
458554
case 'generate':
459-
generate(refresh, minify, rest.slice(1)).catch(failWith('generate'))
555+
generate(refresh, minify, basePath, rest.slice(1)).catch(
556+
failWith('generate'),
557+
)
460558
break
461559
case 'serve':
462-
serve(rest.slice(1))
560+
serve(basePath, rest.slice(1))
463561
break
464562
case 'deploy-val':
465563
deployVal(rest.slice(1)).catch(failWith('deploy-val'))
@@ -473,7 +571,7 @@ function main(): void {
473571
default:
474572
console.error(
475573
'Usage:\n' +
476-
' pnpm walkthrough [--refresh] [--minify] generate <walkthrough.json>\n' +
574+
' pnpm walkthrough [--refresh] [--minify] [--base-path=/prefix] generate <walkthrough.json>\n' +
477575
' pnpm walkthrough serve [--port=8080]\n' +
478576
' pnpm walkthrough deploy-val [--name=<valname>]\n' +
479577
' pnpm walkthrough token <set|clear|status>\n' +

walkthrough-sw.js

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,23 @@
2222
const CACHE_VERSION = 'wt-v1'
2323
const CACHE_NAME = `wt-cache-${CACHE_VERSION}`
2424

25+
// Base path derived from the SW's own scope — works the same whether
26+
// we're hosted at the origin root (/) or under a subdirectory like
27+
// GitHub Pages' /<repo>/. `self.location.pathname` is the path to
28+
// this script file itself; strip the filename to get the dir.
29+
const BASE_PATH = self.location.pathname.replace(/\/[^/]*$/, '')
30+
2531
// Precache list — the critical-path assets the shell needs on first
2632
// paint. HTML entries aren't precached (the page is the request that
2733
// installs the SW); the *next* navigation to them is cached.
2834
const PRECACHE = [
29-
'/walkthrough.css',
30-
'/walkthrough-drag.js',
31-
'/walkthrough-comments.js',
32-
'/favicon.ico',
33-
'/favicon-16x16.png',
34-
'/favicon-32x32.png',
35-
'/apple-touch-icon.png',
35+
`${BASE_PATH}/walkthrough.css`,
36+
`${BASE_PATH}/walkthrough-drag.js`,
37+
`${BASE_PATH}/walkthrough-comments.js`,
38+
`${BASE_PATH}/favicon.ico`,
39+
`${BASE_PATH}/favicon-16x16.png`,
40+
`${BASE_PATH}/favicon-32x32.png`,
41+
`${BASE_PATH}/apple-touch-icon.png`,
3642
]
3743

3844
self.addEventListener('install', event => {

0 commit comments

Comments
 (0)