diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..6313b56c --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.github/workflows/deploy-doc.yml b/.github/workflows/deploy-doc.yml deleted file mode 100644 index 5760df13..00000000 --- a/.github/workflows/deploy-doc.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Deploy documentation - -on: - push: - branches: - - source - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: install node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - name: npm install - run: npm install - - name: npm run deploy - run: | - git config --global user.email "trygve-lie@users.noreply.github.com" - git config --global user.name "trygve-lie" - echo "machine github.com login trygve-lie password ${{ secrets.GIT_TOKEN }}" > ~/.netrc - npm run deploy - env: - CI: true - GIT_USER: trygve-lie - DEPLOYMENT_BRANCH: main diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000..b30118a6 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,31 @@ +name: Publish + +on: + push: + branches: + - source + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - run: npm install + + - run: npm run lint + + - name: npm run deploy + run: | + git config --global user.email "trygve-lie@users.noreply.github.com" + git config --global user.name "trygve-lie" + echo "machine github.com login trygve-lie password ${{ secrets.GIT_TOKEN }}" > ~/.netrc + npm run deploy + env: + CI: true + GIT_USER: trygve-lie + DEPLOYMENT_BRANCH: main diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..244b1cbe --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,22 @@ +name: Test + +on: + pull_request: + branches: + - source + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - run: npm install + + - run: npm run lint + + - run: npm run build diff --git a/.npmrc b/.npmrc index 43c97e71..0ca8d2a0 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,2 @@ package-lock=false +save-exact=true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..0f7a615b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,7 @@ +# Contributing + +Thank you for showing an interest in contributing to Eik 🧡 + +If you change the URL of a page, please add a redirect rule to `docusaurus.config.js`. + +PRs should target the `source` branch. The `main` branch is where the built website is deployed from. diff --git a/README.md b/README.md index b9a485c8..ba4b2b3a 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,18 @@ -# Website +# eik.dev -This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator. +This repo holds the code and docs for the documentation website at [eik.dev](https://eik.dev/). +It is built using [Docusaurus](https://docusaurus.io/). -### Installation +## Commands -``` -npm install -``` +All commands are run from the root of the project. -### Local Development - -``` -npm start -``` - -This command starts a local development server. Most changes are reflected live without having to restart the server. - -### Build - -``` -npm run build -``` - -This command generates static content into the `build` directory and can be served using any static contents hosting service. - -### Deployment - -Handled by GitHub Actions on merge to the `source` branch. +| Command | Action | +| :----------------- | :-------------------------------------------------- | +| `npm install` | Installs dependencies | +| `npm run dev` | Starts local dev server at `localhost:8080` | +| `npm run build` | Build your production site to `./build/` | +| `npm start` | Preview your build locally at `localhost:8080` | +| `npm run deploy` | Run by GitHub Actions to deploy to GitHub Pages | +| `npm run lint` | Run ESLint + Prettier | +| `npm run lint:fix` | Fix ESLint and Prettier issues that are autofixable | diff --git a/docs/ci.md b/docs/ci.md deleted file mode 100644 index 15541ab1..00000000 --- a/docs/ci.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -id: ci -title: CI Overview -sidebar_label: Overview ---- - -While it is fully possible (and in many cases perfectly acceptable) to simply publish new versions of your assets manually as needed, you may find yourself preferring an automated approach. - -To do so, Eik provides a version command that can be used to automate versioning. When running the version command a new version number will be generated in `eik.json` if and only if the assets to be published have changed since the last publish. This makes it safe to run on every CI build. - -The process for publishing new asset versions from CI is as follows: - -1. Login to the Eik server -1. Run the `eik version` command -2. Run the `eik package` command -3. Commit new version change to `eik.json` -4. Push the change back to the repository - -For your specific C.I. platform, see the related guide. - -* [Travis CI](./travis.md) \ No newline at end of file diff --git a/docs/client.md b/docs/client.md deleted file mode 100644 index 4bf6a22e..00000000 --- a/docs/client.md +++ /dev/null @@ -1,529 +0,0 @@ ---- -id: client -title: Publishing to Eik -sidebar_label: Publishing to Eik ---- - -## Install the Eik CLI - -See [Installation](./client_installation.md) for how to set up the CLI you will need for publishing. - -## Configure Eik - -Generate an `eik.json` file in the current directory: - -```sh -eik init -``` - -Set the URL to your Eik server as the `server` property. See [the server docs](/docs/server) if you need to set up a server. - -### Configure which files to publish - -Set the `files` property of `eik.json` with paths to client side -asset files or directories in your project relative to the `eik.json` file. - -```json -{ - "name": "my-app", - "version": "1.0.0", - "server": "https://assets.myserver.com", - "files": "./public" -} -``` - -## Publish - -Run publish to publish your assets to the server - -```sh -eik publish -``` - -## Next steps - -### Publishing global dependencies - -When you wish to share a version of a module, you can use the `dependency` command to do so. - -This feature does the following: - -- converts a module already published to npm to esm -- makes it available through the asset server - -#### Example use case - -You might decide that all teams across your organisation should use the same version of lodash via a published URL (rather than each team bundling their own version). - -To do so you would run: - -```sh -eik dependency lodash 4.17.15 -``` - -After running this, an esm friendly version of lodash will be available at the url: -`http:///pkg/lodash/4.17.15` - -It's now possible for each team to reference this globally published module directly in their -own client side code as follows: - -```js -import lodash from `http:///pkg/lodash/4.17.15`; -``` - -This has the benefit that if all teams are referencing lodash in this way, the browser will cache the module the first time it encounters it and on subsequent pages will not need to download it again. - -### Aliasing published modules - -Aliasing allows you to tag specific published versions of modules with a more general tag or version that you are also able to centrally change as needed. - -The benefit of this is that you can alias a specific version of a dependency and then update that alias overtime as you publish new versions of the dependency and have all dependents immediately receive the change. - -#### Example use case - -Taking the previous example 1 step further, before we saw that we could globally publish a specific version of lodash, in this case `4.17.15`. - -We can now set a major semver alias for this version: - -```sh -eik alias lodash 4.15.15 4 -``` - -We can now change our import statement to: - -```js -import lodash from `http:///pkg/lodash/v4`; -``` - -and everything will work as before. - -When a new version of lodash comes out, we can create a global dependency for it as before: - -```sh -eik dependency lodash 4.17.16 -``` - -And then update the major semver alias to the new version like so: - -```sh -eik alias lodash 4.15.16 4 -``` - -In this way, no client side code will need to be updated to reflect this change and it is considerably easier for multiple teams to stay in sync, using the same global shared dependency - -### Using import maps to map "bare imports" - -Import maps are [an emerging standard](https://github.com/WICG/import-maps) and a way to map "bare imports" such as `foo` in the import statement `import { bar, baz } from 'foo'` to modules to be loaded. With Eik, we provide a way to upload import map files and to specify them for use in bundling. Doing so allows you to specify a common set of shared modules, whether they be `react` or `lit-html` etc. - -Making use of import maps is done as follows. - -1. Define an import map json file -2. Use the Eik CLI to upload the import map to the server -3. Specify the URL to your import map file(s) in your `eik.json` file -4. Use the `publish` commands, your import maps will be used to map bare imports in your code to the URLs you have defined in your import maps - -#### Import maps, an example - -Given the following import map file `import-map.json` - -```json -{ - "imports": { - "lit-html": "http://assets.examplecdn.com/pkg/lit-html/v1/index.js", - "lodash": "http://assets.examplecdn.com/pkg/lodash/v4/index.js" - } -} -``` - -The following command will upload the import map file `./import-map.json` in the current directory using the name `my-import-map` and the version `1.0.0` - -```sh -eik map my-import-map 1.0.0 ./import-map.json -``` - -Given the following line now added to `eik.json` - -```json -{ - "import-map": ["http://assets.examplecdn.com/map/my-import-map/1.0.0"] -} -``` - -When we run `eik publish` any "bare imports" refering to either `lit-html` or `lodash` will be mapped to the URLs in our map. - -In this way, you can control which version of `react` or `lit-html` or `lodash` all your apps are using. In combination with package `alias` URLs, you have a powerful way to manage key shared dependencies for your apps in production without the need to redeploy or rebundle when a new version of a dependency is released. - -### Accessing meta information about a package - -It's possible to access information about a published package with the `meta` command. The command -returns information in JSON format. - -#### Example - -```sh -eik meta lodash 4.17.16 -``` - -## API Documentation - -### Command Summary - -| command | aliases | description | -| ---------- | ------- | ----------------------------------------------------------- | -| init | i | Create an eik.json file in the current directory | -| login | | Authenticates client with eik server | -| ping | | Pings eik server | -| publish | p, pub | Publish an app bundle | -| dependency | d, dep | Publish a dependency bundle | -| map | m | Sets or deletes a "bare" import entry in an import-map file | -| alias | a | Sets a major semver alias for a given dependency or map | -| meta | show | Retrieves meta information for a package | - -### Commands Overview - -#### init - -This command takes no input and creates a new `eik.json` file in the current directory with the following content: - -```json -{ - "name": "", - "server": "", - "js": { - "input": "", - "options": {} - }, - "css": { - "input": "", - "options": {} - }, - "import-map": [] -} -``` - -You will then need to set the various fields as appropriate. If you are running a local asset server, the default server url should be `http://localhost:8080`. - -##### eik.json properties - -| property | description | -| ---------- | ------------------------------------------------------------------ | -| name | App name, must be unique to the Eik server | -| server | Address to the asset server | -| js | Configuration for JavaScript assets | -| css | Configuration for CSS assets | -| import-map | Specify import maps to be used to map bare imports during bundling | - -###### name - -All asset uploads must have a name. When publishing a dependency from npm the name will be the package name taken from the module's `package.json` file. When publishing the assets for your app, the `name` field of your project's `eik.json` file is used. -Names may contain any letters or numbers as well as the `-` and `_` characters. - -```json -{ - "name": "my-awesome-app" -} -``` - -###### server - -This is the address to the asset server you are using. This might be a locally running version of the asset server (usually `http://assets.examplecdn.com`) or an asset server running in production (TBD) - -```json -{ - "server": "http://assets.examplecdn.com" -} -``` - -###### js - -This field is used to configure bundling and publishing of JavaScript assets. Use `js.input` to configure the location on disk, relative to `eik.json`, where the entrypoint for your JavaScript client side assets are located. - -_scripts.js file inside assets folder_ - -```json -{ - "js": { - "input": "./assets/scripts.js" - } -} -``` - -###### css - -This field is used to configure bundling and publishing of CSS assets. Use `css.input` to configure the location on disk, relative to `eik.json`, where the entrypoint for your CSS client side assets are located. - -_styles.css file inside assets folder_ - -```json -{ - "css": { - "input": "./assets/styles.css" - } -} -``` - -###### import-map - -This field is used to configure the location of any import map files to be used when creating bundles. The field should be an array and can hold any number of url strings pointing to locations of import-map files that will be downloaded and merged together - -_defining a single import map file_ - -```json -{ - "import-map": ["http://assets.examplecdn.com/map/my-import-map/1.0.0"] -} -``` - -#### login - -Authenticate with the configured Eik server. The `server` field in `eik.json` will be used to determine which server to authenticate with. It is also possible to set the server without the need for an `eik.json` -file using the command line flag `--server` or `-s` - -The command takes the form: - -```sh -eik login [optional arguments] -``` - -**Example** - -_Authenticate with Eik server using a prompt_ - -```bash -eik login -``` - -_Authenticate with Eik server using a given key_ - -```bash -eik login --key some_key -``` - -#### ping - -Ping the configured Eik server. - -**Example** - -_Ping Eik server_ - -```sh -eik ping -``` - -#### publish - -This command publishes the app's client side assets to an Eik server based on the values in an `eik.json` file in the current directory. - -The command takes the form: - -```sh -eik publish [optional arguments] -``` - -**Example** - -_Publishing app assets to server_ - -```bash -eik publish -``` - -#### dependency - -This command will download the specified (by name and version) package from NPM, create a bundle with it and then publish it to the Eik server. The resulting bundle will be in esm module format, converting from common js if needed. - -_Note_ The arguments `server` and `import-map` are taken from `eik.json` if such a file is present in the current directory. If not, you will need to specify these values with the command line flags `--server` and `--map`. - -The command takes the form: - -```sh -eik dependency [optional arguments] -``` - -**Example** - -_Publishing a dependency from npm_ - -```bash -eik dependency lit-html 1.1.2 -# eik dependency --server http://assets.examplecdn.com --map http://assets.examplecdn.com/finn/map/my-import-map/1.0.0 lit-html 1.1.2 -``` - -#### alias - -This command creates a semver alias for a given published bundle. Creating aliases allows for more flexible referencing of published bundles. You can update an alias to point to the latest version of a bundle without needing to update every client that makes use of your bundle. - -_Note_ The `server` argument is taken from `eik.json` if such a file is present in the current directory. If not, you will need to specify this values with the command line flag `--server`. - -The command takes the form: - -```sh -eik alias [optional arguments] -``` - -_Example_ - -Running the following command... - -```bash -eik alias lit-html 1.1.2 1 -# eik alias --server http://assets.examplecdn.com lit-html 1.1.2 1 -``` - -...will create or update the `lit-html` alias `1` to point at `lit-html` version `1.1.2` - -#### map - -This command uploads an import map json file you have created locally to the server. You must upload the file with a `name` and a `version` and the file must be of the form: - -```json -{ - "imports": { - "": "url to dependency", - "": "url to dependency" - } -} -``` - -_Note_ The argument `server` is taken from `eik.json` if such a file is present in the current directory. If not, you will need to specify this value with the command line flag `--server`. - -The command takes the form: - -```sh -eik map [optional arguments] -``` - -```bash -eik map my-import-map 1.0.0 ./import-map.json -# eik map --server http://assets.examplecdn.com my-import-map 1.0.0 ./import-map.json -``` - -#### meta - -This command fetches and displays meta information about a package from the server - -The command takes the form: - -```sh -eik meta [optional arguments] -``` - -_Example_ - -Running the following command... - -```bash -eik meta lit-html 1.1.2 -# eik meta --server http://assets.examplecdn.com lit-html 1.1.2 -``` - -Will print meta information about the package `lit-html` version `1.1.2` in JSON format. - -## Programmatic Usage - -All of the commands described above can be used programmatically by importing this package. Each command and its programmatic usage is given below. - -### init - -```js -import cli from "@eik/cli"; -const result = await new cli.Init(options).run(); -``` - -#### options - -| name | description | type | default | required | -| ------ | ------------------------------------- | ------ | --------------- | -------- | -| logger | log4j compliant logger object | object | `null` | no | -| cwd | path to current working directory | string | `process.cwd()` | no | -| name | app name | string | `''` | no | -| server | URL to asset server | string | `''` | no | -| js | path to client side script entrypoint | string | `''` | no | -| css | path to client side style entrypoint | string | `''` | no | - -### publish - -```js -import cli from "@eik/cli"; -const result = await new cli.publish.App(options).run(); -``` - -#### options - -| name | description | type | default | required | -| ------ | ------------------------------------- | -------- | --------------- | -------- | -| logger | log4j compliant logger object | object | `null` | no | -| cwd | path to current working directory | string | `process.cwd()` | no | -| name | app name | string | | yes | -| server | URL to asset server | string | | yes | -| js | path to client side script entrypoint | string | | yes | -| css | path to client side style entrypoint | string | | yes | -| map | array of urls of import map files | string[] | `[]` | no | -| dryRun | exit early and print results | boolean | false | no | - -### dependency - -```js -import cli from "@eik/cli"; -const result = await new cli.publish.Dependency(options).run(); -``` - -#### options - -| name | description | type | default | required | -| ------ | --------------------------------- | -------- | --------------- | -------- | -| logger | log4j compliant logger object | object | `null` | no | -| cwd | path to current working directory | string | `process.cwd()` | no | -| name | app name | string | | yes | -| server | URL to asset server | string | | yes | -| map | array of urls of import map files | string[] | `[]` | no | -| dryRun | exit early and print results | boolean | false | no | - -### map - -```js -import cli from "@eik/cli"; -const result = await new cli.publish.Map(options).run(); -``` - -#### options - -| name | description | type | default | required | -| ------- | -------------------------------------- | ------ | --------------- | -------- | -| logger | log4j compliant logger object | object | `null` | no | -| cwd | path to current working directory | string | `process.cwd()` | no | -| name | app name | string | | yes | -| version | app version | string | | yes | -| server | URL to asset server | string | | yes | -| file | path to import map file to be uploaded | string | | yes | - -### alias - -```js -import cli from "@eik/cli"; -const result = await new cli.Alias(options).run(); -``` - -#### options - -| name | description | type | default | choices | required | -| ------ | --------------------------------------- | ------ | ------- | ------------ | -------- | -| logger | log4j compliant logger object | object | `null` | | no | -| server | URL to asset server | string | | | yes | -| type | type of resource to alias | string | | `pkg`, `map` | yes | -| name | app name | string | | | yes | -| alias | major number of a semver version number | string | | | yes | - -### meta - -```js -import cli from "@eik/cli"; -const result = await new cli.Meta(options).run(); -``` - -| name | description | type | default | choices | required | -| ------ | ----------------------------- | ------ | ------- | ------- | -------- | -| logger | log4j compliant logger object | object | `null` | | no | -| server | URL to asset server | string | | | yes | -| name | package name | string | | | yes | diff --git a/docs/client_aliases.md b/docs/client_aliases.md deleted file mode 100644 index 2180de89..00000000 --- a/docs/client_aliases.md +++ /dev/null @@ -1,118 +0,0 @@ ---- -id: client_aliases -title: Aliases -sidebar_label: Aliases ---- - -Aliases are general package versions that point to exact package versions. - -The need to redeploy your application every time you update a client side bundle can be avoided by using aliasing. - -In an application, we can reference an alias instead of a specific version and whenever we need to, we can update our alias and our application will automatically be updated. - -For example, an alias by the name `v1` might be set up to point to the exact package version `1.0.0`. The alias itself is independent of the version and since it is just an HTTP redirect, can be easily updated to point at a new version. - -## Application aliases - -### Using an aliased version - -Creating aliases allows you to include the alias script tags in your application with no need to update the script tag every time you publish a new bundle version. - -```js - -``` - -Any bare references to `@podium/browser` will have been replaced with absolute URLs to the Eik server. \ No newline at end of file diff --git a/docs/dependencies/aliases.md b/docs/dependencies/aliases.md new file mode 100644 index 00000000..01d64f96 --- /dev/null +++ b/docs/dependencies/aliases.md @@ -0,0 +1,48 @@ +--- +title: Package aliases +--- + +Now that you have [published a shared dependency](/docs/dependencies/npm/) to Eik and seen how to update it, it's time to set up an alias. + +A refresher from the [introduction](/docs/introduction#further-improving-performance-with-aliases): + +> Instead of importing specific versions, Eik encourages the use of aliases to share the same major semantic version between applications. + +## Giving a package an alias + +When we left [our `lodash` example](/docs/dependencies/npm) we had +version `4.17.21` published to the URL +`https://eik.store.com/npm/lodash/4.17.21/index.js`. + +The `alias` command in the [Eik CLI](/docs/reference/at-eik-cli) +creates a URL that redirects to a specific version of a library. + +```sh +eik login --key YOUR_EIK_KEY --server https://eik.store.com +eik npm-alias lodash 4.17.21 4 --server https://eik.store.com +``` + +Let's break down the alias command a bit. + +- Its first argument `lodash` is the name from `eik.json`. +- The second argument `4.17.21` is the version we want to alias. +- The third argument `4` is the alias we want to create or update. + +The `--server` argument lets you run the `login` and `alias` commands without having `eik.json` in the current directory. + +Now you should be able to go to `https://eik.store.com/npm/lodash/v4/index.js`, and your browser should be redirected to the version you aliased. + +## Updating an alias + +We saw how to [update a shared dependency](/docs/dependencies/npm#updating-a-published-package), so let's see how to update an alias as well. + +Make a note of the new version you want your alias to point to. Then log in, and run the alias command with the new version number. + +```sh +eik login --key YOUR_EIK_KEY --server https://eik.store.com +eik npm-alias lodash 4.18.0 4 --server https://eik.store.com +``` + +## Next steps + +Now that you've seen how to make aliases it's time to gather up your shared dependencies in import maps so they're easier to use. diff --git a/docs/dependencies/import-maps.md b/docs/dependencies/import-maps.md new file mode 100644 index 00000000..a1f90a45 --- /dev/null +++ b/docs/dependencies/import-maps.md @@ -0,0 +1,120 @@ +--- +title: Import maps +--- + +Publishing shared dependencies and aliasing them helps nothing if application code doesn't use them. Import maps make it easier for developers to discover and use dependencies you publish to your Eik server. + +A refresher from the [introduction](/docs/introduction#import-mapping): + +> You can publish import maps to Eik in a similar way to dependencies and application code. Maps are versioned and immutable, and can be aliased in the same way other assets can. + +Like how the `npm` package type is designed for shared dependencies, `map` is designed to hold your import maps. + +## Create an import map + +In the [previous chapter](/docs/dependencies/aliases) you made an alias for `lodash` so applications can share the same major version and get automatic updates when the Eik alias is updated. + +``` +https://eik.store.com/npm/lodash/v4/index.js +``` + +Let's put that alias URL in an import map. + +Eik import maps use the same syntax as [import maps in the browser](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap), though without the ` +`; +``` + +In the example above, `html` is a string that looks like this. + +```html + +``` + +Place the import map HTML before any other ` +`; +}); +``` diff --git a/docs/guides/esbuild.md b/docs/guides/esbuild.md new file mode 100644 index 00000000..cc6eca46 --- /dev/null +++ b/docs/guides/esbuild.md @@ -0,0 +1,70 @@ +--- +title: Using Eik with ESBuild +--- + +This guide describes how to configure [ESBuild](https://esbuild.github.io/) to use [build-time import mapping](/docs/introduction/workflow#build-time-import-mapping). The guide assumes you have an `eik.json` containing at least one [`"import-map"`](/docs/reference/eik-json#import-map). + +## Getting started + +Install [`@eik/esbuild-plugin`](https://github.com/eik-lib/esbuild-plugin#readme), and `esbuild` if you haven't already. + +```sh +npm install --save-dev esbuild @eik/esbuild-plugin +``` + +## Configure your build + +Create a `build.js` file or extend your existing build script to add the Eik plugin. + +```js +import * as eik from "@eik/esbuild-plugin"; +import esbuild from "esbuild"; + +const options = /** @type {esbuild.BuildOptions}*/ ({ + entryPoints: ["./src/index.js"], + outdir: "./public", + format: "esm", + platform: "browser", + target: ["es2017"], + bundle: true, + sourcemap: true, +}); + +const watch = process.argv.includes("--dev"); +if (watch) { + let ctx = await esbuild.context(options); + await ctx.watch(); + console.log("[esbuild] watching..."); +} else { + // Use the Eik plugin to to import mapping for the production build + // Load the import maps listed in eik.json from the Eik server + await eik.load(); + await esbuild.build({ + ...options, + plugins: [eik.plugin()], + }); +} +``` + +## Run your build + +Add these scripts to `package.json` if you haven't already. + +```json +{ + "scripts": { + "build": "node ./build.js", + "dev": "node ./build.js --dev" + } +} +``` + +Now you can run a production build using the Eik plugin by running this command. + +```sh +npm run build +``` + +## Advanced usage + +See the [plugin documentation](https://github.com/eik-lib/esbuild-plugin#api) for advanced options on loading import maps. diff --git a/docs/guides/github-actions.md b/docs/guides/github-actions.md new file mode 100644 index 00000000..c65bbf33 --- /dev/null +++ b/docs/guides/github-actions.md @@ -0,0 +1,120 @@ +--- +title: Publish to Eik on GitHub Actions +--- + +This guide describes how to use GitHub Actions to publish to an Eik server. It describes two different approaches: + +- [Using Semantic Release](#using-semantic-release) +- [Using the Eik CLI](#using-the-eik-cli) + +Choose the one you prefer. + +## Prerequisites + +Your repo needs to have access to an Eik login key. Store this as a [Secret you can use in GitHub Actions](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions). + +In this guide we'll be using `EIK_TOKEN` as the secret name. + +## Using Semantic Release + +There is an [Eik plugin](https://github.com/eik-lib/semantic-release#readme) for [Semantic Release](https://semantic-release.gitbook.io/semantic-release) you can use to automatically update the version number in `eik.json` based on [conventional commits](https://www.conventionalcommits.org/). + +To use it, update your `release.config.js` to add the plugin, and commit `eik.json` back to Git. + +```js +export default { + plugins: [ + "@eik/semantic-release", + ["@semantic-release/git", { assets: ["eik.json"] }], + ], +}; +``` + +### Semantic release workflow + +With the Eik plugin configured, update [your release workflow](https://semantic-release.gitbook.io/semantic-release/recipes/ci-configurations/github-actions#github-workflows-release.yml-configuration-for-node-projects) to pass on the `EIK_TOKEN` secret to `semantic-release`. + +```yaml +- name: Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + EIK_TOKEN: ${{ secrets.EIK_TOKEN }} + run: npx semantic-release +``` + +## Using the Eik CLI + +You can automate publishing to Eik using the [Eik CLI](/docs/reference/at-eik-cli/). + +This example requires you manually update the version number in `eik.json`. + +```yaml +name: Publish to Eik + +on: + push: + branches: + - main + paths: + - eik.json + +jobs: + publish: + runs-on: [ubuntu-latest] + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + + # you can also add @eik/cli to your devDependencies + - run: npm install --global @eik/cli@^2 + + - name: Publish to Eik + run: | + eik login --key ${{secrets.EIK_TOKEN}} + eik publish +``` + +### Automatically increment the patch version number + +If you don't really care about the version number you can use the `eik version` command to increment the version number on CI. Remember to commit the updated `eik.json` back to your repository. + +```yaml +name: Publish to Eik + +on: + push: + branches: + - main + +jobs: + publish: + runs-on: [ubuntu-latest] + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + + # you can also add @eik/cli to your devDependencies + - run: npm install --global @eik/cli@^2 + + - name: Publish to Eik + run: | + eik login --key ${{secrets.EIK_TOKEN}} + eik version + eik publish + + - name: Commit updated eik.json + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add eik.json + git commit --message "chore: update version number in eik.json [skip ci]" + git push origin HEAD + shell: bash +``` diff --git a/docs/guides/postcss.md b/docs/guides/postcss.md new file mode 100644 index 00000000..41674b7b --- /dev/null +++ b/docs/guides/postcss.md @@ -0,0 +1,69 @@ +--- +title: Using Eik with PostCSS +--- + +This guide describes how to configure [postcss](https://postcss.org/) to use [build-time import mapping](/docs/introduction/workflow#build-time-import-mapping) for your CSS. The guide assumes you have an `eik.json` containing at least one [`"import-map"`](/docs/reference/eik-json#import-map). + +## Getting started + +Install [`@eik/postcss-plugin`](https://github.com/eik-lib/postcss-plugin#readme), and `postcss` if you haven't already. + +```sh +npm install --save-dev postcss postcss-cli @eik/postcss-plugin +``` + +## Configure your build + +Create `postcss.config.js` or extend your existing config to add the Eik plugin. + +```js +import eik from "@eik/postcss-plugin"; + +/** @type {import('postcss-load-config').Config} */ +export default { + map: true, + plugins: [eik()], +}; +``` + +You can also use the JavaScript API for postcss. + +```js +import fs from "node:fs"; +import path from "node:path"; +import eik from "@eik/postcss-plugin"; +import postcss from "postcss"; + +const from = "css/input.css"; +const to = "public/output.css"; + +const css = fs.readFileSync(path.join(process.cwd(), from), "utf-8"); + +postcss() + .use(eik()) + .process(css, { + from, + }) + .then((result) => { + fs.writeFileSync(path.join(process.cwd(), to), result.css, "utf-8"); + }); +``` + +## Run your build + +Assuming you use `postcss-cli` and `postcss.config.js`, add these scripts to `package.json` if you haven't already. + +```json +{ + "scripts": { + "build": "postcss src/input.css --output ./public/output.css --config ./postcss.config.js", + "dev": "npm run build -- --watch" + } +} +``` + +## Advanced usage + +- See [the plugin documentation](https://github.com/eik-lib/postcss-plugin?tab=readme-ov-file#options) for advanced configuration of import maps. +- See [postcss-cli](https://github.com/postcss/postcss-cli) for available options. +- See [postcss](https://github.com/postcss/postcss) for integrations with other build tools. diff --git a/docs/guides/rollup.md b/docs/guides/rollup.md new file mode 100644 index 00000000..490e2def --- /dev/null +++ b/docs/guides/rollup.md @@ -0,0 +1,72 @@ +--- +title: Using Eik with Rollup +--- + +This guide describes how to configure [Rollup](https://rollupjs.org/) to use [build-time import mapping](/docs/introduction/workflow#build-time-import-mapping). The guide assumes you have an `eik.json` containing at least one [`"import-map"`](/docs/reference/eik-json#import-map). + +## Getting started + +Install [`@eik/rollup-plugin`](https://github.com/eik-lib/rollup-plugin#readme), and `rollup` if you haven't already. + +```sh +npm install --save-dev rollup @eik/rollup-plugin +``` + +## Configure your build + +Create `rollup.config.js` or extend your existing config to add the Eik plugin. + +```js +import eik from "@eik/rollup-plugin"; + +export default { + input: "source/main.js", + plugins: [eik()], + output: { + file: "build.js", + format: "esm", + }, +}; +``` + +## Run your build + +Add this script to `package.json` if you haven't already. + +```json +{ + "scripts": { + "build": "rollup --config" + } +} +``` + +Now you can run a production build using the Eik plugin by running this command. + +```sh +npm run build +``` + +## Advanced usage + +See the [plugin documentation](https://github.com/eik-lib/rollup-plugin#description) for advanced options on loading import maps. + +## Vite compatibility + +You can [use this Rollup plugin in your Vite build](https://vitejs.dev/guide/api-plugin.html#rollup-plugin-compatibility) should you need to. + +```js +import eik from "@eik/rollup-plugin"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [ + { + ...eik(), + enforce: "pre", + // only apply plugin for client code, while building + apply: (_config, { command, isSsrBuild }) => command === "build" && !isSsrBuild, + }, + ], +}; +``` diff --git a/docs/travis.md b/docs/guides/travis.md similarity index 92% rename from docs/travis.md rename to docs/guides/travis.md index c839f4ce..fafb6c0b 100644 --- a/docs/travis.md +++ b/docs/guides/travis.md @@ -1,7 +1,5 @@ --- -id: travis -title: Travis -sidebar_label: Travis +title: Publish to Eik on Travis --- Publishing to an Eik server from a Travis CI build can be achieved with a few commands. @@ -22,13 +20,13 @@ The following gives an example of how to run version and publish commands from ` ```yml language: node_js node_js: - - 14 + - 20 before_script: - - npm i -g @eik/cli + - npm install --global @eik/cli@^2 script: - - eik login -k $EIK_SERVER_KEY + - eik login --key $EIK_SERVER_KEY - eik version - - eik package + - eik publish ``` If you have a build step that you need to run before publish, you could just insert that into the `script` section as shown. @@ -36,9 +34,9 @@ If you have a build step that you need to run before publish, you could just ins ```yml script: - - - eik login -k $EIK_SERVER_KEY + - eik login --key $EIK_SERVER_KEY - eik version - - eik package + - eik publish ``` ## Create a commit script @@ -77,8 +75,8 @@ git push origin $BRANCH script: - eik login -k $EIK_SERVER_KEY - eik version - - eik package + - eik publish - ./commit.sh ``` -Once setup, when you push changes to Github, if any of the files to be published have changed, you should automatically get a new published version of your assets on your Eik server and your `eik.json` file will have been updated with the new semver version number. \ No newline at end of file +Once setup, when you push changes to Github, if any of the files to be published have changed, you should automatically get a new published version of your assets on your Eik server and your `eik.json` file will have been updated with the new semver version number. diff --git a/docs/guides/webpack.md b/docs/guides/webpack.md new file mode 100644 index 00000000..f63b3e1b --- /dev/null +++ b/docs/guides/webpack.md @@ -0,0 +1,74 @@ +--- +title: Using Eik with Webpack +--- + +:::warning + +Webpack by default does not output ECMAScript modules (ESM). Eik is designed around ESM. Your Webpack build [must output ESM](https://webpack.js.org/configuration/output/#outputmodule). + +::: + +This guide describes how to configure [Webpack](https://webpack.js.org/) to use [build-time import mapping](/docs/introduction/workflow#build-time-import-mapping). The guide assumes you have an `eik.json` containing at least one [`"import-map"`](/docs/reference/eik-json#import-map). + +## Getting started + +Install [`@eik/webpack-plugin`](https://github.com/eik-lib/webpack-plugin#readme), and `webpack` if you haven't already. + +```sh +npm install --save-dev webpack webpack-cli @eik/webpack-plugin +``` + +## Configure your build + +Create `webpack.config.js` or extend your existing config to add the Eik plugin. + +```js +export default { + entry: "./src/input.js", + output: { + environment: { + // Eik requires ESM output + module: true, + }, + filename: "bundle.js", + path: "./public/", + }, + experiments: { + // Eik requires ESM output + outputModule: true, + }, + module: { + rules: [ + { + test: /\.js$/, + use: { + loader: "@eik/webpack-plugin", + }, + }, + ], + }, +}; +``` + +## Run your build + +Add this script to `package.json` if you haven't already. + +```json +{ + "scripts": { + "build": "webpack", + "dev": "webpack --watch" + } +} +``` + +Now you can run a production build using the Eik plugin by running this command. + +```sh +npm run build +``` + +## Advanced usage + +See the [plugin documentation](https://github.com/eik-lib/webpack-plugin#description) for advanced options on loading import maps. diff --git a/docs/introduction/introduction.md b/docs/introduction/introduction.md new file mode 100644 index 00000000..19be8c1f --- /dev/null +++ b/docs/introduction/introduction.md @@ -0,0 +1,169 @@ +--- +title: Introduction to Eik +--- + +Eik is an asset server designed for performant serving of +[ECMAScript Modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) (ES modules) and CSS, +especially suited for multi-page or micro-frontend applications. + +By hosting ES modules on a central server the browser can leverage both the browser and ES module cache to reduce time spent downloading, parsing and running JavaScript. + +## The problem Eik solves + +A common architecture for web applications is to have separate applications serving different pathnames. These typically have client-side JavaScript, and may often share the same dependencies. + +![An illustrated user journey across three pages on store.com](/img/overview_page_to_page_flow.min.svg) + +Take the illustrated user journey for `store.com` above. + +- `store.com` is one application +- `store.com/shop` is another +- `store.com/checkout` is a third + +A user will normally arrive at the front page, move to browsing the shop and then finish at the checkout. + +Let's say the applications on `store.com` are using [Lit](https://lit.dev) for templating in the browser. Without Eik, all three applications ship with their own version of Lit. The browser needs to download, parse and execute Lit three times. + +## Sharing ES modules with Eik + +Sticking with our `store.com` example, if all three applications import Lit from Eik instead of shipping their own, the user's browser can cache the HTTP request between page views. + +```js +import Lit from "https://eik.store.com/npm/lit/1.0.0/index.js"; +``` + +:::tip + +HTTP imports aren't the prettiest, but we'll be improving things with [import mapping](#import-mapping) soon! In your source code you'll still be using the bare imports you are familiar with when using bundlers. + +::: + +### Taking advantage of the ES module cache + +Eik is designed to take advantage of the ES module cache as well. From Lin Clark's [ES modules: A cartoon deep-dive](https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/): + +> Any module that is in both of these graphs is going to share a module instance. This is because the loader caches module instances. For each module in a particular global scope, there will only be one module instance. +> +> This means less work for the engine. For example, it means that the module file will only be fetched once even if multiple modules depend on it. +> +> [...] +> +> The module map caches the module by canonical URL so that there is only one module record for each module. That ensures each module is only executed once. + +The ES module cache is mostly relevant for micro-frontend architectures (where multiple apps can share the same global scope), but know that even if multiple modules import a shared dependency over HTTP the browser will only download and execute it once. + +## Further improving performance with aliases + +Now that our three apps on `store.com` all use Eik to import Lit we run into a different problem: version management. + +![Example user journey where the three apps use slightly different versions of lit](/img/overview_page_to_page_diff_versions.min.svg) + +Going back to the user journey for `store.com`, imagine that: + +- `store.com` loads `lit@1.2.0` +- `store.com/shop` loads `lit@1.1.1` +- `store.com/checkout` loads `lit@1.1.2` + +At this point we're back to square one. Each page visit means downloading, parsing and executing Lit. + +We can tell from the [semantic version](https://semver.org/) numbers that they could all use `lit@1.2.0` and be fine. This is where [aliases](/docs/dependencies/aliases) come in. + +Instead of importing specific versions, Eik encourages the use of aliases to share the same major semantic version between applications. + +```js +import Lit from "https://eik.store.com/npm/lit/v1/index.js"; +``` + +![The same user journey as before, but using Eik aliases to share the same version](/img/overview_page_to_page_same_versions.min.svg) + +When all three applications point to the same alias the browser's HTTP cache ensures we don't download Lit more than once in the user journey. + +:::tip + +Another bonus with aliases is that new versions of a dependency can be published to Eik, and with an alias update it gets applied to all apps that depend on it – no redeploys needed ✨ + +::: + +## Import mapping + +You may be used to writing bare import strings like this. + +```js +import Lit from "lit"; +``` + +Imports like the above depend on Node's [module resolution algorithm](https://nodejs.org/docs/v20.16.0/api/esm.html#resolution-and-loading-algorithm). You then typically use a bundler to do that module resolution at build-time, and end up with one or more JavaScript files that include all the imported dependencies. + +To use ES modules in the browser you can't rely on Node's resolution algoritm. Specification compliant ES module imports must be either relative or absolute. + +A relative ESM import statement must start with either `/`, `./` or `../`: + +```js +import * as mymod from "/my_module.js"; +import * as mymod from "./my_module.js"; +import * as mymod from "../my_module.js"; +``` + +An absolute import must be a fully formed URL: + +```js +import * as mylib from "https://eik-server.com/pkg/mylib/v3/main.js"; +``` + +### Using bare imports in source code with maps + +[Import maps](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap) is a browser feature that lets developers keep the ergonomics of bare imports, even when using ES modules in the browser. Import maps are a special `script` type where the bare import is a key, and the mapped import string (either relative or absolute) is the value. + +```html + +``` + +#### Sharing the import maps themselves on Eik + +You can [publish import maps to Eik](/docs/dependencies/import-maps) in a similar way to dependencies and application code. Maps are versioned and immutable, and can be aliased in the same way other assets can. + +Import maps on Eik look similar to those in the browser, only without the ` +``` + +### `@eik/node-client` + +You likely want to use a different URL when developing, and only point to Eik in production. Since this is a common operation, Eik includes a [module for Node apps that helps generate these links](/docs/reference/at-eik-node-client). + +```js +import Eik from "@eik/node-client"; + +const development = process.env.NODE_ENV === "development"; + +const client = new Eik({ + base: "/public", // this base path will only be used if development is true + development, +}); +await client.load(); // load the config from eik.json + +if (development) { + // set up your app to serve local versions from disk on /public, + // for instance with serve-static +} + +// value will point to /public/app.js if development is true, +// and https://eik.store.com/pkg/my-app/1.0.0/app.js otherwise +const { value, integrity } = client.file("/app.js"); +const scriptTag = ``; +``` + +## Next steps + +Congratulations! You've taken steps to make your application more performant by sharing ES modules in the browser. + +At this point you may go forth and code, but if you'd like you can learn about + +- publishing shared dependencies to Eik +- gathering shared dependencies in import maps diff --git a/docs/mapping_browser.md b/docs/mapping_browser.md deleted file mode 100644 index cbe045a2..00000000 --- a/docs/mapping_browser.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -id: mapping_browser -title: Browser Support -sidebar_label: Browser Support ---- - -Eventually, browsers will support Import Maps but currently (October 2020) no browser is shipped with Import Map enabled. - -Chromium based browsers does ship support for Import Maps as an [experimental feature](https://www.chromestatus.com/feature/5315286962012160) which has to be turned on by enabling the Experimental Productivity Features flag in Chromium based browsers: - - - [Chrome](chrome://flags/#enable-experimental-productivity-features) - - [Brave](brave://flags/#enable-experimental-productivity-features) - - [Opera](opera://flags/#enable-experimental-productivity-features) - - [Edge](edge://flags/#enable-experimental-productivity-features) - -## Import Map Polyfill - -Import Maps can be polyfilled by the following modules: - - - [es-module-shims](https://github.com/guybedford/es-module-shims) diff --git a/docs/mapping_import_map.md b/docs/mapping_import_map.md deleted file mode 100644 index 7fd3f382..00000000 --- a/docs/mapping_import_map.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -id: mapping_import_map -title: Import Map -sidebar_label: Import Map ---- - -A key concept in Eik is to align the dependents of a module to the same version. A part of this concept is [Import Maps](https://github.com/WICG/import-maps) which makes it possible to map import statements in modules. - -Import Maps are a fairly new concept and will hopefully be supported in browsers in the close future. Import Maps allow [ECMA Script Modules (ESM)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) "bare" import specifiers, such as `import {html, render} from 'lit-html'` which will throw when used in a browser, to work by being mapped to a relative or absolute URLs the browser can use to load the module. - -In other words; in an ESM we can import a module like so: - -```js -import {html, render} from 'lit-html'; -``` - -Then an Import Map can be loaded as following in the browser: - -```html - -``` - -When the Import Map is applied, our code will act as we have written: - -```js -import * as lit from 'https://cdn.eik-server.com/npm/lit-html/v1/lit-html.js' -``` - -Browser support for Import Maps is currently (October 2020) limited. There are polyfills available for Import Maps but its fully possible to apply Import Map to modules ahead of time through build tools. - -Eik does not dictate which strategy, a polyfill or ahead of time, is used for import mapping modules but we recommend that an organization aligns itself with the same strategy across its teams. - -It is also worth keeping in mind that one is not locked to one strategy forever. An Import Map used to apply mapping ahead of time will work as intended in browsers the day there is full browser support for Import Maps. diff --git a/docs/mapping_plugins.md b/docs/mapping_plugins.md deleted file mode 100644 index 95ca8bef..00000000 --- a/docs/mapping_plugins.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -id: mapping_plugins -title: Build Tool Plugins -sidebar_label: Build Tool Plugins ---- - -Eik provides a set of build tool plugins that cater for applying Import Maps ahead of time. - -The common functionallity of these plugins is that they will, if found, load the `eik.json` in a project and fetch the defined Import Maps and then apply these to the code the build tool is processing. - -When using a build tool to apply an Import Map ahead of time, the build process should be run before a module is published to an Eik server. - -## Available plugins - -The following build tool plugins are available: - - - [ESBuild](https://github.com/eik-lib/esbuild-plugin) - - [PostCSS](https://github.com/eik-lib/import-map-postcss-plugin) - - [Rollup](https://github.com/eik-lib/import-map-rollup-plugin) - diff --git a/docs/overview.md b/docs/overview.md deleted file mode 100644 index 2e1b1280..00000000 --- a/docs/overview.md +++ /dev/null @@ -1,121 +0,0 @@ ---- -id: overview -title: Overview -sidebar_label: Overview ---- - -Eik consist of 3 main parts. First of all Eik is an [asset server](/docs/server) for serving [ECMA Script Modules (ESM)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) and CSS assets. Secondly, Eik is a [client](/docs/client) for easy upload and management of your assets to an Eik server. The third part is a set of utilities that can be used to align module dependencies to the same version. - -## Introduction - -To understand Eik we need to understand what we are trying to achieve so lets start with a fairly common issue: - -In a moderate or large sized web site it is very common that the site is built and served as [multiple applications](https://martinfowler.com/articles/microservices.html) which live at different pathnames on the site. It's also common for each of these applications to have a dedicated team of developers with the goal of being as autonomous as possible. To achieve this, each application and team should depend on each other as little as possible. - -![User flow](/img/overview_page_to_page_flow.min.svg) - -Lets say we have a site where the front page (`site.com`) is one application. Then we have a web shop, a second application, on `site.com/shop` and finally there is a third application handling checkout on `site.com/checkout`. A user will normally arrive at the front page, move to browsing the shop and then finish at the checkout. - -Let's also say that all of these applications are using [lit-html](https://lit-html.polymer-project.org/) for templating in the browser. We then have different applications depending on the same library that we want to be developed and deployed to production autonomously. Problems can arise when some of these application start to depend on different versions of the same library. - -![Non-optimised loading of assets](/img/overview_page_to_page_diff_versions.min.svg) - -Our challenge is to avoid the end user having to end up downloading different versions of the same library as they move between the different applications on our site. We want to maximize end user performance by downloading lit-html once and not having to download one specific version when accessing `site.com` (eg. v1.2.0) and then downloading another specific version (eg. v1.1.1) when moving to `site.com/shop` and finally ending up with perhaps having to download yet another version (eg. v1.1.2) when they check out at `site.com/checkout`. - -![Optimised loading of assets](/img/overview_page_to_page_same_versions.min.svg) - -The Eik solution is to make all applications point to the same version of the same library in production despite that the applications are developed using different patch or minor version. If the library then has appropriate HTTP cache headers, the browser will do the rest and make sure the library is loaded over the wire only once during the user's visit to our site. - - -## How Eik works - -The main role of the Eik server is to serve static assets uploaded to the server. Upon upload, assets will be given a new versioned pathname for each upload and are considered immutable. A change in an asset is a new version on the Eik server. By doing so, served assets can be cached forever in the end users browser. - -The Eik server also has the concept called an alias. An alias is a non immutable pathname which can be set to redirect requests to it, to an immutable asset pathname. - -For example, let us say that we upload lit-html version 1.1.1 to an Eik server. This version of lit-html will then live on the immutable URL `/npm/lit-html/1.1.1`. We can then set an alias for lit-html and this alias will be on the non immutable pathname `/npm/lit-html/v1`. Any request to any file under the alias at `/npm/lit-html/v1` will then be redirected to the matching file under `/npm/lit-html/1.1.1`. - -Later on, when we publish lit-html version 1.2.0 to the Eik server, this version will then live on the immutable pathname `/npm/lit-html/1.2.0`. We can then update the existing alias at the non immutable pathname `/npm/lit-html/v1` to point to the new version. Requests to any file under the alias at `/npm/lit-html/v1` will then be redirected to its matching file under `/npm/lit-html/1.2.0`. - -In order to meet the challenge outlined in the introduction above, each of the applications described can load lit-html through its alias (`/npm/lit-html/v1`) and they will all load the same version. The alias acts as a static path to a shared library (in this case lit-html) across all the applications. It's then possible to publish new versions of a library without having to rebuild and redeploy each application to production. - -## ESM imports - -Before we proceed, we should go over some ESM import statement basics. - -ESM import statements can be relative. A relative ESM import statement must start with either `/`, `./` or `../`: - -```js -import * as mymod from '/my_module.js'; -import * as mymod from './my_module.js'; -import * as mymod from '../my_module.js'; -``` - -ESM import statements can also be absolute in which case they must start with an HTTP protocol: - -```js -import * as mylib from 'https://eik-server.com/pkg/mylib/v3/main.js'; -``` - -Due to the prevalence of asset bundling, it's very common to see ESM import statements which do not comply with any of the statements outlined above (note that `my_library` does not start with `/`, `./` or `../`): - -```js -import * as mylib from 'my_library'; -``` - -These type of statements are called "bare imports" and are not legal ESM import statements. A browser can not handle such an import statement. Bare import statements are commonly used when a module is installed through a package manager, such as NPM, and then transpiled through a build step to one of the legal ESM import statements before being served to the browser. - -In Eik, we utilize bare imports to align modules (ex; the applications in our example) to the same version of modules it depends on (ex; lit-html in our example). Which brings us to Import Maps. - -## Import Maps - -[Import Maps](https://github.com/WICG/import-maps) is fairly new and up and coming web standard. An Import Map is a simple object mapping between a bare import statement and a legal ESM import statement. The idea is that an Import Map should be used to map bare import statements to fully qualified import statements in ESM. - -An Import Map looks something like this: - -```json -{ - "my_library": "https://eik-server.com/pkg/mylib/v3/main.js", - "lit-html": "https://eik-server.com/npm/lit-html/v1/lit-html.js" -} -``` - -Eik has support for storing Import Maps under a dedicated namespace. Import Maps are versioned and immutable and can be aliased in the same way that assets can. - -Eik's mapping utils is used to apply Import Maps to assets during bundling. - -## Mapping it together - -In Eik, we use Import Maps and aliasing of assets to align the versions of libraries across multiple applications on a site while maintaining the possibility to develop and deploy each application to production separately. - -Let's go through this, keeping in mind the challenge we outlined in the introduction above. - -We know that lit-html is a library that all our applications will be using so we want to align the version in use across all of them. To do so, we publish version 1.2.0 of lit-html to our Eik server after which time it will be available at `https://eik-server.com/npm/lit-html/1.2.0`. - -To make the reference to lit-html more static over time we create an alias to point to version 1.2.0 and lit-html can be requested through `https://eik-server.com/npm/lit-html/v1`. As mentioned earlier, this give us the possibillity to update versions of lit-html without having to rebuild and redeploy each of our applications to production. - -Next, we need to create a mapping between the bare import statement developers will use when developing the applications and the aliased URL of lit-html. We can do so by making an Import Map as follow: - -```json -{ - "lit-html": "/npm/lit-html/v1/lit-html.js" -} -``` - -Once created, we publish this Import Map to our Eik server and then create an alias for it. If we were to name the Import Map "site-mapping" and versioned it as 1.0.1 during the upload to the Eik server, it would then be available at the alias URL `https://eik-server.com/map/site-mapping/v1`. - -In each application we can now depend on and install lit-html through NPM as is common practice. Each application can then locally reference lit-html through its bare import statement like so: - -```js -import {html, render} from 'lit-html' -``` - -In the build tool used by the applications we can now add the appropriate Eik mapping utility which will read a set of defined Import Maps (in our example, "site-mapping") from the Eik server and apply these Import Maps to the application code. This will map our bare import statements into legal ESM import statements pointing to the lit-html alias defined in the Import Map: - -```js -import * as lit from '/npm/lit-html/v1/lit-html.js'; -``` - -Now our application defines an ESM import statement that points to the alias for lit-html which makes sure multiple applications on our site align to the same version of lit-html. By doing this, we're able to develop our application in isolation without depending or interfering with any other applications that utilize the same library. - -The final step in this process is uploading the application code as a package to the Eik server. Which is done by the Eik client. diff --git a/docs/overview_concepts.md b/docs/overview_concepts.md deleted file mode 100644 index 6fa4aa82..00000000 --- a/docs/overview_concepts.md +++ /dev/null @@ -1,91 +0,0 @@ ---- -id: overview_concepts -title: Concepts -sidebar_label: Concepts ---- - -Eik is for serving ESM and CSS but we use the term packages when we refer what is uploaded to an Eik server. A package can contain any set of files which are considered to be static assets on a web site. Despite being agnostic in its opinion about what a package can contain, Eik does have a package type definition which comes into play when organizing assets. - -## Package Types - -Eik differentiates between the types of packages it hosts. These are: Packages, NPM Packages and Import Maps. On the server, these different types live under separate URL namespaces to avoid conflicts with each other. - -### Eik Packages - -Eik packages are normally produced by websites and web applications. Such a package typically consists of bundled application code which, most of the time, does not really make much sense to publish as a module to a public repository such as NPM. - -Packages live under the [/pkg/](server_rest_api.md#packages) namespace on an Eik server. - -### NPM Packages - -NPM packages are modules replicated from the [NPM repository](https://www.npmjs.com/) with the intention that they be loadable by browsers. This is similar to what [unpkg](https://unpkg.com/) and [pika](https://www.pika.dev/) do, with the exception that Eik does not proxy NPM modules as Unpkg and Pika do. Eik has taken a different approach and it is necessary to manually publish modules from NPM to Eik as well as do any transpiling from (for example) CommonJS to ESM as a manual process before publishing. - -NPM Packages live under the [/npm/](server_rest_api.md#npm-packages) namespace on an Eik server. - -### Import Maps - -Import Maps are handled as single JSON files in Eik and not treated as a package. It's only possible to upload a single file and not a group of files like it is when uploading packages. - -Import Maps live under the [/map/](server_rest_api.md#import-maps) namespace on a Eik server. - -## Naming and versioning - -A package intended to be published to an Eik server should follow the [NPM module naming convention](https://github.com/npm/validate-npm-package-name): - -- package name length should be greater than zero -- all characters in a package name must be lowercase i.e., no uppercase or mixed case names are allowed -- package name *can* consist of hyphens -- package name *must not* contain non-url-safe characters (since name ends up being part of a URL) -- package name *should not* start with `.` or `_` -- package name *should not* contain leading or trailing spaces -- package name *should not* contain any of the following characters: `~)('!*` -- package name *cannot* be the same as a node.js/io.js core module nor a reserved/blacklisted name. For example, the following names are invalid: - + http - + stream - + node_modules - + favicon.ico -- package name length cannot exceed 214 - -All packages published to an Eik server is immutable and versioned with [Semantic Versioning](https://semver.org/). This makes it possible to differentiate between what are considered smaller changes to a package which should not break behaviour in a dependency from changes which can be breaking. - -Since packages are immutable, the Eik server will set an infinite HTTP cache control header on all files belonging to a package to make use of browser caching. - -## Aliasing - -Aliasing can be applied to all packages and is a way of having a static URL reference, which can change, to a version of a package. Aliases are mutable and can be set to point to different versions of a package. The alias URL will do a redirect to the full version that it is set to point to. - -Lets say we publish `lit-html` version `1.1.1` as an NPM package to an Eik server. It will then live on the following URL: - -```sh -https://eik-server.com/npm/lit-html/1.1.1/ -``` - -If we then create an alias for this version it will live on the following URL: - -```sh -https://eik-server.com/npm/lit-html/v1/ -``` - -Any request to any file under this alias will be redirected to the same path under the package version it refers too. - -A request to the following file under the alias: - -```sh -https://eik-server.com/npm/lit-html/v1/lit-html.js -``` - -will redirect to: - -```sh -https://eik-server.com/npm/lit-html/1.1.1/lit-html.js -``` - -Aliasing is not automatic in Eik. Aliases must be created or updated manually when a version of a package is published to an Eik server. - -### Aliasing and versioning - -Aliasing is tied to the semantic version of each package where the alias is the major version as a single digit pointing to a given full version of the same major version. That means that all 1.x versions of a package can have a `v1` alias and all 2.x versions of the same package can have a `v2` alias at the same time. - -This is a feature to make sure there is a way to publish breaking changes in a package without breaking its dependencies. In semantic versioning `patch` and `minor` versions are supposed to not contain breaking changes while a `major` version can contain breaking changes so in a situation where a package introduces a breaking change it should, according to semantic versioning, be published as a new major version. - -This breaks a bit with what Eik is trying to solve if we look at the challenge we outlined in our [introduction](overview.md#introduction) but the handling of breaking changes is probably the one place where it's acceptable not to align all dependencies of a package to the same version of it. diff --git a/docs/overview_eik_json.md b/docs/overview_eik_json.md deleted file mode 100644 index d9bf5f9c..00000000 --- a/docs/overview_eik_json.md +++ /dev/null @@ -1,208 +0,0 @@ ---- -id: overview_eik_json -title: The eik.json File -sidebar_label: The eik.json File ---- - -Eik packaging is configured either by way of a JSON meta file called `eik.json` or by values included in a `package.json` file. Any project that publishes assets to an Eik server must provide this configuration in one (and only one) of these locations. - -### Defining Eik configuration in an eik.json file - -The most common way to configure an Eik setup is to create and populate an `eik.json` file in a project's root. Values placed in this configuration tell the Eik client where the Eik server is location, which files to package, name, version and so on. - -__*Example*__ - -```json -{ - "name": "my-app", - "version": "1.0.0", - "server": "https://assets.myserver.com", - "files": "./public", - "import-map": "https://assets.myserver.com/map/my-map/1.0.0" -} -``` - -### Defining Eik configuration in a package.json file - -Instead of specifying Eik configuration in an `eik.json` file, it is also possible to define the same values in `package.json`. When doing so, the exact same configuration values can be set and everything must be placed under an `eik` key. - -__*Example*__ - -```json -{ - "eik": { - "name": "my-app", - "version": "1.0.0", - "server": "https://assets.myserver.com", - "files": "./public", - "import-map": "https://assets.myserver.com/map/my-map/1.0.0" - } -} -``` - -It is also possible to have Eik use the `package.json` `name` and `version` fields by omitting them from the configuration. - -__*Example*__ - -```json -{ - "name": "my-app", - "version": "1.0.0", - "eik": { - "server": "https://assets.myserver.com", - "files": "./public", - "import-map": "https://assets.myserver.com/map/my-map/1.0.0" - } -} -``` - -## Generating an eik.json file - -The Eik [client](client.md) provides a scaffolding tool that can be used to generate an `eik.json` file in the current directory. - -```sh -eik init -``` - -Once generated, it's necessary to add information about the Eik server URL for the project, asset entrypoints and so on. - -### Example scaffolded eik.json file - -```json -{ - "name": "", - "version": "1.0.0", - "server": "", - "files": {}, -} -``` - -## eik.json file fields - -### name - -* required - -Defines the value that will be used on the Eik server to configure the namespace for the project. This should be unique to an organisation. - -See [application packages](/docs/client_app_packages) for more information. - -### version - -* required - -Defines current Eik package version using [semver](https://semver.org/). This must be unique to a given package name within an organisation. Attempting to republish the same version a second time will fail. - -```json -{ - "version": "1.0.0" -} -``` - -You can manually update this value or use the `eik version` command to automate the process. - -See [application packages](/docs/client_app_packages) for more information. - -### server - -* required - -Defines the location of the Eik server that the project will publish to. - -See the [server docs](/docs/server) for how to setup and configure an Eik server. - -### files - -* required - -Defines JavaScript and CSS file entrypoints to publish. This can be a string defining a folder or a single entrypoint or it can be an object that maps publish paths to local file system file locations. - -#### Defining "files" - -The following specifies that all files in the `public` folder should be uploaded to the Eik server. Note that relative paths and absolute paths can be used as well. - -```json -{ - "files": "./public", -} -``` - -Nested folders are also supported: - -```json -{ - "files": "./public/assets", -} -``` - -You can use glob syntax to decide which files to include: - -```json -{ - "files": "./public/**/*.js", -} -``` - -Additionally, `files` can be an object instead of a string and mappings can be provided. This makes it possible to specify exact files to upload and even rename them in the process. -Absolute and relative paths as well as glob syntax are also supported when defining file mappings in this way. - -```js -files: { - // file `./path/to/esm.js` is uploaded and renamed to `/script.js` - 'script.js': './path/to/esm.js', - - // file `/absolute/path/to/esm.js` is uploaded and renamed to `/script.js` - 'script.js': '/absolute/path/to/esm.js', - - // everything in `./path/to/folder` is uploaded to `/folder` - 'folder': './path/to/folder', - - // everything in `./path/to/folder` is uploaded to `/folder` (but no folder recursion) - 'folder': './path/to/folder/*', - - // everything in `./path/to/folder` is uploaded to `/folder/scripts` - 'folder/scripts': 'path/to/folder/**/*', -} -``` - -Keys (eg. "scripts.js") define publish locations on the Eik server and values (eg. "./path/to/esm.js") define the local file entrypoint locations. This aligns somewhat loosely with [ESM package entrypoints](https://nodejs.org/dist/latest-v14.x/docs/api/esm.html#esm_package_entry_points) in Node.js - -See [application packages](/docs/client_app_packages) for more information. - -### import-map - -* optional - -Can be used to include [import map](https://github.com/WICG/import-maps#the-basic-idea) files from URLs that will be used during a build to map bare import specifiers to given URLs. This can be specified as a single string or as an array of strings if you wish to use more than 1 import map in the build. - -```json -{ - "import-map": "https://assets.myeikserver.com/map/my-map/1.0.0" -} -``` - -or - -```json -{ - "import-map": [ - "https://assets.myeikserver.com/map/my-map/1.0.0", - "https://assets.myeikserver.com/map/my-map-2/1.0.0", - ] -} -``` - -See [import maps](/docs/client_import_maps) for more information. - -### out - -* optional -* default: `./.eik` - -Can be used to configure the app's Eik build directory. By default this value is set to `.eik` which indicates that local Eik files should be placed in a folder called `.eik` in the current working directory. - -```json -{ - "out": "./eik" -} -``` diff --git a/docs/overview_workflow.md b/docs/overview_workflow.md deleted file mode 100644 index 70cd5e3f..00000000 --- a/docs/overview_workflow.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -id: overview_workflow -title: Workflow -sidebar_label: Workflow ---- - -In Eik, Import Maps and aliasing of assets are used to align library versions across multiple applications. Keeping in mind that import maps are a thing of the future, there are different strategies we can apply when mapping assets. Depending on the strategy choosen, the workflow will be a little bit different. - -N.B. Regardless of the strategy one chooses, Eik does not force any structure for how the source is organized or what language it's written in (for example TypeScript). There can also be multiple entry points for the source in your application. - -Configuration for which Eik server to use and which import maps to apply etc is defined in [the eik.json file](/docs/overview_eik_json). Depending on the workflow used, there may be a difference in which workflow module(s) use this configuration. - -## Ahead of time mapping - -When working with ahead of time mapping the workflow is as follow: - -![Workflow of ahead of time mapping](/img/workflow_ahead_of_time_mapping.min.svg) - -When applying mapping ahead of time there must be a build step regardless if the source needs one or not. It is in this step that import statements in your assets will be rewritten with the mapping values from one or more provided import maps. Eik [supports multiple build tools](/docs/mapping_plugins) by providing plugins for these tools which will do this mapping. - -When your build process runs, the Eik plugin used with the build tool will fetch the defined import maps from the Eik server defined for the project. When the build process is complete, the built application assets should be stored in one or more output folders. - -The next step in the workflow is uploading the built application assets to the Eik server. This is done by the [Eik client](/docs/client_app_packages). - -Upon upload Eik will calculate integrity hashes and store these in `./eik/integrity.json`. These hashes can be used for [Subresource Integrity](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity) and should be used when referring to your assets in your HTML. - -At this point your application assets are available on the Eik server and the server side part of the application can be applied to production by referring to the assets on the Eik server in the HTML. - -## Browser mapping - -When working with mapping in the browser the workflow is as follows: - -![Workflow of abrowser mapping](/img/workflow_browser_mapping.min.svg) - -As previously mentioned, Eik doesn't force any particular structure for how your source code should be organized and when it comes to mapping in the browser, the question of whether or not your project should have a build step is completely up to you. The important thing to note with regards to build steps is that the `files` field in `eik.json` must point to either your source files in the case where you have no build step or to the files in the output folder of your build step if you do. - -The main step in this workflow is uploading the assets to the Eik server. This is done by the [Eik client](/docs/client_app_packages). - -Upon upload Eik will calculate integrity hashes and store these in `./eik/integrity.json`. These hashes can be used for [Subresource Integrity](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity) and should be used when referring your assets in your HTML. - -At this point your application assets will be available on the Eik server and the server side part of the application can be applied to production referring to the assets on the Eik server in the HTML. - -To apply the mapping the server side part of the application should also pull the import maps, defined in the eik.json, from the Eik server and include these in the HTML as inline script blocks as according to the specification. - -## Referencing assets - -Absolute URLs to the assets on the Eik server can be built from the fields found in an `eik.json` file: - -```js -fs.readFile('./eik.json', (err, data) => { - if (err) throw err; - const eik = JSON.parse(data); - console.log(`${eik.server}/pkg/${eik.name}/${eik.version}/file.ext`); -}); -``` - -The `@eik/common` module contains multiple [helper methods](https://github.com/eik-lib/common#helpers) to build absolute URLs to assets from `eik.json`. diff --git a/docs/reference/at-eik-cli.md b/docs/reference/at-eik-cli.md new file mode 100644 index 00000000..d51f9fc5 --- /dev/null +++ b/docs/reference/at-eik-cli.md @@ -0,0 +1,333 @@ +--- +title: "@eik/cli reference" +sidebar_label: "@eik/cli" +--- + +The Eik CLI lets you publish to Eik and manage import maps and aliases. + +## Install and run commands + +To install the Eik CLI globally: + +```sh +npm install --global @eik/cli +``` + +This makes the command `eik` available in your shell. + +```sh +eik --help +``` + +To uninstall a globally installed Eik CLI: + +```sh +npm uninstall --global @eik/cli +``` + +### Eik command autocomplete in your shell + +If you would like to be able to use tab key autocompletion for Eik commands, you can concatentate the following script to your `.zshrc` or `.bashrc` file. + +```sh +###-begin-eik-completions-### +_eik_yargs_completions() +{ + local reply + local si=$IFS + IFS=$' +' reply=($(COMP_CWORD="$((CURRENT-1))" COMP_LINE="$BUFFER" COMP_POINT="$CURSOR" eik --get-yargs-completions "${words[@]}")) + IFS=$si + _describe 'values' reply +} +compdef _eik_yargs_completions eik +###-end-eik-completions-### +``` + +Once done, you should be able to hit tab while typing Eik commands and get autocomplete suggestions. + +### As a devDependency + +You can also add the Eik CLI as a `devDependency`. + +```sh +npm install --save-dev @eik/cli +``` + +Now you can use `npx eik` to run commands with the version of the Eik CLI in your project. + +```sh +npx eik --help +``` + +You can also add scripts to `package.json` and run them that way. + +```json +{ + "scripts": { + "eik:publish": "eik publish" + } +} +``` + +```sh +npm run eik:publish +``` + +### Using `npx` + +You can run the Eik CLI from anywhere using `npx`. + +```sh +npx @eik/cli --help +``` + +## Available commands + +Run `eik` with `--help` to see a description of all available commands and their aliases in your shell. + +```sh +eik --help +``` + +You can also get help for each individual command the same way. + +```sh +eik init --help +``` + +### alias + +Create or update an [alias for a package, npm package or import map](/docs/dependencies/aliases/) as identified by its name and version. + +A package with the given name and version must already exist on the Eik server. The alias should be the semver major part of the package version. Eg. for a package of version 5.4.3, you should use 5 as the alias. The alias type (npm, map, package) is detected from `eik.json` in the current working directory. + +```sh +eik alias +``` + +Replaces `package-alias`, `map-alias` and `npm-alias` from version `3.0.0` onward. + +### init + +Creates a new default [`eik.json`] and saves it to the current working directory. + +```sh +eik init +``` + +### integrity + +Retrieve file integrity information for package name and version defined in [`eik.json`], then populate `integrity.json` file with this information [for use in subresource integrity](/docs/introduction/workflow#linking-to-your-assets-from-html). + +```sh +eik integrity [name] [version] +``` + +### login + +Write operations require you to be logged in to the Eik server. To log in, run the `login` command. + +```sh +eik login +``` + +The login command will ask for a server URL and a server key. + +Server keys are configured on the server and, once entered, +the client will authenticate with the server and receive back +a JSON web token which it will save in an `.eikrc` file in the +users home directory for use in subsequent commands. + +![Login screenshot](/img/login.png) + +#### Log in without the command prompt + +It is possible to bypass login prompts by providing the server URL and key via command line flags. + +```sh +eik login --server https://eik.store.com --key YOUR_EIK_KEY +``` + +#### Log in with multiple Eik servers + +It is possible to be authenticated against several Eik servers at once by calling the `eik login` command multiple times and providing different server URLs and keys each time. + +```sh +eik login --server https://eik1.store.com --key YOUR_EIK_KEY +eik login --server https://eik2.store.com --key YOUR_EIK_KEY +``` + +#### Once logged in + +So long as the client is logged in to a single server, all subsequent commands will know which server to use and provide credentials automatically. + +```sh +eik publish +``` + +_N.B._ If the client is authenticated with more than one server, it may be necessary to tell the client which server to use when using commands since the client will not decide which authenticated server to give precedence to. The `--server` (or `-s` for short) flag can be used to do this. + +```sh +eik publish --server https://eik.store.com +``` + +### map-alias + +See [alias](#alias) + +### meta + +Retrieve meta information by package, map or npm name. + +If a given name exists in several types (package and map for example), results will be returned and displayed from all matching types. + +```sh +eik meta +``` + +### npm-alias + +See [alias](#alias) + +### package-alias + +See [alias](#alias) + +### ping + +Ping an Eik server to check that it is responding. + +```sh +eik ping [server] +``` + +### publish + +Publish a package to an Eik server. Reads configuration from [`eik.json`] or `package.json`. + +```sh +eik publish +``` + +### version + +Compares local files with files on server and increments the `"version"` field in [`eik.json`] if necessary. + +```sh +eik version [level] +``` + +## Programatic usage + +If you need to script commands from the Eik CLI, consider importing `@eik/cli` in JavaScript. + +The programatic API is mostly the same as the CLI, although the CLI has some extra named commands that use the same programatic API behind the scenes. + +```js +import cli from "@eik/cli"; +``` + +Your editor should be able to show code suggestions and inline documentation for the API. + +### alias + +Create or update an alias for a package on the Eik server. + +```js +const result = await cli.alias({ + server, + name, + version, + alias, + token, +}); +``` + +### integrity + +Get integrity information of a published asset. + +```js +const result = await cli.integrity({ + server, + name, + version, +}); +``` + +### login + +Log in using a key to get the `token` needed by other commands. + +```js +const result = await cli.integrity({ + server, + key, +}); +``` + +### map + +Publish a map to the Eik server. + +```js +const result = await cli.map({ + server, + name, + version, + file, + token, +}); +``` + +### meta + +Get metadata about a published asset. + +```js +const result = await cli.meta({ + server, + name, + version, +}); +``` + +### ping + +Ping the Eik server. + +```js +const result = await cli.ping({ + server, +}); +``` + +### publish + +Publish a package to the Eik server. + +```js +const result = await cli.publish({ + server, + name, + version, + file, + token, +}); +``` + +### version + +Similar to `npm version`, but updates `eik.json`. + +```js +const result = await cli.version({ + server, + name, + version, + files, +}); +``` + +[`eik.json`]: /docs/reference/eik-json/ diff --git a/docs/reference/at-eik-node-client.md b/docs/reference/at-eik-node-client.md new file mode 100644 index 00000000..a7232851 --- /dev/null +++ b/docs/reference/at-eik-node-client.md @@ -0,0 +1,260 @@ +--- +title: "@eik/node-client" +--- + +`@eik/node-client` is a utility for getting assets and import maps from [Eik servers](/docs/reference/at-eik-service) in Node web applications. For publishing and managing assets to an Eik server from Node scripts, see [`@eik/cli`](/docs/reference/at-eik-cli). + +## Install + +```sh +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. + +For that you use the [`file()` method](#filepathname), which returns an object `{ value, integrity }` where `value` is the link to the file. + +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` +// 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 eik = new Eik({ + development: process.env.NODE_ENV === "development", + // base is only used when `development` is `true` + base: "/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(` + + + + +`); +}); + +app.listen({ + port: 3000, +}); + +console.log("Listening on http://localhost:3000"); +``` + +### Include a ` +`; +``` + +## Constructor + +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 +import Eik from "@eik/node-client"; + +const eik = new Eik(); +await eik.load(); +``` + +### options + +| 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 | `process.cwd()` | `string` | `false` | Path to directory containing an eik.json file or package.json with eik config. | + +## API + +### async .load() + +Reads the Eik config from disk into the object instance. + +If `loadMaps` was set to `true` the import maps defined in the config will be fetched from the Eik server. + +### .file(pathname) + +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 +// 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", +// } +``` + +When `development` is `true` the pathname is prefixed with the `base` option instead of pointing to Eik. + +```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 +// } +``` + +#### 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`) | + +#### returns + +Returns an object with `value` and `integrity`: + +```js +{ + 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`: + +```js +{ + integrity: undefined, + value: '/public/path/to/script.js' +} +``` + +### .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 Eik({ + loadMaps: true, +}); +await client.load(); + +const maps = client.maps(); +const combined = maps.reduce((map, acc) => ({ ...acc, ...map }), {}); + +const html = ` + +`; +``` + +#### 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() + +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. + +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 +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 +``` + +When in development mode, the returned value will be equal to whats set on the `base` argument on the constructor. + +```js +const client = new Eik({ + development: true, + base: "http://localhost:8080/assets", +}); +await client.load(); + +client.base(); // http://localhost:8080/assets +``` diff --git a/docs/reference/at-eik-service.md b/docs/reference/at-eik-service.md new file mode 100644 index 00000000..23e9faed --- /dev/null +++ b/docs/reference/at-eik-service.md @@ -0,0 +1,111 @@ +--- +title: "@eik/service reference" +sidebar_label: "@eik/service" +--- + +[`@eik/service`](https://github.com/eik-lib/service#readme) is the [Eik server itself](/docs/server/). This document describes its JavaScript API. + +## Constructor + +Create a new Eik service instance. + +```js +import Service from "@eik/service"; +const service = new Service(options); +``` + +### options (optional) + +| option | default | type | details | +| -------------------- | ------------------------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| sink | `null` | `object` | The [storage sink] you would like to use. | +| logger | `null` | `object` | An instance of the [pino logger](https://getpino.io/). | +| customSink | `null` | `object` | Deprecated. Use `sink`. | +| aliasCacheControl | `"public, max-age=1200"` | `string` | Cache-Control header to respond with when getting an [alias](/docs/dependencies/aliases). | +| notFoundCacheControl | `"public, max-age=5"` | `string` | Cache-Control header to respond with when returning 404 Not Found. | +| pkgMaxFileSize | `10_000_000` | `number` | The limit in bytes before [PUT /pkg/:name/:version](/docs/server/http-api#upload-a-package) starts returning 413 Content Too Large. | +| mapMaxFileSize | `1_000_000` | `number` | The limit in bytes before [PUT /map/:name/:version](/docs/server/http-api#upload-an-import-map) starts returning 413 Content Too Large. | + +## API + +An Eik service instance has the following API: + +### .api() + +The Eik service as a [Fastify plugin](https://www.fastify.io/docs/latest/Plugins/). The returned function must be passed on to the Fastify `.register()` method: + +```js +import fastify from "fastify"; +import Service from "@eik/service"; + +// Set up the Eik service as a plugin +const service = new Service({ customSink: sink }); + +// Set up Fastify +const app = fastify({ + ignoreTrailingSlash: true, +}); + +// Register the Eik service in Fastify +app.register(service.api()); +``` + +This will make the [Eik HTTP API](/docs/server/http-api/) available. + +Due to how the HTTP API deals with wildcards on pathnames to resolve files, it is recommended that the `ignoreTrailingSlash` option on the Fastify constructor that the plugin is registered to is set to `true`. If this is not done, file resolving might not work as expected. + +### .health() + +Runs a health check on the Eik service. Throws if any of the health checks fails. + +The health check mainly determines if the service is able to run all methods on the configured storage sink. + +We recommend executing the health check before the service begins accepting HTTP traffic. + +```js +const run = async () => { + await service.health(); + await app.listen({ + port: service.config.get("http.port"), + host: service.config.get("http.address"), + }); +}; + +run(); +``` + +## Properties + +An Eik service instance has the following properties: + +### .metrics + +Property that exposes a metric stream. Please see the [metrics section](/docs/server/metrics/) for usage information. + +### .config + +Property that exposes configuration via [convict](https://github.com/mozilla/node-convict). + +```js +import Service from "@eik/service"; + +const service = new Service(); +service.logger.info(`Server is running in ${service.config.get("env")} mode`); +``` + +### .logger + +Property that exposes the [pino logger](https://getpino.io/) instance. + +```js +import Service from "@eik/service"; + +const service = new Service(); +service.logger.info(`Server is running in ${service.config.get("env")} mode`); +``` + +### .sink + +Property that exposes the currently used [storage sink]. + +[storage sink]: /docs/server/storage/ diff --git a/docs/server_sink.md b/docs/reference/at-eik-sink.md similarity index 55% rename from docs/server_sink.md rename to docs/reference/at-eik-sink.md index cd368a11..86480f43 100644 --- a/docs/server_sink.md +++ b/docs/reference/at-eik-sink.md @@ -1,98 +1,39 @@ --- -id: server_sink -title: Eik server - Sink -sidebar_label: Sink +title: "@eik/sink reference" +sidebar_label: "@eik/sink" --- -The Eik server has a file sink concept which caters for the posibillity to write files to, and read files from different storage backends by swapping out sink modules in the server. Because each sink implements the same public API, it is possible to use one sink in one environment and a different sink in another. +The [`@eik/sink` module](https://github.com/eik-lib/sink) specifies the public API for all [Eik storage backends](/docs/server/storage/). The [existing sinks](https://github.com/topics/eik-sink) are good examples to look at when implementing a custom sink. -## Built in sinks +## Constructor -To make it easy to start up an Eik server, the server is shipped with a couple of built in sinks. The file system sink is the default sink in use when a server is started without specifying a sink. - -### File system - -This is the default sink when you start the Eik server. The file system sink will write files to and from the local file system. - -By default all files are stored in the default OS temp folder. Do note that files stored in the default OS temp folder will, on most OSes, be deleted without warning by the OS at some point. To configure a different folder, use the `SINK_PATH` environment variable. - -```sh -SINK_PATH=/var/persistent/storage/eik node server.js -``` - -### In memory - -The in memory sink will write files to and from memory. Files written to this sink will disappear when the Eik server is restarted. This sink is handy for spinning up an Eik server to run tests against. - -To use it, set the `SINK_TYPE` environment variable to `mem`. - -## Custom sinks - -A custom sink is normally pulled in as a dependent module and passed on to the `customSink` property on the constructor of the @eik/service in a [custom server setup](/docs/server#customized-setup). - -Example of using the sink for Google Cloud Storage: - -```js -import fastify from "fastify"; -import Service from "@eik/service"; -import Sink from "@eik/sink-gcs"; - -const sink = new Sink(); -const service = new Service({ customSink: sink }); - -const app = fastify({ - ignoreTrailingSlash: true, -}); - -app.register(service.api()); -``` - -A custom sink normally takes its own set of properties, such as authentication keys etc, so please see the documentation for each sink for what's required. - -### Available custom sinks - -These custom sinks are available: - -- [Google Cloud Storage](https://github.com/eik-lib/sink-gcs) -- [Memory](https://github.com/eik-lib/sink-memory) (like the built-in, but usable with the `customSink` option rather than an environment variable) - -Please feel free to let us know if you have a custom sink you would like to have listed. - -## Implementing a custom sink - -Implementing a custom sink is fairly stright forward. A custom sink must extend the [Eik sink interface](https://github.com/eik-lib/sink) and implement all the methods in the public API and its public properties. If this is not done, the custom sink will not be usable in the Eik server since validation depends upon the extension of the interface. - -The [Google Cloud Storage sink](https://github.com/eik-lib/sink-gcs) is a good example to look at when implementing a custom sink. - -### Constructor - -A sink must be a `class` which extends the [Eik sink interface](https://github.com/eik-lib/sink). +A sink must be a `class` which extends the Eik sink interface. ```js import Sink from "@eik/sink"; class SinkCustom extends Sink { - constructor() { - super(); - } - write() {} - read() {} - delete() {} - exist() {} - get metrics() {} + constructor() { + super(); + } + write() {} + read() {} + delete() {} + exist() {} + get metrics() {} } ``` -### API +## API A sink must implement the following API: -#### write(filePath, contentType) +### write(filePath, contentType) -| argument | default | type | required | details | -| ----------- | ------- | -------- | -------- | --------------------------------------------------------------------------------------------------- | -| filePath | `null` | `string` | `true` | Pathname of the file relative to `root` in the [file structure](/docs/server_file_structure) in Eik | -| contentType | `null` | `string` | `true` | Content type of the file | +| argument | default | type | required | details | +| ----------- | ------- | -------- | -------- | ----------------------------------------------------------------------------------------------------------------------- | +| filePath | `null` | `string` | `true` | Pathname of the file relative to `root` in the [file structure](/docs/server/storage#internal-storage-structure) in Eik | +| contentType | `null` | `string` | `true` | Content type of the file | This method is called when a file is to be written to storage. The method must return a `Promise` and resolve with a `WritableStream` when the storage is ready to be written too. The server will pipe the byte stream of the file to this stream. Upon any errors, the promise should reject with an `Error` object @@ -100,7 +41,7 @@ This method is called when a file is to be written to storage. The method must r import { Writable } from 'node:stream'; import Sink from '@eik/sink'; -const SinkCustom = class SinkCustom extends Sink { +export class SinkCustom extends Sink { constructor() { super(); } @@ -113,11 +54,11 @@ const SinkCustom = class SinkCustom extends Sink { } ``` -#### read(filePath) +### read(filePath) -| argument | default | type | required | details | -| -------- | ------- | -------- | -------- | --------------------------------------------------------------------------------------------------- | -| filePath | `null` | `string` | `true` | Pathname of the file relative to `root` in the [file structure](/docs/server_file_structure) in Eik | +| argument | default | type | required | details | +| -------- | ------- | -------- | -------- | ----------------------------------------------------------------------------------------------------------------------- | +| filePath | `null` | `string` | `true` | Pathname of the file relative to `root` in the [file structure](/docs/server/storage#internal-storage-structure) in Eik | This method is called when a file is to be read from storage. The method must return a `Promise` and resolve with a `ReadableStream` when the storage is ready to be read from. Upon any errors, the promise should reject with an `Error` object @@ -125,7 +66,7 @@ This method is called when a file is to be read from storage. The method must re import { Readable } from 'node:stream'; import Sink from '@eik/sink'; -const SinkCustom = class SinkCustom extends Sink { +export class SinkCustom extends Sink { constructor() { super(); } @@ -138,29 +79,29 @@ const SinkCustom = class SinkCustom extends Sink { } ``` -#### delete(filePath) +### delete(filePath) -| argument | default | type | required | details | -| -------- | ------- | -------- | -------- | --------------------------------------------------------------------------------------------------- | -| filePath | `null` | `string` | `true` | Pathname of the file relative to `root` in the [file structure](/docs/server_file_structure) in Eik | +| argument | default | type | required | details | +| -------- | ------- | -------- | -------- | ----------------------------------------------------------------------------------------------------------------------- | +| filePath | `null` | `string` | `true` | Pathname of the file relative to `root` in the [file structure](/docs/server/storage#internal-storage-structure) in Eik | This method is called when a file is to be deleted from storage. The method must return a `Promise` and resolve with no value when the file is deleted from storage. If any errors occur, the promise should reject with an `Error` object -#### exist(filePath) +### exist(filePath) -| argument | default | type | required | details | -| -------- | ------- | -------- | -------- | --------------------------------------------------------------------------------------------------- | -| filePath | `null` | `string` | `true` | Pathname of the file relative to `root` in the [file structure](/docs/server_file_structure) in Eik | +| argument | default | type | required | details | +| -------- | ------- | -------- | -------- | ----------------------------------------------------------------------------------------------------------------------- | +| filePath | `null` | `string` | `true` | Pathname of the file relative to `root` in the [file structure](/docs/server/storage#internal-storage-structure) in Eik | This method is called to check if a file exists in storage. The method must return a `Promise` and resolve with no value if the file exists in storage. If the file does not exist the promise should reject with no error object. Upon any errors, the promise should reject with an `Error` object. -### Properties +## Properties A sink must implement the following properties: -#### .metrics +### .metrics -A getter for a [metric stream](https://github.com/metrics-js/client). The metric stream can be used to emit metrics from the sink into [the overall metric stream](/docs/server_metrics) in the server. +A getter for a [metric stream](https://github.com/metrics-js/client). The metric stream can be used to emit metrics from the sink into [the overall metric stream](/docs/server/metrics) in the server. Example: @@ -168,7 +109,7 @@ Example: import Metrics from @metrics/client'; import Sink from @eik/sink'; -const SinkCustom = class SinkCustom extends Sink { +export class SinkCustom extends Sink { constructor() { super(); this._metrics = new Metrics(); @@ -186,14 +127,14 @@ const SinkCustom = class SinkCustom extends Sink { } ``` -### Validation +## Validation We recommend you validate the arguments for all methods. The [Eik sink interface](https://github.com/eik-lib/sink) contain static methods to do so which can be used when implementing a sink: ```js import Sink from @eik/sink'; -const SinkCustom = class SinkCustom extends Sink { +export class SinkCustom extends Sink { constructor() { super(); } @@ -212,7 +153,7 @@ const SinkCustom = class SinkCustom extends Sink { } ``` -### Security +## Security A sink should take care of protecting against [Path Traversal](https://owasp.org/www-community/attacks/Path_Traversal). It should not be possible to access files outside the `root` of the file structure in Eik by passing in a hostile pathname through the REST API of Eik. Each `filePath` argument on each method should be checked for such. diff --git a/docs/reference/eik-json.md b/docs/reference/eik-json.md new file mode 100644 index 00000000..1fd12c0a --- /dev/null +++ b/docs/reference/eik-json.md @@ -0,0 +1,199 @@ +--- +title: "eik.json reference" +sidebar_label: "eik.json" +--- + +This document describes the Eik configuration schema. + +Eik is typically configured with an `eik.json` file in the same directory as your `package.json`. However, you can also place the configuration in the `"eik"` field in `package.json`. + +Any project that publishes assets to an Eik server must provide this configuration in one (and only one) of these locations. + +## Using `eik.json` + +The most common way to configure an Eik setup is to create and populate an `eik.json` file in a project's root, next to `package.json`. + +```json +{ + "$schema": "https://raw.githubusercontent.com/eik-lib/common/main/lib/schemas/eikjson.schema.json", + "name": "my-app", + "version": "1.0.0", + "server": "https://eik.store.com", + "files": "./public", + "import-map": ["https://eik.store.com/map/store/v1"] +} +``` + +## Using `package.json` + +It is also possible to configure Eik via `package.json`. + +```json +{ + "eik": { + "$schema": "https://raw.githubusercontent.com/eik-lib/common/main/lib/schemas/eikjson.schema.json", + "name": "my-app", + "version": "1.0.0", + "server": "https://eik.store.com", + "files": "./public", + "import-map": ["https://eik.store.com/map/store/v1"] + } +} +``` + +It is also possible to have Eik use the `package.json` `name` and `version` fields by omitting them from the configuration. + +```json +{ + "name": "my-app", + "version": "1.0.0", + "eik": { + "$schema": "https://raw.githubusercontent.com/eik-lib/common/main/lib/schemas/eikjson.schema.json", + "server": "https://eik.store.com", + "files": "./public", + "import-map": ["https://eik.store.com/map/store/v1"] + } +} +``` + +## Creating `eik.json` + +The [Eik CLI](/docs/reference/at-eik-cli/) includes a scaffolding tool that can be used to generate an `eik.json` file in the current directory. + +```sh +eik init +``` + +## JSON schema + +The schema is available on GitHub. Add this to your configuration to get code suggestions and inline documentation in some editors. + +```json +{ + "$schema": "https://raw.githubusercontent.com/eik-lib/common/main/lib/schemas/eikjson.schema.json" +} +``` + +### name + +- required + +Defines the value that will be used on the Eik server to configure the name for the project. This should be unique to an organisation. + +See [storage sink](/docs/server/storage#internal-storage-structure) for more information. + +### version + +- required + +Defines the current Eik package version using [semantic versioning](https://semver.org/). + +This must be unique to a given package name within an organisation. Attempting to republish the same version a second time will fail. + +You can manually update this value or use the [`eik version` command](/docs/reference/at-eik-cli#version) to automate the process. + +```json +{ + "version": "1.0.0" +} +``` + +### server + +- required + +Defines the URL of the Eik server that the project will publish to. + +See the [server docs](/docs/server) for how to setup and configure an Eik server. + +```json +{ + "server": "https://eik.store.com" +} +``` + +### files + +- required + +Defines files to publish. + +This can be a string defining a folder or a single entrypoint, or it can be an object that maps publish paths to local file system file locations. + +In this example, all files in the `./public` folder would be uploaded to the Eik server. + +```json +{ + "files": "./public" +} +``` + +You can use glob syntax to decide which files to include. + +```json +{ + "files": "./public/**/*.js" +} +``` + +You can configure `"files"` as an object to map different files or folders on disk to public paths. + +Keys define publish locations on the Eik server and values define the local file entrypoint locations. This somewhat resembles [package entrypoints](https://nodejs.org/dist/latest/docs/api/packages.html#package-entry-points in Node. + +```jsonc +{ + "files": { + // `./path/to/esm.js` is uploaded and renamed to `/script.js` + "script.js": "./path/to/esm.js", + + // `/absolute/path/to/esm.js` is uploaded and renamed to `/script.js` + "script.js": "/absolute/path/to/esm.js", + + // everything in `./path/to/folder` is uploaded to `/folder` + "folder": "./path/to/folder", + + // everything in `./path/to/folder` is uploaded to `/folder` (but no folder recursion) + "folder": "./path/to/folder/*", + + // everything in `./path/to/folder` is uploaded to `/folder/scripts` + "folder/scripts": "path/to/folder/**/*", + }, +} +``` + +### import-map + +- optional + +Configure [import maps](/docs/dependencies/import-maps) that will be [used during a build](/docs/introduction/workflow#build-time-import-mapping). +This can be specified as a single string or as an array of strings if you want to use more than one import map in the build. + +```json +{ + "import-map": "https://eik.store.com/map/store/v1" +} +``` + +or + +```json +{ + "import-map": [ + "https://eik.store.com/map/store/v1", + "https://eik.store.com/map/store-2/v1" + ] +} +``` + +### out + +- optional +- default: `./.eik` + +Configure the Eik build directory. Eik will store resource integrity information in this directory, and may use it for other features in the future. + +```json +{ + "out": "./eik" +} +``` diff --git a/docs/server.md b/docs/server.md deleted file mode 100644 index dda245ae..00000000 --- a/docs/server.md +++ /dev/null @@ -1,71 +0,0 @@ ---- -id: server -title: Server -sidebar_label: Server ---- - -The Eik server is a Node.js application distributed as the [`@eik/service`](https://www.npmjs.com/package/@eik/service) NPM package. It aims to be runnable as a service out of the box but still be flexible and customizable enough to suite a wide range of infrastructures needs. - -The only requirement to run the Eik server is [Node.js 18.x or newer](https://nodejs.org/). - -The fastest way to test a running instance of the Eik server is by using `npx`: - -```sh -npx @eik/service -``` - -This will install the latest Eik server and start it at [http://localhost:4001](http://localhost:4001). By default all uploaded assets will be stored in a sub-folder of the OS temp directory (NOTE: they will be lost when the OS clears the temp directory). The default authentication key is `change_me`. - -## Production setup - -The Eik service builds on [Fastify](https://www.fastify.io/) and is in addition to being a standalone server also exposed as a Fastify plugin. This makes it flexible and easy to set up a custom Eik service. - -Example of a custom server using the sink for Google Cloud Storage and extending the HTTP API with custom ready checks: - -```js -import fastify from "fastify"; -import Service from "@eik/service"; -import Sink from "@eik/sink-gcs"; - -// Set up the Google Cloud Storage sink -// https://github.com/eik-lib/sink-gcs?tab=readme-ov-file#example -const sink = new Sink({ - credentials: { - client_email: "a@email.address", - private_key: "[ ...snip... ]", - projectId: "myProject", - }, -}); - -// Set up the Eik service as a plugin -const service = new Service({ customSink: sink }); - -// Set up Fastify -const app = fastify({ - ignoreTrailingSlash: true, - modifyCoreObjects: false, - trustProxy: true, -}); - -// Register the Eik service in Fastify -app.register(service.api()); - -// Append custom HTTP ready checks -app.get("/_/health", (request, reply) => { - reply.send("ok"); -}); - -app.get("/_/ready", (request, reply) => { - reply.send("ok"); -}); - -// Start the server -const run = async () => { - await service.health(); - await app.listen( - service.config.get("http.port"), - service.config.get("http.address"), - ); -}; -run(); -``` diff --git a/docs/server/http-api.md b/docs/server/http-api.md new file mode 100644 index 00000000..39bc37ce --- /dev/null +++ b/docs/server/http-api.md @@ -0,0 +1,566 @@ +--- +title: HTTP API +--- + +This document describes the different HTTP APIs available on a running Eik server. + +## Authentication + +Authentication is needed to execute multiple API calls in the REST API. + +### Endpoint Summary Table + +| Name | Verb | Endpoint | Form Fields | +| --------------- | ---- | ------------- | ----------- | +| [Login](#login) | POST | `/auth/login` | `key` | + +### Login + +**Method:** `POST` + +Logs a user in to the service. + +```bash +https://:assetServerUrl:port/auth/login +``` + +Form parameters: + +- `key` an authentication key + +Status codes: + +- `200` if user is authorized +- `401` if user is not authorized + +Success response: A jwt token + +```json +{ + "token": "..." +} +``` + +Example: + +```bash +curl -X POST -i -F key=rfm940c3 http://localhost:4001/auth/login +``` + +## Packages + +A packages is a set of files (javascript, css etc) that is intended to be referenced from an HTML document and +loaded by a browser. + +Packages are versioned and consist of one or more files. A package of a specific version is immutable. + +### Endpoint Summary Table + +| Name | Verb | Endpoint | Form Fields | +| ----------------------------------------- | ---- | ----------------------------- | ----------- | +| [Public Package URL](#public-package-url) | GET | `/pkg/:name/:version/:extras` | | +| [Upload a Package](#upload-a-package) | PUT | `/pkg/:name/:version` | `package` | + +### Public Package URL + +**Method:** `GET` + +Retrieves files from a package on the service. + +```bash +https://:assetServerUrl:port/pkg/:name/:version/:extras +``` + +URL parameters: + +- `:name` is the name of the package. Validator: Comply with [npm package names](https://github.com/npm/validate-npm-package-name). +- `:version` is the version of the package. Validator: Comply with [semver validation regex](https://semver.org/). +- `:extras` whildcard pathname to any file in the package + +Status codes: + +- `200` if file is successfully retrieved +- `404` if file is not found + +Example: + +```bash +curl -X GET http://localhost:4001/pkg/fuzz/8.4.1/main/index.js +``` + +### Upload a package + +**Method:** `PUT` + +Puts a new package at the service. + +```bash +https://:assetServerUrl:port/pkg/:name/:version +``` + +URL parameters: + +- `:name` is the name of the package. Validator: Comply with [npm package names](https://github.com/npm/validate-npm-package-name). +- `:version` is the version of the package. Validator: Comply with [semver validation regex](https://semver.org/). + +Form parameters: + +- `package` a `tar` or `tar.gz` containing the files of the package + +HTTP headers: + +- `Authorization` a jwt authorization bearer with the token retrieved from a successful [authentication](#login) + +Status codes: + +- `303` if package is successfully uploaded. `location` is root of module +- `400` if validation in URL parameters or form fields fails +- `401` if user is not authorized +- `409` if package already exists or version in a major range is not newer than previous version in a major range +- `413` if package is too large +- `415` if file format of the uploaded file is unsupported +- `422` if a entry in the uploaded file could not be parsed or errored +- `502` if package could not be written to the sink + +Example: + +```bash +curl -X PUT -i -F package=@archive.tgz -H "Authorization: Bearer {:token}" http://localhost:4001/pkg/fuzz/8.4.1 +``` + +### Latest Package versions + +**Method:** `GET` + +Retrieves an overview of the latest major versions of a package. + +```bash +https://:assetServerUrl:port/pkg/:name +``` + +URL parameters: + +- `:name` is the name of the package. Validator: Comply with [npm package names](https://github.com/npm/validate-npm-package-name). + +Status codes: + +- `200` if file is successfully retrieved +- `404` if file is not found + +Example: + +```bash +curl -X GET http://localhost:4001/pkg/fuzz +``` + +### Package version overview + +**Method:** `GET` + +Retrieves an overview of the files of a package version. + +```bash +https://:assetServerUrl:port/pkg/:name/:version +``` + +URL parameters: + +- `:name` is the name of the package. Validator: Comply with [npm package names](https://github.com/npm/validate-npm-package-name). +- `:version` is the version of the package. Validator: Comply with [semver validation regex](https://semver.org/). + +Status codes: + +- `200` if file is successfully retrieved +- `404` if file is not found + +Example: + +```bash +curl -X GET http://localhost:4001/pkg/fuzz +``` + +## NPM Packages + +An NPM package is a local replica of a package found in the [NPM registry](https://www.npmjs.com/) intended to be referenced from an HTML document and +loaded by a browser. In most cases, with some exceptions, an NPM Package will be a library or utillity that other [Packages](#packages) depend upon. + +NPM Packages are versioned and consist of one or more files. An NPM package of a specific version is immutable. + +### Endpoint Summary Table + +| Name | Verb | Endpoint | Form Fields | +| ------------------------------------------------- | ---- | ----------------------------- | ----------- | +| [Public NPM Package URL](#public-npm-package-url) | GET | `/npm/:name/:version/:extras` | | +| [Upload an NPM Package](#upload-a-npm-package) | PUT | `/npm/:name/:version` | `package` | + +### Public NPM Package URL + +**Method:** `GET` + +Retrieves files from an NPM package on the service. + +```bash +https://:assetServerUrl:port/npm/:name/:version/:extras +``` + +URL parameters: + +- `:name` is the name of the NPM package. Validator: Comply with [npm package names](https://github.com/npm/validate-npm-package-name). +- `:version` is the version of the NPM package. Validator: Comply with [semver validation regex](https://semver.org/). +- `:extras` wildcard pathname to any file in the NPM package + +Status codes: + +- `200` if file is successfully retrieved +- `404` if file is not found + +Example: + +```bash +curl -X GET http://localhost:4001/npm/fuzz/8.4.1/main/index.js +``` + +### Upload a NPM Package + +**Method:** `PUT` + +Puts a new NPM package on the service. + +```bash +https://:assetServerUrl:port/npm/:name/:version +``` + +URL parameters: + +- `:name` is the name of the NPM package. Validator: Comply with [npm package names](https://github.com/npm/validate-npm-package-name). +- `:version` is the version of the NPM package. Validator: Comply with [semver validation regex](https://semver.org/). + +Form parameters: + +- `package` a `tar` or `tar.gz` containing the files of the NPM package + +HTTP headers: + +- `Authorization` a JWT authorization bearer with the token retrieved from a successful [authentication](#login) + +Status codes: + +- `303` if NPM package is successfully uploaded. `location` is root of module +- `400` if validation in URL parameters or form fields fails +- `401` if user is not authorized +- `409` if NPM package already exist or version in a major range is not newer than previous version in a major range +- `413` if package is too large +- `415` if file format of the uploaded file is unsupported +- `422` if a entry in the uploaded file could not be parsed or errored +- `502` if NPM package could not be written to the sink + +Example: + +```bash +curl -X PUT -i -F package=@archive.tgz -H "Authorization: Bearer {:token}" http://localhost:4001/npm/fuzz/8.4.1 +``` + +### Latest NPM Package versions + +**Method:** `GET` + +Retrieves an overview of the latest major versions of an NPM package. + +```bash +https://:assetServerUrl:port/npm/:name +``` + +URL parameters: + +- `:name` is the name of the NPM package. Validator: Comply with [npm package names](https://github.com/npm/validate-npm-package-name). + +Status codes: + +- `200` if file is successfully retrieved +- `404` if file is not found + +Example: + +```bash +curl -X GET http://localhost:4001/npm/fuzz +``` + +### NPM Package version overview + +**Method:** `GET` + +Retrieves an overview of the files of an NPM package version. + +```bash +https://:assetServerUrl:port/npm/:name/:version +``` + +URL parameters: + +- `:name` is the name of the NPM package. Validator: Comply with [npm package names](https://github.com/npm/validate-npm-package-name). +- `:version` is the version of the NPM package. Validator: Comply with [semver validation regex](https://semver.org/). + +Status codes: + +- `200` if file is successfully retrieved +- `404` if file is not found + +Example: + +```bash +curl -X GET http://localhost:4001/npm/fuzz +``` + +## Import Maps + +An [import map](https://github.com/WICG/import-maps) holds a mapping or a set of mappings between ECMA Script Module (ESM) bare imports and an absolute URL. +Import maps are versioned and are immutable. + +### Endpoint Summary Table + +| Name | Verb | Endpoint | Form Fields | +| ----------------------------------------------- | ---- | --------------------- | ----------- | +| [Public Import Map URL](#public-import-map-url) | GET | `/map/:name/:version` | | +| [Upload an Import Map](#upload-an-import-map) | PUT | `/map/:name/:version` | `map` | + +### Public Import Map URL + +**Method:** `GET` + +Retrieves an import map from the service. + +```bash +https://:assetServerUrl:port/map/:name/:version +``` + +URL parameters: + +- `:name` is the name of the import map. Validator: Comply with [npm package names](https://github.com/npm/validate-npm-package-name). +- `:version` is the version of the import map. Validator: Comply with [semver validation regex](https://semver.org/). + +Status codes: + +- `200` if import map is successfully retrieved +- `404` if import map is not found + +Example: + +```bash +curl -X GET http://localhost:4001/map/buzz/8.4.1 +``` + +### Upload an Import Map + +**Method:** `PUT` + +Puts a new import map at the service. + +```bash +https://:assetServerUrl:port/map/:name/:version +``` + +URL parameters: + +- `:name` is the name of the import map. Validator: Comply with [npm package names](https://github.com/npm/validate-npm-package-name). +- `:version` is the version of the import map. Validator: Comply with [semver validation regex](https://semver.org/). + +Form parameters: + +- `map` a `json` file (the import map) + +HTTP headers: + +- `Authorization` a jwt authorization bearer with the token retrieved from a successful [authentication](#login) + +Status codes: + +- `303` if import map is successfully uploaded. `location` is [Public Import Map URL](#public-import-map-url) +- `400` if validation in URL parameters or form fields fails +- `401` if user is not authorized +- `409` if import map already exist +- `415` if file format of the uploaded import map is unsupported +- `502` if import map could not be written to the sink + +Example: + +```bash +curl -X PUT -i -F map=@import-map.json -H "Authorization: Bearer {:token}" http://localhost:4001/map/buzz/8.4.1 +``` + +### Latest Import Map versions + +**Method:** `GET` + +Retrieves an overview of the latest versions of a Import Map. + +```bash +https://:assetServerUrl:port/map/:name +``` + +URL parameters: + +- `:name` is the name of the import map. Validator: Comply with [npm package names](https://github.com/npm/validate-npm-package-name). + +Status codes: + +- `200` if file is successfully retrieved +- `404` if file is not found + +Example: + +```bash +curl -X GET http://localhost:4001/map/buzz +``` + +## Aliases + +An alias is a shorthand between a major version of a package / import map and the set exact version of the package / import map. + +### Endpoint Summary Table + +| Name | Verb | Endpoint | Form Fields | +| ------------------------------------- | ------ | ------------------------------ | ----------- | +| [Public Alias URL](#public-alias-url) | GET | `/:type/:name/v:alias/:extras` | | +| [Create Alias](#create-alias) | PUT | `/:type/:name/v:alias` | `version` | +| [Update Alias](#update-alias) | POST | `/:type/:name/v:alias` | `version` | +| [Delete Alias](#delete-alias) | DELETE | `/:type/:name/v:alias` | | + +### Public Alias URL + +**Method:** `GET` + +Retrieves files from a package or an import map at the service. + +```bash +https://:assetServerUrl:port/:type/:name/v:alias/:extras +``` + +URL parameters: + +- `:type` is the type to retrieve from. Validator: `pkg`, `npm` or `map`. +- `:name` is the name of the package / import map. Validator: Comply with [npm package names](https://github.com/npm/validate-npm-package-name). +- `:alias` is the major version of the package / import map. Validator: Comply with [semver validation regex](https://semver.org/). +- `:extras` whildcard pathname to any file in a package. Does not apply to import maps. + +Status codes: + +- `302` if alias exist +- `404` if alias is not found + +Example: + +```bash +curl -X GET -L http://localhost:4001/pkg/fuzz/v8/main/index.js +curl -X GET -L http://localhost:4001/map/buzz/v4 +``` + +### Create Alias + +**Method:** `PUT` + +Create a new alias. + +```bash +https://:assetServerUrl:port/:type/:name/v:alias +``` + +URL parameters: + +- `:type` is the type to retrieve from. Validator: `pkg`, `npm` or `map`. +- `:name` is the name of the package / import map. Validator: Comply with [npm package names](https://github.com/npm/validate-npm-package-name). +- `:alias` is the major version of the package / import map. Validator: Comply with [semver validation regex](https://semver.org/). + +Form parameters: + +- `:version` full version of the package to be aliased + +HTTP headers: + +- `Authorization` a jwt authorization bearer with the token retrieved from a successful [authentication](#login) + +Status codes: + +- `303` if alias is successfully created. `location` points to the alias +- `400` if validation in URL parameters or form fields fails +- `401` if user is not authorized +- `409` if alias already exist +- `502` if alias could not be altered by the sink + +Example: + +```bash +curl -X PUT -i -F version=8.4.1 -H "Authorization: Bearer {:token}" http://localhost:4001/pkg/fuzz/v8 +curl -X PUT -i -F version=4.2.2 -H "Authorization: Bearer {:token}" http://localhost:4001/map/buzz/v4 +``` + +### Update Alias + +**Method:** `POST` + +Updates an existing alias. + +```bash +https://:assetServerUrl:port/:type/:name/v:alias +``` + +URL parameters: + +- `:type` is the type to retrieve from. Validator: `pkg`, `npm` or `map`. +- `:name` is the name of the package / import map. Validator: Comply with [npm package names](https://github.com/npm/validate-npm-package-name). +- `:alias` is the major version of the package / import map. Validator: Comply with [semver validation regex](https://semver.org/). + +Form parameters: + +- `:version` full version of the package to be aliased + +HTTP headers: + +- `Authorization` a jwt authorization bearer with the token retrieved from a successful [authentication](#login) + +Status codes: + +- `303` if alias is successfully created. `location` points to the alias +- `401` if user is not authorized +- `404` if alias does not exist +- `502` if alias could not be altered by the sink + +Example: + +```bash +curl -X POST -i -F version=8.4.1 -H "Authorization: Bearer {:token}" http://localhost:4001/pkg/fuzz/v8 +curl -X POST -i -F version=4.4.2 -H "Authorization: Bearer {:token}" http://localhost:4001/map/buzz/v4 +``` + +### Delete Alias + +**Method:** `DELETE` + +Deletes an existing alias. + +```bash +https://:assetServerUrl:port/:type/:name/v:alias +``` + +URL parameters: + +- `:type` is the type to retrieve from. Validator: `pkg`, `npm` or `map`. +- `:name` is the name of the package / import map. Validator: Comply with [npm package names](https://github.com/npm/validate-npm-package-name). +- `:alias` is the major version of the package / import map. Validator: Comply with [semver validation regex](https://semver.org/). + +HTTP headers: + +- `Authorization` a jwt authorization bearer with the token retrieved from a successful [authentication](#login) + +Status codes: + +- `204` if alias is successfully deleted +- `401` if user is not authorized +- `404` if alias does not exist +- `502` if alias could not be altered by the sink + +Example: + +```bash +curl -X DELETE -H "Authorization: Bearer {:token}" http://localhost:4001/pkg/fuzz/v8 +curl -X DELETE -H "Authorization: Bearer {:token}" http://localhost:4001/map/buzz/v4 +``` diff --git a/docs/server/metrics.md b/docs/server/metrics.md new file mode 100644 index 00000000..9bd4228f --- /dev/null +++ b/docs/server/metrics.md @@ -0,0 +1,70 @@ +--- +title: Metrics +--- + +`@eik/service` exposes a [metrics stream](https://metrics-js.github.io/) that can give insight on how the server is performing. + +## Usage + +You access the metrics stream on the `.metric` property of the Eik service. In this example we'll set up an endpoint so [Prometheus](https://prometheus.io/) can periodically fetch the metrics. + +:::tip + +The metrics stream isn't tied specifically to Prometheus. +See [the MetricsJS documentation](https://metrics-js.github.io/introduction/getting-started/#create-a-consumer) to learn how to consume these metrics if you don't use Prometheus. + +::: + +```js +import MetricsConsumer from "@metrics/prometheus-consumer"; +import prometheus from "prom-client"; +import Service from "@eik/service"; +import fastify from "fastify"; + +const service = new Service(); + +const metricsConsumer = new MetricsConsumer({ + client: prometheus, +}); + +service.metrics.pipe(metricsConsumer); + +const app = fastify(); +app.register(service.api()); + +app.get("/_/metrics", (request, reply) => { + reply.type(metricsConsumer.registry.contentType); + reply.send(metricsConsumer.registry.metrics()); +}); + +const run = async () => { + await service.health(); + await app.listen({ + port: service.config.get("http.port"), + host: service.config.get("http.address"), + }); +}; + +run(); +``` + +## Available metrics + +Each metric provided by the server has a unique `name` and a `type` defining what type (counter, histogram, etc) of +metric it is. + +The server exposes these metrics. + +| Name | Type | Description | +| ----------------------------- | --------- | --------------------------------------------------------------------------------------- | +| eik_core_auth_post_handler | histogram | Time taken in a [login](http-api.md#login) method | +| eik_core_pkg_get_handler | histogram | Time taken in a [public package](http-api.md#public-package-url) method | +| eik_core_pkg_log_handler | histogram | Time taken in a [package version overview](http-api.md#package-version-overview) method | +| eik_core_pkg_put_handler | histogram | Time taken in a [upload package](http-api.md#upload-a-package) method | +| eik_core_versions_get_handler | histogram | Time taken in a [latest package versions](http-api.md#latest-package-versions) method | +| eik_core_alias_get_handler | histogram | Time taken in a [public alias](http-api.md#public-alias-url) method | +| eik_core_alias_put_handler | histogram | Time taken in a [create alias](http-api.md#create-alias) method | +| eik_core_alias_post_handler | histogram | Time taken in a [update alias](http-api.md#update-alias) method | +| eik_core_alias_del_handler | histogram | Time taken in a [delete alias](http-api.md#delete-alias) method | +| eik_core_map_get_handler | histogram | Time taken in a [public import maps](http-api.md#public-import-map-url) method | +| eik_core_map_put_handler | histogram | Time taken in a [upload import maps](http-api.md#upload-an-import-map) method | diff --git a/docs/server/server.md b/docs/server/server.md new file mode 100644 index 00000000..934e313d --- /dev/null +++ b/docs/server/server.md @@ -0,0 +1,125 @@ +--- +title: Setting up an Eik server +--- + +The Eik server is a Node.js application distributed on npm as [`@eik/service`](https://www.npmjs.com/package/@eik/service). It aims to be runnable as a service out of the box, but still be flexible and customizable enough to suite a wide range of infrastructures needs. + +The only requirement to run the Eik server is a current long-term support (LTS) version of [Node.js](https://nodejs.org/). + +## Testing an Eik server from the command line + +The fastest way to test a running instance of the Eik server is by using `npx`. + +```sh +npx @eik/service +``` + +This will install the latest Eik server and start it at [http://localhost:4001](http://localhost:4001). + +:::warning + +By default all uploads are stored in a temporary directory that will be lost when your operating system cleans it up. To configure a persistent storage, set the `SINK_PATH` environment variable to a suitable location on disk. + +::: + +## Production setup + +`@eik/service` also includes a [Fastify](https://www.fastify.dev/) plugin, so you can configure and extend the Eik service to fit your needs. + +In this example we set up an Eik server using the local file system for storage. [Use or implement a different sink](/docs/server/storage/) if you need other another storage solution. + +Create a new project and install the required dependencies + +```sh +mkdir eik-server +cd eik-server +npm init -y +npm install fastify @eik/service @eik/sink-file-system +``` + +Create `index.js` and add the following. + +```js +import path from "node:path"; +import fastify from "fastify"; +import Service from "@eik/service"; +import Sink from "@eik/sink-file-system"; + +const sink = new Sink({ + sinkFsRootPath: path.join(process.cwd(), ".eik", "storage"), +}); + +const service = new Service({ sink }); + +const app = fastify({ + ignoreTrailingSlash: true, + modifyCoreObjects: false, + trustProxy: true, +}); + +app.register(service.api()); + +const run = async () => { + await service.health(); + await app.listen({ + port: service.config.get("http.port"), + host: service.config.get("http.address"), + }); +}; + +run(); +``` + +### Configuring for production + +`@eik/service` reads YAML configuration from `config/` based on `NODE_ENV`. + +Add `config/production.yaml` to change secrets and other commonly used settings. + +```sh +mkdir config +touch config/development.yaml +touch config/production.yaml +``` + +See [the `@eik/service` config](https://github.com/eik-lib/service/blob/main/lib/config.js#L39) to see what values you can configure. Here's a typical `config/production.yaml`. + +```yaml +organization: + # replace with your organisation name + name: store + hostnames: + - localhost + # replace with your Eik server's domain + - eik.store.com +jwt: + # replace with the absolute path to a file holding your + # jwt secret as plaintext (uses convict-format-secrets) + secret: /var/run/secrets/auth_jwt_secret + +basicAuth: + # replace with the absolute path to a file holding your + # basic auth key as plaintext (uses convict-format-secrets) + key: /var/run/secrets/basic_auth_key + +http: + port: 8080 + address: "0.0.0.0" + +log: + level: info +``` + +### Run your Eik server + +To run your Eik server with Node in production: + +```sh +NODE_ENV=production node index.js +``` + +To run your Eik server with Node in development: + +``` +NODE_ENV=development node index.js +``` diff --git a/docs/server/storage.md b/docs/server/storage.md new file mode 100644 index 00000000..b5db9911 --- /dev/null +++ b/docs/server/storage.md @@ -0,0 +1,164 @@ +--- +title: Storage sinks +--- + +Eik uses a [sink interface](/docs/reference/at-eik-sink/) for storage. This design makes it possible to drop in different storage backends, for instance switching between local file storage for development and a cloud storage provider for production. + +## Built in sinks + +`@eik/service` ships with two built in sinks, intended for local development and testing: + +- File system +- Memory + +### File system + +This is the default sink when you start the Eik server. The file system sink will write files to and from the local file system. + +:::warning + +By default all files are stored in the default OS temp folder. Do note that files stored in the default OS temp folder will, on most OSes, be deleted without warning by the OS at some point. To configure a different folder, use the `SINK_PATH` environment variable. + +::: + +You can also import `@eik/sink-file-system` and configure the sink that way. + +```js +import Service from "@eik/service"; +import Sink from "@eik/sink-file-system"; + +const sink = new Sink({ + sinkFsRootPath: path.join(process.cwd(), ".eik", "storage"), +}); + +const service = new Service({ sink }); +``` + +### In memory + +The in memory sink will write files to and from memory. Files written to this sink will disappear when the Eik server is restarted. This sink is handy for spinning up an Eik server to run tests. + +To use it, set the `SINK_TYPE` environment variable to `mem`. + +You can also import `@eik/sink-memory` and configure the sink that way. + +```js +import Service from "@eik/service"; +import Sink from "@eik/sink-memory"; + +const sink = new Sink(); + +const service = new Service({ sink }); +``` + +## Custom sinks + +A custom sink is normally pulled in as a dependent module and passed on to the `sink` property on the constructor of the `@eik/service` in a [production setup](/docs/server#production-setup). + +A custom sink takes its own set of properties, such as authentication keys, so please see the documentation for each sink for what's required. + +### Available custom sinks + +These custom sinks are available: + +- [Google Cloud Storage](https://github.com/eik-lib/sink-gcs) + +Feel free to open a pull request to list a custom sink you've made, and use the [`eik-sink` topic](https://github.com/topics/eik-sink) if you publish on GitHub. + +```js +import fastify from "fastify"; +import Service from "@eik/service"; +import Sink from "@eik/sink-gcs"; + +const sink = new Sink({ + credentials: { + client_email: "an@email.address", + private_key: "[ ...snip... ]", + projectId: "myProject", + }, +}); +const service = new Service({ sink }); + +const app = fastify({ + ignoreTrailingSlash: true, +}); + +app.register(service.api()); +``` + +### Making a custom sink + +A custom sink must extend the [Eik sink interface](/docs/reference/at-eik-sink/) and implement all the methods in the public API and its public properties. + +## Internal storage structure + +The Eik server stores files in the following structure inside the storage sink. + +```sh +:root +└── :org + ├── map + │   └── :name + │   ├── :version.import-map.json + │   ├── :major.alias.json + │ └── versions.json + ├── pkg + │ └── :name + │ ├── :version + │ │   ├── * + │ ├── :version.package.json + │ ├── :major.alias.json + │ └── versions.json + └── npm + └── :name + ├── :version + │   ├── * + ├── :version.package.json + ├── :major.alias.json + └── versions.json +``` + +Parameters: + +- `:root` is the root folder for everything. +- `:org` is the name of an organisation. +- `:name` is the name of a package. +- `:version` is the full semver version of a package. +- `:major` is the major semver version of a full semver version of a package. + +### Packages + +Packages are stored under `/:root/:org/pkg/:name/:version/` and the structure of a package is +arbitrary and untouched during upload by the service. + +The file structure of a package is stored in a package file at `/:root/:org/pkg/:name/:version.package.json`. + +### npm + +NPM packages are packages from the NPM registry that are then published to Eik as a "copy". Packages from +the NPM registry are published under this namespace to avoid name collision with other packages. + +NPM packages are stored under `/:root/:org/npm/:name/:version/` and the structure of a package is +arbitrary and is not changed during upload by the service. + +The file structure of an NPM package is stored in a package meta file at `/:root/:org/pkg/:name/:version.package.json`. + +### Import maps + +Import maps are stored under `/:root/:org/map/:name/:version.import-map.json`. The filename of +import maps is strict and conforms to the version number it's given with a `.json` extension. + +The filename of import maps prior to uploading to the service can be anything. The service will +convert the file name according to the parameters given when uploading it. + +### Aliases + +Packages, NPM packages and import map versions can be aliased. An alias is a semver major version of a +full semver version and is a way to map a major version to a given full semver version of a +package or import map. + +This alias mapping is stored alongside the version of a package or import map version: + +- Package alias path: `/:root/:org/pkg/:name/:major.alias.json` +- NPM package alias path: `/:root/:org/npm/:name/:major.alias.json` +- Import map alias path: `/:root/:org/map/:name/:major.alias.json` diff --git a/docs/server_api.md b/docs/server_api.md deleted file mode 100644 index 2b273f16..00000000 --- a/docs/server_api.md +++ /dev/null @@ -1,122 +0,0 @@ ---- -id: server_api -title: Eik server - Programatic API -sidebar_label: Programatic API ---- - -The Eik service is distributed as a [Fastify](https://www.fastify.io/) plugin and has the following programatic API: - -## Constructor - -Create a new Eik service instance. - -```js -import Service from "@eik/service"; -const service = new Service(options); -``` - -### options (optional) - -An Object containing misc configuration. The following values can be provided: - -| option | default | type | required | details | -| ---------- | ------- | -------- | -------- | ------------- | -| customSink | `null` | `object` | `false` | A custom sink | - -#### customSink - -A custom sink. The sink must extend the [sink interface](https://github.com/eik-lib/sink). - -Example using the [Google Cloud Storage sink](https://github.com/eik-lib/sink-gcs): - -```js -import Service from "@eik/service"; -import Sink from "@eik/sink-gcs"; - -// Set up the Google Cloud Storage sink -const sink = new Sink(); - -// Set up the Eik service as a plugin -const service = new Service({ customSink: sink }); -``` - -## API - -An Eik service instance has the following API: - -### .api() - -The Eik service as a [Fastify plugin](https://www.fastify.io/docs/latest/Plugins/). The returned function must be passed on to the Fastify `.register()` method: - -```js -import fastify from "fastify"; -import Service from "@eik/service"; - -// Set up the Eik service as a plugin -const service = new Service({ customSink: sink }); - -// Set up Fastify -const app = fastify({ - ignoreTrailingSlash: true, -}); - -// Register the Eik service in Fastify -app.register(service.api()); -``` - -This will mount the [Eik REST API](/docs/server_rest_api) into the Fastify application the plugin is registered to. - -Due to how the REST API deals with wildcards on pathnames to resolve files, it is recommended that the `ignoreTrailingSlash` option on the Fastify constructor that the plugin is registered to is set to `true`. If this is not done, file resolving might not work as expected. - -### .health() (async) - -Executes a health check on the Eik service. The health check mainly determines if the service is able to execute all methods needed to function properly using the current configured sink. - -We recommend executing the health check before the service begins accepting HTTP traffic: - -```js -const run = async () => { - await service.health(); - await app.listen( - service.config.get("http.port"), - service.config.get("http.address"), - ); -}; -run(); -``` - -Throws if any of the health checks fails. - -## Properties - -An Eik service instance has the following properties: - -### .metrics - -Property that exposes a metric stream. Please see the [metrics section](/docs/server_metrics) for further documentation. - -### .config - -Property that exposes internal configuration. Can be used to retrieve internal configuration. Config is built upon [Node Convict](https://github.com/mozilla/node-convict). - -```js -import Service from "@eik/service"; - -const service = new Service(); -service.logger.info(`Server is running in ${service.config.get("env")} mode`); -``` - -### .logger - -Property that exposes the internal logger. Can be used to do additional logging. The internal logger is [Pino](https://github.com/pinojs/pino). - -```js -import Service from "@eik/service"; - -const service = new Service(); -service.logger.info(`Server is running in ${service.config.get("env")} mode`); -``` - -### .sink - -Property that exposes the currently used sink. Please see the [sink section](/docs/server_metrics) for further documentation. diff --git a/docs/server_file_structure.md b/docs/server_file_structure.md deleted file mode 100644 index 9d0743c0..00000000 --- a/docs/server_file_structure.md +++ /dev/null @@ -1,76 +0,0 @@ ---- -id: server_file_structure -title: Eik server - File structure -sidebar_label: File structure ---- - -The EIK server stores files in the following structure: - -```sh -:root -└── :org - ├── map - │   └── :name - │   ├── :version.import-map.json - │   ├── :major.alias.json - │ └── versions.json - ├── pkg - │ └── :name - │ ├── :version - │ │   ├── * - │ ├── :version.package.json - │ ├── :major.alias.json - │ └── versions.json - └── npm - └── :name - ├── :version - │   ├── * - ├── :version.package.json - ├── :major.alias.json - └── versions.json -``` - -Parameters: - -- `:root` is the root folder for everything. -- `:org` is the name of an organisation. -- `:name` is the name of a package. -- `:version` is the full semver version of a package. -- `:major` is the major semver version of a full semver version of a package. - -## Packages - -Packages are stored under `/:root/:org/pkg/:name/:version/` and the structure of a package is -arbitrary and untouched during upload by the service. - -The file structure of a package is stored in a package file at `/:root/:org/pkg/:name/:version.package.json`. - -## NPM Packages - -NPM packages are packages from the NPM registry that are then published to Eik as a "copy". Packages from -the NPM registry are published under this namespace to avoid name collision with other packages. - -NPM packages are stored under `/:root/:org/npm/:name/:version/` and the structure of a package is -arbitrary and is not changed during upload by the service. - -The file structure of an NPM package is stored in a package meta file at `/:root/:org/pkg/:name/:version.package.json`. - -## Import Maps - -Import maps are stored under `/:root/:org/map/:name/:version.import-map.json`. The filename of -import maps is strict and conforms to the version number it's given with a `.json` extension. - -The filename of import maps prior to uploading to the service can be anything. The service will -convert the file name according to the parameters given when uploading it. - -## Aliases - -Packages, NPM packages and import map versions can be aliased. An alias is a semver major version of a -full semver version and is a way to map a major version to a given full semver version of a -package or import map. - -This alias mapping is stored alongside the version of a package or import map version: - -- Package alias path: `/:root/:org/pkg/:name/:major.alias.json` -- NPM package alias path: `/:root/:org/npm/:name/:major.alias.json` -- Import map alias path: `/:root/:org/map/:name/:major.alias.json` diff --git a/docs/server_metrics.md b/docs/server_metrics.md deleted file mode 100644 index b1c46de9..00000000 --- a/docs/server_metrics.md +++ /dev/null @@ -1,76 +0,0 @@ ---- -id: server_metrics -title: Eik server - Metrics -sidebar_label: Metrics ---- - -The Eik server exposes a [metric stream](https://github.com/metrics-js/client) which emits internal metrics -on the server. These metrics are intended to give numbers on how the server as an application is behaving and -performing and is an important tool in the toolbox for monitoring the servers health status. These metrics do -not provide statistics on assets uploaded to the asset server. - -The metrics the server provides are not bound to any particular metric system so it's possible to provide the metrics to -any monitoring system as preferred. The metric stream emits a set of generic metric objects that can be altered and -piped as desired. - -Please see [@metrics/client](https://github.com/metrics-js/client) for examples of how to consume these metrics in -your preferred monitoring system. - -## Usage - -The `@eik/service` module exposes a `.metric` property. This property holds a plain Node.js object stream which -emits the metrics as objects on the stream. - -This stream can be piped into a metrics consumer or any other Node.js writable / transform stream for further -processing. - -Example of metrics being piped into a prometheus consumer: - -```js -import MetricsConsumer from "@metrics/prometheus-consumer"; -import prometheus from "prom-client"; -import Service from "@eik/service"; -import fastify from "fastify"; - -const service = new Service(); - -const metricsConsumer = new MetricsConsumer({ - client: prometheus, -}); - -service.metrics.pipe(metricsConsumer); - -const app = fastify(); -app.register(service.api()); - -app.get("/_/metrics", (request, reply) => { - reply.type(metricsConsumer.registry.contentType); - reply.send(metricsConsumer.registry.metrics()); -}); - -const run = async () => { - await app.listen(8080, "0.0.0.0"); -}; -run(); -``` - -## Metrics - -Each metric provided by the server has a unique `name` and a `type` defining what type (counter, histogram, etc) of -metric it is. - -The server exposes these metrics: - -| Name | Type | Description | -| ----------------------------- | --------- | ---------------------------------------------------------------------------------------------- | -| eik_core_auth_post_handler | histogram | Time taken in a [login](server_rest_api.md#login) method | -| eik_core_pkg_get_handler | histogram | Time taken in a [public package](server_rest_api.md#public-package-url) method | -| eik_core_pkg_log_handler | histogram | Time taken in a [package version overview](server_rest_api.md#package-version-overview) method | -| eik_core_pkg_put_handler | histogram | Time taken in a [upload package](server_rest_api.md#upload-a-package) method | -| eik_core_versions_get_handler | histogram | Time taken in a [latest package versions](server_rest_api.md#latest-package-versions) method | -| eik_core_alias_get_handler | histogram | Time taken in a [public alias](server_rest_api.md#public-alias-url) method | -| eik_core_alias_put_handler | histogram | Time taken in a [create alias](server_rest_api.md#create-alias) method | -| eik_core_alias_post_handler | histogram | Time taken in a [update alias](server_rest_api.md#update-alias) method | -| eik_core_alias_del_handler | histogram | Time taken in a [delete alias](server_rest_api.md#delete-alias) method | -| eik_core_map_get_handler | histogram | Time taken in a [public import maps](server_rest_api.md#public-import-maps-url) method | -| eik_core_map_put_handler | histogram | Time taken in a [upload import maps](server_rest_api.md#upload-an-import-map) method | diff --git a/docs/server_rest_api.md b/docs/server_rest_api.md deleted file mode 100644 index f08c830d..00000000 --- a/docs/server_rest_api.md +++ /dev/null @@ -1,569 +0,0 @@ ---- -id: server_rest_api -title: Eik server - REST API -sidebar_label: REST API ---- - -The EIK server has the following REST API: - -## Authentication - -Authentication is needed to execute multiple API calls in the REST API. - -### Endpoint Summary Table - -| Name | Verb | Endpoint | Form Fields | -| ---------------------------- | ---- | --------------------- | ----------- | -| [Login](#login) | POST | `/auth/login` | `key` | - -### Login - -**Method:** `POST` - -Logs a user in to the service. - -```bash -https://:assetServerUrl:port/auth/login -``` - -Form parameters: - -- `key` an authentication key - -Status codes: - -- `200` if user is authorized -- `401` if user is not authorized - -Success response: A jwt token - -```json -{ - "token": "..." -} -``` - -Example: - -```bash -curl -X POST -i -F key=rfm940c3 http://localhost:4001/auth/login -``` - -## Packages - -A packages is a set of files (javascript, css etc) that is intended to be referenced from an HTML document and -loaded by a browser. - -Packages are versioned and consist of one or more files. A package of a specific version is immutable. - -### Endpoint Summary Table - -| Name | Verb | Endpoint | Form Fields | -| ----------------------------------------- | ---- | ----------------------------- | ----------- | -| [Public Package URL](#public-package-url) | GET | `/pkg/:name/:version/:extras` | | -| [Upload a Package](#upload-a-package) | PUT | `/pkg/:name/:version` | `package` | - -### Public Package URL - -**Method:** `GET` - -Retrieves files from a package on the service. - -```bash -https://:assetServerUrl:port/pkg/:name/:version/:extras -``` - -URL parameters: - -- `:name` is the name of the package. Validator: Comply with [npm package names](https://github.com/npm/validate-npm-package-name). -- `:version` is the version of the package. Validator: Comply with [semver validation regex](https://semver.org/). -- `:extras` whildcard pathname to any file in the package - -Status codes: - -- `200` if file is successfully retrieved -- `404` if file is not found - -Example: - -```bash -curl -X GET http://localhost:4001/pkg/fuzz/8.4.1/main/index.js -``` - -### Upload a package - -**Method:** `PUT` - -Puts a new package at the service. - -```bash -https://:assetServerUrl:port/pkg/:name/:version -``` - -URL parameters: - -- `:name` is the name of the package. Validator: Comply with [npm package names](https://github.com/npm/validate-npm-package-name). -- `:version` is the version of the package. Validator: Comply with [semver validation regex](https://semver.org/). - -Form parameters: - -- `package` a `tar` or `tar.gz` containing the files of the package - -HTTP headers: - -- `Authorization` a jwt authorization bearer with the token retrieved from a successful [authentication](#login) - -Status codes: - -- `303` if package is successfully uploaded. `location` is root of module -- `400` if validation in URL parameters or form fields fails -- `401` if user is not authorized -- `409` if package already exists or version in a major range is not newer than previous version in a major range -- `413` if package is too large -- `415` if file format of the uploaded file is unsupported -- `422` if a entry in the uploaded file could not be parsed or errored -- `502` if package could not be written to the sink - -Example: - -```bash -curl -X PUT -i -F package=@archive.tgz -H "Authorization: Bearer {:token}" http://localhost:4001/pkg/fuzz/8.4.1 -``` - -### Latest Package versions - -**Method:** `GET` - -Retrieves an overview of the latest major versions of a package. - -```bash -https://:assetServerUrl:port/pkg/:name -``` - -URL parameters: - -- `:name` is the name of the package. Validator: Comply with [npm package names](https://github.com/npm/validate-npm-package-name). - -Status codes: - -- `200` if file is successfully retrieved -- `404` if file is not found - -Example: - -```bash -curl -X GET http://localhost:4001/pkg/fuzz -``` - -### Package version overview - -**Method:** `GET` - -Retrieves an overview of the files of a package version. - -```bash -https://:assetServerUrl:port/pkg/:name/:version -``` - -URL parameters: - -- `:name` is the name of the package. Validator: Comply with [npm package names](https://github.com/npm/validate-npm-package-name). -- `:version` is the version of the package. Validator: Comply with [semver validation regex](https://semver.org/). - -Status codes: - -- `200` if file is successfully retrieved -- `404` if file is not found - -Example: - -```bash -curl -X GET http://localhost:4001/pkg/fuzz -``` - -## NPM Packages - -An NPM package is a local replica of a package found in the [NPM registry](https://www.npmjs.com/) intended to be referenced from an HTML document and -loaded by a browser. In most cases, with some exceptions, an NPM Package will be a library or utillity that other [Packages](#packages) depend upon. - -NPM Packages are versioned and consist of one or more files. An NPM package of a specific version is immutable. - -### Endpoint Summary Table - -| Name | Verb | Endpoint | Form Fields | -| ------------------------------------------------- | ---- | ----------------------------- | ----------- | -| [Public NPM Package URL](#public-npm-package-url) | GET | `/npm/:name/:version/:extras` | | -| [Upload an NPM Package](#upload-a-npm-package) | PUT | `/npm/:name/:version` | `package` | - -### Public NPM Package URL - -**Method:** `GET` - -Retrieves files from an NPM package on the service. - -```bash -https://:assetServerUrl:port/npm/:name/:version/:extras -``` - -URL parameters: - -- `:name` is the name of the NPM package. Validator: Comply with [npm package names](https://github.com/npm/validate-npm-package-name). -- `:version` is the version of the NPM package. Validator: Comply with [semver validation regex](https://semver.org/). -- `:extras` wildcard pathname to any file in the NPM package - -Status codes: - -- `200` if file is successfully retrieved -- `404` if file is not found - -Example: - -```bash -curl -X GET http://localhost:4001/npm/fuzz/8.4.1/main/index.js -``` - -### Upload a NPM Package - -**Method:** `PUT` - -Puts a new NPM package on the service. - -```bash -https://:assetServerUrl:port/npm/:name/:version -``` - -URL parameters: - -- `:name` is the name of the NPM package. Validator: Comply with [npm package names](https://github.com/npm/validate-npm-package-name). -- `:version` is the version of the NPM package. Validator: Comply with [semver validation regex](https://semver.org/). - -Form parameters: - -- `package` a `tar` or `tar.gz` containing the files of the NPM package - -HTTP headers: - -- `Authorization` a JWT authorization bearer with the token retrieved from a successful [authentication](#login) - -Status codes: - -- `303` if NPM package is successfully uploaded. `location` is root of module -- `400` if validation in URL parameters or form fields fails -- `401` if user is not authorized -- `409` if NPM package already exist or version in a major range is not newer than previous version in a major range -- `413` if package is too large -- `415` if file format of the uploaded file is unsupported -- `422` if a entry in the uploaded file could not be parsed or errored -- `502` if NPM package could not be written to the sink - -Example: - -```bash -curl -X PUT -i -F package=@archive.tgz -H "Authorization: Bearer {:token}" http://localhost:4001/npm/fuzz/8.4.1 -``` - -### Latest NPM Package versions - -**Method:** `GET` - -Retrieves an overview of the latest major versions of an NPM package. - -```bash -https://:assetServerUrl:port/npm/:name -``` - -URL parameters: - -- `:name` is the name of the NPM package. Validator: Comply with [npm package names](https://github.com/npm/validate-npm-package-name). - -Status codes: - -- `200` if file is successfully retrieved -- `404` if file is not found - -Example: - -```bash -curl -X GET http://localhost:4001/npm/fuzz -``` - -### NPM Package version overview - -**Method:** `GET` - -Retrieves an overview of the files of an NPM package version. - -```bash -https://:assetServerUrl:port/npm/:name/:version -``` - -URL parameters: - -- `:name` is the name of the NPM package. Validator: Comply with [npm package names](https://github.com/npm/validate-npm-package-name). -- `:version` is the version of the NPM package. Validator: Comply with [semver validation regex](https://semver.org/). - -Status codes: - -- `200` if file is successfully retrieved -- `404` if file is not found - -Example: - -```bash -curl -X GET http://localhost:4001/npm/fuzz -``` - -## Import Maps - -An [import map](https://github.com/WICG/import-maps) holds a mapping or a set of mappings between ECMA Script Module (ESM) bare imports and an absolute URL. -Import maps are versioned and are immutable. - -### Endpoint Summary Table - -| Name | Verb | Endpoint | Form Fields | -| ----------------------------------------------- | ---- | --------------------- | ----------- | -| [Public Import Map URL](#public-import-map-url) | GET | `/map/:name/:version` | | -| [Upload an Import Map](#upload-an-import-map) | PUT | `/map/:name/:version` | `map` | - -### Public Import Maps URL - -**Method:** `GET` - -Retrieves an import map from the service. - -```bash -https://:assetServerUrl:port/map/:name/:version -``` - -URL parameters: - -- `:name` is the name of the import map. Validator: Comply with [npm package names](https://github.com/npm/validate-npm-package-name). -- `:version` is the version of the import map. Validator: Comply with [semver validation regex](https://semver.org/). - -Status codes: - -- `200` if import map is successfully retrieved -- `404` if import map is not found - -Example: - -```bash -curl -X GET http://localhost:4001/map/buzz/8.4.1 -``` - -### Upload an Import Map - -**Method:** `PUT` - -Puts a new import map at the service. - -```bash -https://:assetServerUrl:port/map/:name/:version -``` - -URL parameters: - -- `:name` is the name of the import map. Validator: Comply with [npm package names](https://github.com/npm/validate-npm-package-name). -- `:version` is the version of the import map. Validator: Comply with [semver validation regex](https://semver.org/). - -Form parameters: - -- `map` a `json` file (the import map) - -HTTP headers: - -- `Authorization` a jwt authorization bearer with the token retrieved from a successful [authentication](#login) - -Status codes: - -- `303` if import map is successfully uploaded. `location` is [Public Import Map URL](#public-import-map-url) -- `400` if validation in URL parameters or form fields fails -- `401` if user is not authorized -- `409` if import map already exist -- `415` if file format of the uploaded import map is unsupported -- `502` if import map could not be written to the sink - -Example: - -```bash -curl -X PUT -i -F map=@import-map.json -H "Authorization: Bearer {:token}" http://localhost:4001/map/buzz/8.4.1 -``` - -### Latest Import Map versions - -**Method:** `GET` - -Retrieves an overview of the latest versions of a Import Map. - -```bash -https://:assetServerUrl:port/map/:name -``` - -URL parameters: - -- `:name` is the name of the import map. Validator: Comply with [npm package names](https://github.com/npm/validate-npm-package-name). - -Status codes: - -- `200` if file is successfully retrieved -- `404` if file is not found - -Example: - -```bash -curl -X GET http://localhost:4001/map/buzz -``` - -## Aliases - -An alias is a shorthand between a major version of a package / import map and the set exact version of the package / import map. - - -### Endpoint Summary Table - -| Name | Verb | Endpoint | Form Fields | -| ------------------------------------- | ------ | ------------------------------ | ----------- | -| [Public Alias URL](#public-alias-url) | GET | `/:type/:name/v:alias/:extras` | | -| [Create Alias](#create-alias) | PUT | `/:type/:name/v:alias` | `version` | -| [Update Alias](#update-alias) | POST | `/:type/:name/v:alias` | `version` | -| [Delete Alias](#delete-alias) | DELETE | `/:type/:name/v:alias` | | - -### Public Alias URL - -**Method:** `GET` - -Retrieves files from a package or an import map at the service. - -```bash -https://:assetServerUrl:port/:type/:name/v:alias/:extras -``` - -URL parameters: - -- `:type` is the type to retrieve from. Validator: `pkg`, `npm` or `map`. -- `:name` is the name of the package / import map. Validator: Comply with [npm package names](https://github.com/npm/validate-npm-package-name). -- `:alias` is the major version of the package / import map. Validator: Comply with [semver validation regex](https://semver.org/). -- `:extras` whildcard pathname to any file in a package. Does not apply to import maps. - -Status codes: - -- `302` if alias exist -- `404` if alias is not found - -Example: - -```bash -curl -X GET -L http://localhost:4001/pkg/fuzz/v8/main/index.js -curl -X GET -L http://localhost:4001/map/buzz/v4 -``` - -### Create Alias - -**Method:** `PUT` - -Create a new alias. - -```bash -https://:assetServerUrl:port/:type/:name/v:alias -``` - -URL parameters: - -- `:type` is the type to retrieve from. Validator: `pkg`, `npm` or `map`. -- `:name` is the name of the package / import map. Validator: Comply with [npm package names](https://github.com/npm/validate-npm-package-name). -- `:alias` is the major version of the package / import map. Validator: Comply with [semver validation regex](https://semver.org/). - -Form parameters: - -- `:version` full version of the package to be aliased - -HTTP headers: - -- `Authorization` a jwt authorization bearer with the token retrieved from a successful [authentication](#login) - -Status codes: - -- `303` if alias is successfully created. `location` points to the alias -- `400` if validation in URL parameters or form fields fails -- `401` if user is not authorized -- `409` if alias already exist -- `502` if alias could not be altered by the sink - -Example: - -```bash -curl -X PUT -i -F version=8.4.1 -H "Authorization: Bearer {:token}" http://localhost:4001/pkg/fuzz/v8 -curl -X PUT -i -F version=4.2.2 -H "Authorization: Bearer {:token}" http://localhost:4001/map/buzz/v4 -``` - -### Update Alias - -**Method:** `POST` - -Updates an existing alias. - -```bash -https://:assetServerUrl:port/:type/:name/v:alias -``` - -URL parameters: - -- `:type` is the type to retrieve from. Validator: `pkg`, `npm` or `map`. -- `:name` is the name of the package / import map. Validator: Comply with [npm package names](https://github.com/npm/validate-npm-package-name). -- `:alias` is the major version of the package / import map. Validator: Comply with [semver validation regex](https://semver.org/). - -Form parameters: - -- `:version` full version of the package to be aliased - -HTTP headers: - -- `Authorization` a jwt authorization bearer with the token retrieved from a successful [authentication](#login) - -Status codes: - -- `303` if alias is successfully created. `location` points to the alias -- `401` if user is not authorized -- `404` if alias does not exist -- `502` if alias could not be altered by the sink - -Example: - -```bash -curl -X POST -i -F version=8.4.1 -H "Authorization: Bearer {:token}" http://localhost:4001/pkg/fuzz/v8 -curl -X POST -i -F version=4.4.2 -H "Authorization: Bearer {:token}" http://localhost:4001/map/buzz/v4 -``` - -### Delete Alias - -**Method:** `DELETE` - -Deletes an existing alias. - -```bash -https://:assetServerUrl:port/:type/:name/v:alias -``` - -URL parameters: - -- `:type` is the type to retrieve from. Validator: `pkg`, `npm` or `map`. -- `:name` is the name of the package / import map. Validator: Comply with [npm package names](https://github.com/npm/validate-npm-package-name). -- `:alias` is the major version of the package / import map. Validator: Comply with [semver validation regex](https://semver.org/). - -HTTP headers: - -- `Authorization` a jwt authorization bearer with the token retrieved from a successful [authentication](#login) - -Status codes: - -- `204` if alias is successfully deleted -- `401` if user is not authorized -- `404` if alias does not exist -- `502` if alias could not be altered by the sink - -Example: - -```bash -curl -X DELETE -H "Authorization: Bearer {:token}" http://localhost:4001/pkg/fuzz/v8 -curl -X DELETE -H "Authorization: Bearer {:token}" http://localhost:4001/map/buzz/v4 -``` diff --git a/docusaurus.config.js b/docusaurus.config.js index bbe66dec..14155e9d 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -1,51 +1,154 @@ module.exports = { - title: 'Eik', - tagline: 'A modern ESM and CSS asset server', - url: 'https://eik.dev', - baseUrl: '/', - favicon: 'img/favicon.ico', - organizationName: 'eik-lib', // Usually your GitHub org/user name. - projectName: 'eik-lib.github.io', // Usually your repo name. - themeConfig: { - navbar: { - title: 'Eik', - logo: { - alt: 'Eik Logo', - src: 'img/logo.svg', - }, - items: [ - { - to: 'docs/overview', - activeBasePath: 'docs', - label: 'Documentation', - position: 'left', - }, - { - href: 'https://github.com/eik-lib', - label: 'GitHub', - position: 'right', - }, - ], - }, - footer: { - style: 'dark', - copyright: `Copyright © ${new Date().getFullYear()} - FINN.no.`, - }, - prism: { - theme: require('prism-react-renderer').themes.github, - }, - }, - presets: [ - [ - '@docusaurus/preset-classic', - { - docs: { - sidebarPath: require.resolve('./sidebars.js') - }, - theme: { - customCss: require.resolve('./src/css/custom.css'), - }, - }, - ], - ], + title: "Eik", + tagline: "A modern ESM and CSS asset server", + url: "https://eik.dev", + baseUrl: "/", + favicon: "img/favicon.ico", + organizationName: "eik-lib", // Usually your GitHub org/user name. + projectName: "eik-lib.github.io", // Usually your repo name. + plugins: [ + require.resolve("docusaurus-lunr-search"), + [ + // Add redirects when moving pages around + // https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-client-redirects#configuration + "@docusaurus/plugin-client-redirects", + { + redirects: [ + { + from: "/docs/overview", + to: "/docs/introduction", + }, + { + from: "/docs/overview_concepts", + to: "/docs/dependencies/introduction", + }, + { + from: "/docs/overview_workflow", + to: "/docs/introduction/workflow", + }, + { + from: "/docs/overview_eik_json", + to: "/docs/reference/eik-json", + }, + { + from: "/docs/client", + to: "/docs/introduction/workflow", + }, + { + from: "/docs/client_installation", + to: "/docs/reference/at-eik-cli", + }, + { + from: "/docs/client_login", + to: "/docs/reference/at-eik-cli", + }, + { + from: "/docs/client_app_packages", + to: "/docs/introduction/workflow", + }, + { + from: "/docs/client_npm_packages", + to: "/docs/dependencies/npm", + }, + { + from: "/docs/client_import_maps", + to: "/docs/dependencies/import-maps", + }, + { + from: "/docs/client_aliases", + to: "/docs/dependencies/aliases", + }, + { + from: "/docs/client_putting_it_all_together", + to: "/docs/introduction/workflow", + }, + { + from: "/docs/mapping_import_map", + to: "/docs/dependencies/import-maps", + }, + { + from: "/docs/mapping_browser", + to: "/docs/introduction#import-mapping", + }, + { + from: "/docs/mapping_plugins", + to: "/docs/introduction/workflow#build-time-import-mapping", + }, + { + from: "/docs/server_api", + to: "/docs/reference/at-eik-service", + }, + { + from: "/docs/server_sink", + to: "/docs/server/storage", + }, + { + from: "/docs/server_rest_api", + to: "/docs/server/http-api", + }, + { + from: "/docs/server_file_structure", + to: "/docs/server/storage#internal-storage-structure", + }, + { + from: "/docs/server_metrics", + to: "/docs/server/metrics", + }, + { + from: "/docs/ci", + to: "/docs/guides/github-actions", + }, + { + from: "/docs/travis", + to: "/docs/guides/travis", + }, + ], + }, + ], + ], + presets: [ + [ + "@docusaurus/preset-classic", + /** @type {import('@docusaurus/preset-classic').Options} */ + ({ + docs: { + sidebarPath: require.resolve("./sidebars.js"), + editUrl: + "https://github.com/eik-lib/eik-lib.github.io/tree/source/docs", + }, + theme: { + customCss: require.resolve("./src/css/custom.css"), + }, + }), + ], + ], + themeConfig: { + navbar: { + title: "Eik", + logo: { + alt: "Eik Logo", + src: "img/logo.svg", + }, + items: [ + { + to: "/docs/introduction/", + activeBasePath: "docs", + label: "Documentation", + position: "left", + }, + { + href: "https://github.com/eik-lib", + label: "GitHub", + position: "right", + }, + ], + }, + footer: { + style: "dark", + copyright: `Copyright © ${new Date().getFullYear()} - FINN.no.`, + }, + prism: { + theme: require("prism-react-renderer").themes.github, + }, + }, }; diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 00000000..0fd1e982 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,15 @@ +import config from "@eik/eslint-config"; +import react from "eslint-plugin-react"; + +export default [ + ...config, + react.configs.flat.recommended, + { + settings: { + react: { + version: "18.2.0", + }, + }, + }, + { ignores: [".docusaurus/*", "build/*"] }, +]; diff --git a/package.json b/package.json index dc280a20..a89267f1 100644 --- a/package.json +++ b/package.json @@ -3,18 +3,31 @@ "version": "0.0.0", "private": true, "scripts": { - "start": "docusaurus start --no-open", "build": "docusaurus build", - "swizzle": "docusaurus swizzle", - "deploy": "docusaurus deploy" + "clean": "rimraf node_modules .docusaurus build", + "deploy": "docusaurus deploy", + "dev": "docusaurus start --no-open", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "start": "docusaurus serve --no-open" }, "dependencies": { - "@docusaurus/core": "3.1.1", - "@docusaurus/preset-classic": "3.1.1", + "@docusaurus/core": "3.4.0", + "@docusaurus/plugin-client-redirects": "3.4.0", + "@docusaurus/preset-classic": "3.4.0", "classnames": "2.5.1", + "docusaurus-lunr-search": "3.4.0", "react": "18.2.0", "react-dom": "18.2.0" }, + "devDependencies": { + "@eik/eslint-config": "1.0.0", + "@eik/prettier-config": "1.0.1", + "eslint": "9.8.0", + "eslint-plugin-react": "7.35.0", + "prettier": "3.3.3", + "rimraf": "6.0.1" + }, "browserslist": { "production": [ ">0.2%", diff --git a/prettier.config.mjs b/prettier.config.mjs new file mode 100644 index 00000000..8983a3cf --- /dev/null +++ b/prettier.config.mjs @@ -0,0 +1,3 @@ +import config from "@eik/prettier-config"; + +export default config; diff --git a/renovate.json b/renovate.json index e3415220..19645ce2 100644 --- a/renovate.json +++ b/renovate.json @@ -1,26 +1,4 @@ { - "extends": [ - "config:base", - "group:linters" - ], - "packageRules": [{ - "depTypeList": ["dependencies"], - "schedule": ["before 4am on monday"], - "automerge": true, - "major": { - "automerge": false - } - }, - { - "depTypeList": ["devDependencies"], - "schedule": ["before 4am on monday"], - "automerge": true, - "major": { - "automerge": false - } - }, - { - "groupName": "docusaurus release packages", - "packagePatterns": ["^@docusaurus/"] - }] + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["github>eik-lib/renovate-presets:top-level-module"] } diff --git a/sidebars.js b/sidebars.js index 8d2dd92a..4baeda6c 100644 --- a/sidebars.js +++ b/sidebars.js @@ -1,37 +1,57 @@ +/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ module.exports = { - mainSidebar: { - Overview: [ - 'overview', - 'overview_concepts', - 'overview_workflow', - 'overview_eik_json', - ], - Client: [ - 'client', - 'client_installation', - 'client_login', - 'client_app_packages', - 'client_npm_packages', - 'client_import_maps', - 'client_aliases', - 'client_putting_it_all_together', - ], - Mapping: [ - 'mapping_import_map', - 'mapping_browser', - 'mapping_plugins', - ], - Server: [ - 'server', - 'server_api', - 'server_sink', - 'server_rest_api', - 'server_file_structure', - 'server_metrics', - ], - 'CI Setup': [ - 'ci', - 'travis' - ] - }, + sidebar: [ + { + type: "category", + label: "Introduction", + collapsed: false, + items: ["introduction/introduction", "introduction/workflow"], + }, + { + type: "category", + label: "Managing dependencies", + collapsed: false, + items: [ + "dependencies/introduction", + "dependencies/npm", + "dependencies/aliases", + "dependencies/import-maps", + ], + }, + { + type: "category", + label: "Managing the Eik server", + collapsed: false, + items: [ + "server/server", + "server/storage", + "server/http-api", + "server/metrics", + ], + }, + { + type: "category", + label: "Guides", + collapsed: false, + items: [ + "guides/browser-importmap", + "guides/esbuild", + "guides/rollup", + "guides/postcss", + "guides/webpack", + "guides/github-actions", + "guides/travis", + ], + }, + { + type: "category", + label: "Reference", + items: [ + { + type: "autogenerated", + dirName: "reference", + }, + ], + }, + ], }; diff --git a/src/css/custom.css b/src/css/custom.css index 175accc0..d25af789 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -7,30 +7,34 @@ /* You can override the default Infima variables here. */ :root { - --eik-brand-color: #f7c800; - --eik-brand-color-darker: #deb400; - --eik-brand-color-lighter: #ffd41d; + --eik-brand-color: #f7c800; + --eik-brand-color-darker: #deb400; + --eik-brand-color-lighter: #ffd41d; +} + +body { + tab-size: 2; } .hero--primary { - --ifm-hero-background-color: var(--eik-brand-color); + --ifm-hero-background-color: var(--eik-brand-color); } :where(.button--primary) { - --ifm-button-background-color: var(--eik-brand-color); - --ifm-button-border-color: var(--eik-brand-color); + --ifm-button-background-color: var(--eik-brand-color); + --ifm-button-border-color: var(--eik-brand-color); } .button--primary.button--active, .button--primary:active, :where(.button--primary):not(.button--outline):hover { - --ifm-button-background-color: var(--eik-brand-color-darker); - --ifm-button-border-color: var(--eik-brand-color-darker); + --ifm-button-background-color: var(--eik-brand-color-darker); + --ifm-button-border-color: var(--eik-brand-color-darker); } .docusaurus-highlight-code-line { - background-color: rgb(72, 77, 91); - display: block; - margin: 0 calc(-1 * var(--ifm-pre-padding)); - padding: 0 var(--ifm-pre-padding); + background-color: rgb(72, 77, 91); + display: block; + margin: 0 calc(-1 * var(--ifm-pre-padding)); + padding: 0 var(--ifm-pre-padding); } diff --git a/src/pages/index.js b/src/pages/index.js index 4c69248a..5a6e170e 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -1,43 +1,64 @@ -import React from 'react'; -import classnames from 'classnames'; -import Layout from '@theme/Layout'; -import Link from '@docusaurus/Link'; -import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; -import useBaseUrl from '@docusaurus/useBaseUrl'; -import styles from './styles.module.css'; +import React from "react"; +import classnames from "classnames"; +import Layout from "@theme/Layout"; +import Link from "@docusaurus/Link"; +import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; +import useBaseUrl from "@docusaurus/useBaseUrl"; +import styles from "./styles.module.css"; function Home() { - const context = useDocusaurusContext(); - const {siteConfig = {}} = context; - const logo = useBaseUrl('img/eik-logo-main.svg'); - return ( - -
-
-
- {siteConfig.title} -
-
-
-
-

{siteConfig.title}

-

{siteConfig.tagline}

-
- - Publishing to Eik - - - Hosting an Eik server - - - Why Eik? - -
-
-
- ); + const context = useDocusaurusContext(); + const { siteConfig = {} } = context; + const logo = useBaseUrl("img/eik-logo-main.svg"); + return ( + +
+
+
+ {siteConfig.title} +
+
+
+
+

{siteConfig.title}

+

{siteConfig.tagline}

+
+ + Get started using Eik + +
+

+ Eik provides{" "} + + performant code reuse + {" "} + in multi-page and micro-frontend web applications by leveraging both + the browser and ES module caches . Host shared ES modules in a central + location and use semantically versioned aliases to shorten JavaScript + load and execution times. +

+ An illustration of a user journey across three pages all using the same version of Lit hosted on Eik, leveraging the browser and ES module cache +

+ Click the links below to learn more about the problem Eik solves and + how to host your own Eik server. +

+
+ Introduction to Eik + Setting up an Eik server +
+
+
+ ); } export default Home; diff --git a/src/pages/styles.module.css b/src/pages/styles.module.css index 3871b795..c0fef1ba 100644 --- a/src/pages/styles.module.css +++ b/src/pages/styles.module.css @@ -5,51 +5,65 @@ */ .heroBanner { - padding: 4rem 0; - text-align: center; - position: relative; - overflow: hidden; + padding: 4rem 0; + text-align: center; + position: relative; + overflow: hidden; } @media screen and (max-width: 966px) { - .heroBanner { - padding: 2rem; - } + .heroBanner { + padding: 2rem; + } } .cta { - width: 100%; - display: flex; - flex-direction: column; - align-items: center; - margin-top: 2.4rem; - margin-bottom: 4.2rem; + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + margin-top: 2.4rem; + margin-bottom: 4.2rem; } .indexCtas { - --ifm-button-size-multiplier: 1.6; - display: flex; - flex-wrap: wrap; - align-items: center; - margin-top: 24px; + --ifm-button-size-multiplier: 1.6; + display: flex; + flex-wrap: wrap; + align-items: center; + margin-top: 24px; } .indexCtas a:first-of-type { - margin-right: 1rem; + margin-right: 1rem; } .indexCtas a, .indexCtas a:hover { - color: var(--ifm-color-content); + color: var(--ifm-color-content); } .indexCtas a:first-of-type, .indexCtas a:first-of-type:hover { - color: black; + color: black; } .topImage { - height: 360px; - width: 360px; - margin-bottom: 20px; + height: 360px; + width: 360px; + margin-bottom: 20px; +} + +.pitch { + margin-top: 42px; + max-width: 60ch; + text-align: center; +} + +.learnMoreCtas { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 16px; + margin-top: 24px; } diff --git a/static/img/overview_page_to_page_diff_versions.min.svg b/static/img/overview_page_to_page_diff_versions.min.svg index e1fe0768..6085d349 100644 --- a/static/img/overview_page_to_page_diff_versions.min.svg +++ b/static/img/overview_page_to_page_diff_versions.min.svg @@ -1 +1 @@ -/1.2.0/lit-html.js (fetched)site.com/1.1.1/lit-html.js (fetched)site.com/shop/1.1.2/lit-html.js (fetched)site.com/checkout \ No newline at end of file +/1.2.0/lit-html.js (fetched)store.com/1.1.1/lit-html.js (fetched)store.com/shop/1.1.2/lit-html.js (fetched)store.com/checkout \ No newline at end of file diff --git a/static/img/overview_page_to_page_flow.min.svg b/static/img/overview_page_to_page_flow.min.svg index 7c7eb4e7..edb9500c 100644 --- a/static/img/overview_page_to_page_flow.min.svg +++ b/static/img/overview_page_to_page_flow.min.svg @@ -1 +1 @@ -site.comsite.com/shopsite.com/checkout \ No newline at end of file +store.comstore.com/shopstore.com/checkout \ No newline at end of file diff --git a/static/img/overview_page_to_page_same_versions.min.svg b/static/img/overview_page_to_page_same_versions.min.svg index df5d1692..5db52cb6 100644 --- a/static/img/overview_page_to_page_same_versions.min.svg +++ b/static/img/overview_page_to_page_same_versions.min.svg @@ -1 +1 @@ -/v1/lit-html.js (fetched)site.com/v1/lit-html.js (cached)site.com/shop/v1/lit-html.js (cached)site.com/checkout \ No newline at end of file +/v1/lit-html.js (fetched)store.com/v1/lit-html.js (cached)store.com/shop/v1/lit-html.js (cached)store.com/checkout \ No newline at end of file diff --git a/static/org/page_to_page_diff_versions.svg b/static/org/page_to_page_diff_versions.svg index 9f83bc3f..00736bcc 100644 --- a/static/org/page_to_page_diff_versions.svg +++ b/static/org/page_to_page_diff_versions.svg @@ -329,7 +329,7 @@ id="tspan4678-5-4" x="9.8829927" y="175.03801" - style="font-size:4.23333359px;stroke-width:0.26458332">site.com + style="font-size:4.23333359px;stroke-width:0.26458332">store.com site.com/shop + style="font-size:4.23333359px;stroke-width:0.26458332">store.com/shop site.com/checkout + style="font-size:4.23333359px;stroke-width:0.26458332">store.com/checkout site.com + style="font-size:4.23333359px;stroke-width:0.26458332">store.com site.com/shop + style="font-size:4.23333359px;stroke-width:0.26458332">store.com/shop site.com/checkout + style="font-size:4.23333359px;stroke-width:0.26458332">store.com/checkout /v1/lit-html.jssite.com/v1/lit-html.jssite.com/shop/v1/lit-html.jssite.com/checkout \ No newline at end of file +/v1/lit-html.jsstore.com/v1/lit-html.jsstore.com/shop/v1/lit-html.jsstore.com/checkout \ No newline at end of file diff --git a/static/org/page_to_page_same_versions.svg b/static/org/page_to_page_same_versions.svg index a48ccfe5..11f0b069 100644 --- a/static/org/page_to_page_same_versions.svg +++ b/static/org/page_to_page_same_versions.svg @@ -329,7 +329,7 @@ id="tspan4678-5-4" x="9.8829927" y="175.03801" - style="font-size:4.23333359px;stroke-width:0.26458332">site.com + style="font-size:4.23333359px;stroke-width:0.26458332">store.com site.com/shop + style="font-size:4.23333359px;stroke-width:0.26458332">store.com/shop site.com/checkout + style="font-size:4.23333359px;stroke-width:0.26458332">store.com/checkout