From b38a47f4e60f2873ae38cf7fecaa01818bc575d5 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Fri, 15 Jan 2021 10:50:53 -0600 Subject: [PATCH 01/16] use os detection for initial dark mode setting --- src/settings/index.ts | 3 +++ src/utils/os.test.ts | 21 ++++++++++++++++++++- src/utils/os.ts | 16 ++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/settings/index.ts b/src/settings/index.ts index 0c9a2406..8fc3f7e0 100644 --- a/src/settings/index.ts +++ b/src/settings/index.ts @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT // Copyright (c) 2021 The Pybricks Authors +import { prefersDarkMode } from '../utils/os'; + // Definitions for user settings. export enum SettingId { @@ -14,6 +16,7 @@ export function getDefaultBooleanValue(id: SettingId): boolean { case SettingId.ShowDocs: return window.innerWidth >= 1024; case SettingId.DarkMode: + return prefersDarkMode(); case SettingId.FlashCurrentProgram: return false; // istanbul ignore next: it is a programmer error if we hit this diff --git a/src/utils/os.test.ts b/src/utils/os.test.ts index 665f08d6..9051252f 100644 --- a/src/utils/os.test.ts +++ b/src/utils/os.test.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // Copyright (c) 2021 The Pybricks Authors -import { isMacOS } from './os'; +import { isMacOS, prefersDarkMode } from './os'; describe('isMacOS', () => { test('is true', () => { @@ -13,3 +13,22 @@ describe('isMacOS', () => { expect(isMacOS()).toBeFalsy(); }); }); + +describe('prefersDarkMode', () => { + test('is true', () => { + window.matchMedia = jest.fn().mockReturnValue({ + matches: true, + } as MediaQueryList); + expect(prefersDarkMode()).toBeTruthy(); + }); + test('is false', () => { + // @ts-expect-error 2790 + delete window.matchMedia; + expect(prefersDarkMode()).toBeFalsy(); + + window.matchMedia = jest.fn().mockReturnValue({ + matches: false, + } as MediaQueryList); + expect(prefersDarkMode()).toBeFalsy(); + }); +}); diff --git a/src/utils/os.ts b/src/utils/os.ts index 44a11446..e32d5471 100644 --- a/src/utils/os.ts +++ b/src/utils/os.ts @@ -3,6 +3,22 @@ // Utility functions for dealing with operating systems. +/** + * Tests if we are running on macOS. + * @returns `true` if running on macOS, otherwise `false`. + */ export function isMacOS(): boolean { return /mac/i.test(navigator.platform); } + +/** + * Tests if the OS is set to dark mode. + * @returns: `true` if dark mode should be preferred, otherwise `false`. + */ +export function prefersDarkMode(): boolean { + if (!window.matchMedia) { + return false; + } + + return window.matchMedia('(prefers-color-scheme: dark)').matches; +} From a66331840ce29665d3c08c3ab28e8bfbb31ad114 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Fri, 15 Jan 2021 10:57:37 -0600 Subject: [PATCH 02/16] remove unused app.action.toggleDocs --- src/actions/app.ts | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/actions/app.ts b/src/actions/app.ts index daaf204c..7864f2e5 100644 --- a/src/actions/app.ts +++ b/src/actions/app.ts @@ -13,8 +13,6 @@ export enum AppActionType { OpenSettings = 'app.action.openSettings', /** Close settings dialog. */ CloseSettings = 'app.action.closeSettings', - /** Toggle documentation visibility. */ - ToggleDocs = 'app.action.toggleDocs', } /** Action that indicates the app has just started. */ @@ -41,17 +39,8 @@ export function closeSettings(): AppCloseSettingsAction { return { type: AppActionType.CloseSettings }; } -/** Action to toggle documentation visibility. */ -export type AppToggleDocsAction = Action; - -/** Creates an action to toggle documentation visibility. */ -export function toggleDocs(): AppToggleDocsAction { - return { type: AppActionType.ToggleDocs }; -} - /** common type for all app actions. */ export type AppAction = | AppStartupAction | AppOpenSettingsAction - | AppCloseSettingsAction - | AppToggleDocsAction; + | AppCloseSettingsAction; From f54beab97fd42a904d71be1533ad57f34152baab Mon Sep 17 00:00:00 2001 From: David Lechner Date: Fri, 15 Jan 2021 11:08:04 -0600 Subject: [PATCH 03/16] use consistent tooltip delay --- src/actions/settings.ts | 2 +- src/components/ActionButton.tsx | 7 ++++++- src/components/LinkButton.tsx | 2 ++ src/components/OpenFileButton.tsx | 2 ++ src/components/SettingsDrawer.tsx | 5 ++--- src/reducers/settings.ts | 2 +- src/sagas/settings.test.ts | 2 +- src/sagas/settings.ts | 2 +- src/settings/ui.ts | 7 +++++++ src/settings/{index.ts => user.ts} | 4 ++-- 10 files changed, 25 insertions(+), 10 deletions(-) create mode 100644 src/settings/ui.ts rename src/settings/{index.ts => user.ts} (94%) diff --git a/src/actions/settings.ts b/src/actions/settings.ts index cfa4ba1a..e2f477d9 100644 --- a/src/actions/settings.ts +++ b/src/actions/settings.ts @@ -2,7 +2,7 @@ // Copyright (c) 2021 The Pybricks Authors import { Action } from 'redux'; -import { SettingId } from '../settings'; +import { SettingId } from '../settings/user'; /** Actions related to settings. */ export enum SettingsActionType { diff --git a/src/components/ActionButton.tsx b/src/components/ActionButton.tsx index 23aad054..7377dcb6 100644 --- a/src/components/ActionButton.tsx +++ b/src/components/ActionButton.tsx @@ -12,6 +12,7 @@ import { } from '@blueprintjs/core'; import { WithI18nProps, withI18n } from '@shopify/react-i18n'; import React from 'react'; +import { tooltipDelay } from '../settings/ui'; import { TooltipId } from './button-i18n'; import en from './button-i18n.en.json'; @@ -42,7 +43,11 @@ class ActionButton extends React.Component { tooltipText += ` (${this.props.keyboardShortcut})`; } return ( - + From b15d00f797700164b5df87a979d0c923b2c498bb Mon Sep 17 00:00:00 2001 From: David Lechner Date: Fri, 15 Jan 2021 16:07:11 -0600 Subject: [PATCH 10/16] add help links --- src/components/AboutDialog.tsx | 4 +- src/components/ExternalLinkIcon.tsx | 21 ++++++++++ src/components/SettingsDrawer.tsx | 58 +++++++++++++++++++++++----- src/components/external-link.scss | 9 +++++ src/components/settings-i18n.en.json | 9 +++++ src/components/settings-i18n.ts | 3 ++ src/settings/ui.ts | 9 +++++ 7 files changed, 103 insertions(+), 10 deletions(-) create mode 100644 src/components/ExternalLinkIcon.tsx create mode 100644 src/components/external-link.scss diff --git a/src/components/AboutDialog.tsx b/src/components/AboutDialog.tsx index 261098e1..162acf9e 100644 --- a/src/components/AboutDialog.tsx +++ b/src/components/AboutDialog.tsx @@ -16,6 +16,7 @@ import { pybricksCopyright, pybricksWebsiteUrl, } from '../settings/ui'; +import ExternalLinkIcon from './ExternalLinkIcon'; import { AboutStringId } from './about-i18n'; import en from './about-i18n.en.json'; @@ -47,7 +48,8 @@ class AboutDialog extends React.Component {

- {i18n.translate(AboutStringId.WebsiteButtonLabel)} + {i18n.translate(AboutStringId.WebsiteButtonLabel)}  +
diff --git a/src/components/ExternalLinkIcon.tsx b/src/components/ExternalLinkIcon.tsx new file mode 100644 index 00000000..810b968a --- /dev/null +++ b/src/components/ExternalLinkIcon.tsx @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2021 The Pybricks Authors + +// Icon for indicating external links + +import { Icon } from '@blueprintjs/core'; +import React from 'react'; + +import './external-link.scss'; + +class ExternalLinkIcon extends React.Component { + render(): JSX.Element { + return ( + + + + ); + } +} + +export default ExternalLinkIcon; diff --git a/src/components/SettingsDrawer.tsx b/src/components/SettingsDrawer.tsx index b1d4b7bf..0b10358c 100644 --- a/src/components/SettingsDrawer.tsx +++ b/src/components/SettingsDrawer.tsx @@ -2,7 +2,9 @@ // Copyright (c) 2021 The Pybricks Authors import { + AnchorButton, Button, + ButtonGroup, Classes, Drawer, FormGroup, @@ -17,9 +19,15 @@ import { Action, Dispatch } from '../actions'; import { closeSettings, openAboutDialog } from '../actions/app'; import { setBoolean } from '../actions/settings'; import { RootState } from '../reducers'; -import { tooltipDelay } from '../settings/ui'; +import { + pybricksBugReportsUrl, + pybricksGitterUrl, + pybricksSupportUrl, + tooltipDelay, +} from '../settings/ui'; import { SettingId } from '../settings/user'; import { isMacOS } from '../utils/os'; +import ExternalLinkIcon from './ExternalLinkIcon'; import { SettingsStringId } from './settings-i18n'; import en from './settings-i18n.en.json'; @@ -143,16 +151,48 @@ class SettingsDrawer extends React.PureComponent {
- + + {i18n.translate(SettingsStringId.HelpSupportLabel)} +   + + + + {i18n.translate(SettingsStringId.HelpChatLabel)} +   + + + + {i18n.translate(SettingsStringId.HelpBugsLabel)} +   + + + + diff --git a/src/components/external-link.scss b/src/components/external-link.scss new file mode 100644 index 00000000..fb81f8d9 --- /dev/null +++ b/src/components/external-link.scss @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2021 The Pybricks Authors + +@import '../variables.scss'; + +// override some button magic in case this is used in a button +.#{$ns}-button .pb-external-link .#{$ns}-icon:first-child:last-child { + margin: 0; +} diff --git a/src/components/settings-i18n.en.json b/src/components/settings-i18n.en.json index f13be9a3..8e30f8bf 100644 --- a/src/components/settings-i18n.en.json +++ b/src/components/settings-i18n.en.json @@ -24,6 +24,15 @@ }, "help": { "title": "Help", + "support": { + "label": "Support" + }, + "chat": { + "label": "Chat" + }, + "bugs": { + "label": "Bug Reports" + }, "about": { "label": "About" } diff --git a/src/components/settings-i18n.ts b/src/components/settings-i18n.ts index 48a11306..13c68db1 100644 --- a/src/components/settings-i18n.ts +++ b/src/components/settings-i18n.ts @@ -15,5 +15,8 @@ export enum SettingsStringId { FirmwareCurrentProgramLabel = 'settings.firmware.flash-current-program.label', FirmwareCurrentProgramTooltip = 'settings.firmware.flash-current-program.tooltip', HelpTitle = 'settings.help.title', + HelpSupportLabel = 'settings.help.support.label', + HelpChatLabel = 'settings.help.chat.label', + HelpBugsLabel = 'settings.help.bugs.label', HelpAboutLabel = 'settings.help.about.label', } diff --git a/src/settings/ui.ts b/src/settings/ui.ts index 9bb44b99..4ae33657 100644 --- a/src/settings/ui.ts +++ b/src/settings/ui.ts @@ -12,6 +12,15 @@ export const appName = 'Pybricks Code'; /** URL to main Pybricks website. */ export const pybricksWebsiteUrl = 'https://pybricks.com'; +/** URL to Pybricks support site. */ +export const pybricksSupportUrl = 'https://github.com/pybricks/support/discussions'; + +/** URL to Pybricks bug report site. */ +export const pybricksBugReportsUrl = 'https://github.com/pybricks/support/issues'; + +/** URL for Pybricks community chat on Gitter */ +export const pybricksGitterUrl = 'https://gitter.im/pybricks/community'; + /** Pybricks copyright statement. */ export const pybricksCopyright = 'Copyright (c) 2020-2021 The Pybricks Authors'; From a3c8c05a79293738de8059bde4658fe4ab772a74 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Fri, 15 Jan 2021 16:55:30 -0600 Subject: [PATCH 11/16] add more info in package.json --- package.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index cfdacca3..d0b560bb 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,12 @@ { "name": "pybricks-code", "version": "0.1.0", - "private": true, + "license": "MIT", + "author": "The Pybricks Authors", + "repository": { + "type": "git", + "url": "https://github.com/pybricks/pybricks-code" + }, "dependencies": { "@blueprintjs/core": "^3.36.0", "@craco/craco": "^6.0.0", From 4b63a80f80eda8d79307e9fea46a287164ce2f03 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sun, 17 Jan 2021 10:20:19 -0600 Subject: [PATCH 12/16] style scroll bars This makes scroll bars in Chrome look more like Firefox and blend in better with the app. --- src/index.scss | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/index.scss b/src/index.scss index a2a72980..d447317d 100644 --- a/src/index.scss +++ b/src/index.scss @@ -42,3 +42,36 @@ body { .#{$ns}-form-group > .#{$ns}-label { font-weight: bolder; } + +::-webkit-scrollbar { + width: 16px; +} + +.#{$ns}-dark ::-webkit-scrollbar-track { + background: $pt-dark-app-background-color; +} + +::-webkit-scrollbar-track { + background: $pt-app-background-color; +} + +.#{$ns}-dark ::-webkit-scrollbar-thumb { + border-color: $pt-dark-app-background-color; + background: $pt-dark-icon-color; +} + +::-webkit-scrollbar-thumb { + border-width: 3px; + border-style: solid; + border-radius: 8px; + border-color: $pt-app-background-color; + background: $pt-icon-color; +} + +.#{$ns}-dark ::-webkit-scrollbar-thumb:hover { + background: $pt-dark-icon-color-hover; +} + +::-webkit-scrollbar-thumb:hover { + background: $pt-icon-color-hover; +} From 1626533ccac91e8635edae9a4c2be69fe5c1485f Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sun, 17 Jan 2021 10:39:38 -0600 Subject: [PATCH 13/16] add license dialog --- craco.config.js | 7 ++ package.json | 1 + src/actions/app.ts | 24 +++- src/actions/index.ts | 2 + src/actions/license.ts | 28 +++++ src/components/AboutDialog.tsx | 14 ++- src/components/App.tsx | 2 - src/components/LicenseDialog.tsx | 163 ++++++++++++++++++++++++++++ src/components/SettingsDrawer.tsx | 2 + src/components/about-i18n.en.json | 1 + src/components/about-i18n.ts | 1 + src/components/license-i18n.en.json | 12 ++ src/components/license-i18n.test.ts | 12 ++ src/components/license-i18n.ts | 13 +++ src/components/license.scss | 33 ++++++ src/reducers/app.ts | 14 ++- src/reducers/index.ts | 3 + src/reducers/license.ts | 43 ++++++++ src/sagas/index.ts | 2 + src/sagas/license.ts | 33 ++++++ yarn.lock | 56 +++++++++- 21 files changed, 454 insertions(+), 12 deletions(-) create mode 100644 src/actions/license.ts create mode 100644 src/components/LicenseDialog.tsx create mode 100644 src/components/license-i18n.en.json create mode 100644 src/components/license-i18n.test.ts create mode 100644 src/components/license-i18n.ts create mode 100644 src/components/license.scss create mode 100644 src/reducers/license.ts create mode 100644 src/sagas/license.ts diff --git a/craco.config.js b/craco.config.js index dd1d624a..28a68095 100644 --- a/craco.config.js +++ b/craco.config.js @@ -6,9 +6,16 @@ const { getLoaders, loaderByName, } = require('@craco/craco'); +const LicensePlugin = require('webpack-license-plugin'); module.exports = { webpack: { + plugins: [ + new LicensePlugin({ + outputFilename: 'static/oss-licenses.json', + replenishDefaultLicenseTexts: true, + }), + ], configure: { resolve: { // need 'esnext' first to avoid compile errors diff --git a/package.json b/package.json index d0b560bb..5a518b6a 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "redux-saga": "^1.1.3", "typescript": "~4.1.3", "web-vitals": "^1.0.1", + "webpack-license-plugin": "^4.1.2", "xterm": "^4.9.0", "xterm-addon-fit": "^0.4.0", "zen-push": "^0.2.1" diff --git a/src/actions/app.ts b/src/actions/app.ts index e47531a1..f6bda357 100644 --- a/src/actions/app.ts +++ b/src/actions/app.ts @@ -17,6 +17,10 @@ export enum AppActionType { OpenAboutDialog = 'app.action.openAboutDialog', /** Close about dialog. */ CloseAboutDialog = 'app.action.closeAboutDialog', + /** Open license dialog. */ + OpenLicenseDialog = 'app.action.openLicenseDialog', + /** Close license dialog. */ + CloseLicenseDialog = 'app.action.closeLicenseDialog', } /** Action that indicates the app has just started. */ @@ -59,10 +63,28 @@ export function closeAboutDialog(): AppCloseAboutDialogAction { return { type: AppActionType.CloseAboutDialog }; } +/** Action to open the license dialog. */ +export type AppOpenLicenseDialogAction = Action; + +/** Creates an action to open the license dialog. */ +export function openLicenseDialog(): AppOpenLicenseDialogAction { + return { type: AppActionType.OpenLicenseDialog }; +} + +/** Action to close the license dialog. */ +export type AppCloseLicenseDialogAction = Action; + +/** Creates an action to close the license dialog. */ +export function closeLicenseDialog(): AppCloseLicenseDialogAction { + return { type: AppActionType.CloseLicenseDialog }; +} + /** common type for all app actions. */ export type AppAction = | AppStartupAction | AppOpenSettingsAction | AppCloseSettingsAction | AppOpenAboutDialogAction - | AppCloseAboutDialogAction; + | AppCloseAboutDialogAction + | AppOpenLicenseDialogAction + | AppCloseLicenseDialogAction; diff --git a/src/actions/index.ts b/src/actions/index.ts index 11b549ac..e4ded3ad 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -8,6 +8,7 @@ import { BleUartAction } from './ble-uart'; import { EditorAction } from './editor'; import { FlashFirmwareAction } from './flash-firmware'; import { HubAction, HubMessageAction } from './hub'; +import { LicenseAction } from './license'; import { BootloaderConnectionAction, BootloaderDidRequestAction, @@ -36,6 +37,7 @@ export type Action = | FlashFirmwareAction | HubAction | HubMessageAction + | LicenseAction | MpyAction | NotificationAction | ServiceWorkerAction diff --git a/src/actions/license.ts b/src/actions/license.ts new file mode 100644 index 00000000..3ab1a567 --- /dev/null +++ b/src/actions/license.ts @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2021 The Pybricks Authors + +import { Action } from 'redux'; +import { LicenseInfo, LicenseList } from '../reducers/license'; + +export enum LicenseActionType { + DidFetchList = 'license.action.didFetchList', + Select = 'license.action.select', +} + +export type LicenseDidFetchListAction = Action & { + list: LicenseList; +}; + +export function didFetchList(list: LicenseList): LicenseDidFetchListAction { + return { type: LicenseActionType.DidFetchList, list }; +} + +export type LicenseSelectAction = Action & { + info: LicenseInfo; +}; + +export function select(info: LicenseInfo): LicenseSelectAction { + return { type: LicenseActionType.Select, info }; +} + +export type LicenseAction = LicenseDidFetchListAction | LicenseSelectAction; diff --git a/src/components/AboutDialog.tsx b/src/components/AboutDialog.tsx index 162acf9e..f7b506dc 100644 --- a/src/components/AboutDialog.tsx +++ b/src/components/AboutDialog.tsx @@ -3,12 +3,12 @@ // The about dialog -import { AnchorButton, Classes, Dialog } from '@blueprintjs/core'; +import { AnchorButton, Button, Classes, Dialog } from '@blueprintjs/core'; import { WithI18nProps, withI18n } from '@shopify/react-i18n'; import React from 'react'; import { connect } from 'react-redux'; import { Action, Dispatch } from '../actions'; -import { closeAboutDialog } from '../actions/app'; +import { closeAboutDialog, openLicenseDialog } from '../actions/app'; import { RootState } from '../reducers'; import { appName, @@ -17,6 +17,7 @@ import { pybricksWebsiteUrl, } from '../settings/ui'; import ExternalLinkIcon from './ExternalLinkIcon'; +import LicenseDialog from './LicenseDialog'; import { AboutStringId } from './about-i18n'; import en from './about-i18n.en.json'; @@ -24,13 +25,13 @@ import './about.scss'; type StateProps = { showAboutDialog: boolean }; -type DispatchProps = { onClose: () => void }; +type DispatchProps = { onClose: () => void; onLicenseButtonClick: () => void }; type AboutDialogProps = StateProps & DispatchProps & WithI18nProps; class AboutDialog extends React.Component { render(): JSX.Element { - const { i18n, showAboutDialog, onClose } = this.props; + const { i18n, showAboutDialog, onClose, onLicenseButtonClick } = this.props; return ( onClose()}>
@@ -47,12 +48,16 @@ class AboutDialog extends React.Component { {legoDisclaimer}

+ {i18n.translate(AboutStringId.WebsiteButtonLabel)} 
+
); } @@ -64,6 +69,7 @@ const mapStateToProps = (state: RootState): StateProps => ({ const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({ onClose: (): Action => dispatch(closeAboutDialog()), + onLicenseButtonClick: (): Action => dispatch(openLicenseDialog()), }); export default connect( diff --git a/src/components/App.tsx b/src/components/App.tsx index f42eb9c9..04a15162 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -5,7 +5,6 @@ import React, { useState } from 'react'; import { useSelector } from 'react-redux'; import SplitterLayout from 'react-splitter-layout'; import { RootState } from '../reducers'; -import AboutDialog from './AboutDialog'; import Editor from './Editor'; import SettingsDrawer from './SettingsDrawer'; import StatusBar from './StatusBar'; @@ -65,7 +64,6 @@ function App(): JSX.Element { - ); } diff --git a/src/components/LicenseDialog.tsx b/src/components/LicenseDialog.tsx new file mode 100644 index 00000000..50e17fb6 --- /dev/null +++ b/src/components/LicenseDialog.tsx @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2021 The Pybricks Authors + +// The license dialog + +import { + Button, + ButtonGroup, + Callout, + Card, + Classes, + Dialog, + NonIdealState, +} from '@blueprintjs/core'; +import { WithI18nProps, withI18n } from '@shopify/react-i18n'; +import React from 'react'; +import { connect } from 'react-redux'; +import { Action, Dispatch } from '../actions'; +import { closeLicenseDialog } from '../actions/app'; +import { select } from '../actions/license'; +import { RootState } from '../reducers'; +import { LicenseInfo, LicenseList } from '../reducers/license'; +import { appName } from '../settings/ui'; +import { LicenseStringId } from './license-i18n'; +import en from './license-i18n.en.json'; + +import './license.scss'; + +type StateProps = { + showLicenseDialog: boolean; + licenseList: LicenseList | null; + licenseInfo: LicenseInfo | null; +}; + +type DispatchProps = { + onClose: () => void; + onSelectPackage: (info: LicenseInfo) => void; +}; + +type LicenseDialogProps = StateProps & DispatchProps & WithI18nProps; + +class LicenseDialog extends React.Component { + render(): JSX.Element { + const infoDiv = React.createRef(); + + const { + i18n, + showLicenseDialog, + onClose, + licenseList, + licenseInfo, + onSelectPackage, + } = this.props; + return ( + onClose()} + className="pb-license-dialog" + > +
+ + {i18n.translate(LicenseStringId.Description, { + name: appName, + })} + + +
+ {licenseList === null ? ( + // TODO: this should be translated and hooked to + // state indicating if download is in progress + // or there was an actual failure. + + Failed to load license data. + + ) : ( + + {licenseList.map((info) => ( + + ))} + + )} +
+
+ {licenseInfo == null ? ( + + {i18n.translate(LicenseStringId.SelectPackageHelp)} + + ) : ( +
+ +

+ + {i18n.translate( + LicenseStringId.PackageLabel, + )} + {' '} + {licenseInfo.name}{' '} + + v{licenseInfo.version} + +

+ {licenseInfo.author && ( +

+ + {' '} + {i18n.translate( + LicenseStringId.AuthorLabel, + )} + {' '} + {licenseInfo.author} +

+ )} +

+ + {' '} + {i18n.translate( + LicenseStringId.LicenseLabel, + )} + {' '} + {licenseInfo.license} +

+
+
+
{licenseInfo.licenseText}
+
+
+ )} +
+
+
+
+ ); + } +} + +const mapStateToProps = (state: RootState): StateProps => ({ + showLicenseDialog: state.app.showLicenseDialog, + licenseList: state.license.list, + licenseInfo: state.license.selected, +}); + +const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({ + onClose: (): Action => dispatch(closeLicenseDialog()), + onSelectPackage: (info): Action => dispatch(select(info)), +}); + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(withI18n({ id: 'license', fallback: en, translations: { en } })(LicenseDialog)); diff --git a/src/components/SettingsDrawer.tsx b/src/components/SettingsDrawer.tsx index 0b10358c..9e379ec6 100644 --- a/src/components/SettingsDrawer.tsx +++ b/src/components/SettingsDrawer.tsx @@ -27,6 +27,7 @@ import { } from '../settings/ui'; import { SettingId } from '../settings/user'; import { isMacOS } from '../utils/os'; +import AboutDialog from './AboutDialog'; import ExternalLinkIcon from './ExternalLinkIcon'; import { SettingsStringId } from './settings-i18n'; import en from './settings-i18n.en.json'; @@ -192,6 +193,7 @@ class SettingsDrawer extends React.PureComponent { > {i18n.translate(SettingsStringId.HelpAboutLabel)} + diff --git a/src/components/about-i18n.en.json b/src/components/about-i18n.en.json index 820e65fb..5c9a2b5f 100644 --- a/src/components/about-i18n.en.json +++ b/src/components/about-i18n.en.json @@ -1,6 +1,7 @@ { "about": { "description": "A simple web app for programming LEGO® Powered Up smart hubs using Pybricks MicroPython.", + "licenseButton": { "label": "Open Source Licenses" }, "websiteButton": { "label": "Pybricks Website" } } } diff --git a/src/components/about-i18n.ts b/src/components/about-i18n.ts index b73a4ef4..04a9ba9d 100644 --- a/src/components/about-i18n.ts +++ b/src/components/about-i18n.ts @@ -5,5 +5,6 @@ export enum AboutStringId { Description = 'about.description', + LicenseButtonLabel = 'about.licenseButton.label', WebsiteButtonLabel = 'about.websiteButton.label', } diff --git a/src/components/license-i18n.en.json b/src/components/license-i18n.en.json new file mode 100644 index 00000000..e49f0af1 --- /dev/null +++ b/src/components/license-i18n.en.json @@ -0,0 +1,12 @@ +{ + "license": { + "title": "Open Source Licenses", + "description": "{name} is built on open source software. By using {name} you are agreeing to the terms and conditions of all of the included software licenses.", + "packageLabel": "Package:", + "authorLabel": "Author:", + "licenseLabel": "License:", + "help": { + "selectPackage": "Select a package to view the license." + } + } +} diff --git a/src/components/license-i18n.test.ts b/src/components/license-i18n.test.ts new file mode 100644 index 00000000..e4bc2625 --- /dev/null +++ b/src/components/license-i18n.test.ts @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2021 The Pybricks Authors + +import { lookup } from '../../test'; +import { LicenseStringId } from './license-i18n'; +import en from './license-i18n.en.json'; + +describe('Ensure .json file has matches for LicenseStringId', () => { + test.each(Object.values(LicenseStringId))('%s', (id) => { + expect(lookup(en, id)).toBeDefined(); + }); +}); diff --git a/src/components/license-i18n.ts b/src/components/license-i18n.ts new file mode 100644 index 00000000..da089f70 --- /dev/null +++ b/src/components/license-i18n.ts @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2021 The Pybricks Authors + +// License dialog translation keys. + +export enum LicenseStringId { + Title = 'license.title', + Description = 'license.description', + PackageLabel = 'license.packageLabel', + AuthorLabel = 'license.authorLabel', + LicenseLabel = 'license.licenseLabel', + SelectPackageHelp = 'license.help.selectPackage', +} diff --git a/src/components/license.scss b/src/components/license.scss new file mode 100644 index 00000000..0b997eaf --- /dev/null +++ b/src/components/license.scss @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2020-2021 The Pybricks Authors + +// Custom styling for the LicenseDialog control. + +@import '../variables.scss'; + +.pb-license-dialog { + width: 1200px; +} + +.pb-license-browser { + margin-top: 16px; + display: flex; + height: 600px; +} + +.pb-license-list { + width: 25%; + flex-flow: column; + overflow: auto; +} + +.pb-license-info { + width: 75%; + flex-flow: column; + padding: 0px 16px; + overflow: auto; +} + +.pb-license-text { + padding: 0px 16px; +} diff --git a/src/reducers/app.ts b/src/reducers/app.ts index 5339d975..3bcce3ea 100644 --- a/src/reducers/app.ts +++ b/src/reducers/app.ts @@ -10,6 +10,7 @@ import { AppActionType } from '../actions/app'; export interface AppState { readonly showSettings: boolean; readonly showAboutDialog: boolean; + readonly showLicenseDialog: boolean; } const showSettings: Reducer = (state = false, action) => { @@ -34,4 +35,15 @@ const showAboutDialog: Reducer = (state = false, action) => { } }; -export default combineReducers({ showSettings, showAboutDialog }); +const showLicenseDialog: Reducer = (state = false, action) => { + switch (action.type) { + case AppActionType.OpenLicenseDialog: + return true; + case AppActionType.CloseLicenseDialog: + return false; + default: + return state; + } +}; + +export default combineReducers({ showSettings, showAboutDialog, showLicenseDialog }); diff --git a/src/reducers/index.ts b/src/reducers/index.ts index a6ccb937..940b7bf4 100644 --- a/src/reducers/index.ts +++ b/src/reducers/index.ts @@ -7,6 +7,7 @@ import ble, { BleState } from './ble'; import bootloader, { BootloaderState } from './bootloader'; import editor, { EditorState } from './editor'; import hub, { HubState } from './hub'; +import license, { LicenseState } from './license'; import notification, { NotificationState } from './notification'; import settings, { SettingsState } from './settings'; import status, { StatusState } from './status'; @@ -21,6 +22,7 @@ export interface RootState { readonly ble: BleState; readonly editor: EditorState; readonly hub: HubState; + readonly license: LicenseState; readonly notification: NotificationState; readonly settings: SettingsState; readonly status: StatusState; @@ -33,6 +35,7 @@ export default combineReducers({ ble, editor, hub, + license, notification, settings, status, diff --git a/src/reducers/license.ts b/src/reducers/license.ts new file mode 100644 index 00000000..78e70fe8 --- /dev/null +++ b/src/reducers/license.ts @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2021 The Pybricks Authors + +import { Reducer, combineReducers } from 'redux'; +import { Action } from '../actions'; +import { LicenseActionType } from '../actions/license'; + +export interface LicenseInfo { + readonly name: string; + readonly version: string; + readonly author: string | undefined; + readonly repository: string | null; + readonly source: string; + readonly license: string; + readonly licenseText: string | null; +} + +export type LicenseList = LicenseInfo[]; + +export interface LicenseState { + readonly list: LicenseList | null; + readonly selected: LicenseInfo | null; +} + +const list: Reducer = (state = null, action) => { + switch (action.type) { + case LicenseActionType.DidFetchList: + return action.list; + default: + return state; + } +}; + +const selected: Reducer = (state = null, action) => { + switch (action.type) { + case LicenseActionType.Select: + return action.info; + default: + return state; + } +}; + +export default combineReducers({ list, selected }); diff --git a/src/sagas/index.ts b/src/sagas/index.ts index 24a2cc0c..7682125d 100644 --- a/src/sagas/index.ts +++ b/src/sagas/index.ts @@ -8,6 +8,7 @@ import editor from './editor'; import errorLog from './error-log'; import flashFirmware from './flash-firmware'; import hub from './hub'; +import license from './license'; import lwp3BootloaderBle from './lwp3-bootloader-ble'; import lwp3BootloaderProtocol from './lwp3-bootloader-protocol'; import mpy from './mpy'; @@ -24,6 +25,7 @@ export default function* (): Generator { errorLog(), flashFirmware(), hub(), + license(), mpy(), settings(), terminal(), diff --git a/src/sagas/license.ts b/src/sagas/license.ts new file mode 100644 index 00000000..ff574f72 --- /dev/null +++ b/src/sagas/license.ts @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2021 The Pybricks Authors + +import { call, put, select, takeEvery } from 'redux-saga/effects'; +import { AppActionType } from '../actions/app'; +import { didFetchList } from '../actions/license'; +import { RootState } from '../reducers'; +import { LicenseList } from '../reducers/license'; + +function* fetchLicenses(): Generator { + const licenses = (yield select( + (s: RootState) => s.license.list, + )) as LicenseList | null; + + // if we already have license list, nothing to do + if (licenses !== null) { + return; + } + + const response = (yield call(() => fetch('static/oss-licenses.json'))) as Response; + if (!response.ok || response.body === null) { + // TODO: dispatch an action to notify user + console.error('failed to fetch oss-licenses.json', response.statusText); + return; + } + + const list = (yield call(() => response.json())) as LicenseList; + yield put(didFetchList(list)); +} + +export default function* (): Generator { + yield takeEvery(AppActionType.OpenLicenseDialog, fetchLicenses); +} diff --git a/yarn.lock b/yarn.lock index aedc6180..8e22a387 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4331,7 +4331,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9: dependencies: ms "2.0.0" -debug@^3.1.1, debug@^3.2.5: +debug@^3.1.1, debug@^3.2.5, debug@^3.2.6: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== @@ -5833,6 +5833,13 @@ get-intrinsic@^1.0.0, get-intrinsic@^1.0.1: has "^1.0.3" has-symbols "^1.0.1" +get-npm-tarball-url@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/get-npm-tarball-url/-/get-npm-tarball-url-2.0.1.tgz#43c15223c35096e3e4068d8a6c6747bbdfc23462" + integrity sha512-POrVRGyS9X5w+855/H46JGVYBGuVgJXyIkbsTCzW+sv5x2qH+rfQjc7652DzkgOskF+cqLevA2En7V0hu0gZCg== + dependencies: + normalize-registry-url "^1.0.0" + get-own-enumerable-property-symbols@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" @@ -6295,7 +6302,7 @@ human-signals@^1.1.1: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== -iconv-lite@0.4.24: +iconv-lite@0.4.24, iconv-lite@^0.4.4: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -8197,6 +8204,15 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= +needle@^2.2.4: + version "2.6.0" + resolved "https://registry.yarnpkg.com/needle/-/needle-2.6.0.tgz#24dbb55f2509e2324b4a99d61f413982013ccdbe" + integrity sha512-KKYdza4heMsEfSWD7VPUIz3zX2XDwOyX2d+geb4vrERZMT5RMU6ujjaD+I5Yr54uZxQ2w6XRTAhHBbSCyovZBg== + dependencies: + debug "^3.2.6" + iconv-lite "^0.4.4" + sax "^1.2.4" + negotiator@0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" @@ -8368,6 +8384,11 @@ normalize-range@^0.1.2: resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI= +normalize-registry-url@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/normalize-registry-url/-/normalize-registry-url-1.0.0.tgz#f75d2c48373da780c76f1f0eeb6382c06e784d13" + integrity sha512-0v6T4851b72ykk5zEtFoN4QX/Fqyk7pouIj9xZyAvAe9jlDhAwT4z6FlwsoQCHjeuK2EGUoAwy/F4y4B1uZq9A== + normalize-url@1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-1.9.1.tgz#2cc0d66b31ea23036458436e3620d85954c66c3c" @@ -10720,7 +10741,7 @@ sass-loader@8.0.2: schema-utils "^2.6.1" semver "^6.3.0" -sax@~1.2.4: +sax@^1.2.4, sax@~1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== @@ -11080,7 +11101,7 @@ sort-keys@^1.0.0: dependencies: is-plain-obj "^1.0.0" -source-list-map@^2.0.0: +source-list-map@^2.0.0, source-list-map@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== @@ -11165,6 +11186,13 @@ spdx-expression-parse@^3.0.0: spdx-exceptions "^2.1.0" spdx-license-ids "^3.0.0" +spdx-expression-validate@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/spdx-expression-validate/-/spdx-expression-validate-2.0.0.tgz#25c9408e1c63fad94fff5517bb7101ffcd23350b" + integrity sha512-b3wydZLM+Tc6CFvaRDBOF9d76oGIHNCLYFeHbftFXUWjnfZWganmDmvtM5sm1cRwJc/VDBMLyGGrsLFd1vOxbg== + dependencies: + spdx-expression-parse "^3.0.0" + spdx-license-ids@^3.0.0: version "3.0.7" resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz#e9c18a410e5ed7e12442a549fbd8afa767038d65" @@ -12393,6 +12421,18 @@ webpack-dev-server@3.11.0: ws "^6.2.1" yargs "^13.3.2" +webpack-license-plugin@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/webpack-license-plugin/-/webpack-license-plugin-4.1.2.tgz#d3eb954babf30ba65c50cc416c0a59d568776215" + integrity sha512-aS/fMx9zGfqs7OSj02iCX7W4hnaiGolihz+sHn6nEwaMTFeTEp0Uu+QIyRMluC731USIB42hFIdI/vje1SKdrQ== + dependencies: + chalk "^4.1.0" + get-npm-tarball-url "^2.0.1" + lodash "^4.17.20" + needle "^2.2.4" + spdx-expression-validate "^2.0.0" + webpack-sources "^2.0.0" + webpack-log@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/webpack-log/-/webpack-log-2.0.0.tgz#5b7928e0637593f119d32f6227c1e0ac31e1b47f" @@ -12426,6 +12466,14 @@ webpack-sources@^1.1.0, webpack-sources@^1.3.0, webpack-sources@^1.4.0, webpack- source-list-map "^2.0.0" source-map "~0.6.1" +webpack-sources@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-2.2.0.tgz#058926f39e3d443193b6c31547229806ffd02bac" + integrity sha512-bQsA24JLwcnWGArOKUxYKhX3Mz/nK1Xf6hxullKERyktjNMC4x8koOeaDNTA2fEJ09BdWLbM/iTW0ithREUP0w== + dependencies: + source-list-map "^2.0.1" + source-map "^0.6.1" + webpack@4.44.2: version "4.44.2" resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.44.2.tgz#6bfe2b0af055c8b2d1e90ed2cd9363f841266b72" From 3a22b19f8391d30ad358e0115ff9faa5e2beb369 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sun, 17 Jan 2021 12:46:08 -0600 Subject: [PATCH 14/16] bump copyright year --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 3e0b328e..29359f3e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 The Pybricks Authors +Copyright (c) 2020-2021 The Pybricks Authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From deac69fdabf6fcc45d79f7567a252116f420a4b9 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sun, 17 Jan 2021 13:11:47 -0600 Subject: [PATCH 15/16] switch to license-webpack-plugin This package is a bit more versatile. For example, it lets us include the app's package too. webpack-license-plugin had a bug that made it unusable anyway. --- craco.config.js | 224 +++++++++++++++++++++++++++++++++++++++- package.json | 5 +- src/reducers/license.ts | 4 +- yarn.lock | 100 +++++++++--------- 4 files changed, 272 insertions(+), 61 deletions(-) diff --git a/craco.config.js b/craco.config.js index 28a68095..4da4d1dc 100644 --- a/craco.config.js +++ b/craco.config.js @@ -1,3 +1,5 @@ +// Configuration file to override create-react-app. +// // https://github.com/gsoft-inc/craco/blob/master/packages/craco/README.md#configuration const { @@ -6,14 +8,232 @@ const { getLoaders, loaderByName, } = require('@craco/craco'); -const LicensePlugin = require('webpack-license-plugin'); +const LicensePlugin = require('license-webpack-plugin').LicenseWebpackPlugin; +const satisfies = require('spdx-satisfies'); + +// Permissive licenses can be added here. We would like to avoid copyleft. +const approvedLicenses = ['0BSD', 'Apache-2.0', 'BSD-3-Clause', 'ISC', 'MIT']; + +/** Converts a package.json "person" to a string. + * + * Ref: https://docs.npmjs.com/cli/v6/configuring-npm/package-json#people-fields-author-contributors + * + * @param person a string or "person" object or undefined + * @returns A string or undefined. + */ +function personToString(person) { + if (typeof person === 'string' || typeof person === 'undefined') { + return person; + } + + let str = person.name; + if (person.email) { + str += ` <${person.email}>`; + } + if (person.url) { + str += ` (${person.url})`; + } + return str; +} + +// Some packages don't have a separate license file, so we have to copy the +// license here e.g. from the README. + +function fedorIndutnyLicense(year) { + return `This software is licensed under the MIT License. + +Copyright Fedor Indutny, ${year}. + +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.`; +} + +const pybricksLicense = `MIT License + +Copyright (c) 2018-2021 The Pybricks Authors + +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. +`; + +const shopifyLicense = `MIT License + +Copyright (c) 2021 Shopify + +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.`; + +const licenseTextOverrides = { + '@pybricks/firmware': pybricksLicense, + '@pybricks/mpy-cross-v5': pybricksLicense, + '@shopify/dates': shopifyLicense, + '@shopify/decorators': shopifyLicense, + '@shopify/function-enhancers': shopifyLicense, + '@shopify/i18n': shopifyLicense, + '@shopify/react': shopifyLicense, + '@shopify/react-hooks': shopifyLicense, + '@shopify/react-i18n': shopifyLicense, + 'bn.js': fedorIndutnyLicense(2015), + brorand: fedorIndutnyLicense(2014), + 'des.js': fedorIndutnyLicense(2015), + elliptic: fedorIndutnyLicense(2014), + 'hash.js': fedorIndutnyLicense(2014), + 'hmac-drbg': fedorIndutnyLicense(2017), + 'miller-rabin': fedorIndutnyLicense(2014), + 'minimalistic-crypto-utils': fedorIndutnyLicense(2017), + gud: `MIT License + +Copyright (c) + +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.`, + isarray: `(MIT) + +Copyright (c) 2013 Julian Gruber + +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.`, + 'popper.js': `The MIT License (MIT) + +Copyright (c) 2019 Federico Zivolo + +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.`, + 'zen-push': `Copyright (c) 2018 zenparsing (Kevin Smith) + +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.`, +}; module.exports = { webpack: { plugins: [ new LicensePlugin({ outputFilename: 'static/oss-licenses.json', - replenishDefaultLicenseTexts: true, + perChunkOutput: false, + renderLicenses: (modules) => { + return JSON.stringify( + modules + .map((m) => ({ + name: m.packageJson.name, + version: m.packageJson.version, + author: personToString(m.packageJson.author), + license: m.licenseId, + licenseText: m.licenseText, + })) + .sort((a, b) => a.name.localeCompare(b.name, 'en')), + ); + }, + licenseTextOverrides, + additionalModules: [ + { name: '@pybricks/pybricks-code', directory: __dirname }, + ], + unacceptableLicenseTest: (licenseType) => + !satisfies(licenseType, `(${approvedLicenses.join(' OR ')})`), }), ], configure: { diff --git a/package.json b/package.json index 5a518b6a..4223d04b 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "pybricks-code", + "name": "@pybricks/pybricks-code", "version": "0.1.0", "license": "MIT", "author": "The Pybricks Authors", @@ -29,6 +29,7 @@ "@types/zen-push": "^0.1.1", "ace-builds": "^1.4.12", "file-saver": "^2.0.5", + "license-webpack-plugin": "^2.3.11", "node-sass": "^4.14.1", "prop-types": "^15.7.2", "react": "^16.13.1", @@ -41,9 +42,9 @@ "redux": "^4.0.5", "redux-logger": "^3.0.6", "redux-saga": "^1.1.3", + "spdx-satisfies": "^5.0.0", "typescript": "~4.1.3", "web-vitals": "^1.0.1", - "webpack-license-plugin": "^4.1.2", "xterm": "^4.9.0", "xterm-addon-fit": "^0.4.0", "zen-push": "^0.2.1" diff --git a/src/reducers/license.ts b/src/reducers/license.ts index 78e70fe8..cae9e9cd 100644 --- a/src/reducers/license.ts +++ b/src/reducers/license.ts @@ -9,10 +9,8 @@ export interface LicenseInfo { readonly name: string; readonly version: string; readonly author: string | undefined; - readonly repository: string | null; - readonly source: string; readonly license: string; - readonly licenseText: string | null; + readonly licenseText: string; } export type LicenseList = LicenseInfo[]; diff --git a/yarn.lock b/yarn.lock index 8e22a387..dd1f9da2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2101,6 +2101,15 @@ "@types/source-list-map" "*" source-map "^0.7.3" +"@types/webpack-sources@^0.1.5": + version "0.1.8" + resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-0.1.8.tgz#078d75410435993ec8a0a2855e88706f3f751f81" + integrity sha512-JHB2/xZlXOjzjBB6fMOpH1eQAfsrpqVVIbneE0Rok16WXwFaznaI5vfg75U5WgGJm7V9W1c4xeRQDjX/zwvghA== + dependencies: + "@types/node" "*" + "@types/source-list-map" "*" + source-map "^0.6.1" + "@types/webpack@^4.41.8": version "4.41.25" resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.41.25.tgz#4d3b5aecc4e44117b376280fbfd2dc36697968c4" @@ -2637,7 +2646,7 @@ arr-union@^3.1.0: resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= -array-find-index@^1.0.1: +array-find-index@^1.0.1, array-find-index@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" integrity sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E= @@ -4331,7 +4340,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9: dependencies: ms "2.0.0" -debug@^3.1.1, debug@^3.2.5, debug@^3.2.6: +debug@^3.1.1, debug@^3.2.5: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== @@ -5833,13 +5842,6 @@ get-intrinsic@^1.0.0, get-intrinsic@^1.0.1: has "^1.0.3" has-symbols "^1.0.1" -get-npm-tarball-url@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/get-npm-tarball-url/-/get-npm-tarball-url-2.0.1.tgz#43c15223c35096e3e4068d8a6c6747bbdfc23462" - integrity sha512-POrVRGyS9X5w+855/H46JGVYBGuVgJXyIkbsTCzW+sv5x2qH+rfQjc7652DzkgOskF+cqLevA2En7V0hu0gZCg== - dependencies: - normalize-registry-url "^1.0.0" - get-own-enumerable-property-symbols@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" @@ -6302,7 +6304,7 @@ human-signals@^1.1.1: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== -iconv-lite@0.4.24, iconv-lite@^0.4.4: +iconv-lite@0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -7608,6 +7610,14 @@ levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +license-webpack-plugin@^2.3.11: + version "2.3.11" + resolved "https://registry.yarnpkg.com/license-webpack-plugin/-/license-webpack-plugin-2.3.11.tgz#0d93188a31fce350a44c86212badbaf33dcd29d8" + integrity sha512-0iVGoX5vx0WDy8dmwTTpOOMYiGqILyUbDeVMFH52AjgBlS58lHwOlFMSoqg5nY8Kxl6+FRKyUZY/UdlQaOyqDw== + dependencies: + "@types/webpack-sources" "^0.1.5" + webpack-sources "^1.2.0" + lie@~3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a" @@ -8204,15 +8214,6 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= -needle@^2.2.4: - version "2.6.0" - resolved "https://registry.yarnpkg.com/needle/-/needle-2.6.0.tgz#24dbb55f2509e2324b4a99d61f413982013ccdbe" - integrity sha512-KKYdza4heMsEfSWD7VPUIz3zX2XDwOyX2d+geb4vrERZMT5RMU6ujjaD+I5Yr54uZxQ2w6XRTAhHBbSCyovZBg== - dependencies: - debug "^3.2.6" - iconv-lite "^0.4.4" - sax "^1.2.4" - negotiator@0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" @@ -8384,11 +8385,6 @@ normalize-range@^0.1.2: resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI= -normalize-registry-url@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/normalize-registry-url/-/normalize-registry-url-1.0.0.tgz#f75d2c48373da780c76f1f0eeb6382c06e784d13" - integrity sha512-0v6T4851b72ykk5zEtFoN4QX/Fqyk7pouIj9xZyAvAe9jlDhAwT4z6FlwsoQCHjeuK2EGUoAwy/F4y4B1uZq9A== - normalize-url@1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-1.9.1.tgz#2cc0d66b31ea23036458436e3620d85954c66c3c" @@ -10741,7 +10737,7 @@ sass-loader@8.0.2: schema-utils "^2.6.1" semver "^6.3.0" -sax@^1.2.4, sax@~1.2.4: +sax@~1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== @@ -11101,7 +11097,7 @@ sort-keys@^1.0.0: dependencies: is-plain-obj "^1.0.0" -source-list-map@^2.0.0, source-list-map@^2.0.1: +source-list-map@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== @@ -11165,6 +11161,15 @@ sourcemap-codec@^1.4.4: resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== +spdx-compare@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/spdx-compare/-/spdx-compare-1.0.0.tgz#2c55f117362078d7409e6d7b08ce70a857cd3ed7" + integrity sha512-C1mDZOX0hnu0ep9dfmuoi03+eOdDoz2yvK79RxbcrVEG1NO1Ph35yW102DHWKN4pk80nwCgeMmSY5L25VE4D9A== + dependencies: + array-find-index "^1.0.2" + spdx-expression-parse "^3.0.0" + spdx-ranges "^2.0.0" + spdx-correct@^3.0.0: version "3.1.1" resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" @@ -11186,18 +11191,25 @@ spdx-expression-parse@^3.0.0: spdx-exceptions "^2.1.0" spdx-license-ids "^3.0.0" -spdx-expression-validate@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/spdx-expression-validate/-/spdx-expression-validate-2.0.0.tgz#25c9408e1c63fad94fff5517bb7101ffcd23350b" - integrity sha512-b3wydZLM+Tc6CFvaRDBOF9d76oGIHNCLYFeHbftFXUWjnfZWganmDmvtM5sm1cRwJc/VDBMLyGGrsLFd1vOxbg== - dependencies: - spdx-expression-parse "^3.0.0" - spdx-license-ids@^3.0.0: version "3.0.7" resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz#e9c18a410e5ed7e12442a549fbd8afa767038d65" integrity sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ== +spdx-ranges@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/spdx-ranges/-/spdx-ranges-2.1.1.tgz#87573927ba51e92b3f4550ab60bfc83dd07bac20" + integrity sha512-mcdpQFV7UDAgLpXEE/jOMqvK4LBoO0uTQg0uvXUewmEFhpiZx5yJSZITHB8w1ZahKdhfZqP5GPEOKLyEq5p8XA== + +spdx-satisfies@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/spdx-satisfies/-/spdx-satisfies-5.0.0.tgz#d740b8f14caeada36fb307629dee87146970a256" + integrity sha512-/hGhwh20BeGmkA+P/lm06RvXD94JduwNxtx/oX3B5ClPt1/u/m5MCaDNo1tV3Y9laLkQr/NRde63b9lLMhlNfw== + dependencies: + spdx-compare "^1.0.0" + spdx-expression-parse "^3.0.0" + spdx-ranges "^2.0.0" + spdy-transport@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" @@ -12421,18 +12433,6 @@ webpack-dev-server@3.11.0: ws "^6.2.1" yargs "^13.3.2" -webpack-license-plugin@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/webpack-license-plugin/-/webpack-license-plugin-4.1.2.tgz#d3eb954babf30ba65c50cc416c0a59d568776215" - integrity sha512-aS/fMx9zGfqs7OSj02iCX7W4hnaiGolihz+sHn6nEwaMTFeTEp0Uu+QIyRMluC731USIB42hFIdI/vje1SKdrQ== - dependencies: - chalk "^4.1.0" - get-npm-tarball-url "^2.0.1" - lodash "^4.17.20" - needle "^2.2.4" - spdx-expression-validate "^2.0.0" - webpack-sources "^2.0.0" - webpack-log@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/webpack-log/-/webpack-log-2.0.0.tgz#5b7928e0637593f119d32f6227c1e0ac31e1b47f" @@ -12458,7 +12458,7 @@ webpack-merge@^4.2.2: dependencies: lodash "^4.17.15" -webpack-sources@^1.1.0, webpack-sources@^1.3.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3: +webpack-sources@^1.1.0, webpack-sources@^1.2.0, webpack-sources@^1.3.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ== @@ -12466,14 +12466,6 @@ webpack-sources@^1.1.0, webpack-sources@^1.3.0, webpack-sources@^1.4.0, webpack- source-list-map "^2.0.0" source-map "~0.6.1" -webpack-sources@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-2.2.0.tgz#058926f39e3d443193b6c31547229806ffd02bac" - integrity sha512-bQsA24JLwcnWGArOKUxYKhX3Mz/nK1Xf6hxullKERyktjNMC4x8koOeaDNTA2fEJ09BdWLbM/iTW0ithREUP0w== - dependencies: - source-list-map "^2.0.1" - source-map "^0.6.1" - webpack@4.44.2: version "4.44.2" resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.44.2.tgz#6bfe2b0af055c8b2d1e90ed2cd9363f841266b72" From 8c1e9a8273102c39b75e9fe7d81800f418e4c6f4 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sun, 17 Jan 2021 14:59:03 -0600 Subject: [PATCH 16/16] implement license saga tests --- src/actions/license.ts | 14 +++++++- src/sagas/error-log.test.ts | 15 +++++++++ src/sagas/error-log.ts | 6 ++++ src/sagas/license.test.ts | 67 +++++++++++++++++++++++++++++++++++++ src/sagas/license.ts | 5 ++- 5 files changed, 103 insertions(+), 4 deletions(-) create mode 100644 src/sagas/license.test.ts diff --git a/src/actions/license.ts b/src/actions/license.ts index 3ab1a567..5a593a20 100644 --- a/src/actions/license.ts +++ b/src/actions/license.ts @@ -6,6 +6,7 @@ import { LicenseInfo, LicenseList } from '../reducers/license'; export enum LicenseActionType { DidFetchList = 'license.action.didFetchList', + DidFailToFetchList = 'license.action.didFailToFetchList', Select = 'license.action.select', } @@ -17,6 +18,14 @@ export function didFetchList(list: LicenseList): LicenseDidFetchListAction { return { type: LicenseActionType.DidFetchList, list }; } +export type LicenseDidFailToFetchListAction = Action & { + reason: Response; +}; + +export function didFailToFetchList(reason: Response): LicenseDidFailToFetchListAction { + return { type: LicenseActionType.DidFailToFetchList, reason }; +} + export type LicenseSelectAction = Action & { info: LicenseInfo; }; @@ -25,4 +34,7 @@ export function select(info: LicenseInfo): LicenseSelectAction { return { type: LicenseActionType.Select, info }; } -export type LicenseAction = LicenseDidFetchListAction | LicenseSelectAction; +export type LicenseAction = + | LicenseDidFetchListAction + | LicenseDidFailToFetchListAction + | LicenseSelectAction; diff --git a/src/sagas/error-log.test.ts b/src/sagas/error-log.test.ts index 15478f98..b0bd8f25 100644 --- a/src/sagas/error-log.test.ts +++ b/src/sagas/error-log.test.ts @@ -7,6 +7,7 @@ import { didFailToConnect as bleDidFailToConnect, } from '../actions/ble'; import { didFailToWrite } from '../actions/ble-uart'; +import { didFailToFetchList } from '../actions/license'; import { BootloaderConnectionFailureReason, didError, @@ -68,3 +69,17 @@ test('bootloaderDidError', async () => { await saga.end(); }); + +test('licenseDidFailToFetch', async () => { + const saga = new AsyncSaga(errorLog); + + console.error = jest.fn(); + saga.put( + didFailToFetchList( + new Response(undefined, { status: 404, statusText: 'not found' }), + ), + ); + expect(console.error).toHaveBeenCalledTimes(1); + + await saga.end(); +}); diff --git a/src/sagas/error-log.ts b/src/sagas/error-log.ts index 7457c029..d7e96dbe 100644 --- a/src/sagas/error-log.ts +++ b/src/sagas/error-log.ts @@ -8,6 +8,7 @@ import { BleDeviceFailToConnectReasonType, } from '../actions/ble'; import { BleUartActionType, BleUartDidFailToWriteAction } from '../actions/ble-uart'; +import { LicenseActionType, LicenseDidFailToFetchListAction } from '../actions/license'; import { BootloaderConnectionActionType, BootloaderConnectionDidErrorAction, @@ -39,6 +40,10 @@ function bootloaderDidError(action: BootloaderConnectionDidErrorAction): void { console.error(action.err); } +function licenseDidFailToFetch(action: LicenseDidFailToFetchListAction): void { + console.error(`Failed to fetch licenses: ${action.reason.statusText}`); +} + export default function* (): Generator { yield takeEvery(BleDeviceActionType.DidFailToConnect, bleDeviceDidFailToConnect); yield takeEvery(BleUartActionType.DidFailToWrite, bleDataDidFailToWrite); @@ -47,4 +52,5 @@ export default function* (): Generator { bootloaderDidFailToConnect, ); yield takeEvery(BootloaderConnectionActionType.DidError, bootloaderDidError); + yield takeEvery(LicenseActionType.DidFailToFetchList, licenseDidFailToFetch); } diff --git a/src/sagas/license.test.ts b/src/sagas/license.test.ts new file mode 100644 index 00000000..609072f3 --- /dev/null +++ b/src/sagas/license.test.ts @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2020-2021 The Pybricks Authors + +// Tests for license sagas. + +import { AsyncSaga, delay } from '../../test'; +import { openLicenseDialog } from '../actions/app'; +import { didFailToFetchList, didFetchList } from '../actions/license'; +import { LicenseList, LicenseState } from '../reducers/license'; +import license from './license'; + +afterAll(() => { + jest.restoreAllMocks(); +}); + +describe('fetchLicenses', () => { + test('first call', async () => { + const testLicenseList: LicenseList = []; + const saga = new AsyncSaga(license); + + jest.spyOn(globalThis, 'fetch').mockResolvedValue( + new Response(JSON.stringify(testLicenseList)), + ); + + // initially, license list starts as null, so fetch is called to get + // the list + saga.setState({ license: { list: null } as LicenseState }); + saga.put(openLicenseDialog()); + + const action = await saga.take(); + expect(action).toEqual(didFetchList(testLicenseList)); + + await saga.end(); + }); + test('second call', async () => { + const testLicenseList: LicenseList = []; + const saga = new AsyncSaga(license); + + jest.spyOn(globalThis, 'fetch').mockRejectedValue( + 'fetch () should not have been called', + ); + + // after we have the list, we don't fetch it again since it will + // always be the same list + saga.setState({ license: { list: testLicenseList } as LicenseState }); + saga.put(openLicenseDialog()); + + // have to yield to be sure fetch call would have taken place on error + await delay(0); + + await saga.end(); + }); + test('failed fetch', async () => { + const failResponse = new Response(undefined, { status: 404 }); + const saga = new AsyncSaga(license); + + jest.spyOn(globalThis, 'fetch').mockResolvedValue(failResponse); + + saga.setState({ license: { list: null } as LicenseState }); + saga.put(openLicenseDialog()); + + const action = await saga.take(); + expect(action).toEqual(didFailToFetchList(failResponse)); + + await saga.end(); + }); +}); diff --git a/src/sagas/license.ts b/src/sagas/license.ts index ff574f72..0bd1eac0 100644 --- a/src/sagas/license.ts +++ b/src/sagas/license.ts @@ -3,7 +3,7 @@ import { call, put, select, takeEvery } from 'redux-saga/effects'; import { AppActionType } from '../actions/app'; -import { didFetchList } from '../actions/license'; +import { didFailToFetchList, didFetchList } from '../actions/license'; import { RootState } from '../reducers'; import { LicenseList } from '../reducers/license'; @@ -19,8 +19,7 @@ function* fetchLicenses(): Generator { const response = (yield call(() => fetch('static/oss-licenses.json'))) as Response; if (!response.ok || response.body === null) { - // TODO: dispatch an action to notify user - console.error('failed to fetch oss-licenses.json', response.statusText); + yield put(didFailToFetchList(response)); return; }