diff --git a/.replit b/.replit new file mode 100644 index 0000000..e39f5d1 --- /dev/null +++ b/.replit @@ -0,0 +1,31 @@ +run = "npm run build; npm run serve" +hidden = [".config", "package-lock.json", "tsconfig.json"] + +[packager] +language = "nodejs" + [packager.features] + enabledForHosting = false + packageSearch = true + guessImports = true + +[nix] +channel = "stable-22_11" + +[env] +XDG_CONFIG_HOME = "$REPL_HOME/.config" +PATH = "$REPL_HOME/node_modules/.bin:$REPL_HOME/.config/npm/node_global/bin" +npm_config_prefix = "$REPL_HOME/.config/npm/node_global" + +[gitHubImport] +requiredFiles = [".replit", "replit.nix", ".config"] + +[languages] + [languages.typescript] + pattern = "**/{*.ts,*.js,*.tsx,*.jsx,*.json}" + [languages.typescript.languageServer] + start = "typescript-language-server --stdio" + +[deployment] +run = ["sh", "-c", "npm run start"] +deploymentTarget = "cloudrun" +ignorePorts = false diff --git a/lib/dom.ts b/lib/dom.ts index 66a1270..3efef1e 100644 --- a/lib/dom.ts +++ b/lib/dom.ts @@ -32,54 +32,34 @@ export function mergeHead(nextDoc: Document): void { // Update head // Head elements that changed on next document const getValidNodes = (doc: Document): Element[] => Array.from(doc.querySelectorAll('head>:not([rel="prefetch"]')); - const oldNodes = getValidNodes(document); - const nextNodes = getValidNodes(nextDoc); - const { staleNodes, freshNodes } = partitionNodes(oldNodes, nextNodes); + // Spc - Ofc, we simple; + const { staleNodes, freshNodes } = partitionNodes( + getValidNodes(document), + getValidNodes(nextDoc) + ); staleNodes.forEach((node) => node.remove()); document.head.append(...freshNodes); } -function partitionNodes(oldNodes: Element[], nextNodes: Element[]): PartitionedNodes { - const staleNodes: Element[] = []; - const freshNodes: Element[] = []; - let oldMark = 0; - let nextMark = 0; - while (oldMark < oldNodes.length || nextMark < nextNodes.length) { - const old = oldNodes[oldMark]; - const next = nextNodes[nextMark]; - if (old?.isEqualNode(next)) { - oldMark++; - nextMark++; - continue; - } - const oldInFresh = old ? freshNodes.findIndex((node) => node.isEqualNode(old)) : -1; - if (oldInFresh !== -1) { - freshNodes.splice(oldInFresh, 1); - oldMark++; - continue; - } - const nextInStale = next ? staleNodes.findIndex((node) => node.isEqualNode(next)) : -1; - if (nextInStale !== -1) { - staleNodes.splice(nextInStale, 1); - nextMark++; - continue; - } - old && staleNodes.push(old); - next && freshNodes.push(next); - oldMark++; - nextMark++; - } - - return { staleNodes, freshNodes }; -} - +// Spc - Move type above only use... type PartitionedNodes = { freshNodes: Element[]; staleNodes: Element[]; }; +function partitionNodes(oldNodes: Element[], nextNodes: Element[]): PartitionedNodes { + // Spc - Holy Jesus... + const oldNodeSet = new Set(oldNodes); + const nextNodeSet = new Set(nextNodes); + + const staleNodes = oldNodes.filter(node => !nextNodeSet.has(node)); + const freshNodes = nextNodes.filter(node => !oldNodeSet.has(node)); + + return { staleNodes, freshNodes }; +} + /** * Runs JS in the fetched document * head scripts will only run with data-reload attr diff --git a/lib/handlers.ts b/lib/handlers.ts index 738ead4..40a5135 100644 --- a/lib/handlers.ts +++ b/lib/handlers.ts @@ -9,10 +9,12 @@ export function scrollTo(type: string, id?: string): void { if (['link', 'go'].includes(type)) { if (id) { const el = document.querySelector(id); - el ? el.scrollIntoView({ behavior: 'smooth', block: 'start' }) : window.scrollTo({ top: 0 }); - } else { - window.scrollTo({ top: 0 }); - } + el && el.scrollIntoView({ behavior: 'smooth', block: 'start' }) + return; + } + + // Spc - Remove redundant Block + window.scrollTo({ top: 0 }); } } /** diff --git a/lib/main.ts b/lib/main.ts index 6a7b5ac..db4ee97 100644 --- a/lib/main.ts +++ b/lib/main.ts @@ -10,9 +10,7 @@ export default (opts?: FlamethrowerOptions): Router => { const router = new Router(opts); // eslint-disable-next-line no-console opts.log && console.log('🔥 flamethrower engaged'); - if (window) { - const flame = window as FlameWindow; - flame.flamethrower = router; - } + + globalThis?.window && ((window as FlameWindow).flamethrower = router); return router; }; diff --git a/lib/router.ts b/lib/router.ts index 2504e40..21fa7ea 100644 --- a/lib/router.ts +++ b/lib/router.ts @@ -19,10 +19,11 @@ export class Router { document.addEventListener('click', (e) => this.onClick(e)); window.addEventListener('popstate', (e) => this.onPop(e)); this.prefetch(); - } else { - console.warn('flamethrower router not supported in this browser or environment'); - this.enabled = false; + return; } + + console.warn('Flamethrower router is not supported in this Browser or Environment'); + this.enabled = false; } /** @@ -74,8 +75,6 @@ export class Router { this.prefetchVisible(); } else if (this.opts.prefetch === 'hover') { this.prefetchOnHover(); - } else { - return; } } @@ -105,15 +104,14 @@ export class Router { entries.forEach((entry) => { const url = entry.target.getAttribute('href'); - if (this.prefetched.has(url)) { + if (this.prefetched.has(url) || entry.isIntersecting) { + // Spc - Please nest guys... + entry.isIntersecting && this.createLink(url); + observer.unobserve(entry.target); return; } - if (entry.isIntersecting) { - this.createLink(url); - observer.unobserve(entry.target); - } }); }, intersectionOpts); this.allLinks.forEach((node) => this.observer.observe(node)); @@ -125,13 +123,13 @@ export class Router { * Create a link to prefetch */ private createLink(url: string): void { - const linkEl = document.createElement('link'); - linkEl.rel = 'prefetch'; - linkEl.href = url; - linkEl.as = 'document'; - - linkEl.onload = () => this.log('🌩️ prefetched', url); - linkEl.onerror = (err) => this.log('🤕 can\'t prefetch', url, err); + const linkEl = Object.assign(document.createElement('link'), { + rel: 'prefetch', + href: url, + as: 'document', + onload: () => this.log('🌩️ prefetched', url), + onerror: (err) => this.log('🤕 can\'t prefetch', url, err), + }); document.head.appendChild(linkEl); @@ -143,7 +141,8 @@ export class Router { * @param {MouseEvent} e * Handle clicks on links */ - private onClick(e: MouseEvent): void { + onClick(e: MouseEvent): void { + console.log(e, this, 'asd'); this.reconstructDOM(handleLinkClick(e)); } @@ -151,7 +150,8 @@ export class Router { * @param {PopStateEvent} e * Handle popstate events like back/forward */ - private onPop(e: PopStateEvent): void { + onPop(e: PopStateEvent): void { + console.log(e, this, 'asd'); this.reconstructDOM(handlePopState(e)); } /** @@ -164,9 +164,10 @@ export class Router { return; } - try { - this.log('⚡', type); + // Does not need to be tried; should be type string, otherwise no log? + this.log('⚡', type); + try { // Check type && window href destination // Disqualify if fetching same URL if (['popstate', 'link', 'go'].includes(type) && next !== prev) { @@ -180,48 +181,37 @@ export class Router { } // Fetch next document - const res = await fetch(next, { headers: { 'X-Flamethrower': '1' } }) - .then((res) => { - const reader = res.body.getReader(); - const length = parseInt(res.headers.get('Content-Length')); - let bytesReceived = 0; - - // take each received chunk and emit an event, pass through to new stream which will be read as text - return new ReadableStream({ - start(controller) { - // The following function handles each data chunk - function push() { - // "done" is a Boolean and value a "Uint8Array" - reader.read().then(({ done, value }) => { - // If there is no more data to read - if (done) { - controller.close(); - return; - } - - bytesReceived += value.length; - window.dispatchEvent( - new CustomEvent('flamethrower:router:fetch-progress', { - detail: { - // length may be NaN if no Content-Length header was found - progress: Number.isNaN(length) ? 0 : (bytesReceived / length) * 100, - received: bytesReceived, - length: length || 0, - }, - }), - ); - // Get the data and send it to the browser via the controller - controller.enqueue(value); - // Check chunks by logging to the console - push(); - }); - } - - push(); - }, - }); - }) - .then((stream) => new Response(stream, { headers: { 'Content-Type': 'text/html' } })); + const response = await fetch(next, { headers: { 'X-Flamethrower': '1' } }); + const reader = response.body.getReader(); + const length = parseInt(response.headers.get('Content-Length') || '0', 10); + + let bytesReceived = 0; + // take each received chunk and emit an event, pass through to new stream which will be read as text + const stream = new ReadableStream({ + start(controller) { + void async function push() { + const { done, value } = await reader.read(); + if (done) { + controller.close(); + return; + } + bytesReceived += value.length; + window.dispatchEvent( + new CustomEvent('flamethrower:router:fetch-progress', { + detail: { + progress: Number.isNaN(length) ? 0 : (bytesReceived / length) * 100, + received: bytesReceived, + length: length || 0, + }, + }), + ); + controller.enqueue(value); + await push(); + }(); + }, + }); + + const res = new Response(stream, { headers: { 'Content-Type': 'text/html' } }); const html = await res.text(); const nextDoc = formatNextDocument(html); @@ -231,19 +221,18 @@ export class Router { // Merge BODY // with optional native browser page transitions - if (this.opts.pageTransitions && (document as any).createDocumentTransition) { - const transition = (document as any).createDocumentTransition(); - transition.start(() => { - replaceBody(nextDoc); - runScripts(); - scrollTo(type, scrollId); - }); - } else { + const handleMerge = () => { replaceBody(nextDoc); runScripts(); scrollTo(type, scrollId); } + if (this.opts.pageTransitions && (document as any).createDocumentTransition) { + const transition = (document as any).createDocumentTransition(); + transition.start(handleMerge); + } else { + handleMerge(); + } window.dispatchEvent(new CustomEvent('flamethrower:router:end')); @@ -257,7 +246,7 @@ export class Router { } catch (err) { window.dispatchEvent(new CustomEvent('flamethrower:router:error', err)); this.opts.log && console.timeEnd('⏱️'); - console.error('💥 router fetch failed', err); + console.error('💥 Router fetch failed', err); return false; } } diff --git a/package-lock.json b/package-lock.json index 846afbb..88ccce5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "flamethrower-router", - "version": "0.0.0-meme.8", + "version": "0.0.0-meme.12", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "flamethrower-router", - "version": "0.0.0-meme.8", + "version": "0.0.0-meme.12", "license": "ISC", "devDependencies": { "@playwright/test": "^1.25.0", diff --git a/playwright.config.ts b/playwright.config.ts index 3788f32..befd88c 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -98,10 +98,10 @@ const config: PlaywrightTestConfig = { // outputDir: 'test-results/', /* Run your local dev server before starting the tests */ - // webServer: { - // command: 'npm run start', - // port: 3000, - // }, + webServer: { + command: 'npm run serve', + port: 3000, + }, }; export default config; diff --git a/replit.nix b/replit.nix new file mode 100644 index 0000000..573982c --- /dev/null +++ b/replit.nix @@ -0,0 +1,10 @@ +{ pkgs }: { + deps = [ + pkgs.yarn + pkgs.esbuild + pkgs.nodejs-18_x + + pkgs.nodePackages.typescript + pkgs.nodePackages.typescript-language-server + ]; +} \ No newline at end of file