Setting the file. One moment.
Sveltekit Adapter · Impeccable · pbakaus/impeccable · Skills Docs
ContentsBack to the top of the page — line 262
This file
Number 1.136
Position 136 of 144
Type JavaScript
Size 11 KB
Lines 316 scripts/live/ sveltekit-adapter.mjs
JavaScript · 316 lines · 11 KB
'node:path'
;
13
14 export const SVELTE_LIVE_ROOT_COMPONENT = 'src/lib/impeccable/ImpeccableLiveRoot.svelte' ;
15 export const SVELTE_LAYOUT_MARKER_OPEN = '<!-- impeccable-live-svelte-start -->' ;
16 export const SVELTE_LAYOUT_MARKER_CLOSE = '<!-- impeccable-live-svelte-end -->' ;
17 export const SVELTE_ROOT_IMPORT = "import ImpeccableLiveRoot from '$lib/impeccable/ImpeccableLiveRoot.svelte';" ;
18 // Matches the import at ANY revision (or none). [ \t]* bounds only, never
19 // \s*: a greedy \s* after the statement swallowed the next line's
20 // indentation on removal, leaving a formatting scar in user layouts.
21 const SVELTE_ROOT_IMPORT_LINE_RE = / ^ [ \t] * import ImpeccableLiveRoot from ' \$ lib \/ impeccable \/ ImpeccableLiveRoot \. svelte(?: \? [ ^ '] * ) ? '; [ \t] * \r ? \n ? / gm ;
22
23 /**
24 * The import specifier carries a token-derived revision query. The adapter
25 * component embeds the helper token, and Vite (client AND SSR) can keep
26 * serving a stale compiled module after the file is rewritten on a helper
27 * restart; the browser then requests /live.js with a rotated-out token and
28 * gets a 401 with no picker. A changed specifier is a different module id,
29 * which no cache survives.
30 */
31 export function svelteRootImportLine ( rev ) {
32 if ( ! rev) return SVELTE_ROOT_IMPORT ;
33 return "import ImpeccableLiveRoot from '$lib/impeccable/ImpeccableLiveRoot.svelte?impeccable-live=" + rev + "';" ;
34 }
35
36 export function svelteAdapterRev ( token ) {
37 if ( ! token) return null ;
38 return crypto. createHash ( 'sha256' ). update ( String (token)). digest ( 'hex' ). slice ( 0 , 8 );
39 }
40
41 export function detectSvelteKitProject ( cwd = process. cwd (), config = null ) {
42 const appHtml = findSvelteKitAppHtml (cwd, config);
43 if ( ! appHtml) return null ;
44 const hasTemplateMarkers = fileIncludes (path. join (cwd, appHtml), '%sveltekit.body%' )
45 && fileIncludes (path. join (cwd, appHtml), '%sveltekit.head%' );
46 if ( ! hasTemplateMarkers) return null ;
47
48 const hasSvelteConfig = fs. existsSync (path. join (cwd, 'svelte.config.js' ))
49 || fs. existsSync (path. join (cwd, 'svelte.config.mjs' ))
50 || fs. existsSync (path. join (cwd, 'svelte.config.cjs' ))
51 || fs. existsSync (path. join (cwd, 'svelte.config.ts' ));
52 const hasKitPackage = packageHasSvelteKit (cwd);
53 if ( ! hasSvelteConfig && ! hasKitPackage) return null ;
54
55 return {
56 appHtml,
57 layoutFile: findSvelteKitLayout (cwd),
58 rootComponent: SVELTE_LIVE_ROOT_COMPONENT ,
59 };
60 }
61
62 export function applySvelteKitLiveAdapter ({ cwd = process. cwd (), port , token , config = null } = {}) {
63 if ( ! Number. isFinite ( Number (port))) {
64 throw new Error ( 'SvelteKit live adapter requires a numeric port' );
65 }
66 const detected = detectSvelteKitProject (cwd, config);
67 if ( ! detected) return null ;
68
69 ensureSvelteLiveRootComponent (cwd, Number (port), token);
70
71 const layoutRel = detected.layoutFile;
72 const layoutAbs = path. join (cwd, layoutRel);
73 fs. mkdirSync (path. dirname (layoutAbs), { recursive: true });
74 const layoutExisted = fs. existsSync (layoutAbs);
75 const before = layoutExisted ? fs. readFileSync (layoutAbs, 'utf-8' ) : defaultSvelteLayout ();
76 const after = patchSvelteLayout (before, { rev: svelteAdapterRev (token) });
77 fs. writeFileSync (layoutAbs, after, 'utf-8' );
78
79 return {
80 file: layoutRel,
81 adapter: 'sveltekit' ,
82 inserted: after !== before || ! layoutExisted,
83 appHtmlUntouched: true ,
84 rootComponent: SVELTE_LIVE_ROOT_COMPONENT ,
85 };
86 }
87
88 export function removeSvelteKitLiveAdapter ({ cwd = process. cwd (), config = null } = {}) {
89 const detected = detectSvelteKitProject (cwd, config);
90 if ( ! detected) return null ;
91
92 const layoutAbs = path. join (cwd, detected.layoutFile);
93 let removed = false ;
94 if (fs. existsSync (layoutAbs)) {
95 const before = fs. readFileSync (layoutAbs, 'utf-8' );
96 const after = unpatchSvelteLayout (before);
97 if (after !== before) {
98 fs. writeFileSync (layoutAbs, after, 'utf-8' );
99 removed = true ;
100 }
101 }
102
103 const rootAbs = path. join (cwd, SVELTE_LIVE_ROOT_COMPONENT );
104 if (fs. existsSync (rootAbs)) {
105 fs. rmSync (rootAbs, { force: true });
106 removed = true ;
107 }
108
109 pruneEmptyDir (path. dirname (rootAbs), path. join (cwd, 'src' ));
110
111 return {
112 file: detected.layoutFile,
113 adapter: 'sveltekit' ,
114 removed,
115 appHtmlUntouched: true ,
116 rootComponent: SVELTE_LIVE_ROOT_COMPONENT ,
117 };
118 }
119
120 export function patchSvelteLayout ( content , { rev = null } = {}) {
121 let out = String (content || '' );
122 const importLine = svelteRootImportLine (rev);
123 if ( ! out. includes (importLine)) {
124 // An import at an older revision is replaced in place, keeping its
125 // indentation; only a layout with no impeccable import gets an insert.
126 let replaced = false ;
127 out = out. replace ( SVELTE_ROOT_IMPORT_LINE_RE , ( line ) => {
128 if (replaced) return '' ;
129 replaced = true ;
130 const indent = (line. match ( / ^ [ \t] * / ) || [ '' ])[ 0 ];
131 return indent + importLine + ' \n ' ;
132 });
133 if ( ! replaced) {
134 const scriptMatch = out. match ( /<script(?: \s[ ^ >] * ) ? >/ i );
135 if (scriptMatch) {
136 const insertAt = scriptMatch.index + scriptMatch[ 0 ]. length ;
137 out = out. slice ( 0 , insertAt) + ' \n ' + importLine + out. slice (insertAt);
138 } else {
139 out = `<script> \n ${ importLine } \n </script> \n\n ` + out;
140 }
141 }
142 }
143
144 if ( ! out. includes ( SVELTE_LAYOUT_MARKER_OPEN )) {
145 const block = `${ SVELTE_LAYOUT_MARKER_OPEN } \n <ImpeccableLiveRoot /> \n ${ SVELTE_LAYOUT_MARKER_CLOSE } \n ` ;
146 const renderMatch = out. match ( / \{ @render \s + children(?: \?\. ) ? \(\) \s * \} / );
147 const slotMatch = out. match ( /<slot \s * \/ ? >/ );
148 const match = renderMatch || slotMatch;
149 if (match) {
150 out = out. slice ( 0 , match.index) + block + out. slice (match.index);
151 } else {
152 out = out. replace ( / \s *$ / , ' \n\n ' + block);
153 }
154 }
155
156 return out;
157 }
158
159 export function unpatchSvelteLayout ( content ) {
160 let out = String (content || '' );
161 const blockRe = new RegExp (
162 '([ \\ t]*)' + escapeRegExp ( SVELTE_LAYOUT_MARKER_OPEN )
163 + ' \\ n<ImpeccableLiveRoot \\ s*/> \\ n'
164 + escapeRegExp ( SVELTE_LAYOUT_MARKER_CLOSE )
165 + ' \\ n?' ,
166 'g' ,
167 );
168 out = out. replace (blockRe, '$1' );
169 out = out. replace ( SVELTE_ROOT_IMPORT_LINE_RE , '' );
170 out = out. replace ( /<script> \s * < \/ script> [ \t] * \r ? \n ? / g , '' );
171 return out. replace ( / \n {3,} / g , ' \n\n ' );
172 }
173
174 export function ensureSvelteLiveRootComponent ( cwd , port , token ) {
175 const file = path. join (cwd, SVELTE_LIVE_ROOT_COMPONENT );
176 fs. mkdirSync (path. dirname (file), { recursive: true });
177 fs. writeFileSync (file, buildSvelteLiveRootComponent (port, token), 'utf-8' );
178 return file;
179 }
180
181 export function buildSvelteLiveRootComponent ( port , token ) {
182 const liveUrl = 'http://localhost:' + Number (port) + '/live.js'
183 + (token ? '?token=' + encodeURIComponent (token) : '' );
184 return `<script>
185 import { onMount } from 'svelte';
186
187 const LIVE_URL = '${ liveUrl }';
188 const HOST_ID = 'impeccable-live-root';
189
190 onMount(() => {
191 let host = document.querySelector('impeccable-live-root#' + HOST_ID) || document.getElementById(HOST_ID);
192 if (!host) {
193 host = document.createElement('impeccable-live-root');
194 host.id = HOST_ID;
195 document.body.appendChild(host);
196 }
197
198 host.dataset.impeccableLiveAdapter = 'sveltekit';
199 host.style.setProperty('all', 'initial', 'important');
200 host.style.setProperty('display', 'block', 'important');
201 host.style.setProperty('position', 'fixed', 'important');
202 host.style.setProperty('top', '0', 'important');
203 host.style.setProperty('left', '0', 'important');
204 host.style.setProperty('width', '0', 'important');
205 host.style.setProperty('height', '0', 'important');
206 host.style.setProperty('overflow', 'visible', 'important');
207 host.style.setProperty('z-index', '2147483000', 'important');
208 host.style.setProperty('pointer-events', 'none', 'important');
209
210 const root = host.shadowRoot || host.attachShadow({ mode: 'open' });
211 if (!root.querySelector('style[data-impeccable-live-reset]')) {
212 const reset = document.createElement('style');
213 reset.dataset.impeccableLiveReset = 'true';
214 reset.textContent = ':host, :host *, * { box-sizing: border-box; }';
215 root.appendChild(reset);
216 }
217
218 window.__IMPECCABLE_LIVE_ADAPTER__ = 'sveltekit';
219 window.__IMPECCABLE_LIVE_UI_ROOT__ = root;
220 window.__IMPECCABLE_LIVE_CHROME_MOUNT__ = {
221 adapter: 'sveltekit',
222 version: 1,
223 host,
224 root,
225 };
226
227 const script = document.createElement('script');
228 script.src = LIVE_URL;
229 script.async = true;
230 script.dataset.impeccableLiveScript = 'true';
231 script.onerror = () => console.error(
232 '[impeccable] live.js failed to load from ' + LIVE_URL
233 + ' (helper down, or the token rotated while a stale adapter module was cached).'
234 + ' Re-run the live boot, then reload this page.'
235 );
236 document.head.appendChild(script);
237
238 return () => {
239 script.remove();
240 if (window.__IMPECCABLE_LIVE_UI_ROOT__ === root) delete window.__IMPECCABLE_LIVE_UI_ROOT__;
241 if (window.__IMPECCABLE_LIVE_CHROME_MOUNT__?.root === root) delete window.__IMPECCABLE_LIVE_CHROME_MOUNT__;
242 if (window.__IMPECCABLE_LIVE_ADAPTER__ === 'sveltekit') delete window.__IMPECCABLE_LIVE_ADAPTER__;
243 };
244 });
245 </script>
246 ` ;
247 }
248
249 function findSvelteKitAppHtml ( cwd , config ) {
250 const files = Array. isArray (config?.files) ? config.files : [ 'src/app.html' ];
251 for ( const rel of files) {
252 if (rel. includes ( '*' )) continue ;
253 const normalized = rel. split (path.sep). join ( '/' );
254 if ( ! normalized. endsWith ( 'app.html' )) continue ;
255 const abs = path. join (cwd, normalized);
256 if (fs. existsSync (abs)) return normalized;
257 }
258 const fallback = 'src/app.html' ;
259 return fs. existsSync (path. join (cwd, fallback)) ? fallback : null ;
260 }
261
262 function findSvelteKitLayout ( cwd ) {
263 const candidates = [
264 'src/routes/+layout.svelte' ,
265 'src/routes/(app)/+layout.svelte' ,
266 ];
267 for ( const rel of candidates) {
268 if (fs. existsSync (path. join (cwd, rel))) return rel;
269 }
270 return 'src/routes/+layout.svelte' ;
271 }
272
273 function defaultSvelteLayout () {
274 return `<script> \n let { children } = $props(); \n </script> \n\n {@render children?.()} \n ` ;
275 }
276
277 function packageHasSvelteKit ( cwd ) {
278 const file = path. join (cwd, 'package.json' );
279 if ( ! fs. existsSync (file)) return false ;
280 try {
281 const pkg = JSON . parse (fs. readFileSync (file, 'utf-8' ));
282 const deps = {
283 ... (pkg.dependencies || {}),
284 ... (pkg.devDependencies || {}),
285 ... (pkg.peerDependencies || {}),
286 };
287 return Boolean (deps[ '@sveltejs/kit' ] || deps[ '@sveltejs/vite-plugin-svelte' ] || deps.svelte);
288 } catch {
289 return false ;
290 }
291 }
292
293 function fileIncludes ( file , text ) {
294 try {
295 return fs. readFileSync (file, 'utf-8' ). includes (text);
296 } catch {
297 return false ;
298 }
299 }
300
301 function pruneEmptyDir ( dir , stopDir ) {
302 let current = dir;
303 while (current. startsWith (stopDir) && current !== stopDir) {
304 try {
305 if (fs. readdirSync (current). length > 0 ) return ;
306 fs. rmdirSync (current);
307 current = path. dirname (current);
308 } catch {
309 return ;
310 }
311 }
312 }
313
314 function escapeRegExp ( value ) {
315 return String (value). replace ( / [.*+?^${}()|[ \]\\ ] / g , ' \\ $&' );
316 }