diff --git a/.changeset/large-crews-divide.md b/.changeset/large-crews-divide.md new file mode 100644 index 000000000..de2b5cce7 --- /dev/null +++ b/.changeset/large-crews-divide.md @@ -0,0 +1,5 @@ +--- +"@web5/browser": patch +--- + +Initial publish diff --git a/.changeset/serious-ads-cheer.md b/.changeset/serious-ads-cheer.md new file mode 100644 index 000000000..205d55ff2 --- /dev/null +++ b/.changeset/serious-ads-cheer.md @@ -0,0 +1,5 @@ +--- +"@web5/api": patch +--- + +Moving web-features to @web5/browser package diff --git a/.github/workflows/alpha-npm.yml b/.github/workflows/alpha-npm.yml index a722ea5fd..810f26c12 100644 --- a/.github/workflows/alpha-npm.yml +++ b/.github/workflows/alpha-npm.yml @@ -22,7 +22,7 @@ jobs: env: # Packages not listed here will be excluded from publishing # These are currently in a specific order due to dependency requirements - PACKAGES: "crypto crypto-aws-kms common dids credentials agent identity-agent proxy-agent user-agent api" + PACKAGES: "common crypto crypto-aws-kms dids credentials agent identity-agent proxy-agent user-agent api browser" steps: - name: Checkout source diff --git a/.github/workflows/release-snapshot.yml b/.github/workflows/release-snapshot.yml index 2c14375dc..ba8566741 100644 --- a/.github/workflows/release-snapshot.yml +++ b/.github/workflows/release-snapshot.yml @@ -27,6 +27,7 @@ jobs: [ "agent", "api", + "browser", "common", "credentials", "crypto", diff --git a/.github/workflows/tests-ci.yml b/.github/workflows/tests-ci.yml index 208117b60..c96cb48c9 100644 --- a/.github/workflows/tests-ci.yml +++ b/.github/workflows/tests-ci.yml @@ -64,7 +64,7 @@ jobs: run: pnpm --recursive --stream --sequential build:tests:node - name: Run tests for all packages - run: pnpm --recursive --stream exec c8 mocha -- --color + run: pnpm --recursive --filter '!browser' --stream exec c8 mocha -- --color env: TEST_DWN_URL: http://localhost:3000 @@ -89,7 +89,7 @@ jobs: - group: "B" packages: "--filter dids --filter identity-agent" - group: "C" - packages: "--filter api" + packages: "--filter api --filter browser" - group: "D" packages: "--filter crypto" - group: "E" diff --git a/CODEOWNERS b/CODEOWNERS index 440d9cff9..bc74a78e3 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -27,4 +27,5 @@ /packages/user-agent @lirancohen @csuwildcat @shamilovtim /packages/identity-agent @lirancohen @csuwildcat @shamilovtim /packages/api @lirancohen @csuwildcat @shamilovtim @nitro-neal +/packages/browser @lirancohen @csuwildcat diff --git a/package.json b/package.json index 765a28f44..8198bb4a2 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "packages/user-agent", "packages/proxy-agent", "packages/api", - "packages/identity-agent" + "packages/identity-agent", + "packages/browser" ], "scripts": { "clean": "pnpm npkill -d $(pwd)/packages -t dist && pnpm npkill -d $(pwd) -t node_modules", diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index b147a3185..fe62c9e2d 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -30,7 +30,6 @@ export * from './protocol.js'; export * from './record.js'; export * from './vc-api.js'; export * from './web5.js'; -export * from './web-features.js'; import * as utils from './utils.js'; export { utils }; \ No newline at end of file diff --git a/packages/browser/.c8rc.json b/packages/browser/.c8rc.json new file mode 100644 index 000000000..b69be7895 --- /dev/null +++ b/packages/browser/.c8rc.json @@ -0,0 +1,19 @@ +{ + "all": true, + "cache": false, + "extension": [ + ".js" + ], + "include": [ + "tests/compiled/**/src/**" + ], + "exclude": [ + "tests/compiled/**/src/index.js", + "tests/compiled/**/src/types.js", + "tests/compiled/**/src/types/**" + ], + "reporter": [ + "cobertura", + "text" + ] +} diff --git a/packages/browser/CHANGELOG.md b/packages/browser/CHANGELOG.md new file mode 100644 index 000000000..e69de29bb diff --git a/packages/browser/README.md b/packages/browser/README.md new file mode 100644 index 000000000..1f59541ef --- /dev/null +++ b/packages/browser/README.md @@ -0,0 +1,61 @@ +# Web5 Browser package + +| Web5 tools and features to use in the browser | +| --------------------------------------------- | + +[![NPM Package][browser-npm-badge]][browser-npm-link] +[![NPM Downloads][browser-downloads-badge]][browser-npm-link] + +[![Build Status][browser-build-badge]][browser-build-link] +[![Open Issues][browser-issues-badge]][browser-issues-link] +[![Code Coverage][browser-coverage-badge]][browser-coverage-link] + +--- + +- [Web5 Browser](#introduction) + - [Activate Polyfills](#activate-polyfills) + - [Project Resources](#project-resources) + +--- + + + +This package contains browser-specific helpers for building DWAs (Decentralized Web Apps). + +### Activate Polyfills + +This enables a service worker that can handle Web5 features in the browser such as resolving DRLs that look like: `http://dweb/did:dht:abc123/protocols/read/aHR0cHM6Ly9hcmV3ZXdlYjV5ZXQuY29tL3NjaGVtYXMvcHJvdG9jb2xz/avatar` + +To enable this functionality import and run `activatePolyfills()` at the entrypoint of your project, or within an existing service worker. + +## Project Resources + +| Resource | Description | +| --------------------------------------- | ----------------------------------------------------------------------------- | +| [CODEOWNERS][codeowners-link] | Outlines the project lead(s) | +| [CODE OF CONDUCT][code-of-conduct-link] | Expected behavior for project contributors, promoting a welcoming environment | +| [CONTRIBUTING][contributing-link] | Developer guide to build, test, run, access CI, chat, discuss, file issues | +| [GOVERNANCE][governance-link] | Project governance | +| [LICENSE][license-link] | Apache License, Version 2.0 | + +[browser-npm-badge]: https://img.shields.io/npm/v/@web5/browser.svg?style=flat&color=blue&santize=true +[browser-npm-link]: https://www.npmjs.com/package/@web5/browser +[browser-downloads-badge]: https://img.shields.io/npm/dt/@web5/browser?&color=blue +[browser-build-badge]: https://img.shields.io/github/actions/workflow/status/TBD54566975/web5-js/tests-ci.yml?branch=main&label=build +[browser-build-link]: https://github.com/TBD54566975/web5-js/actions/workflows/tests-ci.yml +[browser-coverage-badge]: https://img.shields.io/codecov/c/gh/TBD54566975/web5-js/main?style=flat&token=YI87CKF1LI +[browser-coverage-link]: https://app.codecov.io/github/TBD54566975/web5-js/tree/main/packages%2Fbrowser +[browser-issues-badge]: https://img.shields.io/github/issues/TBD54566975/web5-js/package:%20browser?label=issues +[browser-issues-link]: https://github.com/TBD54566975/web5-js/issues?q=is%3Aopen+is%3Aissue+label%3A"package%3A+browser" +[browser-repo-link]: https://github.com/TBD54566975/web5-js/tree/main/packages/browser +[browser-jsdelivr-link]: https://www.jsdelivr.com/package/npm/@web5/browser +[browser-jsdelivr-browser]: https://cdn.jsdelivr.net/npm/@web5/browser/dist/browser.mjs +[browser-unpkg-link]: https://unpkg.com/@web5/browser +[browser-unpkg-browser]: https://unpkg.com/@web5/browser/dist/browser.mjs +[codeowners-link]: https://github.com/TBD54566975/web5-js/blob/main/CODEOWNERS +[code-of-conduct-link]: https://github.com/TBD54566975/web5-js/blob/main/CODE_OF_CONDUCT.md +[contributing-link]: https://github.com/TBD54566975/web5-js/blob/main/CONTRIBUTING.md +[governance-link]: https://github.com/TBD54566975/web5-js/blob/main/GOVERNANCE.md +[license-link]: https://github.com/TBD54566975/web5-js/blob/main/LICENSE +[discord-badge]: https://img.shields.io/discord/937858703112155166?color=5865F2&logo=discord&logoColor=white +[discord-link]: https://discord.com/channels/937858703112155166/969272658501976117 diff --git a/packages/browser/build/esbuild-browser-config.cjs b/packages/browser/build/esbuild-browser-config.cjs new file mode 100644 index 000000000..8fd899321 --- /dev/null +++ b/packages/browser/build/esbuild-browser-config.cjs @@ -0,0 +1,13 @@ +/** @type {import('esbuild').BuildOptions} */ +module.exports = { + entryPoints : ['./src/index.ts'], + bundle : true, + format : 'esm', + sourcemap : true, + minify : true, + platform : 'browser', + target : ['chrome101', 'firefox108', 'safari16'], + define : { + 'global': 'globalThis', + }, +}; \ No newline at end of file diff --git a/packages/browser/build/esbuild-tests.cjs b/packages/browser/build/esbuild-tests.cjs new file mode 100644 index 000000000..e93c6902d --- /dev/null +++ b/packages/browser/build/esbuild-tests.cjs @@ -0,0 +1,16 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const esbuild = require('esbuild'); +const browserConfig = require('./esbuild-browser-config.cjs'); + +esbuild.build({ + ...browserConfig, + format : 'esm', + entryPoints : ['./tests/**/*.spec.*'], + bundle : true, + minify : false, + outdir : 'tests/compiled', + define : { + ...browserConfig.define, + 'process.env.TEST_DWN_URL': JSON.stringify(process.env.TEST_DWN_URL ?? null), + }, +}); diff --git a/packages/browser/package.json b/packages/browser/package.json new file mode 100644 index 000000000..b007bbaf4 --- /dev/null +++ b/packages/browser/package.json @@ -0,0 +1,84 @@ +{ + "name": "@web5/browser", + "version": "0.0.1", + "description": "Web5 tools and features to use in the browser", + "type": "module", + "main": "./dist/esm/index.js", + "module": "./dist/esm/index.js", + "types": "./dist/types/index.d.ts", + "scripts": { + "clean": "rimraf dist coverage tests/compiled", + "build:tests": "rimraf tests/compiled && node build/esbuild-tests.cjs", + "build:esm": "rimraf dist/esm dist/types && pnpm tsc -p tsconfig.json", + "build:browser": "pnpm build:esm", + "build": "pnpm clean && pnpm build:esm", + "lint": "eslint . --max-warnings 0", + "lint:fix": "eslint . --fix", + "test:browser": "pnpm build:tests && web-test-runner" + }, + "homepage": "https://github.com/TBD54566975/web5-js/tree/main/packages/browser#readme", + "bugs": "https://github.com/TBD54566975/web5-js/issues", + "repository": { + "type": "git", + "url": "git+https://github.com/TBD54566975/web5-js.git", + "directory": "packages/browser" + }, + "license": "Apache-2.0", + "contributors": [ + { + "name": "Daniel Buchner", + "url": "https://github.com/csuwildcat" + }, + { + "name": "Liran Cohen", + "url": "https://github.com/lirancohen" + } + ], + "files": [ + "dist", + "src" + ], + "keywords": [ + "decentralized", + "decentralized-applications", + "decentralized-identity", + "decentralized-web", + "DID", + "sdk", + "verifiable-credentials", + "web5", + "web5-sdk", + "browser", + "tools" + ], + "publishConfig": { + "access": "public", + "provenance": true + }, + "dependencies": { + "@web5/dids": "workspace:*" + }, + "devDependencies": { + "@playwright/test": "1.45.3", + "@types/chai": "4.3.6", + "@types/eslint": "8.56.10", + "@types/mocha": "10.0.1", + "@types/sinon": "17.0.3", + "@typescript-eslint/eslint-plugin": "7.9.0", + "@typescript-eslint/parser": "7.14.1", + "@web/test-runner": "0.18.2", + "@web/test-runner-playwright": "0.11.0", + "c8": "9.1.0", + "chai": "4.3.10", + "esbuild": "0.19.8", + "eslint": "9.3.0", + "eslint-plugin-mocha": "10.4.3", + "mocha": "10.2.0", + "mocha-junit-reporter": "2.2.1", + "playwright": "1.45.3", + "rimraf": "4.4.0", + "sinon": "18.0.0", + "source-map-loader": "4.0.2", + "typescript": "5.1.6" + } +} diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts new file mode 100644 index 000000000..07bee8b79 --- /dev/null +++ b/packages/browser/src/index.ts @@ -0,0 +1 @@ +export * from './web-features.js'; \ No newline at end of file diff --git a/packages/api/src/web-features.ts b/packages/browser/src/web-features.ts similarity index 76% rename from packages/api/src/web-features.ts rename to packages/browser/src/web-features.ts index 5eebe5269..d8e7df347 100644 --- a/packages/api/src/web-features.ts +++ b/packages/browser/src/web-features.ts @@ -1,3 +1,5 @@ +//@ts-nocheck + /* This file is run in dual environments to make installation of the Service Worker code easier. Be mindful that code placed in any open excution space may be evaluated multiple times in different contexts, @@ -6,32 +8,38 @@ import { UniversalResolver, DidDht, DidWeb } from '@web5/dids'; -declare const ServiceWorkerGlobalScope: any; - -const DidResolver = new UniversalResolver({ didResolvers: [DidDht, DidWeb] }); -const didUrlRegex = /^https?:\/\/dweb\/([^/]+)\/?(.*)?$/; -const httpToHttpsRegex = /^http:/; -const trailingSlashRegex = /\/$/; - // This is in place to prevent our `bundler-bonanza` repo from failing for Node CJS builds // Not sure if this is working as expected in all environments, crated an issue // TODO: https://github.com/TBD54566975/web5-js/issues/767 function importMetaIfSupported() { try { return new Function('return import.meta')(); - } catch(_error) { + } catch (_error) { return undefined; } } +declare const ServiceWorkerGlobalScope: any; + +const DidResolver = new UniversalResolver({ didResolvers: [DidDht, DidWeb] }); +const didUrlRegex = /^https?:\/\/dweb\/([^/]+)\/?(.*)?$/; +const httpToHttpsRegex = /^http:/; +const trailingSlashRegex = /\/$/; + async function getDwnEndpoints(did) { const { didDocument } = await DidResolver.resolve(did); - let endpoints = didDocument?.service?.find(service => service.type === 'DecentralizedWebNode')?.serviceEndpoint; - return (Array.isArray(endpoints) ? endpoints : [endpoints]).filter(url => url.startsWith('http')); + const endpoints = didDocument?.service?.find( + (service) => service.type === 'DecentralizedWebNode' + )?.serviceEndpoint; + return (Array.isArray(endpoints) ? endpoints : [endpoints]).filter((url) => + url.startsWith('http') + ); } -async function handleEvent(event, did, path, options){ - const drl = event.request.url.replace(httpToHttpsRegex, 'https:').replace(trailingSlashRegex, ''); +async function handleEvent(event, did, path, options) { + const drl = event.request.url + .replace(httpToHttpsRegex, 'https:') + .replace(trailingSlashRegex, ''); const responseCache = await caches.open('drl'); const cachedResponse = await responseCache.match(drl); if (cachedResponse) { @@ -39,7 +47,10 @@ async function handleEvent(event, did, path, options){ const match = await options?.onCacheCheck(event, drl); if (match) { const cacheTime = cachedResponse.headers.get('dwn-cache-time'); - if (cacheTime && Date.now() < Number(cacheTime) + (Number(match.ttl) || 0)) { + if ( + cacheTime && + Date.now() < Number(cacheTime) + (Number(match.ttl) || 0) + ) { return cachedResponse; } } @@ -50,13 +61,12 @@ async function handleEvent(event, did, path, options){ return new Response(JSON.stringify(response), { status : 200, headers : { - 'Content-Type': 'application/json' - } + 'Content-Type': 'application/json', + }, }); - } - else return await fetchResource(event, did, drl, path, responseCache, options); - } - catch(error){ + } else + return await fetchResource(event, did, drl, path, responseCache, options); + } catch (error) { if (error instanceof Response) { return error; } @@ -68,7 +78,10 @@ async function handleEvent(event, did, path, options){ async function fetchResource(event, did, drl, path, responseCache, options) { const endpoints = await getDwnEndpoints(did); if (!endpoints?.length) { - throw new Response('DWeb Node resolution failed: no valid endpoints found.', { status: 530 }); + throw new Response( + 'DWeb Node resolution failed: no valid endpoints found.', + { status: 530 } + ); } for (const endpoint of endpoints) { try { @@ -82,16 +95,19 @@ async function fetchResource(event, did, drl, path, responseCache, options) { return response; } console.log(`DWN endpoint error: ${response.status}`); - return new Response('DWeb Node request failed', { status: response.status }); - } - catch (error) { + return new Response('DWeb Node request failed', { + status: response.status, + }); + } catch (error) { console.log(`DWN endpoint error: ${error}`); - return new Response('DWeb Node request failed: ' + error, { status: 500 }); + return new Response('DWeb Node request failed: ' + error, { + status: 500, + }); } } } -async function cacheResponse(drl, url, response, cache){ +async function cacheResponse(drl, url, response, cache) { const clonedResponse = response.clone(); const headers = new Headers(clonedResponse.headers); headers.append('dwn-cache-time', Date.now().toString()); @@ -107,13 +123,16 @@ async function installWorker(options: any = {}): Promise { try { // Check to see if we are in a Service Worker already, if so, proceed // You can call the activatePolyfills() function in your own worker, or standalone as a root worker - if (typeof ServiceWorkerGlobalScope !== 'undefined' && workerSelf instanceof ServiceWorkerGlobalScope) { + if ( + typeof ServiceWorkerGlobalScope !== 'undefined' && + workerSelf instanceof ServiceWorkerGlobalScope + ) { workerSelf.skipWaiting(); - workerSelf.addEventListener('activate', event => { + workerSelf.addEventListener('activate', (event) => { // Claim clients to make the service worker take control immediately event.waitUntil(workerSelf.clients.claim()); }); - workerSelf.addEventListener('fetch', event => { + workerSelf.addEventListener('fetch', (event) => { const match = event.request.url.match(didUrlRegex); if (match) { event.respondWith(handleEvent(event, match[1], match[2], options)); @@ -124,16 +143,27 @@ async function installWorker(options: any = {}): Promise { else if (globalThis?.navigator?.serviceWorker) { const registration = await navigator.serviceWorker.getRegistration('/'); // You can only have one worker per path, so check to see if one is already registered - if (!registration){ + if (!registration) { // @ts-ignore - const installUrl = options.path || (globalThis.document ? document?.currentScript?.src : importMetaIfSupported()?.url); - if (installUrl) navigator.serviceWorker.register(installUrl, { type: 'module' }).catch(error => { - console.error('DWeb networking feature installation failed: ', error); - }); + const installUrl = + options.path || + (globalThis.document + ? document?.currentScript?.src + : importMetaIfSupported()?.url); + if (installUrl) + navigator.serviceWorker + .register(installUrl, { type: 'module' }) + .catch((error) => { + console.error( + 'DWeb networking feature installation failed: ', + error + ); + }); } - } - else { - throw new Error('DWeb networking features are not available for install in this environment'); + } else { + throw new Error( + 'DWeb networking features are not available for install in this environment' + ); } } catch (error) { console.error('Error in installing networking features:', error); @@ -218,7 +248,7 @@ const loaderStyles = ` } .drl-loading-overlay span::before { - content: "✕ "; + content: '✕ '; margin: 0 0.4em 0 0; color: red; font-size: 65%; @@ -226,7 +256,7 @@ const loaderStyles = ` } .drl-loading-overlay span::after { - content: "stop"; + content: 'stop'; display: block; font-size: 60%; line-height: 0; @@ -234,7 +264,7 @@ const loaderStyles = ` } .drl-loading-overlay.new-tab-overlay span::after { - content: "close"; + content: 'close'; } @keyframes drl-loading-spinner { @@ -250,10 +280,10 @@ const loaderStyles = ` `; const tabContent = ` - + - - + + Loading DRL... -
-
+
+
Loading DRL
- +
@@ -299,37 +329,37 @@ function injectElements() { `; document.head.append(style); - let overlay = document.createElement('div'); + const overlay = document.createElement('div'); overlay.classList.add('drl-loading-overlay'); overlay.innerHTML = ` -
+
Loading DRL
- + `; overlay.lastElementChild.addEventListener('click', cancelNavigation); document.body.prepend(overlay); elementsInjected = true; } -function cancelNavigation(){ +function cancelNavigation() { document.documentElement.removeAttribute('drl-link-loading'); activeNavigation = null; } let activeNavigation; let linkFeaturesActive = false; -function addLinkFeatures(){ +function addLinkFeatures() { if (!linkFeaturesActive) { document.addEventListener('click', async (event: any) => { - let anchor = event.target.closest('a'); + const anchor = event.target.closest('a'); if (anchor) { - let href = anchor.href; + const href = anchor.href; const match = href.match(didUrlRegex); if (match) { - let did = match[1]; - let path = match[2]; + const did = match[1]; + const path = match[2]; const openAsTab = anchor.target === '_blank'; event.preventDefault(); try { @@ -337,27 +367,33 @@ function addLinkFeatures(){ if (openAsTab) { tab = window.open('', '_blank'); tab.document.write(tabContent); - } - else { + } else { activeNavigation = path; // this is to allow for cached DIDs to instantly load without any flash of loading UI - setTimeout(() => document.documentElement.setAttribute('drl-link-loading', ''), 50); + setTimeout( + () => + document.documentElement.setAttribute('drl-link-loading', ''), + 50 + ); } const endpoints = await getDwnEndpoints(did); if (!endpoints.length) throw null; - let url = `${endpoints[0].replace(trailingSlashRegex, '')}/${did}/${path}`; + const url = `${endpoints[0].replace( + trailingSlashRegex, + '' + )}/${did}/${path}`; if (openAsTab) { if (!tab.closed) tab.location.href = url; - } - else if (activeNavigation === path) { + } else if (activeNavigation === path) { window.location.href = url; } - } - catch(e) { + } catch (e) { if (activeNavigation === path) { cancelNavigation(); } - throw new Error(`DID endpoint resolution failed for the DRL: ${href}`); + throw new Error( + `DID endpoint resolution failed for the DRL: ${href}` + ); } } } @@ -366,21 +402,26 @@ function addLinkFeatures(){ document.addEventListener('pointercancel', resetContextMenuTarget); document.addEventListener('pointerdown', async (event: any) => { const target = event.composedPath()[0]; - if ((event.pointerType === 'mouse' && event.button === 2) || - (event.pointerType === 'touch' && event.isPrimary)) { + if ( + (event.pointerType === 'mouse' && event.button === 2) || + (event.pointerType === 'touch' && event.isPrimary) + ) { resetContextMenuTarget(); if (target && target?.src?.match(didUrlRegex)) { contextMenuTarget = target; target.__src__ = target.src; - const drl = target.src.replace(httpToHttpsRegex, 'https:').replace(trailingSlashRegex, ''); + const drl = target.src + .replace(httpToHttpsRegex, 'https:') + .replace(trailingSlashRegex, ''); const responseCache = await caches.open('drl'); const response = await responseCache.match(drl); const url = response.headers.get('dwn-composed-url'); if (url) target.src = url; - target.addEventListener('pointerup', resetContextMenuTarget, { once: true }); + target.addEventListener('pointerup', resetContextMenuTarget, { + once: true, + }); } - } - else if (target === contextMenuTarget) { + } else if (target === contextMenuTarget) { resetContextMenuTarget(); } }); @@ -390,9 +431,9 @@ function addLinkFeatures(){ } let contextMenuTarget; -async function resetContextMenuTarget(e?: any){ +async function resetContextMenuTarget(e?: any) { if (e?.type === 'pointerup') { - await new Promise(r => requestAnimationFrame(r)); + await new Promise((r) => requestAnimationFrame(r)); } if (contextMenuTarget) { contextMenuTarget.src = contextMenuTarget.__src__; @@ -429,8 +470,8 @@ async function resetContextMenuTarget(e?: any){ * @example * // Activate polyfills, but without Service Worker activation * activatePolyfills({ serviceWorker: false }); -*/ -export function activatePolyfills(options: any = {}){ + */ +export function activatePolyfills(options: any = {}) { if (options.serviceWorker !== false) { installWorker(options); } @@ -438,9 +479,11 @@ export function activatePolyfills(options: any = {}){ if (options.injectStyles !== false) { if (document.readyState !== 'loading') injectElements(); else { - document.addEventListener('DOMContentLoaded', injectElements, { once: true }); + document.addEventListener('DOMContentLoaded', injectElements, { + once: true, + }); } } if (options.links !== false) addLinkFeatures(); } -} \ No newline at end of file +} diff --git a/packages/browser/tests/tsconfig.json b/packages/browser/tests/tsconfig.json new file mode 100644 index 000000000..7c6d2c8e7 --- /dev/null +++ b/packages/browser/tests/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "compiled", + "declarationDir": "compiled/types", + "sourceMap": true, + }, + "include": [ + "../src", + ".", + ], + "exclude": [ + "./compiled" + ] +} \ No newline at end of file diff --git a/packages/browser/tests/web-features.spec.ts b/packages/browser/tests/web-features.spec.ts new file mode 100644 index 000000000..c6a220c48 --- /dev/null +++ b/packages/browser/tests/web-features.spec.ts @@ -0,0 +1,11 @@ +import { expect } from 'chai'; + +import { activatePolyfills } from '../src/web-features.js'; + +describe('web features', () => { + describe('activatePolyfills', () => { + it('does not throw', () => { + expect(() => activatePolyfills()).to.not.throw(); + }); + }); +}); diff --git a/packages/browser/tsconfig.json b/packages/browser/tsconfig.json new file mode 100644 index 000000000..ad92cd4ef --- /dev/null +++ b/packages/browser/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "lib": ["DOM", "ES6", "WebWorker"], + "strict": false, + "declarationDir": "dist/types", + "outDir": "dist/esm" + }, + "include": [ + "src" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/packages/browser/web-test-runner.config.cjs b/packages/browser/web-test-runner.config.cjs new file mode 100644 index 000000000..e5adb980d --- /dev/null +++ b/packages/browser/web-test-runner.config.cjs @@ -0,0 +1,30 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const playwrightLauncher = + require('@web/test-runner-playwright').playwrightLauncher; + +/** + * @type {import('@web/test-runner').TestRunnerConfig} + */ +module.exports = { + files : 'tests/compiled/**/*.spec.js', + playwright : true, + nodeResolve : true, + browsers : [ + playwrightLauncher({ + product: 'chromium', + }), + playwrightLauncher({ + product: 'firefox', + }), + playwrightLauncher({ + product: 'webkit', + }), + ], + testsFinishTimeout : 300000, + concurrentBrowsers : 2, + testFramework : { + config: { + timeout: '15000', + }, + }, +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index acf5b49c2..05e5de743 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -261,6 +261,76 @@ importers: specifier: 5.1.6 version: 5.1.6 + packages/browser: + dependencies: + '@web5/dids': + specifier: workspace:* + version: link:../dids + devDependencies: + '@playwright/test': + specifier: 1.45.3 + version: 1.45.3 + '@types/chai': + specifier: 4.3.6 + version: 4.3.6 + '@types/eslint': + specifier: 8.56.10 + version: 8.56.10 + '@types/mocha': + specifier: 10.0.1 + version: 10.0.1 + '@types/sinon': + specifier: 17.0.3 + version: 17.0.3 + '@typescript-eslint/eslint-plugin': + specifier: 7.9.0 + version: 7.9.0(@typescript-eslint/parser@7.14.1(eslint@9.3.0)(typescript@5.1.6))(eslint@9.3.0)(typescript@5.1.6) + '@typescript-eslint/parser': + specifier: 7.14.1 + version: 7.14.1(eslint@9.3.0)(typescript@5.1.6) + '@web/test-runner': + specifier: 0.18.2 + version: 0.18.2 + '@web/test-runner-playwright': + specifier: 0.11.0 + version: 0.11.0 + c8: + specifier: 9.1.0 + version: 9.1.0 + chai: + specifier: 4.3.10 + version: 4.3.10 + esbuild: + specifier: 0.19.8 + version: 0.19.8 + eslint: + specifier: 9.3.0 + version: 9.3.0 + eslint-plugin-mocha: + specifier: 10.4.3 + version: 10.4.3(eslint@9.3.0) + mocha: + specifier: 10.2.0 + version: 10.2.0 + mocha-junit-reporter: + specifier: 2.2.1 + version: 2.2.1(mocha@10.2.0) + playwright: + specifier: 1.45.3 + version: 1.45.3 + rimraf: + specifier: 4.4.0 + version: 4.4.0 + sinon: + specifier: 18.0.0 + version: 18.0.0 + source-map-loader: + specifier: 4.0.2 + version: 4.0.2(webpack@5.93.0(esbuild@0.19.8)) + typescript: + specifier: 5.1.6 + version: 5.1.6 + packages/common: dependencies: '@isaacs/ttlcache': diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index bf5d0d166..50ad8dd32 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -9,3 +9,4 @@ packages: - "packages/proxy-agent" - "packages/identity-agent" - "packages/api" + - "packages/browser" diff --git a/web5-js.code-workspace b/web5-js.code-workspace index ef7448c45..61d805d72 100644 --- a/web5-js.code-workspace +++ b/web5-js.code-workspace @@ -15,6 +15,11 @@ "name": "api", "path": "packages/api", }, + { + //@web5/browser + "name": "browser", + "path": "packages/browser", + }, { // @web5/common "name": "common",