From 85556225a2388abf658b1e1b224fb7ed9fcfa481 Mon Sep 17 00:00:00 2001 From: William Killerud Date: Tue, 6 Aug 2024 13:10:42 +0200 Subject: [PATCH 01/10] test: add schema to fixture --- fixtures/eik.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fixtures/eik.json b/fixtures/eik.json index aaa9c7f..d19d80f 100644 --- a/fixtures/eik.json +++ b/fixtures/eik.json @@ -1,4 +1,5 @@ { + "$schema": "https://raw.githubusercontent.com/eik-lib/common/main/lib/schemas/eikjson.schema.json", "name": "eik-fixture", "version": "1.0.2", "server": "https://cdn.eik.dev", @@ -8,4 +9,4 @@ "https://cdn.eik.dev/map/mod-a/v2", "https://cdn.eik.dev/map/mod-b/v1" ] -} \ No newline at end of file +} From 1f9adff1a84ba78dcd112e45bf894533adb5f5f3 Mon Sep 17 00:00:00 2001 From: William Killerud Date: Tue, 6 Aug 2024 13:55:01 +0200 Subject: [PATCH 02/10] fix: include type definitions Update some devtools to use our shared configs while at it. --- .eslintignore | 6 ----- .eslintrc | 16 ------------ .gitattributes | 1 + .gitignore | 1 + .npmrc | 1 + .prettierrc | 19 -------------- CONTRIBUTING.md | 9 +++++++ README.md | 63 +++++++++++++++++++++++++--------------------- eslint.config.js | 3 +++ package.json | 35 +++++++++++++++----------- prettier.config.js | 3 +++ release.config.js | 2 +- src/index.js | 23 +++++++++-------- tsconfig.json | 7 ++++++ tsconfig.test.json | 4 +++ 15 files changed, 97 insertions(+), 96 deletions(-) delete mode 100644 .eslintignore delete mode 100644 .eslintrc create mode 100644 .gitattributes delete mode 100644 .prettierrc create mode 100644 CONTRIBUTING.md create mode 100644 eslint.config.js create mode 100644 prettier.config.js create mode 100644 tsconfig.json create mode 100644 tsconfig.test.json diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index d7256cb..0000000 --- a/.eslintignore +++ /dev/null @@ -1,6 +0,0 @@ -tap-snapshots -node_modules -modules -utils -dist -tmp \ No newline at end of file diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index c6ff5bf..0000000 --- a/.eslintrc +++ /dev/null @@ -1,16 +0,0 @@ -{ - "extends": ["airbnb-base", "prettier"], - "plugins": ["prettier"], - "parser": "@babel/eslint-parser", - "parserOptions": { - "requireConfigFile": false, - "ecmaVersion": 2020, - "sourceType": "module" - }, - "rules": { - "lines-between-class-members": [0], - "class-methods-use-this": [0], - "import/extensions": [0], - "strict": [0, "global"] - } -} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.gitignore b/.gitignore index b16a259..62f13a2 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ coverage .vscode dist/ .tap +types/ diff --git a/.npmrc b/.npmrc index 43c97e7..0ca8d2a 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,2 @@ package-lock=false +save-exact=true diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index ba7d8cc..0000000 --- a/.prettierrc +++ /dev/null @@ -1,19 +0,0 @@ -{ - "singleQuote": true, - "trailingComma": "all", - "tabWidth": 4, - "overrides": [ - { - "files": [ - ".prettierrc", - "*.json", - "*.yml", - ".travis.yml", - ".eslintrc" - ], - "options": { - "tabWidth": 2 - } - } - ] -} \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..b80f28f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,9 @@ +# Contributing + +Thank you for showing an interest in contributing to Eik 🧡 + +Commits should follow the [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) format. + +This repo uses [semantic release](https://github.com/semantic-release/semantic-release) to automate releases whenever changes are merged to the default branch. + +We use JSDoc to generate type definitions. If you're new to that workflow, [the TypeScript docs](https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html) and [this blogpost by Alex Harri](https://alexharri.com/blog/jsdoc-as-an-alternative-typescript-syntax) are good starting points. diff --git a/README.md b/README.md index 1ee127e..4f86ef6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ # @eik/node-client -The Eik Node.js client facilitates loading Eik config and providing URLs to assets on an Eik server or in local development plus loading import maps from the Eik server. +This is an Eik utility for servers running on Node. With it you can: + +- generate different URLs to assets on an Eik server depending on environment (development vs production). +- get the import maps you have configured in `eik.json` from the Eik server, should you want to use them in the HTML response. ## Install @@ -8,14 +11,16 @@ The Eik Node.js client facilitates loading Eik config and providing URLs to asse npm install @eik/node-client ``` -## Basic usage +## Usage + +The most common use case for this module is linking to a file. When developing you typically want to use a local version of the file, then link to the published version on Eik when running in production. ```js -import EikNodeClient from '@eik/node-client'; +import Eik from '@eik/node-client'; -const client = new EikNodeClient({ +const client = new Eik({ development: false, - base: '/public' + base: '/public', }); await client.load(); @@ -36,17 +41,17 @@ import EikNodeClient from '@eik/node-client'; const client = new EikNodeClient({ development: false, - base: 'http://localhost:8080/public' + base: 'http://localhost:8080/public', }); await client.load(); -// Will, for example, output: +// Will, for example, output: // { // integrity: sha512-zHQjnDpMW7IKVyTpT9cOPT1+xhUSOcbgXj6qHCPSPu1CbQfgwDEsIniXU54zDIN71zqmxLSp3hfIljpt69ok0w== -// value: https://cdn.eik.dev/pkg/mymodue/2.4.1/path/script.js +// value: https://cdn.eik.dev/pkg/mymodue/2.4.1/path/script.js // } -client.file('/path/script.js') +client.file('/path/script.js'); ``` The following is the same as above but in development mode. The output will then be based on the vaule set for `base`: @@ -56,17 +61,17 @@ import EikNodeClient from '@eik/node-client'; const client = new EikNodeClient({ development: true, - base: 'http://localhost:8080/public' + base: 'http://localhost:8080/public', }); await client.load(); -// Will, for example, output: +// Will, for example, output: // { // integrity: null // value: http://localhost:8080/public/path/script.js // } -client.file('/path/script.js') +client.file('/path/script.js'); ``` ## Constructor @@ -79,11 +84,11 @@ const client = new EikNodeClient(options); ### options -| option | default | type | required | details | -| ----------- | --------------- | --------- | -------- | ------------------------------------------------------------------------------ | -| path | `process.cwd()` | `string` | `false` | Path to directory containing an eik.json file or package.json with eik config. | -| base | `null` | `string` | `false` | Base root to be used for returned asset files. | -| development | `false` | `boolean` | `false` | Set the module in development mode or not. | +| option | default | type | required | details | +| ----------- | --------------- | --------- | -------- | ------------------------------------------------------------------------------------------------ | +| path | `process.cwd()` | `string` | `false` | Path to directory containing an eik.json file or package.json with eik config. | +| base | `null` | `string` | `false` | Base root to be used for returned asset files. | +| development | `false` | `boolean` | `false` | Set the module in development mode or not. | | loadMaps | `false` | `boolean` | `false` | Specifies whether import maps defined in the config should be loaded from the Eik server or not. | #### path @@ -108,7 +113,7 @@ This module has the following API ### async .load() -Loads Eik config from the Eik config into the object instance. If `loadMaps` is set to `true` on the constructor, the import maps defined in the config will be loaded from the Eik server. +Loads Eik config from the Eik config into the object instance. If `loadMaps` is set to `true` on the constructor, the import maps defined in the config will be loaded from the Eik server. ### .base() @@ -119,11 +124,11 @@ When in non development mode, the returned value will be built up by the values ```js const client = new EikNodeClient({ development: false, - base: 'http://localhost:8080/assets' + base: 'http://localhost:8080/assets', }); await client.load(); -client.base() // https://cdn.eik.dev/pkg/mymodue/2.4.1 +client.base(); // https://cdn.eik.dev/pkg/mymodue/2.4.1 ``` When in development mode, the returned value will be equal to whats set on the `base` argument on the constructor. @@ -131,11 +136,11 @@ When in development mode, the returned value will be equal to whats set on the ` ```js const client = new EikNodeClient({ development: true, - base: 'http://localhost:8080/assets' + base: 'http://localhost:8080/assets', }); await client.load(); -client.base() // http://localhost:8080/assets +client.base(); // http://localhost:8080/assets ``` ### .file(file) @@ -147,11 +152,11 @@ When in non development mode, the returned value will be built up by the values ```js const client = new EikNodeClient({ development: false, - base: 'http://localhost:8080/assets' + base: 'http://localhost:8080/assets', }); await client.load(); -client.file('/js/script.js') // Returns an asset.value like: https://cdn.eik.dev/pkg/mymodue/2.4.1/js/script.js +client.file('/js/script.js'); // Returns an asset.value like: https://cdn.eik.dev/pkg/mymodue/2.4.1/js/script.js ``` When in development mode, the returned value will be equal to whats set on the `base` argument on the constructor plus the provided value to the `file` argument on the method. @@ -159,18 +164,18 @@ When in development mode, the returned value will be equal to whats set on the ` ```js const client = new EikNodeClient({ development: true, - base: 'http://localhost:8080/assets' + base: 'http://localhost:8080/assets', }); await client.load(); -client.file('/js/script.js') // Returns an asset.value like: http://localhost:8080/assets/js/script.js +client.file('/js/script.js'); // Returns an asset.value like: http://localhost:8080/assets/js/script.js ``` #### arguments -| option | default | type | required | details | -| ----------- | --------------- | ---------- | -------- | -------------------------------------------------------------------------------- | -| file | `null` | `string` | `false` | File to append to the base of a full URL to an asset | +| option | default | type | required | details | +| ------ | ------- | -------- | -------- | ---------------------------------------------------- | +| file | `null` | `string` | `false` | File to append to the base of a full URL to an asset | Returns a object with as follow: diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..3eae3e6 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,3 @@ +import config from '@eik/eslint-config'; + +export default config; diff --git a/package.json b/package.json index c69eb6c..bd50322 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,11 @@ { "name": "@eik/node-client", "version": "1.1.61", - "description": "A node.js client for interacting with a Eik server.", + "description": "A utility for generating URLs to assets on an Eik server depending on environment.", "type": "module", "main": "./dist/index.cjs", "exports": { + "types": "./types/index.d.ts", "import": "./src/index.js", "require": "./dist/index.cjs" }, @@ -13,14 +14,18 @@ "package.json", "LICENSE", "dist", - "src" + "src", + "types" ], "scripts": { - "test": "tap --disable-coverage --allow-empty-coverage", - "lint:fix": "eslint --fix .", + "build": "rollup -c", "lint": "eslint .", - "prepare": "npm run -s build", - "build": "rollup -c" + "lint:fix": "eslint --fix .", + "test": "tap --disable-coverage --allow-empty-coverage", + "types": "run-s types:module types:test", + "types:module": "tsc", + "types:test": "tsc --project tsconfig.test.json", + "prepare": "npm run -s build" }, "repository": { "type": "git", @@ -42,18 +47,18 @@ "undici": "5.28.4" }, "devDependencies": { - "@babel/eslint-parser": "7.24.7", - "@eik/semantic-release-config": "^1.0.0", + "@eik/eslint-config": "1.0.1", + "@eik/prettier-config": "1.0.1", + "@eik/semantic-release-config": "1.0.0", + "@eik/typescript-config": "1.0.0", "@semantic-release/changelog": "6.0.3", "@semantic-release/git": "10.0.1", - "eslint": "8.57.0", - "eslint-config-airbnb-base": "15.0.0", - "eslint-config-prettier": "9.1.0", - "eslint-plugin-import": "2.29.1", - "eslint-plugin-prettier": "5.1.3", + "eslint": "9.8.0", + "npm-run-all": "4.1.5", "prettier": "3.3.2", "rollup": "4.18.0", - "semantic-release": "^23.1.1", - "tap": "18.7.2" + "semantic-release": "23.1.1", + "tap": "18.7.2", + "typescript": "5.5.4" } } diff --git a/prettier.config.js b/prettier.config.js new file mode 100644 index 0000000..8983a3c --- /dev/null +++ b/prettier.config.js @@ -0,0 +1,3 @@ +import config from "@eik/prettier-config"; + +export default config; diff --git a/release.config.js b/release.config.js index 720c888..3241499 100644 --- a/release.config.js +++ b/release.config.js @@ -1,3 +1,3 @@ export default { - extends: '@eik/semantic-release-config', + extends: "@eik/semantic-release-config", }; diff --git a/src/index.js b/src/index.js index a38ef2c..5a19002 100644 --- a/src/index.js +++ b/src/index.js @@ -6,16 +6,15 @@ import Asset from './asset.js'; const trimSlash = (value = '') => { if (value.endsWith('/')) return value.substring(0, value.length - 1); return value; -} +}; const fetchImportMaps = async (urls = []) => { - try{ + try { const maps = urls.map(async (map) => { - const { - statusCode, - body - } = await request(map, { maxRedirections: 2 }); - + const { statusCode, body } = await request(map, { + maxRedirections: 2, + }); + if (statusCode === 404) { throw new Error('Import map could not be found on server'); } else if (statusCode >= 400 && statusCode < 500) { @@ -31,7 +30,7 @@ const fetchImportMaps = async (urls = []) => { `Unable to load import map file from server: ${err.message}`, ); } -} +}; export default class NodeClient { #development; @@ -40,6 +39,7 @@ export default class NodeClient { #path; #base; #maps; + constructor({ development = false, loadMaps = false, @@ -83,7 +83,8 @@ export default class NodeClient { } get pathname() { - if (this.#config.type && this.#config.name && this.#config.version) return join('/', this.type, this.name, this.version); + if (this.#config.type && this.#config.name && this.#config.version) + return join('/', this.type, this.name, this.version); throw new Error('Eik config was not loaded before calling .pathname'); } @@ -101,6 +102,8 @@ export default class NodeClient { maps() { if (this.#config.version && this.#loadMaps) return this.#maps; - throw new Error('Eik config was not loaded or "loadMaps" is "false" when calling .maps()'); + throw new Error( + 'Eik config was not loaded or "loadMaps" is "false" when calling .maps()', + ); } } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..436df04 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@eik/typescript-config/module.json", + "include": ["./src/**/*.js"], + "compilerOptions": { + "outDir": "types" + } +} diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 0000000..2d07b9a --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,4 @@ +{ + "extends": "@eik/typescript-config/test.json", + "include": ["./test/**/*.js"] +} From a1705f9eb486e11405e116699e6959fae1861076 Mon Sep 17 00:00:00 2001 From: William Killerud Date: Tue, 6 Aug 2024 13:55:32 +0200 Subject: [PATCH 03/10] chore: lint fix --- eslint.config.js | 2 +- release.config.js | 2 +- rollup.config.js | 23 +- src/asset.js | 10 +- src/index.js | 180 +++++++-------- test/asset.test.js | 68 +++--- test/index.test.js | 563 ++++++++++++++++++++++----------------------- 7 files changed, 413 insertions(+), 435 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index 3eae3e6..b940db0 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,3 +1,3 @@ -import config from '@eik/eslint-config'; +import config from "@eik/eslint-config"; export default config; diff --git a/release.config.js b/release.config.js index 3241499..dd5cd51 100644 --- a/release.config.js +++ b/release.config.js @@ -1,3 +1,3 @@ export default { - extends: "@eik/semantic-release-config", + extends: "@eik/semantic-release-config", }; diff --git a/rollup.config.js b/rollup.config.js index 75b9b92..1f22cc9 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,16 +1,11 @@ export default { - input: 'src/index.js', - external: [ - '@eik/common', - 'undici', - 'abslog', - 'path', - ], - output: [ - { - exports: 'auto', - format: 'cjs', - file: 'dist/index.cjs', - }, - ], + input: "src/index.js", + external: ["@eik/common", "undici", "abslog", "path"], + output: [ + { + exports: "auto", + format: "cjs", + file: "dist/index.cjs", + }, + ], }; diff --git a/src/asset.js b/src/asset.js index 6167d96..da228ba 100644 --- a/src/asset.js +++ b/src/asset.js @@ -1,8 +1,6 @@ export default class Asset { - constructor({ - value = '', - } = {}) { - this.integrity = undefined; - this.value = value; - } + constructor({ value = "" } = {}) { + this.integrity = undefined; + this.value = value; + } } diff --git a/src/index.js b/src/index.js index 5a19002..f047926 100644 --- a/src/index.js +++ b/src/index.js @@ -1,109 +1,109 @@ -import { helpers } from '@eik/common'; -import { request } from 'undici'; -import { join } from 'path'; -import Asset from './asset.js'; +import { helpers } from "@eik/common"; +import { request } from "undici"; +import { join } from "path"; +import Asset from "./asset.js"; -const trimSlash = (value = '') => { - if (value.endsWith('/')) return value.substring(0, value.length - 1); - return value; +const trimSlash = (value = "") => { + if (value.endsWith("/")) return value.substring(0, value.length - 1); + return value; }; const fetchImportMaps = async (urls = []) => { - try { - const maps = urls.map(async (map) => { - const { statusCode, body } = await request(map, { - maxRedirections: 2, - }); + try { + const maps = urls.map(async (map) => { + const { statusCode, body } = await request(map, { + maxRedirections: 2, + }); - if (statusCode === 404) { - throw new Error('Import map could not be found on server'); - } else if (statusCode >= 400 && statusCode < 500) { - throw new Error('Server rejected client request'); - } else if (statusCode >= 500) { - throw new Error('Server error'); - } - return body.json(); - }); - return await Promise.all(maps); - } catch (err) { - throw new Error( - `Unable to load import map file from server: ${err.message}`, - ); - } + if (statusCode === 404) { + throw new Error("Import map could not be found on server"); + } else if (statusCode >= 400 && statusCode < 500) { + throw new Error("Server rejected client request"); + } else if (statusCode >= 500) { + throw new Error("Server error"); + } + return body.json(); + }); + return await Promise.all(maps); + } catch (err) { + throw new Error( + `Unable to load import map file from server: ${err.message}`, + ); + } }; export default class NodeClient { - #development; - #loadMaps; - #config; - #path; - #base; - #maps; + #development; + #loadMaps; + #config; + #path; + #base; + #maps; - constructor({ - development = false, - loadMaps = false, - base = '', - path = process.cwd(), - } = {}) { - this.#development = development; - this.#loadMaps = loadMaps; - this.#config = {}; - this.#path = path; - this.#base = trimSlash(base); - this.#maps = []; - } + constructor({ + development = false, + loadMaps = false, + base = "", + path = process.cwd(), + } = {}) { + this.#development = development; + this.#loadMaps = loadMaps; + this.#config = {}; + this.#path = path; + this.#base = trimSlash(base); + this.#maps = []; + } - async load() { - this.#config = await helpers.getDefaults(this.#path); - if (this.#loadMaps) { - this.#maps = await fetchImportMaps(this.#config.map); - } - } + async load() { + this.#config = await helpers.getDefaults(this.#path); + if (this.#loadMaps) { + this.#maps = await fetchImportMaps(this.#config.map); + } + } - get name() { - if (this.#config.name) return this.#config.name; - throw new Error('Eik config was not loaded before calling .name'); - } + get name() { + if (this.#config.name) return this.#config.name; + throw new Error("Eik config was not loaded before calling .name"); + } - get version() { - if (this.#config.version) return this.#config.version; - throw new Error('Eik config was not loaded before calling .version'); - } + get version() { + if (this.#config.version) return this.#config.version; + throw new Error("Eik config was not loaded before calling .version"); + } - get type() { - if (this.#config.type && this.#config.type === 'package') return 'pkg'; - if (this.#config.type) return this.#config.type; - throw new Error('Eik config was not loaded before calling .type'); - } + get type() { + if (this.#config.type && this.#config.type === "package") return "pkg"; + if (this.#config.type) return this.#config.type; + throw new Error("Eik config was not loaded before calling .type"); + } - get server() { - if (this.#config.server) return this.#config.server; - throw new Error('Eik config was not loaded before calling .server'); - } + get server() { + if (this.#config.server) return this.#config.server; + throw new Error("Eik config was not loaded before calling .server"); + } - get pathname() { - if (this.#config.type && this.#config.name && this.#config.version) - return join('/', this.type, this.name, this.version); - throw new Error('Eik config was not loaded before calling .pathname'); - } + get pathname() { + if (this.#config.type && this.#config.name && this.#config.version) + return join("/", this.type, this.name, this.version); + throw new Error("Eik config was not loaded before calling .pathname"); + } - base() { - if (this.#development) return this.#base; - return `${this.server}${this.pathname}`; - } + base() { + if (this.#development) return this.#base; + return `${this.server}${this.pathname}`; + } - file(file = '') { - const base = this.base(); - return new Asset({ - value: `${base}${file}`, - }); - } + file(file = "") { + const base = this.base(); + return new Asset({ + value: `${base}${file}`, + }); + } - maps() { - if (this.#config.version && this.#loadMaps) return this.#maps; - throw new Error( - 'Eik config was not loaded or "loadMaps" is "false" when calling .maps()', - ); - } + maps() { + if (this.#config.version && this.#loadMaps) return this.#maps; + throw new Error( + 'Eik config was not loaded or "loadMaps" is "false" when calling .maps()', + ); + } } diff --git a/test/asset.test.js b/test/asset.test.js index 512a719..fc5e4f8 100644 --- a/test/asset.test.js +++ b/test/asset.test.js @@ -1,44 +1,44 @@ -import tap from 'tap'; -import Asset from '../src/asset.js'; +import tap from "tap"; +import Asset from "../src/asset.js"; -tap.test('Asset - Default values', (t) => { - const asset = new Asset(); - t.equal(asset.integrity, undefined, 'should be "undefined"'); - t.equal(asset.value, '', 'should be empty string'); - t.end(); +tap.test("Asset - Default values", (t) => { + const asset = new Asset(); + t.equal(asset.integrity, undefined, 'should be "undefined"'); + t.equal(asset.value, "", "should be empty string"); + t.end(); }); -tap.test('Asset - Set values through constructor', (t) => { - const asset = new Asset({ - value: 'foo' - }); - t.equal(asset.value, 'foo', 'should have set value'); - t.end(); +tap.test("Asset - Set values through constructor", (t) => { + const asset = new Asset({ + value: "foo", + }); + t.equal(asset.value, "foo", "should have set value"); + t.end(); }); -tap.test('Asset - Set values through properties', (t) => { - const asset = new Asset(); - asset.integrity = 'bar'; - asset.value = 'foo'; - t.equal(asset.integrity, 'bar', 'should have set value'); - t.equal(asset.value, 'foo', 'should have set value'); - t.end(); +tap.test("Asset - Set values through properties", (t) => { + const asset = new Asset(); + asset.integrity = "bar"; + asset.value = "foo"; + t.equal(asset.integrity, "bar", "should have set value"); + t.equal(asset.value, "foo", "should have set value"); + t.end(); }); -tap.test('Asset - Stringify object with default values', (t) => { - const asset = new Asset(); - const obj = JSON.parse(JSON.stringify(asset)); - t.equal(obj.integrity, undefined, 'should be "undefined"'); - t.equal(obj.value, '', 'should be empty string'); - t.end(); +tap.test("Asset - Stringify object with default values", (t) => { + const asset = new Asset(); + const obj = JSON.parse(JSON.stringify(asset)); + t.equal(obj.integrity, undefined, 'should be "undefined"'); + t.equal(obj.value, "", "should be empty string"); + t.end(); }); -tap.test('Asset - Stringify object with set values', (t) => { - const asset = new Asset(); - asset.integrity = 'bar'; - asset.value = 'foo'; - const obj = JSON.parse(JSON.stringify(asset)); - t.equal(obj.integrity, 'bar', 'should have set value'); - t.equal(obj.value, 'foo', 'should have set value'); - t.end(); +tap.test("Asset - Stringify object with set values", (t) => { + const asset = new Asset(); + asset.integrity = "bar"; + asset.value = "foo"; + const obj = JSON.parse(JSON.stringify(asset)); + t.equal(obj.integrity, "bar", "should have set value"); + t.equal(obj.value, "foo", "should have set value"); + t.end(); }); diff --git a/test/index.test.js b/test/index.test.js index 0968f14..0beeb9a 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -1,338 +1,323 @@ -import { mkdtemp, writeFile } from 'fs/promises'; -import { helpers } from '@eik/common'; -import path from 'path'; -import http from 'http'; -import tap from 'tap'; -import os from 'os'; +import { mkdtemp, writeFile } from "fs/promises"; +import { helpers } from "@eik/common"; +import path from "path"; +import http from "http"; +import tap from "tap"; +import os from "os"; -import NodeClient from '../src/index.js'; +import NodeClient from "../src/index.js"; const FIXTURE_PATH = `${process.cwd()}/fixtures`; const FIXTURE_FILE = await helpers.getDefaults(FIXTURE_PATH); const writeTempConfig = async (address) => { - const pathname = await mkdtemp( - path.join(os.tmpdir(), `eik-${address.port.toString()}-`), - ); - const config = JSON.parse(JSON.stringify(FIXTURE_FILE)); + const pathname = await mkdtemp( + path.join(os.tmpdir(), `eik-${address.port.toString()}-`), + ); + const config = JSON.parse(JSON.stringify(FIXTURE_FILE)); - config.server = `http://${address.address}:${address.port}`; - config['import-map'] = [ - `http://${address.address}:${address.port}/map/mod-a/v2`, - `http://${address.address}:${address.port}/map/mod-b/v1`, - ]; + config.server = `http://${address.address}:${address.port}`; + config["import-map"] = [ + `http://${address.address}:${address.port}/map/mod-a/v2`, + `http://${address.address}:${address.port}/map/mod-b/v1`, + ]; - await writeFile(path.join(pathname, 'eik.json'), JSON.stringify(config)); + await writeFile(path.join(pathname, "eik.json"), JSON.stringify(config)); - return pathname; + return pathname; }; class Server { - constructor() { - this.server = http.createServer((req, res) => { - if (req.url.startsWith('/map/mod')) { - res.statusCode = 200; - res.setHeader('Content-Type', 'application/json'); - res.end( - JSON.stringify({ - imports: { - eik: '/src/eik.js', - }, - }), - ); - return; - } - - res.statusCode = 404; - res.setHeader('Content-Type', 'text/plain'); - res.end('Not found'); - }); - } - - listen() { - return new Promise((resolve) => { - const connection = this.server.listen(0, '127.0.0.1', () => { - resolve(connection); - }); - }); - } - - close() { - return new Promise((resolve) => { - this.server.close(() => { - resolve(); - }); - }); - } + constructor() { + this.server = http.createServer((req, res) => { + if (req.url.startsWith("/map/mod")) { + res.statusCode = 200; + res.setHeader("Content-Type", "application/json"); + res.end( + JSON.stringify({ + imports: { + eik: "/src/eik.js", + }, + }), + ); + return; + } + + res.statusCode = 404; + res.setHeader("Content-Type", "text/plain"); + res.end("Not found"); + }); + } + + listen() { + return new Promise((resolve) => { + const connection = this.server.listen(0, "127.0.0.1", () => { + resolve(connection); + }); + }); + } + + close() { + return new Promise((resolve) => { + this.server.close(() => { + resolve(); + }); + }); + } } tap.beforeEach(async (t) => { - const server = new Server(); - const app = await server.listen(); - const address = app.address(); - const fixture = await writeTempConfig(address); - - // eslint-disable-next-line no-param-reassign - t.context = { - app, - address: `http://${address.address}:${address.port}`, - fixture, - }; + const server = new Server(); + const app = await server.listen(); + const address = app.address(); + const fixture = await writeTempConfig(address); + + t.context = { + app, + address: `http://${address.address}:${address.port}`, + fixture, + }; }); tap.afterEach(async (t) => { - await t.context.app.close(); + await t.context.app.close(); }); -tap.test('Client - Default settings - Config is not loaded', (t) => { - const client = new NodeClient(); - - t.throws( - () => { - // eslint-disable-next-line no-unused-vars - const val = client.name; - }, - /Eik config was not loaded before calling .name/, - 'Should throw', - ); - - t.throws( - () => { - // eslint-disable-next-line no-unused-vars - const val = client.version; - }, - /Eik config was not loaded before calling .version/, - 'Should throw', - ); - - t.throws( - () => { - // eslint-disable-next-line no-unused-vars - const val = client.type; - }, - /Eik config was not loaded before calling .type/, - 'Should throw', - ); - - t.throws( - () => { - // eslint-disable-next-line no-unused-vars - const val = client.server; - }, - /Eik config was not loaded before calling .server/, - 'Should throw', - ); - - t.throws( - () => { - // eslint-disable-next-line no-unused-vars - const val = client.pathname; - }, - /Eik config was not loaded before calling .pathname/, - 'Should throw', - ); - - t.throws( - () => { - // eslint-disable-next-line no-unused-vars - const val = client.maps(); - }, - /Eik config was not loaded or "loadMaps" is "false" when calling .maps()/, - 'Should throw', - ); - - t.end(); +tap.test("Client - Default settings - Config is not loaded", (t) => { + const client = new NodeClient(); + + t.throws( + () => { + // eslint-disable-next-line no-unused-vars + const val = client.name; + }, + /Eik config was not loaded before calling .name/, + "Should throw", + ); + + t.throws( + () => { + // eslint-disable-next-line no-unused-vars + const val = client.version; + }, + /Eik config was not loaded before calling .version/, + "Should throw", + ); + + t.throws( + () => { + // eslint-disable-next-line no-unused-vars + const val = client.type; + }, + /Eik config was not loaded before calling .type/, + "Should throw", + ); + + t.throws( + () => { + // eslint-disable-next-line no-unused-vars + const val = client.server; + }, + /Eik config was not loaded before calling .server/, + "Should throw", + ); + + t.throws( + () => { + // eslint-disable-next-line no-unused-vars + const val = client.pathname; + }, + /Eik config was not loaded before calling .pathname/, + "Should throw", + ); + + t.throws( + () => { + // eslint-disable-next-line no-unused-vars + const val = client.maps(); + }, + /Eik config was not loaded or "loadMaps" is "false" when calling .maps()/, + "Should throw", + ); + + t.end(); }); -tap.test('Client - Default settings - Config is loaded', async (t) => { - const client = new NodeClient({ - path: t.context.fixture, - }); - await client.load(); - - t.equal(client.name, 'eik-fixture', 'Should be same as "name" in eik.json'); - t.equal(client.version, '1.0.2', 'Should be same as "version" in eik.json'); - t.equal(client.type, 'pkg', 'Should be "pkg" in eik.json'); - t.equal( - client.server, - t.context.address, - 'Should be same as "server" in eik.json', - ); - t.equal( - client.pathname, - '/pkg/eik-fixture/1.0.2', - 'Should be composed path based on "type", "name" and "version"', - ); - t.end(); +tap.test("Client - Default settings - Config is loaded", async (t) => { + const client = new NodeClient({ + path: t.context.fixture, + }); + await client.load(); + + t.equal(client.name, "eik-fixture", 'Should be same as "name" in eik.json'); + t.equal(client.version, "1.0.2", 'Should be same as "version" in eik.json'); + t.equal(client.type, "pkg", 'Should be "pkg" in eik.json'); + t.equal( + client.server, + t.context.address, + 'Should be same as "server" in eik.json', + ); + t.equal( + client.pathname, + "/pkg/eik-fixture/1.0.2", + 'Should be composed path based on "type", "name" and "version"', + ); + t.end(); }); tap.test( - 'Client - Default settings - Config is loaded and development mode is set to "true"', - async (t) => { - const client = new NodeClient({ - development: true, - path: t.context.fixture, - }); - await client.load(); - - t.equal( - client.name, - 'eik-fixture', - 'Should be same as "name" in eik.json', - ); - t.equal( - client.version, - '1.0.2', - 'Should be same as "version" in eik.json', - ); - t.equal(client.type, 'pkg', 'Should be "pkg" in eik.json'); - t.equal( - client.server, - t.context.address, - 'Should be same as "server" in eik.json', - ); - t.equal( - client.pathname, - '/pkg/eik-fixture/1.0.2', - 'Should be composed path based on "type", "name" and "version"', - ); - t.end(); - }, + 'Client - Default settings - Config is loaded and development mode is set to "true"', + async (t) => { + const client = new NodeClient({ + development: true, + path: t.context.fixture, + }); + await client.load(); + + t.equal(client.name, "eik-fixture", 'Should be same as "name" in eik.json'); + t.equal(client.version, "1.0.2", 'Should be same as "version" in eik.json'); + t.equal(client.type, "pkg", 'Should be "pkg" in eik.json'); + t.equal( + client.server, + t.context.address, + 'Should be same as "server" in eik.json', + ); + t.equal( + client.pathname, + "/pkg/eik-fixture/1.0.2", + 'Should be composed path based on "type", "name" and "version"', + ); + t.end(); + }, ); tap.test( - 'Client - Retrieve a file path - Development mode is set to "false"', - async (t) => { - const client = new NodeClient({ - path: t.context.fixture, - }); - await client.load(); - - const file = '/some/path/foo.js'; - const resolved = client.file(file); - - t.equal(resolved.value, `${client.server}${client.pathname}${file}`); - t.end(); - }, + 'Client - Retrieve a file path - Development mode is set to "false"', + async (t) => { + const client = new NodeClient({ + path: t.context.fixture, + }); + await client.load(); + + const file = "/some/path/foo.js"; + const resolved = client.file(file); + + t.equal(resolved.value, `${client.server}${client.pathname}${file}`); + t.end(); + }, ); tap.test( - 'Client - Retrieve a file path - Development mode is set to "true" - Base is unset', - async (t) => { - const client = new NodeClient({ - development: true, - path: t.context.fixture, - }); - await client.load(); - - const resolved = client.file('/some/path/foo.js'); - - t.equal(resolved.value, '/some/path/foo.js'); - t.end(); - }, + 'Client - Retrieve a file path - Development mode is set to "true" - Base is unset', + async (t) => { + const client = new NodeClient({ + development: true, + path: t.context.fixture, + }); + await client.load(); + + const resolved = client.file("/some/path/foo.js"); + + t.equal(resolved.value, "/some/path/foo.js"); + t.end(); + }, ); tap.test( - 'Client - Retrieve a file path - Development mode is set to "true" - Base is set to absolute URL', - async (t) => { - const client = new NodeClient({ - development: true, - base: 'http://localhost:7777/prefix/', - path: t.context.fixture, - }); - await client.load(); - - const resolved = client.file('/some/path/foo.js'); - - t.equal( - resolved.value, - 'http://localhost:7777/prefix/some/path/foo.js', - ); - t.end(); - }, + 'Client - Retrieve a file path - Development mode is set to "true" - Base is set to absolute URL', + async (t) => { + const client = new NodeClient({ + development: true, + base: "http://localhost:7777/prefix/", + path: t.context.fixture, + }); + await client.load(); + + const resolved = client.file("/some/path/foo.js"); + + t.equal(resolved.value, "http://localhost:7777/prefix/some/path/foo.js"); + t.end(); + }, ); -tap.test('Client - Load maps', async (t) => { - const client = new NodeClient({ - loadMaps: true, - path: t.context.fixture, - }); - await client.load(); - - const maps = client.maps(); - t.same( - maps, - [ - { imports: { eik: '/src/eik.js' } }, - { imports: { eik: '/src/eik.js' } }, - ], - 'Should return maps', - ); - - t.end(); +tap.test("Client - Load maps", async (t) => { + const client = new NodeClient({ + loadMaps: true, + path: t.context.fixture, + }); + await client.load(); + + const maps = client.maps(); + t.same( + maps, + [{ imports: { eik: "/src/eik.js" } }, { imports: { eik: "/src/eik.js" } }], + "Should return maps", + ); + + t.end(); }); tap.test( - 'Client - Retrieve a base - Development mode is set to "true" - Base is unset', - async (t) => { - const client = new NodeClient({ - development: true, - path: t.context.fixture, - }); - await client.load(); - - const resolved = client.base(); - - t.equal(resolved, '', 'Should be an empty string'); - t.end(); - }, + 'Client - Retrieve a base - Development mode is set to "true" - Base is unset', + async (t) => { + const client = new NodeClient({ + development: true, + path: t.context.fixture, + }); + await client.load(); + + const resolved = client.base(); + + t.equal(resolved, "", "Should be an empty string"); + t.end(); + }, ); tap.test( - 'Client - Retrieve a base - Development mode is set to "true" - Base is set to a relative URL', - async (t) => { - const client = new NodeClient({ - development: true, - base: '/prefix', - path: t.context.fixture, - }); - await client.load(); - - const resolved = client.base(); - - t.equal(resolved, '/prefix'); - t.end(); - }, + 'Client - Retrieve a base - Development mode is set to "true" - Base is set to a relative URL', + async (t) => { + const client = new NodeClient({ + development: true, + base: "/prefix", + path: t.context.fixture, + }); + await client.load(); + + const resolved = client.base(); + + t.equal(resolved, "/prefix"); + t.end(); + }, ); tap.test( - 'Client - Retrieve a base - Development mode is set to "true" - Base is set to a absolute URL', - async (t) => { - const client = new NodeClient({ - development: true, - base: 'http://localhost:7777/prefix/some/path/', - path: t.context.fixture, - }); - await client.load(); - - const resolved = client.base(); - - t.equal(resolved, 'http://localhost:7777/prefix/some/path'); - t.end(); - }, + 'Client - Retrieve a base - Development mode is set to "true" - Base is set to a absolute URL', + async (t) => { + const client = new NodeClient({ + development: true, + base: "http://localhost:7777/prefix/some/path/", + path: t.context.fixture, + }); + await client.load(); + + const resolved = client.base(); + + t.equal(resolved, "http://localhost:7777/prefix/some/path"); + t.end(); + }, ); tap.test( - 'Client - Retrieve a base - Development mode is set to "false"', - async (t) => { - const client = new NodeClient({ - path: t.context.fixture, - }); - await client.load(); - - const resolved = client.base(); - - t.equal(resolved, `${t.context.address}/pkg/eik-fixture/1.0.2`); - t.end(); - }, + 'Client - Retrieve a base - Development mode is set to "false"', + async (t) => { + const client = new NodeClient({ + path: t.context.fixture, + }); + await client.load(); + + const resolved = client.base(); + + t.equal(resolved, `${t.context.address}/pkg/eik-fixture/1.0.2`); + t.end(); + }, ); From a60821cb0259e2b43e505ebc7204f0e0f861fd97 Mon Sep 17 00:00:00 2001 From: William Killerud Date: Tue, 6 Aug 2024 15:11:12 +0200 Subject: [PATCH 04/10] docs: jsdocs with examples --- src/asset.js | 46 +++++++++++++++++++++++++++++- src/index.js | 71 ++++++++++++++++++++++++++++++++++++++++++++-- test/asset.test.js | 56 ++++++++++++++++++------------------ 3 files changed, 141 insertions(+), 32 deletions(-) diff --git a/src/asset.js b/src/asset.js index da228ba..ee191ee 100644 --- a/src/asset.js +++ b/src/asset.js @@ -1,4 +1,48 @@ -export default class Asset { +/** + * @typedef {object} AssetOptions + * @property {string} [value=""] + */ + +/** + * Holds attributes for use when linking to assets hosted on Eik. + * + * @example + * ``` + * // JS and `; + * ``` + * @example + * ``` + * // CSS and + * const styles = eik.file("/styles.css"); + * const html = ``; + * ``` + */ +export class Asset { + /** + * Value for use in [subresource integrity](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity#examples). + * Not calculated if `development` is `true`. + * + * @type {string | undefined} + */ + integrity = undefined; + + /** + * URL to the file for use in ` + * `); + * }); + * + * app.listen({ + * port: 3000, + * }); + * + * console.log("Listening on http://localhost:3000"); + * ``` + */ export default class NodeClient { #development; #loadMaps; @@ -93,10 +150,18 @@ export default class NodeClient { return `${this.server}${this.pathname}`; } - file(file = "") { + /** + * Get a link to a file that is published on Eik when running in production. + * When `development` is true, the pathname is prefixed with the `base` option. + * You can use this feature to serve a local version when developing. + * + * @param {string} pathname pathname to the file relative to the root 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) + * @returns {import('./asset.js').Asset} + */ + file(pathname = "") { const base = this.base(); return new Asset({ - value: `${base}${file}`, + value: `${base}${pathname}`, }); } diff --git a/test/asset.test.js b/test/asset.test.js index fc5e4f8..ee2df55 100644 --- a/test/asset.test.js +++ b/test/asset.test.js @@ -1,44 +1,44 @@ import tap from "tap"; -import Asset from "../src/asset.js"; +import { Asset } from "../src/asset.js"; tap.test("Asset - Default values", (t) => { - const asset = new Asset(); - t.equal(asset.integrity, undefined, 'should be "undefined"'); - t.equal(asset.value, "", "should be empty string"); - t.end(); + const asset = new Asset(); + t.equal(asset.integrity, undefined, 'should be "undefined"'); + t.equal(asset.value, "", "should be empty string"); + t.end(); }); tap.test("Asset - Set values through constructor", (t) => { - const asset = new Asset({ - value: "foo", - }); - t.equal(asset.value, "foo", "should have set value"); - t.end(); + const asset = new Asset({ + value: "foo", + }); + t.equal(asset.value, "foo", "should have set value"); + t.end(); }); tap.test("Asset - Set values through properties", (t) => { - const asset = new Asset(); - asset.integrity = "bar"; - asset.value = "foo"; - t.equal(asset.integrity, "bar", "should have set value"); - t.equal(asset.value, "foo", "should have set value"); - t.end(); + const asset = new Asset(); + asset.integrity = "bar"; + asset.value = "foo"; + t.equal(asset.integrity, "bar", "should have set value"); + t.equal(asset.value, "foo", "should have set value"); + t.end(); }); tap.test("Asset - Stringify object with default values", (t) => { - const asset = new Asset(); - const obj = JSON.parse(JSON.stringify(asset)); - t.equal(obj.integrity, undefined, 'should be "undefined"'); - t.equal(obj.value, "", "should be empty string"); - t.end(); + const asset = new Asset(); + const obj = JSON.parse(JSON.stringify(asset)); + t.equal(obj.integrity, undefined, 'should be "undefined"'); + t.equal(obj.value, "", "should be empty string"); + t.end(); }); tap.test("Asset - Stringify object with set values", (t) => { - const asset = new Asset(); - asset.integrity = "bar"; - asset.value = "foo"; - const obj = JSON.parse(JSON.stringify(asset)); - t.equal(obj.integrity, "bar", "should have set value"); - t.equal(obj.value, "foo", "should have set value"); - t.end(); + const asset = new Asset(); + asset.integrity = "bar"; + asset.value = "foo"; + const obj = JSON.parse(JSON.stringify(asset)); + t.equal(obj.integrity, "bar", "should have set value"); + t.equal(obj.value, "foo", "should have set value"); + t.end(); }); From 6489f2eb1dacf0bbf728a5726035803d10f58844 Mon Sep 17 00:00:00 2001 From: William Killerud Date: Tue, 6 Aug 2024 16:15:05 +0200 Subject: [PATCH 05/10] docs: update jsdoc and readme examples --- README.md | 284 ++++++++++++++++++++++++++++----------------- package.json | 3 +- src/index.js | 135 ++++++++++++++++++++- test/asset.test.js | 56 ++++----- test/index.test.js | 45 +++++-- 5 files changed, 369 insertions(+), 154 deletions(-) diff --git a/README.md b/README.md index 4f86ef6..ddb6391 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ This is an Eik utility for servers running on Node. With it you can: -- generate different URLs to assets on an Eik server depending on environment (development vs production). -- get the import maps you have configured in `eik.json` from the Eik server, should you want to use them in the HTML response. +- generate different URLs to assets on an Eik server depending on environment (development vs production). +- get the import maps you have configured in `eik.json` from the Eik server, should you want to use them in the HTML response. ## Install @@ -13,184 +13,252 @@ npm install @eik/node-client ## Usage -The most common use case for this module is linking to a file. When developing you typically want to use a local version of the file, then link to the published version on Eik when running in production. +The most common use case for this module is linking to a file. -```js -import Eik from '@eik/node-client'; +When developing you typically want to use a local version of the file, then link to the published version on Eik when running in production. -const client = new Eik({ - development: false, - base: '/public', +```js +// Serve a local version of a file from `./public` +// in development and from Eik in production +import path from "node:path"; +import Eik from "@eik/node-client"; +import fastifyStatic from "@fastify/static"; +import fastify from "fastify"; + +const app = fastify(); +app.register(fastifyStatic, { + root: path.join(process.cwd(), "public"), + prefix: "/public/", }); -await client.load(); - -const scriptPath = client.file('/a-script-file.js'); -``` - -## Description - -This module will load Eik config from either an `eik.json` file or from values set in `package.json` and then use these values to provide absolute URLs to assets on a Eik server. In addition to this it's possible to set a `base` URL which will be used as the "base root" for files when this module is set in development mode. This makes it easy to retrieve absolute URLs to assets on a Eik server when an application is running in production but also get URLs to the same assets when in development. - -In addition this module can also download the import maps defined in Eik config and provide these for inclusion in an application. - -The following will use the information in Eik config and provide an absolute URL to a file on an Eik server: - -```js -import EikNodeClient from '@eik/node-client'; +const eik = new Eik({ + development: process.env.NODE_ENV === "development", + // base is only used when `development` is `true` + base: "/public", +}); -const client = new EikNodeClient({ - development: false, - base: 'http://localhost:8080/public', +// load information from `eik.json` and the Eik server +await eik.load(); + +// when development is true script.value will be /public/script.js +// when development is false script.value will be +// https://{server}/pkg/{name}/{version}/script.js +// where {server}, {name} and {version} are read from eik.json +const script = eik.file("/script.js"); + +app.get("/", (req, reply) => { + reply.type("text/html; charset=utf-8"); + reply.send(` + + + + +`); }); -await client.load(); +app.listen({ + port: 3000, +}); -// Will, for example, output: -// { -// integrity: sha512-zHQjnDpMW7IKVyTpT9cOPT1+xhUSOcbgXj6qHCPSPu1CbQfgwDEsIniXU54zDIN71zqmxLSp3hfIljpt69ok0w== -// value: https://cdn.eik.dev/pkg/mymodue/2.4.1/path/script.js -// } -client.file('/path/script.js'); +console.log("Listening on http://localhost:3000"); ``` -The following is the same as above but in development mode. The output will then be based on the vaule set for `base`: +### Include a ` +`; ``` ## Constructor -Create a new client instance. +Use the default export to create a new instance. + +You must call `load` before using the instance so it can read from `eik.json` and your Eik server. ```js -const client = new EikNodeClient(options); +import Eik from "@eik/node-client"; + +const eik = new Eik(); +await eik.load(); ``` ### options | option | default | type | required | details | | ----------- | --------------- | --------- | -------- | ------------------------------------------------------------------------------------------------ | -| path | `process.cwd()` | `string` | `false` | Path to directory containing an eik.json file or package.json with eik config. | | base | `null` | `string` | `false` | Base root to be used for returned asset files. | | development | `false` | `boolean` | `false` | Set the module in development mode or not. | | loadMaps | `false` | `boolean` | `false` | Specifies whether import maps defined in the config should be loaded from the Eik server or not. | - -#### path - -Path to directory containing a eik.json file or package.json with eik config. - -#### base - -Base root to be used for returned asset files. Can be either an absolute URL or relative URL. Will only be applied when the module is in development mode. - -#### development - -Set the module in development mode or not. - -#### loadMaps - -Whether import maps defined in the config should be loaded from the Eik server or not. The import maps is loaded by calling the `.load()` method and loaded the maps can be retrieved with the `.maps()` method. The import maps will be cached in the module. +| path | `process.cwd()` | `string` | `false` | Path to directory containing an eik.json file or package.json with eik config. | ## API -This module has the following API - ### async .load() -Loads Eik config from the Eik config into the object instance. If `loadMaps` is set to `true` on the constructor, the import maps defined in the config will be loaded from the Eik server. +Reads the Eik config from disk into the object instance. -### .base() +If `loadMaps` was set to `true` the import maps defined in the config will be fetched from the Eik server. -Constructs a URL to the base of a package of assets. The returned value will differ depending on if development mode is set to true or not. +### .file(pathname) -When in non development mode, the returned value will be built up by the values found in the loaded Eik config and provide a URL to where the files can be expected to be on the Eik server. +Get a link to a file that will differ based on environment (development vs production). + +When running in production the returned link will point to the file on Eik. ```js -const client = new EikNodeClient({ - development: false, - base: 'http://localhost:8080/assets', +// in production +const eik = new Eik({ + development: false, }); -await client.load(); +await eik.load(); -client.base(); // https://cdn.eik.dev/pkg/mymodue/2.4.1 +const file = eik.file("/path/to/script.js"); +// { +// value: https://eik.store.com/pkg/my-app/1.0.0/path/to/script.js +// integrity: sha512-zHQjnD-etc. +// } +// where the server URL, app name and version are read from eik.json +// { +// "name": "my-app", +// "version": "1.0.0", +// "server": "https://eik.store.com", +// } ``` -When in development mode, the returned value will be equal to whats set on the `base` argument on the constructor. +When `development` is `true` the pathname is prefixed with the `base` option instead of pointing to Eik. ```js -const client = new EikNodeClient({ - development: true, - base: 'http://localhost:8080/assets', +// in development +const eik = new Eik({ + development: true, + base: "/public", }); -await client.load(); +await eik.load(); -client.base(); // http://localhost:8080/assets +const file = eik.file("/path/to/script.js"); +// { +// value: /public/path/to/script.js +// integrity: undefined +// } ``` -### .file(file) +#### arguments + +| option | default | type | details | +| -------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| pathname | `null` | `string` | Pathname 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`) | -Constructs a full URL to an asset. The URL is built up by appending the value of the `file` argument to a `base` root. The returned value will differ depending on if development mode is set to true or not. +#### returns -When in non development mode, the returned value will be built up by the values found in the loaded Eik config and provide a URL to where the files can be expected to be on the Eik server plus the provided value to the `file` argument on the method. +Returns an object with `value` and `integrity`: ```js -const client = new EikNodeClient({ - development: false, - base: 'http://localhost:8080/assets', -}); -await client.load(); +{ + integrity: 'sha512-zHQjnDpMW7IKVyTpT9cOPT1+xhUSOcbgXj6qHCPSPu1CbQfgwDEsIniXU54zDIN71zqmxLSp3hfIljpt69ok0w==', + value: 'https://eik.store.com/pkg/my-app/1.0.0/path/to/script.js' +} +``` + +`integrity` is `undefined` if `development` is `true`: -client.file('/js/script.js'); // Returns an asset.value like: https://cdn.eik.dev/pkg/mymodue/2.4.1/js/script.js +```js +{ + integrity: undefined, + value: '/public/path/to/script.js' +} ``` -When in development mode, the returned value will be equal to whats set on the `base` argument on the constructor plus the provided value to the `file` argument on the method. +### .maps() + +When `loadMaps` is `true` and you call `load`, the client fetches the configured import maps from the Eik server. + +This method returns the import maps that were fetched during `load`. ```js -const client = new EikNodeClient({ - development: true, - base: 'http://localhost:8080/assets', +const client = new Eik({ + loadMaps: true, }); await client.load(); -client.file('/js/script.js'); // Returns an asset.value like: http://localhost:8080/assets/js/script.js +const maps = client.maps(); +const combined = maps + .map((map) => map.imports) + .reduce((map, acc) => ({ ...acc, ...map }), {}); + +const html = ` + +`; ``` -#### arguments +#### returns + +A list of Eik import maps. + +```json +[ + { + "imports": { + "date-fns": "https://eik.store.com/npm/date-fns/v3/index.js", + "lodash": "https://eik.store.com/npm/lodash/v4/index.js" + } + }, + { + "imports": { + "lit": "https://eik.store.com/npm/lit/v3/index.js" + } + } +] +``` + +### .base() -| option | default | type | required | details | -| ------ | ------- | -------- | -------- | ---------------------------------------------------- | -| file | `null` | `string` | `false` | File to append to the base of a full URL to an asset | +Constructs a URL to the base of a package of assets. The returned value will differ depending on if development mode is set to true or not. -Returns a object with as follow: +When in non development mode, the returned value will be built up by the values found in the loaded Eik config and provide a URL to where the files can be expected to be on the Eik server. ```js -{ - integrity: 'sha512-zHQjnDpMW7IKVyTpT9cOPT1+xhUSOcbgXj6qHCPSPu1CbQfgwDEsIniXU54zDIN71zqmxLSp3hfIljpt69ok0w==', - value: 'https://cdn.eik.dev/pkg/mymodue/2.4.1/path/script.js' -} +const client = new Eik({ + development: false, + base: "http://localhost:8080/assets", +}); +await client.load(); + +client.base(); // https://cdn.eik.dev/pkg/mymodue/2.4.1 ``` -If `integrity` of the file is not available, the value for `integrity` will be `null`. This will be the case when in development mode since integrity is calculated upon publish of a package to a Eik server. +When in development mode, the returned value will be equal to whats set on the `base` argument on the constructor. -### .maps() +```js +const client = new Eik({ + development: true, + base: "http://localhost:8080/assets", +}); +await client.load(); -Returns the import maps defined in Eik config from the Eik server. For the maps to be returned they need to be loaded from the Eik server. This is done by setting the `loadMaps` option on the constructor to `true`. +client.base(); // http://localhost:8080/assets +``` ## License diff --git a/package.json b/package.json index bd50322..cece5e8 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,10 @@ { "name": "@eik/node-client", "version": "1.1.61", - "description": "A utility for generating URLs to assets on an Eik server depending on environment.", + "description": "Utilities for working with assets and import maps on an Eik server", "type": "module", "main": "./dist/index.cjs", + "types": "./types/index.d.ts", "exports": { "types": "./types/index.d.ts", "import": "./src/index.js", diff --git a/src/index.js b/src/index.js index 2cb5d3f..76934bd 100644 --- a/src/index.js +++ b/src/index.js @@ -32,6 +32,19 @@ const fetchImportMaps = async (urls = []) => { } }; +/** + * @typedef {object} Options + * @property {string} [base=null] + * @property {boolean} [development=false] + * @property {boolean} [loadMaps=false] + * @property {string} [path=process.cwd()] + */ + +/** + * @typedef {object} ImportMap + * @property {Record} imports + */ + /** * An Eik utility for servers running on Node. With it you can: * - generate different URLs to assets on an Eik server depending on environment (development vs production). @@ -47,7 +60,8 @@ const fetchImportMaps = async (urls = []) => { * ``` * @example * ```js - * // Serve a local version of a file from `./public` in development and from Eik in production + * // Serve a local version of a file from `./public` + * // in development and from Eik in production * import path from "node:path"; * import Eik from "@eik/node-client"; * import fastifyStatic from "@fastify/static"; @@ -68,7 +82,8 @@ const fetchImportMaps = async (urls = []) => { * await eik.load(); * * // when development is true script.value will be /public/script.js. - * // when development is false script.value will be https://{server}/pkg/{name}/{version}/script.js + * // when development is false script.value will be + * // https://{server}/pkg/{name}/{version}/script.js * // where {server}, {name} and {version} are read from eik.json * const script = eik.file("/script.js"); * @@ -89,7 +104,7 @@ const fetchImportMaps = async (urls = []) => { * console.log("Listening on http://localhost:3000"); * ``` */ -export default class NodeClient { +export default class Eik { #development; #loadMaps; #config; @@ -97,6 +112,9 @@ export default class NodeClient { #base; #maps; + /** + * @param {Options} options + */ constructor({ development = false, loadMaps = false, @@ -111,6 +129,13 @@ export default class NodeClient { this.#maps = []; } + /** + * Reads the Eik config from disk into the object instance, used for building {@link file} links in production. + * + * If {@link Options.loadMaps} is `true` the import maps + * defined in the Eik config will be fetched from the Eik server for + * use in {@link maps}. + */ async load() { this.#config = await helpers.getDefaults(this.#path); if (this.#loadMaps) { @@ -118,33 +143,64 @@ export default class NodeClient { } } + /** + * The `"name"` field from the Eik config + * @throws if read before calling {@link load} + */ get name() { if (this.#config.name) return this.#config.name; throw new Error("Eik config was not loaded before calling .name"); } + /** + * The `"version"` field from the Eik config + * @throws if read before calling {@link load} + */ get version() { if (this.#config.version) return this.#config.version; throw new Error("Eik config was not loaded before calling .version"); } + /** + * The `"type"` field from the Eik config mapped to its URL equivalent (eg. "package" is "pkg"). + * @throws if read before calling {@link load} + */ get type() { if (this.#config.type && this.#config.type === "package") return "pkg"; if (this.#config.type) return this.#config.type; throw new Error("Eik config was not loaded before calling .type"); } + /** + * The `"server"` field from the Eik config + * @throws if read before calling {@link load} + */ get server() { if (this.#config.server) return this.#config.server; throw new Error("Eik config was not loaded before calling .server"); } + /** + * The pathname to the base on Eik (ex. /pkg/my-app/1.0.0/) + * @throws if read before calling {@link load} + */ get pathname() { if (this.#config.type && this.#config.name && this.#config.version) return join("/", this.type, this.name, this.version); throw new Error("Eik config was not loaded before calling .pathname"); } + /** + * Similar to {@link file}, this method returns a path to the base on Eik + * (ex. https://eik.store.com/pkg/my-app/1.0.0), or {@link Options.base} + * if {@link Options.development} is true. + * + * You can use this instead of `file` if you have a directory full of files + * and you don't need {@link Asset.integrity}. + * + * @returns {string} The base path for assets published on Eik + * @throws when {@link Options.development} is false if called before calling {@link load} + */ base() { if (this.#development) return this.#base; return `${this.server}${this.pathname}`; @@ -152,11 +208,48 @@ export default class NodeClient { /** * Get a link to a file that is published on Eik when running in production. - * When `development` is true, the pathname is prefixed with the `base` option. - * You can use this feature to serve a local version when developing. + * When {@link Options.development} is `true` the pathname is prefixed + * with the {@link Options.base} option instead of pointing to Eik. * - * @param {string} pathname pathname to the file relative to the root 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) + * @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) * @returns {import('./asset.js').Asset} + * @throws when {@link Options.development} is false if called before calling {@link load} + * + * @example + * ```js + * // in production + * const eik = new Eik({ + * development: false, + * }); + * await eik.load(); + * + * const file = eik.file("/path/to/script.js"); + * // { + * // value: https://eik.store.com/pkg/my-app/1.0.0/path/to/script.js + * // integrity: sha512-zHQjnD-etc. + * // } + * // where the server URL, app name and version are read from eik.json + * // { + * // "name": "my-app", + * // "version": "1.0.0", + * // "server": "https://eik.store.com", + * // } + * ``` + * @example + * ```js + * // in development + * const eik = new Eik({ + * development: true, + * base: "/public", + * }); + * await eik.load(); + * + * const file = eik.file("/path/to/script.js"); + * // { + * // value: /public/path/to/script.js + * // integrity: undefined + * // } + * ``` */ file(pathname = "") { const base = this.base(); @@ -165,6 +258,36 @@ export default class NodeClient { }); } + /** + * When {@link Options.loadMaps} is `true` and you call {@link load}, the client + * fetches the configured import maps from the Eik server. + * + * This method returns the import maps that were fetched during `load`. + * + * @returns {ImportMap[]} + * @throws if {@link Options.loadMaps} is not `true` or called before calling {@link load} + * + * @example + * ```js + * // generate a + * `; + * ``` + */ maps() { if (this.#config.version && this.#loadMaps) return this.#maps; throw new Error( diff --git a/test/asset.test.js b/test/asset.test.js index ee2df55..b0858b3 100644 --- a/test/asset.test.js +++ b/test/asset.test.js @@ -1,44 +1,46 @@ +/// + import tap from "tap"; import { Asset } from "../src/asset.js"; tap.test("Asset - Default values", (t) => { - const asset = new Asset(); - t.equal(asset.integrity, undefined, 'should be "undefined"'); - t.equal(asset.value, "", "should be empty string"); - t.end(); + const asset = new Asset(); + t.equal(asset.integrity, undefined, 'should be "undefined"'); + t.equal(asset.value, "", "should be empty string"); + t.end(); }); tap.test("Asset - Set values through constructor", (t) => { - const asset = new Asset({ - value: "foo", - }); - t.equal(asset.value, "foo", "should have set value"); - t.end(); + const asset = new Asset({ + value: "foo", + }); + t.equal(asset.value, "foo", "should have set value"); + t.end(); }); tap.test("Asset - Set values through properties", (t) => { - const asset = new Asset(); - asset.integrity = "bar"; - asset.value = "foo"; - t.equal(asset.integrity, "bar", "should have set value"); - t.equal(asset.value, "foo", "should have set value"); - t.end(); + const asset = new Asset(); + asset.integrity = "bar"; + asset.value = "foo"; + t.equal(asset.integrity, "bar", "should have set value"); + t.equal(asset.value, "foo", "should have set value"); + t.end(); }); tap.test("Asset - Stringify object with default values", (t) => { - const asset = new Asset(); - const obj = JSON.parse(JSON.stringify(asset)); - t.equal(obj.integrity, undefined, 'should be "undefined"'); - t.equal(obj.value, "", "should be empty string"); - t.end(); + const asset = new Asset(); + const obj = JSON.parse(JSON.stringify(asset)); + t.equal(obj.integrity, undefined, 'should be "undefined"'); + t.equal(obj.value, "", "should be empty string"); + t.end(); }); tap.test("Asset - Stringify object with set values", (t) => { - const asset = new Asset(); - asset.integrity = "bar"; - asset.value = "foo"; - const obj = JSON.parse(JSON.stringify(asset)); - t.equal(obj.integrity, "bar", "should have set value"); - t.equal(obj.value, "foo", "should have set value"); - t.end(); + const asset = new Asset(); + asset.integrity = "bar"; + asset.value = "foo"; + const obj = JSON.parse(JSON.stringify(asset)); + t.equal(obj.integrity, "bar", "should have set value"); + t.equal(obj.value, "foo", "should have set value"); + t.end(); }); diff --git a/test/index.test.js b/test/index.test.js index 0beeb9a..278696c 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -1,3 +1,5 @@ +/// + import { mkdtemp, writeFile } from "fs/promises"; import { helpers } from "@eik/common"; import path from "path"; @@ -5,7 +7,7 @@ import http from "http"; import tap from "tap"; import os from "os"; -import NodeClient from "../src/index.js"; +import Eik from "../src/index.js"; const FIXTURE_PATH = `${process.cwd()}/fixtures`; const FIXTURE_FILE = await helpers.getDefaults(FIXTURE_PATH); @@ -84,7 +86,7 @@ tap.afterEach(async (t) => { }); tap.test("Client - Default settings - Config is not loaded", (t) => { - const client = new NodeClient(); + const client = new Eik(); t.throws( () => { @@ -144,7 +146,7 @@ tap.test("Client - Default settings - Config is not loaded", (t) => { }); tap.test("Client - Default settings - Config is loaded", async (t) => { - const client = new NodeClient({ + const client = new Eik({ path: t.context.fixture, }); await client.load(); @@ -168,7 +170,7 @@ tap.test("Client - Default settings - Config is loaded", async (t) => { tap.test( 'Client - Default settings - Config is loaded and development mode is set to "true"', async (t) => { - const client = new NodeClient({ + const client = new Eik({ development: true, path: t.context.fixture, }); @@ -194,7 +196,7 @@ tap.test( tap.test( 'Client - Retrieve a file path - Development mode is set to "false"', async (t) => { - const client = new NodeClient({ + const client = new Eik({ path: t.context.fixture, }); await client.load(); @@ -210,7 +212,7 @@ tap.test( tap.test( 'Client - Retrieve a file path - Development mode is set to "true" - Base is unset', async (t) => { - const client = new NodeClient({ + const client = new Eik({ development: true, path: t.context.fixture, }); @@ -226,7 +228,7 @@ tap.test( tap.test( 'Client - Retrieve a file path - Development mode is set to "true" - Base is set to absolute URL', async (t) => { - const client = new NodeClient({ + const client = new Eik({ development: true, base: "http://localhost:7777/prefix/", path: t.context.fixture, @@ -241,7 +243,7 @@ tap.test( ); tap.test("Client - Load maps", async (t) => { - const client = new NodeClient({ + const client = new Eik({ loadMaps: true, path: t.context.fixture, }); @@ -254,13 +256,32 @@ tap.test("Client - Load maps", async (t) => { "Should return maps", ); + const combined = maps + .map((map) => map.imports) + .reduce((map, acc) => ({ ...acc, ...map }), {}); + + t.same(combined, { eik: "/src/eik.js" }); + + const html = ``; + + t.same( + html, + ``, + ); + t.end(); }); tap.test( 'Client - Retrieve a base - Development mode is set to "true" - Base is unset', async (t) => { - const client = new NodeClient({ + const client = new Eik({ development: true, path: t.context.fixture, }); @@ -276,7 +297,7 @@ tap.test( tap.test( 'Client - Retrieve a base - Development mode is set to "true" - Base is set to a relative URL', async (t) => { - const client = new NodeClient({ + const client = new Eik({ development: true, base: "/prefix", path: t.context.fixture, @@ -293,7 +314,7 @@ tap.test( tap.test( 'Client - Retrieve a base - Development mode is set to "true" - Base is set to a absolute URL', async (t) => { - const client = new NodeClient({ + const client = new Eik({ development: true, base: "http://localhost:7777/prefix/some/path/", path: t.context.fixture, @@ -310,7 +331,7 @@ tap.test( tap.test( 'Client - Retrieve a base - Development mode is set to "false"', async (t) => { - const client = new NodeClient({ + const client = new Eik({ path: t.context.fixture, }); await client.load(); From 15aa14d140cab7934d93f770d617a6cd4821561c Mon Sep 17 00:00:00 2001 From: William Killerud Date: Tue, 6 Aug 2024 16:17:13 +0200 Subject: [PATCH 06/10] chore: run types script on CI --- .github/workflows/publish.yml | 2 ++ .github/workflows/test.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0083ece..632776b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -32,6 +32,8 @@ jobs: - run: npm test + - run: npm run types + - run: npx semantic-release env: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5462b3a..bba87f8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,3 +30,5 @@ jobs: - run: npm run build - run: npm test + + - run: npm run types From 2354157eab1b918677a403200938335cc285dc3d Mon Sep 17 00:00:00 2001 From: William Killerud Date: Tue, 6 Aug 2024 16:18:40 +0200 Subject: [PATCH 07/10] chore: update devDependencies --- package.json | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index cece5e8..f47c908 100644 --- a/package.json +++ b/package.json @@ -33,8 +33,7 @@ "url": "git@github.com:eik-lib/node-client.git" }, "keywords": [ - "eik", - "esm" + "eik" ], "author": "Finn.no", "license": "MIT", @@ -52,14 +51,12 @@ "@eik/prettier-config": "1.0.1", "@eik/semantic-release-config": "1.0.0", "@eik/typescript-config": "1.0.0", - "@semantic-release/changelog": "6.0.3", - "@semantic-release/git": "10.0.1", "eslint": "9.8.0", "npm-run-all": "4.1.5", - "prettier": "3.3.2", - "rollup": "4.18.0", - "semantic-release": "23.1.1", - "tap": "18.7.2", + "prettier": "3.3.3", + "rollup": "4.20.0", + "semantic-release": "24.0.0", + "tap": "21.0.0", "typescript": "5.5.4" } } From 7b22693684290e98d9cb60bbbe26ce845cabcd11 Mon Sep 17 00:00:00 2001 From: William Killerud Date: Tue, 6 Aug 2024 16:22:59 +0200 Subject: [PATCH 08/10] chore: update eslint config to ignore dist --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f47c908..abc1141 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "undici": "5.28.4" }, "devDependencies": { - "@eik/eslint-config": "1.0.1", + "@eik/eslint-config": "1.0.2", "@eik/prettier-config": "1.0.1", "@eik/semantic-release-config": "1.0.0", "@eik/typescript-config": "1.0.0", From d2c87460094704f55859e2187f7de5963f98faef Mon Sep 17 00:00:00 2001 From: William Killerud Date: Wed, 7 Aug 2024 08:44:21 +0200 Subject: [PATCH 09/10] docs: review feedback --- README.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ddb6391..49907c6 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,6 @@ # @eik/node-client -This is an Eik utility for servers running on Node. With it you can: - -- generate different URLs to assets on an Eik server depending on environment (development vs production). -- get the import maps you have configured in `eik.json` from the Eik server, should you want to use them in the HTML response. +This is a utility for getting assets and import maps from [Eik servers](https://github.com/eik-lib/service#readme) in Node web applications. For publishing and managing assets to an Eik server from Node scripts, see [`@eik/cli`](https://github.com/eik-lib/cli#readme). ## Install @@ -13,9 +10,11 @@ npm install @eik/node-client ## Usage -The most common use case for this module is linking to a file. +The most common use case for this module is linking to a file. When developing you typically want to use a local version of the file, then link to the published version on Eik when running in production. + +For that you use the [`file()` method](#filepathname), which returns an object `{ value, integrity }` where `value` is the link to the file. -When developing you typically want to use a local version of the file, then link to the published version on Eik when running in production. +When running in production the link will point to the file on Eik. When `development` is `true` the pathname is prefixed with the `base` option instead of pointing to Eik, so your app can use a local version. ```js // Serve a local version of a file from `./public` @@ -26,6 +25,8 @@ import fastifyStatic from "@fastify/static"; import fastify from "fastify"; const app = fastify(); + +// Serve the contents of the ./public folder on the path /public app.register(fastifyStatic, { root: path.join(process.cwd(), "public"), prefix: "/public/", From 49fd3a7ac8266c516f53f2a971d81bc4fb691a64 Mon Sep 17 00:00:00 2001 From: William Killerud Date: Wed, 7 Aug 2024 08:49:52 +0200 Subject: [PATCH 10/10] docs: fix example for import map so it uses correct syntax --- README.md | 8 ++------ test/index.test.js | 10 +++++----- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 49907c6..8c1aab7 100644 --- a/README.md +++ b/README.md @@ -79,9 +79,7 @@ const client = new Eik({ await client.load(); const maps = client.maps(); -const combined = maps - .map((map) => map.imports) - .reduce((map, acc) => ({ ...acc, ...map }), {}); +const combined = maps.reduce((map, acc) => ({ ...acc, ...map }), {}); const html = ` `, );