ghtml 2.0.4 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- Replace your template engine with fast JavaScript by leveraging the power of [tagged templates](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates).
1
+ ta**ghtml** lets you replace your template engine with fast JavaScript by leveraging the power of [tagged templates](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates).
2
2
 
3
3
  Inspired by [html-template-tag](https://github.com/AntonioVdlC/html-template-tag).
4
4
 
package/bin/README.md ADDED
@@ -0,0 +1,66 @@
1
+ Append unique hashes to assets referenced in your views to aggressively cache them while guaranteeing that clients receive the most recent versions.
2
+
3
+ ## Usage
4
+
5
+ Running the following command will scan asset files found in the `roots` path(s) and replace their references with hashed versions in the `refs` path(s):
6
+
7
+ ```sh
8
+ npx ghtml --roots="path/to/scan/assets1/,path/to/scan/assets2/" --refs="views/path/to/append/hashes1/,views/path/to/append/hashes2/"
9
+ ```
10
+
11
+ ## Example (Fastify)
12
+
13
+ Register `@fastify/static`:
14
+
15
+ ```js
16
+ await fastify.register(import("@fastify/static"), {
17
+ root: new URL("assets/", import.meta.url).pathname,
18
+ prefix: "/p/assets/",
19
+ wildcard: false,
20
+ index: false,
21
+ immutable: true,
22
+ maxAge: process.env.NODE_ENV === "production" ? 31536000 * 1000 : 0,
23
+ });
24
+ ```
25
+
26
+ Add the `ghtml` command to the build script:
27
+
28
+ ```json
29
+ "scripts": {
30
+ "build": "npx ghtml --roots=assets/ --refs=views/,routes/",
31
+ },
32
+ ```
33
+
34
+ Make sure to `npm run build` in `Dockerfile`:
35
+
36
+ ```dockerfile
37
+ FROM node:latest
38
+
39
+ WORKDIR /app
40
+
41
+ COPY package*.json ./
42
+
43
+ RUN npm ci --include=dev
44
+
45
+ COPY . .
46
+
47
+ RUN npm run build
48
+
49
+ RUN npm prune --omit=dev
50
+
51
+ CMD ["npm", "start"]
52
+ ```
53
+
54
+ ## Demo
55
+
56
+ A full project that uses the `ghtml` executable can be found in the `example` folder:
57
+
58
+ ```sh
59
+ cd example
60
+
61
+ npm i
62
+
63
+ npm run build
64
+
65
+ node .
66
+ ```
Binary file
@@ -0,0 +1,19 @@
1
+ body {
2
+ display: flex;
3
+ flex-direction: column;
4
+ align-items: center;
5
+ justify-content: center;
6
+ height: 100vh;
7
+ margin: 0;
8
+ padding: 0;
9
+ }
10
+
11
+ img {
12
+ max-width: 100%;
13
+ height: auto;
14
+ }
15
+
16
+ .caption {
17
+ text-align: center;
18
+ margin-top: 10px;
19
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "type": "module",
3
+ "main": "./server.js",
4
+ "scripts": {
5
+ "start": "node server.js",
6
+ "build": "node ../src/index.js --roots=assets/ --refs=routes/"
7
+ },
8
+ "dependencies": {
9
+ "@fastify/static": "^7.0.1",
10
+ "fastify": "^4.26.1",
11
+ "fastify-html": "^0.3.3"
12
+ }
13
+ }
@@ -0,0 +1,28 @@
1
+ export default async (fastify) => {
2
+ const { html } = fastify;
3
+
4
+ fastify.addLayout((inner) => {
5
+ return html`<!doctype html>
6
+ <html lang="en">
7
+ <head>
8
+ <meta charset="UTF-8" />
9
+ <meta
10
+ name="viewport"
11
+ content="width=device-width, initial-scale=1.0"
12
+ />
13
+ <title>Document</title>
14
+ <link rel="stylesheet" href="/p/assets/style.css" />
15
+ </head>
16
+ <body>
17
+ !${inner}
18
+ </body>
19
+ </html>`;
20
+ });
21
+
22
+ fastify.get("/", async (request, reply) => {
23
+ return reply.html`
24
+ <h1 class="caption">Hello, world!</h1>
25
+ <img width="500" src="/p/assets/cat.jpeg" alt="Picture of a cat" />
26
+ `;
27
+ });
28
+ };
@@ -0,0 +1,22 @@
1
+ /* eslint n/no-missing-import: "off" */
2
+
3
+ import Fastify from "fastify";
4
+
5
+ const fastify = Fastify();
6
+
7
+ // Plugins
8
+ await fastify.register(import("@fastify/static"), {
9
+ root: new URL("assets/", import.meta.url).pathname,
10
+ prefix: "/p/assets/",
11
+ wildcard: false,
12
+ index: false,
13
+ immutable: true,
14
+ maxAge: 31536000 * 1000,
15
+ });
16
+ await fastify.register(import("fastify-html"));
17
+
18
+ // Routes
19
+ fastify.register(import("./routes/index.js"));
20
+
21
+ await fastify.listen({ port: 5050 });
22
+ console.warn("Server listening at http://localhost:5050");
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { generateHashesAndReplace } from "./utils.js";
4
+ import process from "node:process";
5
+
6
+ const parseArguments = (args) => {
7
+ let roots = null;
8
+ let refs = null;
9
+
10
+ for (const arg of args) {
11
+ if (arg.startsWith("--roots=")) {
12
+ roots = arg.split("=", 2)[1].split(",");
13
+ } else if (arg.startsWith("--refs=")) {
14
+ refs = arg.split("=", 2)[1].split(",");
15
+ }
16
+ }
17
+
18
+ if (!roots || !refs) {
19
+ console.error(
20
+ 'Usage: npx ghtml --roots="path/to/scan/assets1/,path/to/scan/assets2/" --refs="views/path/to/append/hashes1/,views/path/to/append/hashes2/"',
21
+ );
22
+ process.exit(1);
23
+ }
24
+
25
+ return { roots, refs };
26
+ };
27
+
28
+ const main = async () => {
29
+ const { roots, refs } = parseArguments(process.argv.slice(2));
30
+
31
+ try {
32
+ console.warn(`Generating hashes and updating file paths...`);
33
+ console.warn(`Scanning files in: ${roots}`);
34
+ console.warn(`Updating files in: ${refs}`);
35
+
36
+ await generateHashesAndReplace({
37
+ roots,
38
+ refs,
39
+ });
40
+
41
+ console.warn("Hash generation and file updates completed successfully.");
42
+ } catch (error) {
43
+ console.error(`Error occurred: ${error.message}`);
44
+ process.exit(1);
45
+ }
46
+ };
47
+
48
+ main();
@@ -0,0 +1,129 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile, writeFile } from "node:fs/promises";
3
+ import { win32, posix } from "node:path";
4
+ import { cpus } from "node:os";
5
+ import { Glob } from "glob";
6
+ import { promise as fastq } from "fastq";
7
+ const fastqConcurrency = Math.max(1, cpus().length - 1);
8
+
9
+ const generateFileHash = async (filePath) => {
10
+ try {
11
+ const fileBuffer = await readFile(filePath);
12
+ return createHash("md5").update(fileBuffer).digest("hex").slice(0, 16);
13
+ } catch (err) {
14
+ if (err.code !== "ENOENT") {
15
+ throw err;
16
+ }
17
+ return "";
18
+ }
19
+ };
20
+
21
+ const updateFilePathsWithHashes = async (
22
+ fileHashes,
23
+ refs,
24
+ includeDotFiles,
25
+ skipPatterns,
26
+ ) => {
27
+ for (let ref of refs) {
28
+ ref = ref.split(win32.sep).join(posix.sep);
29
+ if (!ref.endsWith("/")) {
30
+ ref += "/";
31
+ }
32
+
33
+ const filesIterable = new Glob("**/**", {
34
+ nodir: true,
35
+ follow: true,
36
+ absolute: true,
37
+ cwd: ref,
38
+ dot: includeDotFiles,
39
+ ignore: skipPatterns,
40
+ });
41
+
42
+ for await (const file of filesIterable) {
43
+ let content = await readFile(file, "utf8");
44
+ let found = false;
45
+
46
+ for (const [originalPath, hash] of fileHashes) {
47
+ const escapedPath = originalPath.replace(
48
+ /[$()*+.?[\\\]^{|}]/gu,
49
+ "\\$&",
50
+ );
51
+ const regex = new RegExp(
52
+ `(?<path>${escapedPath})(\\?(?<queryString>[^#"'\`]*))?`,
53
+ "gu",
54
+ );
55
+
56
+ content = content.replace(
57
+ regex,
58
+ (match, p1, p2, p3, offset, string, groups) => {
59
+ found = true;
60
+ const { path, queryString } = groups;
61
+
62
+ return !queryString
63
+ ? `${path}?hash=${hash}`
64
+ : queryString.includes("hash=")
65
+ ? `${path}?${queryString.replace(/(?<hash>hash=)[\dA-Fa-f]*/u, `$1${hash}`)}`
66
+ : `${path}?hash=${hash}&${queryString}`;
67
+ },
68
+ );
69
+ }
70
+
71
+ if (found) {
72
+ await writeFile(file, content);
73
+ }
74
+ }
75
+ }
76
+ };
77
+
78
+ const generateHashesAndReplace = async ({
79
+ roots,
80
+ refs,
81
+ includeDotFiles = false,
82
+ skipPatterns = ["**/node_modules/**"],
83
+ }) => {
84
+ const fileHashes = new Map();
85
+ roots = Array.isArray(roots) ? roots : [roots];
86
+ refs = Array.isArray(refs) ? refs : [refs];
87
+
88
+ for (let rootPath of roots) {
89
+ rootPath = rootPath.split(win32.sep).join(posix.sep);
90
+ if (!rootPath.endsWith("/")) {
91
+ rootPath += "/";
92
+ }
93
+
94
+ const queue = fastq(generateFileHash, fastqConcurrency);
95
+ const queuePromises = [];
96
+ const files = [];
97
+
98
+ const filesIterable = new Glob("**/**", {
99
+ nodir: true,
100
+ follow: true,
101
+ absolute: true,
102
+ cwd: rootPath,
103
+ dot: includeDotFiles,
104
+ ignore: skipPatterns,
105
+ });
106
+
107
+ for await (let file of filesIterable) {
108
+ file = file.split(win32.sep).join(posix.sep);
109
+ files.push(file);
110
+ queuePromises.push(queue.push(file));
111
+ }
112
+
113
+ const hashes = await Promise.all(queuePromises);
114
+
115
+ for (let i = 0; i < files.length; i++) {
116
+ const fileRelativePath = posix.relative(rootPath, files[i]);
117
+ fileHashes.set(fileRelativePath, hashes[i]);
118
+ }
119
+ }
120
+
121
+ await updateFilePathsWithHashes(
122
+ fileHashes,
123
+ refs,
124
+ includeDotFiles,
125
+ skipPatterns,
126
+ );
127
+ };
128
+
129
+ export { generateFileHash, generateHashesAndReplace };
package/package.json CHANGED
@@ -3,8 +3,9 @@
3
3
  "description": "Replace your template engine with fast JavaScript by leveraging the power of tagged templates.",
4
4
  "author": "Gürgün Dayıoğlu",
5
5
  "license": "MIT",
6
- "version": "2.0.4",
6
+ "version": "2.1.0",
7
7
  "type": "module",
8
+ "bin": "./bin/src/index.js",
8
9
  "main": "./src/index.js",
9
10
  "exports": {
10
11
  ".": "./src/index.js",
@@ -19,9 +20,13 @@
19
20
  "lint": "eslint . && prettier --check .",
20
21
  "lint:fix": "eslint --fix . && prettier --write ."
21
22
  },
23
+ "dependencies": {
24
+ "fastq": "^1.17.1",
25
+ "glob": "^10.4.2"
26
+ },
22
27
  "devDependencies": {
23
28
  "@fastify/pre-commit": "^2.1.0",
24
- "c8": "^10.0.0",
29
+ "c8": "^10.1.2",
25
30
  "grules": "^0.17.2",
26
31
  "tinybench": "^2.8.0"
27
32
  },
package/src/html.js CHANGED
@@ -1,7 +1,3 @@
1
- const arrayIsArray = Array.isArray;
2
- const symbolIterator = Symbol.iterator;
3
- const symbolAsyncIterator = Symbol.asyncIterator;
4
-
5
1
  const escapeRegExp = /["&'<>`]/;
6
2
  const escapeFunction = (string) => {
7
3
  const stringLength = string.length;
@@ -9,34 +5,34 @@ const escapeFunction = (string) => {
9
5
  let end = 0;
10
6
  let escaped = "";
11
7
 
12
- do {
13
- switch (string.charCodeAt(end++)) {
8
+ for (; end !== stringLength; ++end) {
9
+ switch (string.charCodeAt(end)) {
14
10
  case 34: // "
15
- escaped += string.slice(start, end - 1) + "&#34;";
16
- start = end;
11
+ escaped += string.slice(start, end) + "&#34;";
12
+ start = end + 1;
17
13
  continue;
18
14
  case 38: // &
19
- escaped += string.slice(start, end - 1) + "&#38;";
20
- start = end;
15
+ escaped += string.slice(start, end) + "&#38;";
16
+ start = end + 1;
21
17
  continue;
22
18
  case 39: // '
23
- escaped += string.slice(start, end - 1) + "&#39;";
24
- start = end;
19
+ escaped += string.slice(start, end) + "&#39;";
20
+ start = end + 1;
25
21
  continue;
26
22
  case 60: // <
27
- escaped += string.slice(start, end - 1) + "&#60;";
28
- start = end;
23
+ escaped += string.slice(start, end) + "&#60;";
24
+ start = end + 1;
29
25
  continue;
30
26
  case 62: // >
31
- escaped += string.slice(start, end - 1) + "&#62;";
32
- start = end;
27
+ escaped += string.slice(start, end) + "&#62;";
28
+ start = end + 1;
33
29
  continue;
34
30
  case 96: // `
35
- escaped += string.slice(start, end - 1) + "&#96;";
36
- start = end;
31
+ escaped += string.slice(start, end) + "&#96;";
32
+ start = end + 1;
37
33
  continue;
38
34
  }
39
- } while (end !== stringLength);
35
+ }
40
36
 
41
37
  escaped += string.slice(start, end);
42
38
 
@@ -61,7 +57,7 @@ const html = ({ raw: literals }, ...expressions) => {
61
57
  ? expression
62
58
  : expression == null
63
59
  ? ""
64
- : arrayIsArray(expression)
60
+ : Array.isArray(expression)
65
61
  ? expression.join("")
66
62
  : `${expression}`;
67
63
 
@@ -98,7 +94,7 @@ const htmlGenerator = function* ({ raw: literals }, ...expressions) {
98
94
  } else if (expression == null) {
99
95
  string = "";
100
96
  } else {
101
- if (expression[symbolIterator]) {
97
+ if (expression[Symbol.iterator]) {
102
98
  const isRaw =
103
99
  literal !== "" && literal.charCodeAt(literal.length - 1) === 33;
104
100
 
@@ -118,7 +114,7 @@ const htmlGenerator = function* ({ raw: literals }, ...expressions) {
118
114
  continue;
119
115
  }
120
116
 
121
- if (expression[symbolIterator]) {
117
+ if (expression[Symbol.iterator]) {
122
118
  for (expression of expression) {
123
119
  if (typeof expression === "string") {
124
120
  string = expression;
@@ -195,7 +191,7 @@ const htmlAsyncGenerator = async function* ({ raw: literals }, ...expressions) {
195
191
  } else if (expression == null) {
196
192
  string = "";
197
193
  } else {
198
- if (expression[symbolIterator] || expression[symbolAsyncIterator]) {
194
+ if (expression[Symbol.iterator] || expression[Symbol.asyncIterator]) {
199
195
  const isRaw =
200
196
  literal !== "" && literal.charCodeAt(literal.length - 1) === 33;
201
197
 
@@ -215,7 +211,10 @@ const htmlAsyncGenerator = async function* ({ raw: literals }, ...expressions) {
215
211
  continue;
216
212
  }
217
213
 
218
- if (expression[symbolIterator] || expression[symbolAsyncIterator]) {
214
+ if (
215
+ expression[Symbol.iterator] ||
216
+ expression[Symbol.asyncIterator]
217
+ ) {
219
218
  for await (expression of expression) {
220
219
  if (typeof expression === "string") {
221
220
  string = expression;