From 96027fe432b3a454c7a9f59b449761e8436b996d Mon Sep 17 00:00:00 2001 From: Piral Release Bot Date: Tue, 12 Dec 2023 23:49:39 +0000 Subject: [PATCH 01/10] Auto update documentation From 8b7ccac67f5817f325724a9c5a43756a9ab1a931 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20R=C3=B6mer?= <30902964+manuelroemer@users.noreply.github.com> Date: Wed, 13 Dec 2023 12:46:49 +0100 Subject: [PATCH 02/10] Added support for middleware functions in piral-fetch. --- src/plugins/piral-fetch/src/config.ts | 19 +++++ src/plugins/piral-fetch/src/create.test.ts | 44 ++++++++++ src/plugins/piral-fetch/src/fetch.ts | 99 ++++++++++++++-------- 3 files changed, 126 insertions(+), 36 deletions(-) diff --git a/src/plugins/piral-fetch/src/config.ts b/src/plugins/piral-fetch/src/config.ts index 4261551ba..bc80666e3 100644 --- a/src/plugins/piral-fetch/src/config.ts +++ b/src/plugins/piral-fetch/src/config.ts @@ -1,3 +1,16 @@ +import type { FetchOptions, FetchResponse, PiletFetchApiFetch } from './types'; + +export interface FetchMiddleware { + /** + * A middleware function for the fetch API. + * @param path The target of the fetch. + * @param options The options to be used. + * @param next A function that invokes the next middleware or the final `fetch`. + * @returns The promise waiting for the response to arrive. + */ + (path: string, options: FetchOptions, next: PiletFetchApiFetch): Promise>; +} + export interface FetchConfig { /** * Sets the default request init settings. @@ -9,4 +22,10 @@ export interface FetchConfig { * @default location.origin */ base?: string; + /** + * An ordered list of middleware functions which can intercept and transform any request made via `piral-fetch`. + * Middleware functions are executed in a top-down order for each fetch request. + * @default [] + */ + middlewares?: Array; } diff --git a/src/plugins/piral-fetch/src/create.test.ts b/src/plugins/piral-fetch/src/create.test.ts index 15415e81f..bd289ad17 100644 --- a/src/plugins/piral-fetch/src/create.test.ts +++ b/src/plugins/piral-fetch/src/create.test.ts @@ -83,4 +83,48 @@ describe('Create fetch API Module', () => { const result = response.body; expect(result.substr(0, 5)).toBe(` { + const context = { emit: vitest.fn() } as any; + const middleware = vitest.fn((path, options, next) => next(path, options)); + const { fetch } = createFetchApi({ + base: `http://localhost:${port}`, + middlewares: [middleware], + })(context) as any; + const response = await fetch('json'); + const result = response.body; + expect(Array.isArray(result)).toBeTruthy(); + expect(result.length).toBe(10); + expect(result[0]).toBe(1); + expect(middleware).toHaveBeenCalledOnce(); + }); + + it('invokes middleware functions in top-down order', async () => { + const context = { emit: vitest.fn() } as any; + const invocationOrder: Array = []; + const createMiddleware = (myPosition: number) => (path, options, next) => { + invocationOrder.push(myPosition); + return next(path, options); + }; + const { fetch } = createFetchApi({ + base: `http://localhost:${port}`, + middlewares: [createMiddleware(1), createMiddleware(2), createMiddleware(3)], + })(context) as any; + await fetch('json'); + expect(invocationOrder).toEqual([1, 2, 3]); + }); + + it('allows middleware functions to terminate middleware chain', async () => { + const context = { emit: vitest.fn() } as any; + const expectedResponse = { code: 200, body: 'Terminated by middleware', text: 'Terminated by middleware' }; + const middleware = () => Promise.resolve(expectedResponse); + const { fetch } = createFetchApi({ + base: `http://localhost:${port}`, + middlewares: [middleware], + })(context) as any; + const globalFetch = vitest.spyOn(global, 'fetch'); + const response = await fetch('json'); + expect(response).toBe(expectedResponse); + expect(globalFetch).not.toHaveBeenCalled(); + }); }); diff --git a/src/plugins/piral-fetch/src/fetch.ts b/src/plugins/piral-fetch/src/fetch.ts index 13b35192a..aca74e036 100644 --- a/src/plugins/piral-fetch/src/fetch.ts +++ b/src/plugins/piral-fetch/src/fetch.ts @@ -1,47 +1,74 @@ -import { FetchConfig } from './config'; -import { FetchOptions, FetchResponse } from './types'; +import { FetchConfig, FetchMiddleware } from './config'; +import { FetchOptions, FetchResponse, PiletFetchApiFetch } from './types'; const headerAccept = 'accept'; const headerContentType = 'content-type'; const mimeApplicationJson = 'application/json'; export function httpFetch(config: FetchConfig, path: string, options: FetchOptions = {}): Promise> { - const baseInit = config.default || {}; - const baseHeaders = baseInit.headers || {}; - const baseUrl = config.base || location.origin; - const { method = 'get', body, headers = {}, cache = baseInit.cache, mode = baseInit.mode, result = 'auto', signal } = options; - const json = - Array.isArray(body) || - typeof body === 'number' || - (typeof body === 'object' && body instanceof FormData === false && body instanceof Blob === false); - const url = new URL(path, baseUrl); - const init: RequestInit = { - ...baseInit, - method, - body: json ? JSON.stringify(body) : (body as BodyInit), - headers: { - ...baseHeaders, - ...headers, - }, - cache, - mode, - signal, - }; + // fetcher makes the actual HTTP request. + // It is used as the last step in the upcoming middleware chain and does *not* call/require next + // (which is undefined in this case). + const fetcher: FetchMiddleware = (path, options) => { + const baseInit = config.default || {}; + const baseHeaders = baseInit.headers || {}; + const baseUrl = config.base || location.origin; + const { + method = 'get', + body, + headers = {}, + cache = baseInit.cache, + mode = baseInit.mode, + result = 'auto', + signal, + } = options; + const json = + Array.isArray(body) || + typeof body === 'number' || + (typeof body === 'object' && body instanceof FormData === false && body instanceof Blob === false); + const url = new URL(path, baseUrl); + const init: RequestInit = { + ...baseInit, + method, + body: json ? JSON.stringify(body) : (body as BodyInit), + headers: { + ...baseHeaders, + ...headers, + }, + cache, + mode, + signal, + }; - if (json) { - init.headers[headerContentType] = 'application/json'; - init.headers[headerAccept] = mimeApplicationJson; - } + if (json) { + init.headers[headerContentType] = 'application/json'; + init.headers[headerAccept] = mimeApplicationJson; + } - return fetch(url.href, init).then((res) => { - const contentType = res.headers.get(headerContentType); - const json = result === 'json' || (result === 'auto' && !!contentType && contentType.indexOf('json') !== -1); - const promise = json ? res.json() : res.text(); + return fetch(url.href, init).then((res) => { + const contentType = res.headers.get(headerContentType); + const json = result === 'json' || (result === 'auto' && !!contentType && contentType.indexOf('json') !== -1); + const promise = json ? res.json() : res.text(); - return promise.then((body) => ({ - body, - code: res.status, - text: res.statusText, - })); + return promise.then((body) => ({ + body, + code: res.status, + text: res.statusText, + })); + }); + }; + + // Prepare the middleware chain. Middlewares are called in a top-down order. + // Every configured middleware function must receive a `next` function with the same shape + // as a `fetch` function. `next` invokes the next function in the chain. + // `fetcher` from above is the last function in the chain and always terminates it. + const middlewareFns = [...(config.middlewares || []), fetcher]; + let middlewareChain: Array; + middlewareChain = middlewareFns.map((middleware, i) => { + const next: PiletFetchApiFetch = (path, options) => middlewareChain[i + 1](path, options); + const invoke: PiletFetchApiFetch = (path, options) => middleware(path, options, next); + return invoke; }); + + return middlewareChain[0](path, options); } From 79bd91db94740943c801048b243b356fc65881da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20R=C3=B6mer?= <30902964+manuelroemer@users.noreply.github.com> Date: Wed, 13 Dec 2023 12:57:11 +0100 Subject: [PATCH 03/10] Document middlewares. --- src/plugins/piral-fetch/README.md | 42 +++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/plugins/piral-fetch/README.md b/src/plugins/piral-fetch/README.md index 52140384b..646f03532 100644 --- a/src/plugins/piral-fetch/README.md +++ b/src/plugins/piral-fetch/README.md @@ -83,6 +83,48 @@ const instance = createInstance({ **Note**: `piral-fetch` plays nicely together with authentication providers such as `piral-adal`. As such authentication tokens are automatically inserted on requests to the base URL. +### Middlewares + +`piral-fetch` allows you to configure middleware functions which are executed on each `fetch` call. Middleware functions receive the same parameters as `fetch`, plus a `next` function which calls either the next middleware or the actual `fetch` function. The following code shows an exemplary middleware which logs when requests start and finish: + +```ts +const logRequests: FetchMiddleware = async ( + path: string, + options: FetchOptions, + next: PiletFetchApiFetch, +): Promise> => { + try { + console.log(`Making request to ${path}...`); + const response = await next(path, options); + console.log(`Request to ${path} returned status code ${response.code}.`); + return response; + } catch (e) { + console.error(`Request to ${path} threw an error: `, e); + throw e; + } +}; +``` + +Middlewares must be configured in the Piral instance: + +```ts +const instance = createInstance({ + plugins: [createFetchApi({ + // important part + middlewares: [ + firstMiddleware, + secondMiddleware, + thirdMiddleware, + logRequests, + ], + // ...other options... + })], + // ... +}); +``` + +Middlewares are invoked in a top-down order. In the above example, this means that `firstMiddleware` is invoked first, then `secondMiddleware`, then `thirdMiddleware`, then `logRequests` and finally the actual `fetch` function. + ::: ## License From 1af8bb41446b29a3eee5826e53a113a6dd1ac4f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20R=C3=B6mer?= <30902964+manuelroemer@users.noreply.github.com> Date: Wed, 13 Dec 2023 13:19:46 +0100 Subject: [PATCH 04/10] Added middleware changes to changelog. --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18a9d8f79..4b7d4ccee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Piral Changelog +## 1.4.1 (tbd) + +- Added support for middleware functions in `piral-fetch` (#655) + ## 1.4.0 (December 12, 2023) - Fixed issue when target tarball is part of tarball content in `pilet pack` (#642) From 0e20935799b943a47e1536a293280827e5e474d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20R=C3=B6mer?= <30902964+manuelroemer@users.noreply.github.com> Date: Wed, 13 Dec 2023 14:34:05 +0100 Subject: [PATCH 05/10] Updated CHANGELOG.md. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b7d4ccee..172755f1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 1.4.1 (tbd) -- Added support for middleware functions in `piral-fetch` (#655) +- Added support for middleware functions in `piral-fetch` (#645) ## 1.4.0 (December 12, 2023) From a6034d505b3c19fa65707b874d4b3380826b0a6b Mon Sep 17 00:00:00 2001 From: Florian Rappl Date: Thu, 14 Dec 2023 01:14:57 +0100 Subject: [PATCH 06/10] Fixed monorepo issue --- CHANGELOG.md | 1 + .../piral-cli/src/injectors/pilet-injector.ts | 23 +++++++++++-------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 172755f1c..f75438c48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 1.4.1 (tbd) +- Fixed issue with pilet injector for certain apps in monorepos - Added support for middleware functions in `piral-fetch` (#645) ## 1.4.0 (December 12, 2023) diff --git a/src/tooling/piral-cli/src/injectors/pilet-injector.ts b/src/tooling/piral-cli/src/injectors/pilet-injector.ts index 4ac2e3d60..86491b466 100644 --- a/src/tooling/piral-cli/src/injectors/pilet-injector.ts +++ b/src/tooling/piral-cli/src/injectors/pilet-injector.ts @@ -1,5 +1,5 @@ import { URL } from 'url'; -import { join } from 'path'; +import { join, resolve } from 'path'; import { EventEmitter } from 'events'; import { readFile, stat, writeFile } from 'fs/promises'; import { KrasInjector, KrasRequest, KrasInjectorConfig, KrasConfiguration, KrasResult } from 'kras'; @@ -140,15 +140,18 @@ export default class PiletInjector implements KrasInjector { const cbs = {}; if (app.endsWith('/app')) { - const packageJson = require(`${app}/../package.json`); - - if (typeof packageJson.piralCLI.source === 'string') { - this.proxyInfo = { - source: packageJson.piralCLI.source, - files: packageJson.files, - date: new Date(packageJson.piralCLI.timestamp), - }; - } + try { + const path = resolve(app, '..', 'package.json'); + const packageJson = require(path); + + if (typeof packageJson.piralCLI.source === 'string') { + this.proxyInfo = { + source: packageJson.piralCLI.source, + files: packageJson.files, + date: new Date(packageJson.piralCLI.timestamp), + }; + } + } catch {} } core.on('user-connected', (e) => { From 465ec06a5f176910d74f2bad73e6fd1ccb3a8a05 Mon Sep 17 00:00:00 2001 From: Florian Rappl Date: Thu, 14 Dec 2023 09:05:42 +0100 Subject: [PATCH 07/10] Improved test --- src/framework/piral-base/src/events.test.ts | 36 ++++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/src/framework/piral-base/src/events.test.ts b/src/framework/piral-base/src/events.test.ts index 82eacbdbe..46450277e 100644 --- a/src/framework/piral-base/src/events.test.ts +++ b/src/framework/piral-base/src/events.test.ts @@ -4,31 +4,35 @@ import { describe, it, expect, vitest } from 'vitest'; import { createListener } from './events'; +function nextCycle(time = 0) { + return new Promise(resolve => setTimeout(resolve, 0)); +} + describe('Events Module', () => { - it('add and emit event', () => { + it('add and emit event', async () => { const events = createListener(undefined); const mockCallback = vitest.fn(); events.on('init', mockCallback); events.emit('init', undefined); - setTimeout(() => { - expect(mockCallback).toHaveBeenCalledTimes(1); - }, 1); + await nextCycle(10); + + expect(mockCallback).toHaveBeenCalledTimes(1); }); - it('does only react to self events when different states', () => { + it('does only react to self events when different states', async () => { const events1 = createListener({}); const events2 = createListener({}); const mockCallback = vitest.fn(); events1.on('init', mockCallback); events2.emit('init', undefined); - setTimeout(() => { - expect(mockCallback).toHaveBeenCalledTimes(0); - }, 1); + await nextCycle(10); + + expect(mockCallback).toHaveBeenCalledTimes(0); }); - it('does only react to self events when same state', () => { + it('does only react to self events when same state', async () => { const state = {}; const events1 = createListener(state); const events2 = createListener(state); @@ -36,9 +40,9 @@ describe('Events Module', () => { events1.on('init', mockCallback); events2.emit('init', undefined); - setTimeout(() => { - expect(mockCallback).toHaveBeenCalledTimes(1); - }, 1); + await nextCycle(10); + + expect(mockCallback).toHaveBeenCalledTimes(1); }); it('emit on empty event should be fine', () => { @@ -51,7 +55,7 @@ describe('Events Module', () => { events.off('init', vitest.fn()); }); - it('should not be possible to emit after event removed', () => { + it('should not be possible to emit after event removed', async () => { const events = createListener(undefined); const mockCallback = vitest.fn(); events.on('init', mockCallback); @@ -59,8 +63,8 @@ describe('Events Module', () => { events.off('init', mockCallback); events.emit('init', undefined); - setTimeout(() => { - expect(mockCallback).toHaveBeenCalledTimes(1); - }, 1); + await nextCycle(10); + + expect(mockCallback).toHaveBeenCalledTimes(1); }); }); From 308fc8be08ae8dbe9ac5d89f38fd584d52827fd7 Mon Sep 17 00:00:00 2001 From: Florian Rappl Date: Thu, 14 Dec 2023 09:06:15 +0100 Subject: [PATCH 08/10] Updated Node version --- .github/workflows/size.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/size.yml b/.github/workflows/size.yml index 774918e6b..50fee8996 100644 --- a/.github/workflows/size.yml +++ b/.github/workflows/size.yml @@ -14,7 +14,7 @@ jobs: - name: Setup node uses: actions/setup-node@v1 with: - node-version: "16.20.0" + node-version: "20.10.0" - run: yarn install - name: Report changes run: node ./tools/size-reporter.mjs From 5845cb5baa186a754494d3b55f724f0631344503 Mon Sep 17 00:00:00 2001 From: Florian Rappl Date: Thu, 14 Dec 2023 09:19:42 +0100 Subject: [PATCH 09/10] Restored piral-react-15 package --- mlc_config.json | 2 +- src/converters/README.md | 1 + src/converters/piral-react-15/LICENSE | 21 ++++ src/converters/piral-react-15/README.md | 105 ++++++++++++++++++ src/converters/piral-react-15/convert.ts | 29 +++++ src/converters/piral-react-15/package.json | 71 ++++++++++++ .../piral-react-15/src/converter.ts | 30 +++++ src/converters/piral-react-15/src/create.ts | 28 +++++ .../piral-react-15/src/extension.ts | 62 +++++++++++ src/converters/piral-react-15/src/index.ts | 2 + src/converters/piral-react-15/src/mount.ts | 41 +++++++ .../piral-react-15/src/react15.d.ts | 9 ++ src/converters/piral-react-15/src/types.ts | 37 ++++++ src/converters/piral-react-15/tsconfig.json | 12 ++ 14 files changed, 449 insertions(+), 1 deletion(-) create mode 100644 src/converters/piral-react-15/LICENSE create mode 100644 src/converters/piral-react-15/README.md create mode 100644 src/converters/piral-react-15/convert.ts create mode 100644 src/converters/piral-react-15/package.json create mode 100644 src/converters/piral-react-15/src/converter.ts create mode 100644 src/converters/piral-react-15/src/create.ts create mode 100644 src/converters/piral-react-15/src/extension.ts create mode 100644 src/converters/piral-react-15/src/index.ts create mode 100644 src/converters/piral-react-15/src/mount.ts create mode 100644 src/converters/piral-react-15/src/react15.d.ts create mode 100644 src/converters/piral-react-15/src/types.ts create mode 100644 src/converters/piral-react-15/tsconfig.json diff --git a/mlc_config.json b/mlc_config.json index f5248b188..d6c5acee7 100644 --- a/mlc_config.json +++ b/mlc_config.json @@ -19,7 +19,7 @@ "pattern": "^https://aka.ms/" }, { - "pattern": "^https://azure.microsoft.com/free" + "pattern": "^https://azure.microsoft.com/" } ], "aliveStatusCodes": [429, 200] diff --git a/src/converters/README.md b/src/converters/README.md index 3d26f37dc..976e6304f 100644 --- a/src/converters/README.md +++ b/src/converters/README.md @@ -17,6 +17,7 @@ Piral is developed as a monorepo. - [piral-ng](./piral-ng/README.md) provides integration for *Angular* - [piral-ngjs](./piral-ngjs/README.md) provides integration for *Angular.js* - [piral-preact](./piral-preact/README.md) provides integration for *Preact* +- [piral-react](./piral-react/README.md) provides integration for *React* 16+ - [piral-react-15](./piral-react-15/README.md) provides integration for *React* in version 15 - [piral-riot](./piral-riot/README.md) provides integration for *Riot* - [piral-solid](./piral-solid/README.md) provides integration for *Solid.js* diff --git a/src/converters/piral-react-15/LICENSE b/src/converters/piral-react-15/LICENSE new file mode 100644 index 000000000..45c7a955c --- /dev/null +++ b/src/converters/piral-react-15/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 - 2023 smapiot + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/converters/piral-react-15/README.md b/src/converters/piral-react-15/README.md new file mode 100644 index 000000000..63205c2e4 --- /dev/null +++ b/src/converters/piral-react-15/README.md @@ -0,0 +1,105 @@ +[![Piral Logo](https://github.com/smapiot/piral/raw/main/docs/assets/logo.png)](https://piral.io) + +# [Piral React 15](https://piral.io) · [![GitHub License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/smapiot/piral/blob/main/LICENSE) [![npm version](https://img.shields.io/npm/v/piral-react-15.svg?style=flat)](https://www.npmjs.com/package/piral-react-15) [![tested with jest](https://img.shields.io/badge/tested_with-jest-99424f.svg)](https://jestjs.io) [![Gitter Chat](https://badges.gitter.im/gitterHQ/gitter.png)](https://gitter.im/piral-io/community) + +This is a plugin that only has a peer dependency to `react@^15` and `react-dom@^15`. What `piral-react-15` brings to the table is a set of Pilet API extensions that can be used with `piral` or `piral-core`. + +The set includes a React 15.x converter for any component registration, as well as a `fromReact15` shortcut and a `React15Extension` component. + +By default, these API extensions are not integrated in `piral`, so you'd need to add them to your Piral instance. + +## Documentation + +The following functions are brought to the Pilet API. + +### `fromReact15()` + +Transforms a standard React 15.x component into a component that can be used in Piral, essentially wrapping it with a reference to the corresponding converter. + +### `React15Extension` + +The extension slot component to be used in React 15.x components. + +## Usage + +::: summary: For pilet authors + +You can use the `fromReact15` function from the Pilet API to convert your React v15 components to components usable by your Piral instance. + +Example use: + +```ts +import { PiletApi } from ''; +import { React15Page } from './React15Page'; + +export function setup(piral: PiletApi) { + piral.registerPage('/sample', piral.fromReact15(React15Page)); +} +``` + +Within React v15 components the Piral React v15 extension component can be used by referring to `React15Extension`, e.g., + +```jsx + +``` + +Alternatively, if `piral-react-15` has not been added to the Piral instance you can install and use the package also from a pilet directly. + +```ts +import { PiletApi } from ''; +import { fromReact15 } from 'piral-react-15/convert'; +import { React15Page } from './React15Page'; + +export function setup(piral: PiletApi) { + piral.registerPage('/sample', fromReact15(React15Page)); +} +``` + +::: + +::: summary: For Piral instance developers + +Using React v15 with Piral is as simple as installing `piral-react-15` and `react-15`. For `react-15` add the following two packages to your project's dependencies: + +```json +{ + "dependencies": { + "react-15`": "npm:react@^15", + "react-dom-15": "npm:react@^15" + } +} +``` + +Now you are ready to use the `piral-react-15` converter: + +```ts +import { createReact15Api } from 'piral-react-15'; +``` + +The integration looks like: + +```ts +const instance = createInstance({ + // important part + plugins: [createReact15Api()], + // ... +}); +``` + +The `react-15` package (or whatever alias you've chosen) should be shared with the pilets via the *package.json*: + +```json +{ + "importmap": { + "imports": { + "react-15": "" + } + } +} +``` + +::: + +## License + +Piral is released using the MIT license. For more information see the [license file](./LICENSE). diff --git a/src/converters/piral-react-15/convert.ts b/src/converters/piral-react-15/convert.ts new file mode 100644 index 000000000..3ff5084a8 --- /dev/null +++ b/src/converters/piral-react-15/convert.ts @@ -0,0 +1,29 @@ +import { createConverter } from './esm/converter'; + +export interface HtmlComponent { + component: { + mount(element: HTMLElement, props: TProps, ctx: any, locals: any): void; + update?(element: HTMLElement, props: TProps, ctx: any, locals: any): void; + unmount?(element: HTMLElement, locals: any): void; + }; + type: 'html'; +} + +export interface React15Converter { + (...params: Parameters>): HtmlComponent; +} + +export function createReact15Converter(...params: Parameters) { + const convert = createConverter(...params); + const Extension = convert.Extension; + const from: React15Converter = (root) => ({ + type: 'html', + component: convert(root), + }); + + return { from, Extension }; +} + +const { from: fromReact15, Extension: React15Extension } = createReact15Converter(); + +export { fromReact15, React15Extension }; diff --git a/src/converters/piral-react-15/package.json b/src/converters/piral-react-15/package.json new file mode 100644 index 000000000..1ba1e7566 --- /dev/null +++ b/src/converters/piral-react-15/package.json @@ -0,0 +1,71 @@ +{ + "name": "piral-react-15", + "version": "1.4.0", + "description": "Plugin for integrating React v15 components in Piral.", + "keywords": [ + "piral", + "pilet-api", + "smapiot", + "portal", + "modules", + "api", + "plugin", + "plugin-converter", + "react15" + ], + "author": "smapiot", + "homepage": "https://piral.io", + "license": "MIT", + "module": "esm/index.js", + "main": "lib/index.js", + "typings": "lib/index.d.ts", + "exports": { + ".": { + "import": "./esm/index.js", + "require": "./lib/index.js" + }, + "./convert": { + "import": "./convert.js" + }, + "./esm/*": { + "import": "./esm/*" + }, + "./lib/*": { + "require": "./lib/*" + }, + "./_/*": { + "import": "./esm/*.js", + "require": "./lib/*.js" + }, + "./package.json": "./package.json" + }, + "sideEffects": false, + "files": [ + "esm", + "lib", + "src", + "convert.d.ts", + "convert.js" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/smapiot/piral.git" + }, + "bugs": { + "url": "https://github.com/smapiot/piral/issues" + }, + "scripts": { + "cleanup": "rimraf esm lib convert.d.ts convert.js", + "build": "yarn build:commonjs && yarn build:esnext && yarn build:convert", + "build:convert": "tsc convert.ts --skipLibCheck --declaration --module esnext", + "build:commonjs": "tsc --project tsconfig.json --outDir lib --module commonjs", + "build:esnext": "tsc --project tsconfig.json --outDir esm --module esnext", + "typedoc": "typedoc --json ../../../docs/types/piral-react-15.json src --exclude \"src/**/*.test.*\"", + "test": "echo \"Error: run tests from root\" && exit 1" + }, + "devDependencies": { + "piral-core": "^1.4.0", + "react-15": "npm:react@15", + "react-dom-15": "npm:react-dom@15" + } +} diff --git a/src/converters/piral-react-15/src/converter.ts b/src/converters/piral-react-15/src/converter.ts new file mode 100644 index 000000000..a8304fa2f --- /dev/null +++ b/src/converters/piral-react-15/src/converter.ts @@ -0,0 +1,30 @@ +import type { ForeignComponent, BaseComponentProps } from 'piral-core'; +import type { ComponentType } from 'react-15'; +import { createExtension } from './extension'; +import { mountReact15, unmountReact15 } from './mount'; + +export interface React15ConverterOptions { + /** + * Defines the name of the root element. + * @default piral-slot + */ + rootName?: string; +} + +export function createConverter(config: React15ConverterOptions = {}) { + const { rootName = 'piral-slot' } = config; + const Extension = createExtension(rootName); + const convert = (root: ComponentType): ForeignComponent => ({ + mount(el, props, ctx) { + mountReact15(el, root, props, ctx); + }, + update(el, props, ctx) { + mountReact15(el, root, props, ctx); + }, + unmount(el) { + unmountReact15(el); + }, + }); + convert.Extension = Extension; + return convert; +} diff --git a/src/converters/piral-react-15/src/create.ts b/src/converters/piral-react-15/src/create.ts new file mode 100644 index 000000000..95e709eca --- /dev/null +++ b/src/converters/piral-react-15/src/create.ts @@ -0,0 +1,28 @@ +import type { PiralPlugin } from 'piral-core'; +import { createConverter, React15ConverterOptions } from './converter'; +import type { PiletReact15Api } from './types'; + +/** + * Available configuration options for the React 15.x plugin. + */ +export interface React15Config extends React15ConverterOptions {} + +/** + * Creates Pilet API extensions for integrating React 15.x. + */ +export function createReact15Api(config: React15Config = {}): PiralPlugin { + return (context) => { + const convert = createConverter(config); + context.converters.react15 = ({ root }) => convert(root); + + return { + fromReact15(root) { + return { + type: 'react15', + root, + }; + }, + React15Extension: convert.Extension, + }; + }; +} diff --git a/src/converters/piral-react-15/src/extension.ts b/src/converters/piral-react-15/src/extension.ts new file mode 100644 index 000000000..8ed82e79d --- /dev/null +++ b/src/converters/piral-react-15/src/extension.ts @@ -0,0 +1,62 @@ +import type { ExtensionSlotProps, PiletApi } from 'piral-core'; +import { createElement, Component } from 'react-15'; +import { anyPropType } from './mount'; + +function compareObjects(a: any, b: any) { + for (const i in a) { + if (!(i in b)) { + return false; + } + } + + for (const i in b) { + if (!compare(a[i], b[i])) { + return false; + } + } + + return true; +} + +function compare(a: T, b: T) { + if (a !== b) { + const ta = typeof a; + const tb = typeof b; + + if (ta === tb && ta === 'object' && a && b) { + return compareObjects(a, b); + } + + return false; + } + + return true; +} + +export function createExtension(rootName: string) { + const React15Extension: any = class extends Component { + static contextTypes = { + piral: anyPropType, + }; + + private onRefChange = (element: HTMLElement) => { + if (element) { + const { piral } = this.context as { piral: PiletApi }; + element.innerHTML = ''; + piral.renderHtmlExtension(element, this.props); + } + }; + + shouldComponentUpdate(nextProps: ExtensionSlotProps) { + return !compare(this.props, nextProps); + } + + render() { + return createElement(rootName, { + ref: this.onRefChange, + }); + } + }; + + return React15Extension; +} diff --git a/src/converters/piral-react-15/src/index.ts b/src/converters/piral-react-15/src/index.ts new file mode 100644 index 000000000..4cb82feeb --- /dev/null +++ b/src/converters/piral-react-15/src/index.ts @@ -0,0 +1,2 @@ +export * from './create'; +export * from './types'; diff --git a/src/converters/piral-react-15/src/mount.ts b/src/converters/piral-react-15/src/mount.ts new file mode 100644 index 000000000..2d2162dda --- /dev/null +++ b/src/converters/piral-react-15/src/mount.ts @@ -0,0 +1,41 @@ +import type { BaseComponentProps } from 'piral-core'; +import { render } from 'react-dom-15'; +import { createElement, ComponentType, Component } from 'react-15'; + +// tslint:disable-next-line:no-null-keyword +export const anyPropType = () => null; + +export function mountReact15( + el: HTMLElement, + root: ComponentType, + props: T, + ctx: any = {}, +) { + const contextTypes = {}; + + ['piral', ...Object.keys(ctx)].forEach((key) => { + contextTypes[key] = anyPropType; + }); + + class Provider extends Component<{ children?: any }> { + static childContextTypes = contextTypes; + + getChildContext() { + return { + piral: props.piral, + ...ctx, + }; + } + + render() { + return this.props.children; + } + } + + render(createElement(Provider, {}, createElement(root as any, props)), el); +} + +export function unmountReact15(el: HTMLElement) { + // tslint:disable-next-line:no-null-keyword + render(null, el); +} diff --git a/src/converters/piral-react-15/src/react15.d.ts b/src/converters/piral-react-15/src/react15.d.ts new file mode 100644 index 000000000..ef7d5ee00 --- /dev/null +++ b/src/converters/piral-react-15/src/react15.d.ts @@ -0,0 +1,9 @@ +declare module 'react-15' { + import { ComponentType, Component, createElement } from 'react'; + export { ComponentType, Component, createElement }; +} + +declare module 'react-dom-15' { + import { render } from 'react-dom'; + export { render }; +} diff --git a/src/converters/piral-react-15/src/types.ts b/src/converters/piral-react-15/src/types.ts new file mode 100644 index 000000000..80c51eacc --- /dev/null +++ b/src/converters/piral-react-15/src/types.ts @@ -0,0 +1,37 @@ +import type { ForeignComponent, ExtensionSlotProps } from 'piral-core'; +import type { Component, ComponentType } from 'react-15'; + +declare module 'piral-core/lib/types/custom' { + interface PiletCustomApi extends PiletReact15Api {} + + interface PiralCustomComponentConverters { + react15(component: React15Component): ForeignComponent; + } +} + +export interface React15Component { + /** + * The component root. + */ + root: ComponentType; + /** + * The type of the React 15.x component. + */ + type: 'react15'; +} + +/** + * Defines the provided set of Pilet API extensions from the React 15.x plugin. + */ +export interface PiletReact15Api { + /** + * Wraps an React 15.x component for use in Piral. + * @param component The component root. + * @returns The Piral React 15.x component. + */ + fromReact15(component: ComponentType): React15Component; + /** + * React 15.x component for displaying extensions of the given name. + */ + React15Extension: Component; +} diff --git a/src/converters/piral-react-15/tsconfig.json b/src/converters/piral-react-15/tsconfig.json new file mode 100644 index 000000000..3c8e07c9b --- /dev/null +++ b/src/converters/piral-react-15/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "skipLibCheck": true, + "lib": ["dom", "es2015", "es2018"], + "jsx": "react", + "importHelpers": true + }, + "include": [ + "./src" + ] + } From a486c7752bb63d5f00f41d6660d60846b3f66f33 Mon Sep 17 00:00:00 2001 From: Florian Rappl Date: Thu, 14 Dec 2023 09:27:17 +0100 Subject: [PATCH 10/10] Improved checks for mono repo debug --- .../src/build/run-debug-mono-piral.ts | 2 +- .../piral-cli/src/injectors/pilet-injector.ts | 28 +++++++++++-------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/tooling/piral-cli/src/build/run-debug-mono-piral.ts b/src/tooling/piral-cli/src/build/run-debug-mono-piral.ts index 549f47ce5..cbe247ca2 100644 --- a/src/tooling/piral-cli/src/build/run-debug-mono-piral.ts +++ b/src/tooling/piral-cli/src/build/run-debug-mono-piral.ts @@ -52,7 +52,7 @@ process.on('message', async (msg: any) => { break; case 'start': const root = process.cwd(); - const outDir = resolve(root, 'dist', 'app'); + const outDir = resolve(root, 'dist'); const bundler = await run( root, msg.outFile, diff --git a/src/tooling/piral-cli/src/injectors/pilet-injector.ts b/src/tooling/piral-cli/src/injectors/pilet-injector.ts index 86491b466..7f2ca3386 100644 --- a/src/tooling/piral-cli/src/injectors/pilet-injector.ts +++ b/src/tooling/piral-cli/src/injectors/pilet-injector.ts @@ -1,6 +1,7 @@ import { URL } from 'url'; import { join, resolve } from 'path'; import { EventEmitter } from 'events'; +import { existsSync } from 'fs'; import { readFile, stat, writeFile } from 'fs/promises'; import { KrasInjector, KrasRequest, KrasInjectorConfig, KrasConfiguration, KrasResult } from 'kras'; import { log } from '../common/log'; @@ -140,18 +141,23 @@ export default class PiletInjector implements KrasInjector { const cbs = {}; if (app.endsWith('/app')) { - try { - const path = resolve(app, '..', 'package.json'); - const packageJson = require(path); - - if (typeof packageJson.piralCLI.source === 'string') { - this.proxyInfo = { - source: packageJson.piralCLI.source, - files: packageJson.files, - date: new Date(packageJson.piralCLI.timestamp), - }; + const path = resolve(app, '..', 'package.json'); + + if (existsSync(path)) { + try { + const packageJson = require(path); + + if (typeof packageJson.piralCLI.source === 'string') { + this.proxyInfo = { + source: packageJson.piralCLI.source, + files: packageJson.files, + date: new Date(packageJson.piralCLI.timestamp), + }; + } + } catch { + // silently ignore errors - we just continue as-is } - } catch {} + } } core.on('user-connected', (e) => {