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/.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 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..8c1aab7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # @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 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 @@ -8,184 +8,254 @@ The Eik Node.js client facilitates loading Eik config and providing URLs to asse npm install @eik/node-client ``` -## Basic usage +## Usage -```js -import EikNodeClient from '@eik/node-client'; - -const client = new EikNodeClient({ - development: false, - base: '/public' -}); - -await client.load(); +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. -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. +For that you use the [`file()` method](#filepathname), which returns an object `{ value, integrity }` where `value` is the link to the file. -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: +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 -import EikNodeClient from '@eik/node-client'; +// 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(); + +// Serve the contents of the ./public folder on the path /public +app.register(fastifyStatic, { + root: path.join(process.cwd(), "public"), + prefix: "/public/", +}); -const client = new EikNodeClient({ - development: false, - base: 'http://localhost:8080/public' +const eik = new Eik({ + development: process.env.NODE_ENV === "development", + // base is only used when `development` is `true` + base: "/public", }); -await client.load(); +// 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(` + +
+ + +`); +}); -// 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') +app.listen({ + port: 3000, +}); + +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. | +| option | default | type | required | details | +| ----------- | --------------- | --------- | -------- | ------------------------------------------------------------------------------------------------ | +| 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.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/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..b940db0 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,3 @@ +import config from "@eik/eslint-config"; + +export default config; 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 +} diff --git a/package.json b/package.json index c69eb6c..abc1141 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,12 @@ { "name": "@eik/node-client", "version": "1.1.61", - "description": "A node.js client for interacting with a Eik server.", + "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", "require": "./dist/index.cjs" }, @@ -13,22 +15,25 @@ "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", "url": "git@github.com:eik-lib/node-client.git" }, "keywords": [ - "eik", - "esm" + "eik" ], "author": "Finn.no", "license": "MIT", @@ -42,18 +47,16 @@ "undici": "5.28.4" }, "devDependencies": { - "@babel/eslint-parser": "7.24.7", - "@eik/semantic-release-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", - "prettier": "3.3.2", - "rollup": "4.18.0", - "semantic-release": "^23.1.1", - "tap": "18.7.2" + "@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", + "eslint": "9.8.0", + "npm-run-all": "4.1.5", + "prettier": "3.3.3", + "rollup": "4.20.0", + "semantic-release": "24.0.0", + "tap": "21.0.0", + "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..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..ee191ee 100644 --- a/src/asset.js +++ b/src/asset.js @@ -1,8 +1,50 @@ -export default class Asset { - constructor({ - value = '', - } = {}) { - this.integrity = undefined; - this.value = value; - } +/** + * @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 ` + *