First commit
This commit is contained in:
commit
f4edf96236
9 changed files with 303 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
/dist/
|
||||||
|
/node_modules/
|
1
.projectile
Normal file
1
.projectile
Normal file
|
@ -0,0 +1 @@
|
||||||
|
-/dist/
|
15
LICENSE
Normal file
15
LICENSE
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
Copyright (C) 2023 by 0x1eef <0x1eef@protonmail.com>
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this
|
||||||
|
software for any purpose with or without fee is hereby
|
||||||
|
granted.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS
|
||||||
|
ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO
|
||||||
|
EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
|
||||||
|
RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
|
||||||
|
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
|
||||||
|
OF THIS SOFTWARE.
|
67
README.md
Normal file
67
README.md
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
## About
|
||||||
|
|
||||||
|
Postman delivers the assets of a web page. The library is
|
||||||
|
typically paired with a progress bar that reports progress
|
||||||
|
to the client.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Progress bar
|
||||||
|
|
||||||
|
The following example delivers fonts, scripts, images
|
||||||
|
and stylesheets with the help of a progress bar. The
|
||||||
|
progress bar is removed once the delivery is complete:
|
||||||
|
|
||||||
|
**index.html**
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<head>
|
||||||
|
<title>Postman</title>
|
||||||
|
<script type="module" src="/postman.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="postman loader">
|
||||||
|
<progress value="0" max="100"></progress>
|
||||||
|
<span class="percentage"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
**postman.js**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import postman, { item } from "postman";
|
||||||
|
|
||||||
|
const progressBar = document.querySelector("progress")
|
||||||
|
const span = document.querySelector(".percentage");
|
||||||
|
postman(
|
||||||
|
item.font("Kanit Regular", "url(/fonts/kanit-regular.ttf)"),
|
||||||
|
item.script("/js/app.js"),
|
||||||
|
item.image("/images/app.png"),
|
||||||
|
item.css("/css/app.css"),
|
||||||
|
item.progress((percent) => {
|
||||||
|
progressBar.value = percent;
|
||||||
|
span.innerText = `${percent}%`;
|
||||||
|
})
|
||||||
|
).fetch()
|
||||||
|
.then((package) => {
|
||||||
|
/* Add page assets */
|
||||||
|
package.fonts.forEach((font) => documents.fonts.add(font));
|
||||||
|
package.scripts.forEach((script) => document.body.appendChild(script));
|
||||||
|
package.css.forEach((css) => document.head.appendChild(css));
|
||||||
|
|
||||||
|
/* Replace progress bar */
|
||||||
|
progressBar.remove();
|
||||||
|
span.remove();
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[BSD Zero Clause](https://choosealicense.com/licenses/0bsd/)
|
||||||
|
<br>
|
||||||
|
See [LICENSE](./LICENSE)
|
||||||
|
|
21
package.json
Normal file
21
package.json
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"name": "postman",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Delivers the assets of a web page",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"types": ["dist/index.d.ts"],
|
||||||
|
"scripts": {
|
||||||
|
"build": "npm exec tsc",
|
||||||
|
"prepare": "npm run build"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/0x1eef/postman.git"
|
||||||
|
},
|
||||||
|
"author": "0x1eef",
|
||||||
|
"license": "0BSDL",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^16.18",
|
||||||
|
"typescript": "^4.5"
|
||||||
|
}
|
||||||
|
}
|
74
src/index.ts
Normal file
74
src/index.ts
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
import type { Item, FontItem } from './postman/item';
|
||||||
|
import item from './postman/item';
|
||||||
|
import request from './postman/request';
|
||||||
|
|
||||||
|
type Postman = { fetch: () => Promise<Package> };
|
||||||
|
type Args = Array<Item | FontItem | Function>
|
||||||
|
type Items = Array<Item | FontItem>;
|
||||||
|
type Package = {
|
||||||
|
fonts: FontFace[]
|
||||||
|
images: HTMLElement[]
|
||||||
|
css: HTMLElement[]
|
||||||
|
scripts: HTMLElement[]
|
||||||
|
json: HTMLElement[]
|
||||||
|
};
|
||||||
|
|
||||||
|
function parseArgs(args: Args): [Items, Function] {
|
||||||
|
const items: Items = [];
|
||||||
|
let callback: Function = (n: number) => n
|
||||||
|
args.forEach((item) => {
|
||||||
|
if (typeof item === 'function') {
|
||||||
|
callback = item;
|
||||||
|
} else {
|
||||||
|
items.push(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return [items, callback];
|
||||||
|
}
|
||||||
|
|
||||||
|
export { item };
|
||||||
|
|
||||||
|
export default function (...args: Args) {
|
||||||
|
const self: Postman = Object.create(null);
|
||||||
|
const result: Package = { fonts: [], images: [], css: [], scripts: [], json: [] };
|
||||||
|
const [items, callback] = parseArgs(args);
|
||||||
|
items.sort((i1, i2) => i1.priority >= i2.priority ? 1 : -1);
|
||||||
|
|
||||||
|
let index = 0;
|
||||||
|
const onProgress = <T>(el: T) => {
|
||||||
|
index++;
|
||||||
|
if (index <= items.length) {
|
||||||
|
callback(100 * (index / items.length));
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
};
|
||||||
|
|
||||||
|
const spawnRequests = () => {
|
||||||
|
const reqs = items.map((item: Item | FontItem) => {
|
||||||
|
if ('fontFamily' in item) {
|
||||||
|
const req = request.font;
|
||||||
|
return req(item)
|
||||||
|
.then((el) => onProgress<FontFace>(el))
|
||||||
|
.then((font) => result.fonts.push(font))
|
||||||
|
.then(() => result);
|
||||||
|
} else if(item.requestId !== 'font' && item.group !== 'fonts') {
|
||||||
|
const req = request[item.requestId];
|
||||||
|
const ary = result[item.group];
|
||||||
|
return req(item)
|
||||||
|
.then((el) => onProgress<HTMLElement>(el))
|
||||||
|
.then((el) => ary.push(el))
|
||||||
|
.then(() => result);
|
||||||
|
}
|
||||||
|
/* unreachable */
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
return reqs as Array<Promise<Package>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.fetch = async () => {
|
||||||
|
await Promise.all<Package>(spawnRequests());
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
return self;
|
||||||
|
}
|
65
src/postman/item.ts
Normal file
65
src/postman/item.ts
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
type Group = 'fonts' | 'images' | 'css' | 'scripts' | 'json';
|
||||||
|
type RequestID = 'font' | 'image' | 'css' | 'script' | 'json';
|
||||||
|
|
||||||
|
export type Item = {
|
||||||
|
priority: number
|
||||||
|
group: Group
|
||||||
|
requestId: RequestID
|
||||||
|
href: string
|
||||||
|
props?: Partial<HTMLElement>
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FontItem = {
|
||||||
|
fontFamily: string
|
||||||
|
} & Item;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
font(fontFamily: string, href: string): FontItem {
|
||||||
|
return {
|
||||||
|
priority: 1,
|
||||||
|
group: 'fonts',
|
||||||
|
requestId: 'font',
|
||||||
|
href, fontFamily,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
image(href: string, props?: Partial<HTMLElement>): Item {
|
||||||
|
return {
|
||||||
|
priority: 2,
|
||||||
|
group: 'images',
|
||||||
|
requestId: 'image',
|
||||||
|
href, props
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
css(href: string, props?: Partial<HTMLElement>): Item {
|
||||||
|
return {
|
||||||
|
priority: 3,
|
||||||
|
group: 'css',
|
||||||
|
requestId: 'css',
|
||||||
|
href, props
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
script(href: string, props?: Partial<HTMLElement>): Item {
|
||||||
|
return {
|
||||||
|
priority: 4,
|
||||||
|
group: 'scripts',
|
||||||
|
requestId: 'script',
|
||||||
|
href, props
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
json(href: string, props?: Partial<HTMLElement>): Item {
|
||||||
|
return {
|
||||||
|
priority: 5,
|
||||||
|
group: 'json',
|
||||||
|
requestId: 'json',
|
||||||
|
href, props
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
progress(fn: (percent: number) => void): (percent: number) => void {
|
||||||
|
return fn;
|
||||||
|
}
|
||||||
|
};
|
43
src/postman/request.ts
Normal file
43
src/postman/request.ts
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import type { Item, FontItem } from './item';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
font(item: FontItem): Promise<FontFace> {
|
||||||
|
const { fontFamily, href } = item;
|
||||||
|
return new FontFace(fontFamily, href).load();
|
||||||
|
},
|
||||||
|
|
||||||
|
script(item: Item, options: RequestInit = {}): Promise<HTMLElement> {
|
||||||
|
const { href } = item;
|
||||||
|
return fetch(href, options)
|
||||||
|
.then((res) => res.text())
|
||||||
|
.then((text) => ({ type: 'application/javascript', text }))
|
||||||
|
.then((props) => Object.assign(document.createElement('script'), props));
|
||||||
|
},
|
||||||
|
|
||||||
|
css(item: Item, options: RequestInit = {}): Promise<HTMLElement> {
|
||||||
|
const { href } = item;
|
||||||
|
return fetch(href, options)
|
||||||
|
.then((res) => res.text())
|
||||||
|
.then((text) => ({ innerText: text }))
|
||||||
|
.then((props) => Object.assign(document.createElement('style'), props));
|
||||||
|
},
|
||||||
|
|
||||||
|
image(item: Item): Promise<HTMLElement> {
|
||||||
|
const { href } = item;
|
||||||
|
return new Promise<HTMLElement>((resolve, reject) => {
|
||||||
|
const el = document.createElement('img');
|
||||||
|
el.onload = () => resolve(el);
|
||||||
|
el.onerror = reject;
|
||||||
|
el.src = href;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
json(item: Item, options: RequestInit = {}): Promise<HTMLElement> {
|
||||||
|
const { href } = item;
|
||||||
|
return fetch(href, options)
|
||||||
|
.then((res) => res.text())
|
||||||
|
.then((text) => ({type: 'application/json', text}))
|
||||||
|
.then((props) => Object.assign(props, item.props || {}))
|
||||||
|
.then((props) => Object.assign(document.createElement('script'), props));
|
||||||
|
}
|
||||||
|
};
|
15
tsconfig.json
Normal file
15
tsconfig.json
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true,
|
||||||
|
"module": "ESNEXT",
|
||||||
|
"target": "ES2020",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
|
||||||
|
"baseUrl": "src/",
|
||||||
|
"paths": { "*": ["*"] },
|
||||||
|
|
||||||
|
"outDir": "dist",
|
||||||
|
"declaration": true,
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue