@eik/node-client 1.1.61 → 1.1.62

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/src/index.js CHANGED
@@ -1,106 +1,297 @@
1
- import { helpers } from '@eik/common';
2
- import { request } from 'undici';
3
- import { join } from 'path';
4
- import Asset from './asset.js';
5
-
6
- const trimSlash = (value = '') => {
7
- if (value.endsWith('/')) return value.substring(0, value.length - 1);
8
- return value;
9
- }
1
+ import { helpers } from "@eik/common";
2
+ import { request } from "undici";
3
+ import { join } from "path";
4
+ import { Asset } from "./asset.js";
5
+
6
+ const trimSlash = (value = "") => {
7
+ if (value.endsWith("/")) return value.substring(0, value.length - 1);
8
+ return value;
9
+ };
10
10
 
11
11
  const fetchImportMaps = async (urls = []) => {
12
- try{
13
- const maps = urls.map(async (map) => {
14
- const {
15
- statusCode,
16
- body
17
- } = await request(map, { maxRedirections: 2 });
18
-
19
- if (statusCode === 404) {
20
- throw new Error('Import map could not be found on server');
21
- } else if (statusCode >= 400 && statusCode < 500) {
22
- throw new Error('Server rejected client request');
23
- } else if (statusCode >= 500) {
24
- throw new Error('Server error');
25
- }
26
- return body.json();
27
- });
28
- return await Promise.all(maps);
29
- } catch (err) {
30
- throw new Error(
31
- `Unable to load import map file from server: ${err.message}`,
32
- );
33
- }
34
- }
12
+ try {
13
+ const maps = urls.map(async (map) => {
14
+ const { statusCode, body } = await request(map, {
15
+ maxRedirections: 2,
16
+ });
17
+
18
+ if (statusCode === 404) {
19
+ throw new Error("Import map could not be found on server");
20
+ } else if (statusCode >= 400 && statusCode < 500) {
21
+ throw new Error("Server rejected client request");
22
+ } else if (statusCode >= 500) {
23
+ throw new Error("Server error");
24
+ }
25
+ return body.json();
26
+ });
27
+ return await Promise.all(maps);
28
+ } catch (err) {
29
+ throw new Error(
30
+ `Unable to load import map file from server: ${err.message}`,
31
+ );
32
+ }
33
+ };
34
+
35
+ /**
36
+ * @typedef {object} Options
37
+ * @property {string} [base=null]
38
+ * @property {boolean} [development=false]
39
+ * @property {boolean} [loadMaps=false]
40
+ * @property {string} [path=process.cwd()]
41
+ */
42
+
43
+ /**
44
+ * @typedef {object} ImportMap
45
+ * @property {Record<string, string>} imports
46
+ */
47
+
48
+ /**
49
+ * An Eik utility for servers running on Node. With it you can:
50
+ * - generate different URLs to assets on an Eik server depending on environment (development vs production).
51
+ * - get the import maps you have configured in `eik.json` from the Eik server, should you want to use them in the HTML response.
52
+ *
53
+ * @example
54
+ * ```js
55
+ * // Create an instance, then load information from `eik.json` and the Eik server
56
+ * import Eik from "@eik/node-client";
57
+ *
58
+ * const eik = new Eik();
59
+ * await eik.load();
60
+ * ```
61
+ * @example
62
+ * ```js
63
+ * // Serve a local version of a file from `./public`
64
+ * // in development and from Eik in production
65
+ * import path from "node:path";
66
+ * import Eik from "@eik/node-client";
67
+ * import fastifyStatic from "@fastify/static";
68
+ * import fastify from "fastify";
69
+ *
70
+ * const app = fastify();
71
+ * app.register(fastifyStatic, {
72
+ * root: path.join(process.cwd(), "public"),
73
+ * prefix: "/public/",
74
+ * });
75
+ *
76
+ * const eik = new Eik({
77
+ * development: process.env.NODE_ENV === "development",
78
+ * base: "/public",
79
+ * });
80
+ *
81
+ * // load information from `eik.json` and the Eik server
82
+ * await eik.load();
83
+ *
84
+ * // when development is true script.value will be /public/script.js.
85
+ * // when development is false script.value will be
86
+ * // https://{server}/pkg/{name}/{version}/script.js
87
+ * // where {server}, {name} and {version} are read from eik.json
88
+ * const script = eik.file("/script.js");
89
+ *
90
+ * app.get("/", (req, reply) => {
91
+ * reply.type("text/html; charset=utf-8");
92
+ * reply.send(`<html><body>
93
+ * <script
94
+ * src="${script.value}"
95
+ * ${script.integrity ? `integrity="${script.integrity}"` : ""}
96
+ * type="module"></script>
97
+ * </body></html>`);
98
+ * });
99
+ *
100
+ * app.listen({
101
+ * port: 3000,
102
+ * });
103
+ *
104
+ * console.log("Listening on http://localhost:3000");
105
+ * ```
106
+ */
107
+ export default class Eik {
108
+ #development;
109
+ #loadMaps;
110
+ #config;
111
+ #path;
112
+ #base;
113
+ #maps;
114
+
115
+ /**
116
+ * @param {Options} options
117
+ */
118
+ constructor({
119
+ development = false,
120
+ loadMaps = false,
121
+ base = "",
122
+ path = process.cwd(),
123
+ } = {}) {
124
+ this.#development = development;
125
+ this.#loadMaps = loadMaps;
126
+ this.#config = {};
127
+ this.#path = path;
128
+ this.#base = trimSlash(base);
129
+ this.#maps = [];
130
+ }
131
+
132
+ /**
133
+ * Reads the Eik config from disk into the object instance, used for building {@link file} links in production.
134
+ *
135
+ * If {@link Options.loadMaps} is `true` the import maps
136
+ * defined in the Eik config will be fetched from the Eik server for
137
+ * use in {@link maps}.
138
+ */
139
+ async load() {
140
+ this.#config = await helpers.getDefaults(this.#path);
141
+ if (this.#loadMaps) {
142
+ this.#maps = await fetchImportMaps(this.#config.map);
143
+ }
144
+ }
145
+
146
+ /**
147
+ * The `"name"` field from the Eik config
148
+ * @throws if read before calling {@link load}
149
+ */
150
+ get name() {
151
+ if (this.#config.name) return this.#config.name;
152
+ throw new Error("Eik config was not loaded before calling .name");
153
+ }
154
+
155
+ /**
156
+ * The `"version"` field from the Eik config
157
+ * @throws if read before calling {@link load}
158
+ */
159
+ get version() {
160
+ if (this.#config.version) return this.#config.version;
161
+ throw new Error("Eik config was not loaded before calling .version");
162
+ }
163
+
164
+ /**
165
+ * The `"type"` field from the Eik config mapped to its URL equivalent (eg. "package" is "pkg").
166
+ * @throws if read before calling {@link load}
167
+ */
168
+ get type() {
169
+ if (this.#config.type && this.#config.type === "package") return "pkg";
170
+ if (this.#config.type) return this.#config.type;
171
+ throw new Error("Eik config was not loaded before calling .type");
172
+ }
173
+
174
+ /**
175
+ * The `"server"` field from the Eik config
176
+ * @throws if read before calling {@link load}
177
+ */
178
+ get server() {
179
+ if (this.#config.server) return this.#config.server;
180
+ throw new Error("Eik config was not loaded before calling .server");
181
+ }
182
+
183
+ /**
184
+ * The pathname to the base on Eik (ex. /pkg/my-app/1.0.0/)
185
+ * @throws if read before calling {@link load}
186
+ */
187
+ get pathname() {
188
+ if (this.#config.type && this.#config.name && this.#config.version)
189
+ return join("/", this.type, this.name, this.version);
190
+ throw new Error("Eik config was not loaded before calling .pathname");
191
+ }
192
+
193
+ /**
194
+ * Similar to {@link file}, this method returns a path to the base on Eik
195
+ * (ex. https://eik.store.com/pkg/my-app/1.0.0), or {@link Options.base}
196
+ * if {@link Options.development} is true.
197
+ *
198
+ * You can use this instead of `file` if you have a directory full of files
199
+ * and you don't need {@link Asset.integrity}.
200
+ *
201
+ * @returns {string} The base path for assets published on Eik
202
+ * @throws when {@link Options.development} is false if called before calling {@link load}
203
+ */
204
+ base() {
205
+ if (this.#development) return this.#base;
206
+ return `${this.server}${this.pathname}`;
207
+ }
208
+
209
+ /**
210
+ * Get a link to a file that is published on Eik when running in production.
211
+ * When {@link Options.development} is `true` the pathname is prefixed
212
+ * with the {@link Options.base} option instead of pointing to Eik.
213
+ *
214
+ * @param {string} pathname pathname to the file relative to the base on Eik (ex: /path/to/script.js for a prod URL https://eik.store.com/pkg/my-app/1.0.0/path/to/script.js)
215
+ * @returns {import('./asset.js').Asset}
216
+ * @throws when {@link Options.development} is false if called before calling {@link load}
217
+ *
218
+ * @example
219
+ * ```js
220
+ * // in production
221
+ * const eik = new Eik({
222
+ * development: false,
223
+ * });
224
+ * await eik.load();
225
+ *
226
+ * const file = eik.file("/path/to/script.js");
227
+ * // {
228
+ * // value: https://eik.store.com/pkg/my-app/1.0.0/path/to/script.js
229
+ * // integrity: sha512-zHQjnD-etc.
230
+ * // }
231
+ * // where the server URL, app name and version are read from eik.json
232
+ * // {
233
+ * // "name": "my-app",
234
+ * // "version": "1.0.0",
235
+ * // "server": "https://eik.store.com",
236
+ * // }
237
+ * ```
238
+ * @example
239
+ * ```js
240
+ * // in development
241
+ * const eik = new Eik({
242
+ * development: true,
243
+ * base: "/public",
244
+ * });
245
+ * await eik.load();
246
+ *
247
+ * const file = eik.file("/path/to/script.js");
248
+ * // {
249
+ * // value: /public/path/to/script.js
250
+ * // integrity: undefined
251
+ * // }
252
+ * ```
253
+ */
254
+ file(pathname = "") {
255
+ const base = this.base();
256
+ return new Asset({
257
+ value: `${base}${pathname}`,
258
+ });
259
+ }
35
260
 
36
- export default class NodeClient {
37
- #development;
38
- #loadMaps;
39
- #config;
40
- #path;
41
- #base;
42
- #maps;
43
- constructor({
44
- development = false,
45
- loadMaps = false,
46
- base = '',
47
- path = process.cwd(),
48
- } = {}) {
49
- this.#development = development;
50
- this.#loadMaps = loadMaps;
51
- this.#config = {};
52
- this.#path = path;
53
- this.#base = trimSlash(base);
54
- this.#maps = [];
55
- }
56
-
57
- async load() {
58
- this.#config = await helpers.getDefaults(this.#path);
59
- if (this.#loadMaps) {
60
- this.#maps = await fetchImportMaps(this.#config.map);
61
- }
62
- }
63
-
64
- get name() {
65
- if (this.#config.name) return this.#config.name;
66
- throw new Error('Eik config was not loaded before calling .name');
67
- }
68
-
69
- get version() {
70
- if (this.#config.version) return this.#config.version;
71
- throw new Error('Eik config was not loaded before calling .version');
72
- }
73
-
74
- get type() {
75
- if (this.#config.type && this.#config.type === 'package') return 'pkg';
76
- if (this.#config.type) return this.#config.type;
77
- throw new Error('Eik config was not loaded before calling .type');
78
- }
79
-
80
- get server() {
81
- if (this.#config.server) return this.#config.server;
82
- throw new Error('Eik config was not loaded before calling .server');
83
- }
84
-
85
- get pathname() {
86
- if (this.#config.type && this.#config.name && this.#config.version) return join('/', this.type, this.name, this.version);
87
- throw new Error('Eik config was not loaded before calling .pathname');
88
- }
89
-
90
- base() {
91
- if (this.#development) return this.#base;
92
- return `${this.server}${this.pathname}`;
93
- }
94
-
95
- file(file = '') {
96
- const base = this.base();
97
- return new Asset({
98
- value: `${base}${file}`,
99
- });
100
- }
101
-
102
- maps() {
103
- if (this.#config.version && this.#loadMaps) return this.#maps;
104
- throw new Error('Eik config was not loaded or "loadMaps" is "false" when calling .maps()');
105
- }
261
+ /**
262
+ * When {@link Options.loadMaps} is `true` and you call {@link load}, the client
263
+ * fetches the configured import maps from the Eik server.
264
+ *
265
+ * This method returns the import maps that were fetched during `load`.
266
+ *
267
+ * @returns {ImportMap[]}
268
+ * @throws if {@link Options.loadMaps} is not `true` or called before calling {@link load}
269
+ *
270
+ * @example
271
+ * ```js
272
+ * // generate a <script type="importmap">
273
+ * // for import mapping in the browser
274
+ * const client = new Eik({
275
+ * loadMaps: true,
276
+ * });
277
+ * await client.load();
278
+ *
279
+ * const maps = client.maps();
280
+ * const combined = maps
281
+ * .map((map) => map.imports)
282
+ * .reduce((map, acc) => ({ ...acc, ...map }), {});
283
+ *
284
+ * const html = `
285
+ * <script type="importmap">
286
+ * ${JSON.stringify(combined, null, 2)}
287
+ * </script>
288
+ * `;
289
+ * ```
290
+ */
291
+ maps() {
292
+ if (this.#config.version && this.#loadMaps) return this.#maps;
293
+ throw new Error(
294
+ 'Eik config was not loaded or "loadMaps" is "false" when calling .maps()',
295
+ );
296
+ }
106
297
  }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * @typedef {object} AssetOptions
3
+ * @property {string} [value=""]
4
+ */
5
+ /**
6
+ * Holds attributes for use when linking to assets hosted on Eik.
7
+ *
8
+ * @example
9
+ * ```
10
+ * // JS and <script>
11
+ * const script = eik.file("/app.js");
12
+ * const html = `<script
13
+ * src="${script.value}"
14
+ * ${script.integrity ? `integrity="${script.integrity}"` : ""}
15
+ * type="module"></script>`;
16
+ * ```
17
+ * @example
18
+ * ```
19
+ * // CSS and <link>
20
+ * const styles = eik.file("/styles.css");
21
+ * const html = `<link
22
+ * href="${styles.value}"
23
+ * ${styles.integrity ? `integrity="${styles.integrity}"` : ""}
24
+ * rel="stylesheet" />`;
25
+ * ```
26
+ */
27
+ export class Asset {
28
+ /**
29
+ * @param {AssetOptions} options
30
+ */
31
+ constructor({ value }?: AssetOptions);
32
+ /**
33
+ * Value for use in [subresource integrity](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity#examples).
34
+ * Not calculated if `development` is `true`.
35
+ *
36
+ * @type {string | undefined}
37
+ */
38
+ integrity: string | undefined;
39
+ /**
40
+ * URL to the file for use in `<script>` or `<link>`.
41
+ * @type {string}
42
+ */
43
+ value: string;
44
+ }
45
+ export type AssetOptions = {
46
+ value?: string;
47
+ };
@@ -0,0 +1,208 @@
1
+ /**
2
+ * @typedef {object} Options
3
+ * @property {string} [base=null]
4
+ * @property {boolean} [development=false]
5
+ * @property {boolean} [loadMaps=false]
6
+ * @property {string} [path=process.cwd()]
7
+ */
8
+ /**
9
+ * @typedef {object} ImportMap
10
+ * @property {Record<string, string>} imports
11
+ */
12
+ /**
13
+ * An Eik utility for servers running on Node. With it you can:
14
+ * - generate different URLs to assets on an Eik server depending on environment (development vs production).
15
+ * - get the import maps you have configured in `eik.json` from the Eik server, should you want to use them in the HTML response.
16
+ *
17
+ * @example
18
+ * ```js
19
+ * // Create an instance, then load information from `eik.json` and the Eik server
20
+ * import Eik from "@eik/node-client";
21
+ *
22
+ * const eik = new Eik();
23
+ * await eik.load();
24
+ * ```
25
+ * @example
26
+ * ```js
27
+ * // Serve a local version of a file from `./public`
28
+ * // in development and from Eik in production
29
+ * import path from "node:path";
30
+ * import Eik from "@eik/node-client";
31
+ * import fastifyStatic from "@fastify/static";
32
+ * import fastify from "fastify";
33
+ *
34
+ * const app = fastify();
35
+ * app.register(fastifyStatic, {
36
+ * root: path.join(process.cwd(), "public"),
37
+ * prefix: "/public/",
38
+ * });
39
+ *
40
+ * const eik = new Eik({
41
+ * development: process.env.NODE_ENV === "development",
42
+ * base: "/public",
43
+ * });
44
+ *
45
+ * // load information from `eik.json` and the Eik server
46
+ * await eik.load();
47
+ *
48
+ * // when development is true script.value will be /public/script.js.
49
+ * // when development is false script.value will be
50
+ * // https://{server}/pkg/{name}/{version}/script.js
51
+ * // where {server}, {name} and {version} are read from eik.json
52
+ * const script = eik.file("/script.js");
53
+ *
54
+ * app.get("/", (req, reply) => {
55
+ * reply.type("text/html; charset=utf-8");
56
+ * reply.send(`<html><body>
57
+ * <script
58
+ * src="${script.value}"
59
+ * ${script.integrity ? `integrity="${script.integrity}"` : ""}
60
+ * type="module"></script>
61
+ * </body></html>`);
62
+ * });
63
+ *
64
+ * app.listen({
65
+ * port: 3000,
66
+ * });
67
+ *
68
+ * console.log("Listening on http://localhost:3000");
69
+ * ```
70
+ */
71
+ export default class Eik {
72
+ /**
73
+ * @param {Options} options
74
+ */
75
+ constructor({ development, loadMaps, base, path, }?: Options);
76
+ /**
77
+ * Reads the Eik config from disk into the object instance, used for building {@link file} links in production.
78
+ *
79
+ * If {@link Options.loadMaps} is `true` the import maps
80
+ * defined in the Eik config will be fetched from the Eik server for
81
+ * use in {@link maps}.
82
+ */
83
+ load(): Promise<void>;
84
+ /**
85
+ * The `"name"` field from the Eik config
86
+ * @throws if read before calling {@link load}
87
+ */
88
+ get name(): any;
89
+ /**
90
+ * The `"version"` field from the Eik config
91
+ * @throws if read before calling {@link load}
92
+ */
93
+ get version(): any;
94
+ /**
95
+ * The `"type"` field from the Eik config mapped to its URL equivalent (eg. "package" is "pkg").
96
+ * @throws if read before calling {@link load}
97
+ */
98
+ get type(): any;
99
+ /**
100
+ * The `"server"` field from the Eik config
101
+ * @throws if read before calling {@link load}
102
+ */
103
+ get server(): any;
104
+ /**
105
+ * The pathname to the base on Eik (ex. /pkg/my-app/1.0.0/)
106
+ * @throws if read before calling {@link load}
107
+ */
108
+ get pathname(): string;
109
+ /**
110
+ * Similar to {@link file}, this method returns a path to the base on Eik
111
+ * (ex. https://eik.store.com/pkg/my-app/1.0.0), or {@link Options.base}
112
+ * if {@link Options.development} is true.
113
+ *
114
+ * You can use this instead of `file` if you have a directory full of files
115
+ * and you don't need {@link Asset.integrity}.
116
+ *
117
+ * @returns {string} The base path for assets published on Eik
118
+ * @throws when {@link Options.development} is false if called before calling {@link load}
119
+ */
120
+ base(): string;
121
+ /**
122
+ * Get a link to a file that is published on Eik when running in production.
123
+ * When {@link Options.development} is `true` the pathname is prefixed
124
+ * with the {@link Options.base} option instead of pointing to Eik.
125
+ *
126
+ * @param {string} pathname pathname to the file relative to the base on Eik (ex: /path/to/script.js for a prod URL https://eik.store.com/pkg/my-app/1.0.0/path/to/script.js)
127
+ * @returns {import('./asset.js').Asset}
128
+ * @throws when {@link Options.development} is false if called before calling {@link load}
129
+ *
130
+ * @example
131
+ * ```js
132
+ * // in production
133
+ * const eik = new Eik({
134
+ * development: false,
135
+ * });
136
+ * await eik.load();
137
+ *
138
+ * const file = eik.file("/path/to/script.js");
139
+ * // {
140
+ * // value: https://eik.store.com/pkg/my-app/1.0.0/path/to/script.js
141
+ * // integrity: sha512-zHQjnD-etc.
142
+ * // }
143
+ * // where the server URL, app name and version are read from eik.json
144
+ * // {
145
+ * // "name": "my-app",
146
+ * // "version": "1.0.0",
147
+ * // "server": "https://eik.store.com",
148
+ * // }
149
+ * ```
150
+ * @example
151
+ * ```js
152
+ * // in development
153
+ * const eik = new Eik({
154
+ * development: true,
155
+ * base: "/public",
156
+ * });
157
+ * await eik.load();
158
+ *
159
+ * const file = eik.file("/path/to/script.js");
160
+ * // {
161
+ * // value: /public/path/to/script.js
162
+ * // integrity: undefined
163
+ * // }
164
+ * ```
165
+ */
166
+ file(pathname?: string): import("./asset.js").Asset;
167
+ /**
168
+ * When {@link Options.loadMaps} is `true` and you call {@link load}, the client
169
+ * fetches the configured import maps from the Eik server.
170
+ *
171
+ * This method returns the import maps that were fetched during `load`.
172
+ *
173
+ * @returns {ImportMap[]}
174
+ * @throws if {@link Options.loadMaps} is not `true` or called before calling {@link load}
175
+ *
176
+ * @example
177
+ * ```js
178
+ * // generate a <script type="importmap">
179
+ * // for import mapping in the browser
180
+ * const client = new Eik({
181
+ * loadMaps: true,
182
+ * });
183
+ * await client.load();
184
+ *
185
+ * const maps = client.maps();
186
+ * const combined = maps
187
+ * .map((map) => map.imports)
188
+ * .reduce((map, acc) => ({ ...acc, ...map }), {});
189
+ *
190
+ * const html = `
191
+ * <script type="importmap">
192
+ * ${JSON.stringify(combined, null, 2)}
193
+ * </script>
194
+ * `;
195
+ * ```
196
+ */
197
+ maps(): ImportMap[];
198
+ #private;
199
+ }
200
+ export type Options = {
201
+ base?: string;
202
+ development?: boolean;
203
+ loadMaps?: boolean;
204
+ path?: string;
205
+ };
206
+ export type ImportMap = {
207
+ imports: Record<string, string>;
208
+ };