First commit

This commit is contained in:
0x1eef 2024-07-10 11:21:15 -03:00
commit f4edf96236
9 changed files with 303 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/dist/
/node_modules/

1
.projectile Normal file
View file

@ -0,0 +1 @@
-/dist/

15
LICENSE Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View file

@ -0,0 +1,15 @@
{
"compilerOptions": {
"strict": true,
"module": "ESNEXT",
"target": "ES2020",
"esModuleInterop": true,
"moduleResolution": "node",
"baseUrl": "src/",
"paths": { "*": ["*"] },
"outDir": "dist",
"declaration": true,
}
}