also looking at this
chore(release): promote main to production
#19
8 files
+325
-31
Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.
| @@ -0,0 +1,88 @@ | ||
| 1 | /** | |
| 2 | * Rule 8 (stx-standards 08-no-vanilla-js): no vanilla DOM in a client script. | |
| 3 | * | |
| 4 | * config/ui.ts turns stx's strict mode on, but that is only a partial gate. | |
| 5 | * The renderer validates whatever reaches validateClientScript, and a | |
| 6 | * signal-using <script client> block is re-tagged data-stx-scoped by | |
| 7 | * processScriptSetup and then skipped by process.js's own skipAttrs -- so the | |
| 8 | * blocks most likely to touch the DOM are the ones it never sees. It also | |
| 9 | * only ever checks pages something actually rendered, which makes a | |
| 10 | * dev-server boot a sampling exercise rather than a check. | |
| 11 | * | |
| 12 | * This reads the source instead, where no block has been re-tagged yet, so it | |
| 13 | * sees every one of them. It calls stx's own exported validator rather than | |
| 14 | * re-listing the prohibited patterns, so the rules cannot drift from the | |
| 15 | * engine's, and honours `stx-strict-ignore` markers because that validator | |
| 16 | * parses them itself. | |
| 17 | */ | |
| 18 | import { describe, expect, it } from 'bun:test' | |
| 19 | import { readdirSync, readFileSync, statSync } from 'node:fs' | |
| 20 | import { join, relative } from 'node:path' | |
| 21 | import { validateClientScript } from '@stacksjs/stx' | |
| 22 | ||
| 23 | const root = process.cwd() | |
| 24 | ||
| 25 | /** Vendored or generated; their violations are not this repo's to fix. */ | |
| 26 | const SKIP = new Set(['node_modules', 'storage', 'pantry', '.git', 'dist', 'public', 'coverage']) | |
| 27 | ||
| 28 | /** | |
| 29 | * Not a client script: server blocks, external sources, and any explicit | |
| 30 | * non-JS type. Mirrors the skipAttrs process.js passes its scanner, minus the | |
| 31 | * two render-time markers (data-stx-scoped, data-stx-router) that by | |
| 32 | * definition cannot be in a source file -- excluding them here is what makes | |
| 33 | * this broader than the renderer. | |
| 34 | */ | |
| 35 | const NOT_CLIENT = /\bserver\b|\bsrc\s*=|\btype\s*=\s*["'](?!(?:text\/javascript|application\/javascript|module)["'])[^"']*["']/ | |
| 36 | const SCRIPT = /<script\b([^>]*)>([\s\S]*?)<\/script>/g | |
| 37 | ||
| 38 | function stxFiles(dir: string): string[] { | |
| 39 | let found: string[] = [] | |
| 40 | for (const name of readdirSync(dir)) { | |
| 41 | if (SKIP.has(name) || name.startsWith('.') && name !== '.stx') continue | |
| 42 | const path = join(dir, name) | |
| 43 | let entry | |
| 44 | try { entry = statSync(path) } | |
| 45 | catch { continue } | |
| 46 | if (entry.isDirectory()) found = found.concat(stxFiles(path)) | |
| 47 | else if (path.endsWith('.stx')) found.push(path) | |
| 48 | } | |
| 49 | return found | |
| 50 | } | |
| 51 | ||
| 52 | function scan(): { blocks: number, files: number, violations: string[] } { | |
| 53 | const violations: string[] = [] | |
| 54 | const files = stxFiles(root) | |
| 55 | let blocks = 0 | |
| 56 | for (const file of files) { | |
| 57 | const name = relative(root, file) | |
| 58 | for (const [, attrs, body] of readFileSync(file, 'utf8').matchAll(SCRIPT)) { | |
| 59 | if (NOT_CLIENT.test(attrs ?? '')) continue | |
| 60 | blocks++ | |
| 61 | // failOnViolation rather than reading console.warn: the validator | |
| 62 | // memoises per (content, file, config) and a remembered verdict | |
| 63 | // re-throws but does not warn again, so two identical blocks would | |
| 64 | // otherwise report once. | |
| 65 | try { validateClientScript(body ?? '', name, { enabled: true, failOnViolation: true, allowPatterns: [] }, name) } | |
| 66 | catch (error) { violations.push(error instanceof Error ? error.message : String(error)) } | |
| 67 | } | |
| 68 | } | |
| 69 | return { blocks, files: files.length, violations } | |
| 70 | } | |
| 71 | ||
| 72 | const result = scan() | |
| 73 | ||
| 74 | describe('rule 8: no vanilla JS in client scripts', () => { | |
| 75 | it('reaches the client scripts it is meant to check', () => { | |
| 76 | // Without this the suite would pass just as happily on a scan that | |
| 77 | // matched nothing at all, which is the one failure mode a green test | |
| 78 | // cannot tell you about. | |
| 79 | expect(result.files).toBeGreaterThan(0) | |
| 80 | expect(result.blocks).toBeGreaterThan(0) | |
| 81 | }) | |
| 82 | ||
| 83 | it('finds no prohibited DOM calls', () => { | |
| 84 | // Asserting on the joined report rather than a count: the diff on failure | |
| 85 | // is the list of offending files, lines and suggested composables. | |
| 86 | expect(result.violations.join('\n\n')).toBe('') | |
| 87 | }) | |
| 88 | }) | |