Skip to content

Commit

Permalink
Merge pull request #37 from swup/typescript
Browse files Browse the repository at this point in the history
Port to Typescript
  • Loading branch information
daun authored Jul 30, 2023
2 parents bd83da8 + 520475e commit 4f63daf
Show file tree
Hide file tree
Showing 11 changed files with 1,399 additions and 5,424 deletions.
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

<!-- ## [Unreleased] -->

## [2.1.0] - 2023-07-30

- Port to TypeScript

## [2.0.0] - 2023-07-26

- Switch to microbundle
Expand All @@ -28,8 +32,9 @@

- Initial release

[Unreleased]: https://github.com/swup/head-plugin/compare/2.0.0...HEAD
[Unreleased]: https://github.com/swup/head-plugin/compare/2.1.0...HEAD

[2.1.0]: https://github.com/swup/head-plugin/releases/tag/2.1.0
[2.0.0]: https://github.com/swup/head-plugin/releases/tag/2.0.0
[1.3.0]: https://github.com/swup/head-plugin/releases/tag/1.3.0
[1.2.1]: https://github.com/swup/head-plugin/releases/tag/1.2.1
Expand Down
6,666 changes: 1,296 additions & 5,370 deletions package-lock.json

Large diffs are not rendered by default.

15 changes: 9 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
{
"name": "@swup/head-plugin",
"amdName": "SwupHeadPlugin",
"version": "2.0.0",
"version": "2.1.0",
"description": "A swup plugin for updating the contents of the head tag",
"type": "module",
"source": "src/index.js",
"source": "src/index.ts",
"main": "./dist/index.cjs",
"module": "./dist/index.module.js",
"exports": "./dist/index.modern.js",
"unpkg": "./dist/index.umd.js",
"files": [
"dist"
],
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.modern.js",
"require": "./dist/index.cjs"
}
},
"scripts": {
"build": "swup-plugin build",
"dev": "swup-plugin dev",
Expand Down
23 changes: 18 additions & 5 deletions src/index.js → src/index.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,35 @@
import { Handler } from 'swup';
import Plugin from '@swup/plugin';

import mergeHeadContents from './mergeHeadContents.js';
import updateLangAttribute from './updateLangAttribute.js';
import waitForAssets from './waitForAssets.js';

type Options = {
/** Whether to keep orphaned `link`, `style` and `script` tags from the old page. Default: `false` */
persistAssets: boolean;
/** Tags that will be persisted when a new page is loaded. Boolean, selector or predicate function. Default: `false` */
persistTags: boolean | string | ((el: Element) => boolean);
/** Delay the transition to the new page until all newly added assets have finished loading. Default: `false` */
awaitAssets: boolean;
/** How long to wait for assets before continuing anyway. Only applies if `awaitAssets` is enabled. Default: `3000` */
timeout: number;
};

export default class SwupHeadPlugin extends Plugin {
name = 'SwupHeadPlugin';

requires = { swup: '>=4' };

defaults = {
defaults: Options = {
persistTags: false,
persistAssets: false,
awaitAssets: false,
timeout: 3000
};
options: Options;

constructor(options = {}) {
constructor(options: Partial<Options> = {}) {
super();

this.options = { ...this.defaults, ...options };
Expand All @@ -31,7 +44,7 @@ export default class SwupHeadPlugin extends Plugin {
this.before('content:replace', this.updateHead);
}

async updateHead(visit, { page: { html } }) {
updateHead: Handler<'content:replace'> = async (visit, { page: { html } }) => {
const newDocument = new DOMParser().parseFromString(html, 'text/html');

const { removed, added } = mergeHeadContents(document.head, newDocument.head, { shouldPersist: (el) => this.isPersistentTag(el) });
Expand All @@ -49,9 +62,9 @@ export default class SwupHeadPlugin extends Plugin {
await Promise.all(assetLoadPromises);
}
}
}
};

isPersistentTag(el) {
isPersistentTag(el: Element) {
const { persistTags } = this.options;
if (typeof persistTags === 'function') {
return persistTags(el);
Expand Down
33 changes: 20 additions & 13 deletions src/mergeHeadContents.js → src/mergeHeadContents.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,57 @@
export default function mergeHeadContents(currentHead, newHead, { shouldPersist = () => false } = {}) {
type ElementCollection = { el: Element; index?: number }[];

export default function mergeHeadContents(
currentHead: HTMLHeadElement,
newHead: HTMLHeadElement,
{ shouldPersist = () => false }: { shouldPersist?: (el: Element) => boolean } = {}
) {
const currentTags = Array.from(currentHead.children);
const newChildren = Array.from(newHead.children);

const addTags = getTagsToAdd(currentTags, newChildren);
const removeTags = getTagsToRemove(currentTags, newChildren);

// Remove tags in reverse to keep indexes, keep persistant elements
removeTags.reverse()
removeTags
.reverse()
.filter(({ el }) => shouldManageTag(el))
.filter(({ el }) => !shouldPersist(el))
.forEach(({ el }) => currentHead.removeChild(el));

// Insert tag *after* previous version of itself to preserve JS variable scope and CSS cascade
addTags
.filter(({ el }) => shouldManageTag(el))
.forEach(({ el, index }) => {
.forEach(({ el, index = 0 }) => {
currentHead.insertBefore(el, currentHead.children[index + 1] || null);
});

return {
removed: removeTags.map(({ el }) => el),
added: addTags.map(({ el }) => el),
added: addTags.map(({ el }) => el)
};
};
}

function getTagsToRemove(currentEls, newEls) {
function getTagsToRemove(currentEls: Element[], newEls: Element[]): ElementCollection {
return currentEls.reduce((tags, el) => {
const isAmongNew = newEls.some((newEl) => compareTags(el, newEl));
if (!isAmongNew) {
tags.push({ el });
}
return tags;
}, []);
};
}, [] as ElementCollection);
}

function getTagsToAdd(currentEls, newEls) {
function getTagsToAdd(currentEls: Element[], newEls: Element[]): ElementCollection {
return newEls.reduce((tags, el, index) => {
const isAmongCurrent = currentEls.some((currentEl) => compareTags(el, currentEl));
if (!isAmongCurrent) {
tags.push({ el, index });
}
return tags;
}, []);
};
}, [] as ElementCollection);
}

function shouldManageTag(el) {
function shouldManageTag(el: Element) {
// Let swup manage the title tag
if (el.localName === 'title') {
return false;
Expand All @@ -56,6 +63,6 @@ function shouldManageTag(el) {
return true;
}

function compareTags(oldTag, newTag) {
function compareTags(oldTag: Element, newTag: Element) {
return oldTag.outerHTML === newTag.outerHTML;
}
7 changes: 5 additions & 2 deletions src/updateLangAttribute.js → src/updateLangAttribute.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
export default function updateLangAttribute(currentHtml, newHtml) {
export default function updateLangAttribute(
currentHtml: HTMLElement,
newHtml: HTMLElement
): string | null {
if (currentHtml.lang !== newHtml.lang) {
currentHtml.lang = newHtml.lang;
return currentHtml.lang;
} else {
return null;
}
};
}
7 changes: 0 additions & 7 deletions src/waitForAssets.js

This file was deleted.

9 changes: 9 additions & 0 deletions src/waitForAssets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import waitForStylesheet from './waitForStylesheet.js';

function isStylesheet(el: Element): el is HTMLLinkElement {
return el.matches('link[rel=stylesheet][href]');
}

export default function waitForAssets(elements: Element[], timeoutMs: number = 0) {
return elements.filter(isStylesheet).map((el) => waitForStylesheet(el, timeoutMs));
}
20 changes: 0 additions & 20 deletions src/waitForStylesheet.js

This file was deleted.

22 changes: 22 additions & 0 deletions src/waitForStylesheet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export default function waitForStylesheet(element: HTMLLinkElement, timeoutMs: number = 0) {
const isLoaded = ({ href }: HTMLLinkElement) => {
return Array.from(document.styleSheets)
.map(({ href }) => href)
.includes(href);
};

const whenLoaded = (cb: (value?: unknown) => void) => {
if (isLoaded(element)) {
cb();
} else {
setTimeout(() => whenLoaded(cb), 10);
}
};

return new Promise((resolve) => {
whenLoaded(resolve);
if (timeoutMs > 0) {
setTimeout(resolve, timeoutMs);
}
});
}
14 changes: 14 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"include": ["src"],
"compilerOptions": {
"target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
"moduleResolution": "Node16", /* Specify how TypeScript looks up a file from a given module specifier. */
"rootDirs": ["./src"], /* Allow multiple folders to be treated as one when resolving modules. */
"resolveJsonModule": true, /* Enable importing .json files. */
"allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
"outDir": "./dist", /* Specify an output folder for all emitted files. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
"strict": true, /* Enable all strict type-checking options. */
"noImplicitAny": true /* Enable error reporting for expressions and declarations with an implied 'any' type. */
}
}

0 comments on commit 4f63daf

Please sign in to comment.