Commit e3467f5d authored by Kyle Keating's avatar Kyle Keating Committed by Kyle Keating

Remove frontend-share subfolder

- Remove github actions related to frontend-shared
- Remove scripts and gulp commands related to frontend-shared
- frontend-shared is now managed from its own repository.
parent 4ac2b1b5
......@@ -3,4 +3,3 @@ build/**
**/coverage/**
docs/_build/*
dev-server/static/**/*.js
frontend-shared/lib/**
name: 'Get Updated Package Version'
inputs:
# The shared package name, "@hypothesis/frontend-shared"
shared-package:
required: true
default: ''
description: 'The npm package name'
# The client package name, "hypothesis"
parent-package:
required: true
default: ''
description: 'The npm package name'
outputs:
updated_version:
description: 'The new version of the npm package'
runs:
using: 'node12'
main: 'index.js'
\ No newline at end of file
'use strict'
const core = require('@actions/core');
const util = require('util');
const exec = util.promisify(require('child_process').exec);
/**
* Returns true is there is a non-zero length diff between the last published version
* and master within the frontend-shared/ folder.
*/
async function haveChangesOccurred() {
const parentPackage = core.getInput('parent-package');
const versionResult = await exec(`npm view ${parentPackage}@latest version`);
const version = versionResult.stdout.trim();
const diffResult = await exec(`git diff --stat v${version}..master frontend-shared/`)
return diffResult.stdout.length > 0;
}
/**
* Increment the minor part of a `MAJOR.MINOR.PATCH` semver version.
*/
function bumpMinorVersion(version) {
const parts = version.split('.')
if (parts.length !== 3) {
throw new Error(`${version} is not a valid MAJOR.MINOR.PATCH version`);
}
const majorVersion = parseInt(parts[0]);
const newMinorVersion = parseInt(parts[1]) + 1;
const patchVersion = parseInt(parts[2]);
if (isNaN(majorVersion) || isNaN(newMinorVersion) || isNaN(patchVersion)) {
throw new Error(`${version} does not have valid integer parts`);
}
return `${parts[0]}.${newMinorVersion}.${parts[2]}`
}
/**
* Get the current version from npm and bump the minor part by 1.
*/
async function getNewVersion() {
const sharedPackage = core.getInput('shared-package');
const result = await exec(`npm view ${sharedPackage} version`);
const newVersion = bumpMinorVersion(result.stdout);
return newVersion;
}
async function main() {
if (await haveChangesOccurred()) {
// eslint-disable-next-line no-console
console.log('Changes detected in frontend-shared/, publishing new version...');
const newVersion = await getNewVersion();
core.setOutput("updated_version", newVersion);
} else {
// eslint-disable-next-line no-console
console.log('No changes detected in frontend-shared/');
core.setOutput("updated_version", null);
}
}
main().catch(error => {
core.setFailed(error.message);
})
......@@ -13,8 +13,6 @@ jobs:
key: ${{ runner.os }}-node_modules-${{ hashFiles('yarn.lock') }}
- name: Install
run: yarn install --frozen-lockfile
- name: Set up frontend-shared
run: yarn setup-frontend-shared
- name: Format
run: yarn checkformatting
- name: Lint
......
name: frontend-shared package
on:
release:
types:
- published
jobs:
update-frontend-shared-version:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
# Additionally fetch master to be able to diff for changes in the frontend-shared/ folder
- name: Fetch origin
run: git fetch --tags origin master:master
# Setup .npmrc file to publish to npm
- name: Setup Nodejs
uses: actions/setup-node@v1
with:
node-version: '12.x'
registry-url: 'https://registry.npmjs.org'
- name: Install client
run: yarn install --frozen-lockfile
- name: Get new version
uses: ./.github/actions/frontend-shared-version
id: version
with:
shared-package: '@hypothesis/frontend-shared'
parent-package: 'hypothesis'
- name: Set up frontend-shared
run: yarn setup-frontend-shared
# If updated_version is null, skip this step
if: ${{ steps.version.outputs.updated_version }}
- name: Set new version in package.json
working-directory: frontend-shared
run: yarn version --no-git-tag-version --new-version ${{ steps.version.outputs.updated_version }}
# If updated_version is null, skip this step
if: ${{ steps.version.outputs.updated_version }}
- name: Publish package
working-directory: frontend-shared
run: npm publish --access public
# If updated_version is null, skip this step
if: ${{ steps.version.outputs.updated_version }}
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
......@@ -76,7 +76,6 @@ node {
stage('Setup') {
nodeEnv.inside("-e HOME=${workspace}") {
sh "yarn install"
sh "yarn setup-frontend-shared"
}
}
......
'use strict';
module.exports = {
"presets": [
[
"@babel/preset-react",
{
"runtime": "automatic",
"importSource": "preact"
}
],
[
"@babel/preset-env",
{
"bugfixes": true,
"targets": {
"chrome": "57",
"firefox": "53",
"safari": "10.1",
"edge": "17"
}
}
]
],
"env": {
"development": {
"presets": [
[
"@babel/preset-react",
{
"development": true,
"runtime": "automatic",
// Use `preact/compat/jsx-dev-runtime` which is an alias for `preact/jsx-runtime`.
// See https://github.com/preactjs/preact/issues/2974.
"importSource": "preact/compat"
}
]
]
}
}
}
{
"parserOptions": {
"sourceType": "module"
}
}
# Shared resources for Hypothesis front-end applications
A package of resources for Hypothesis front-end applications.
#### Requirements
- preact
- browserify
### Usage
```
$ npm install @hypothesis/frontend-shared --save
```
#### In SASS modules
To import default styling of frontend-shared components, include this line in the main project's SASS.
```sass
@use '@hypothesis/frontend-shared/styles';
```
Mixins can be imported directly
```sass
@use "@hypothesis/frontend-shared/styles/mixins" as mixins;
```
#### In JS
```js
import { SvgIcon } from '@hypothesis/frontend-shared';
```
### License
The Hypothesis client is released under the [2-Clause BSD License][bsd2c],
sometimes referred to as the "Simplified BSD License". Some third-party
components are included. They are subject to their own licenses. All of the
license information can be found in the included [LICENSE][license] file.
[bsd2c]: http://www.opensource.org/licenses/BSD-2-Clause
[license]: https://github.com/hypothesis/client/blob/master/LICENSE
# Frontend Shared Package
The `frontend-shared` package is a library of shared utilities, components and styles common to multiple Hypothesis front-end applications.
The `frontend-shared` package source is located in `./frontend-shared` in this repository.
Note: The long-term goal is to move this package into its own repository. There are some short-term advantages to the current setup: it piggy-backs on existing tooling and gives developers the benefit of working with just one repository when making changes (which primarily benefit the `client` in the first place).
## How it works
Frontend-shared works by creating a symlink via `$ yarn link` between the working dir
`./frontend-shared/` and the installed dir `node_modules/@hypothesis/frontend-shared/`.
## Client setup
To set up `frontend-shared` in the client, run
```shell
$ make dev
```
as normal for development in the `client` application.
This will trigger the `frontend-shared` build which:
- Builds transpiled JSX into the `frontend-shared/lib` directory
- Creates a linked package in the `node_modules/@hypothesis` folder (this is linked to `frontend-shared/lib`)
When `$ make dev` is running, file changes will automatically trigger a rebuild. To rebuild manually, run:
```shell
$ gulp setup-frontend-shared
```
## Usage in other repositories
If you have modified the `frontend-shared` package locally and would like to preview changes in another Hypothesis applications that imports the package--that is, if you want to test uncommitted changes in `frontend-shared` in another local repository before publishing changes--follow these steps:
1. In this repository's directory
```shell
$ gulp setup-frontend-shared
```
1. In the target repository
```shell
$ yarn link @hypothesis/frontend-shared
```
_Note_: the package will need to be rebuilt after any changes are made to it before those changes can be seen in the consuming repository. This can be done by performing either of the following steps:
- Run `$ make dev` in this repository, which will **automatically** re-build on any file changes, or
- Run `$ gulp build-frontend-shared-js` in this repository to re-build **manually** after changes
#### Removing the link
If you wish to revert back to a published version, as opposed to a local version, you'll have to remove the local link and force the install:
```shell
$ yarn unlink @hypothesis/frontend-shared
$ yarn install --force
```
## Other caveats
1. The `./frontend-shared/node_modules` should never exist locally and only needs to be installed during package releases performed by CI. The existence of that folder will cause problems for the client and it won't be able to run correctly. If you accidentally run `yarn install` from the `./frontend-shared` folder, remove the `node_modules/` dir.
2. If you have two copies of the client repository checked out, then running the link step twice is problematic in that only the first package works. In order override the link and use a the second copy, you'll need remove the link from the first copy by going into the `./frontend-shared` folder of the first copy and running `$ yarn unlink`.
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" aria-hidden="true" focusable="false" class="Icon Icon--arrow-left"><g fill-rule="evenodd"><rect fill="none" stroke="none" x="0" y="0" width="16" height="16"></rect><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 12L3 8l4-4M4 8h9-9z"></path></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" aria-hidden="true" focusable="false" class="Icon Icon--arrow-right"><g fill-rule="evenodd"><rect fill="none" stroke="none" x="0" y="0" width="16" height="16"></rect><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 4l4 4-4 4m3-4H3h9z"></path></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" class="Icon Icon--check"><g fill-rule="evenodd"><rect fill="none" stroke="none" x="0" y="0" width="16" height="16"></rect><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 3L6 13 3 8"></path></g></svg>
{
"name": "@hypothesis/frontend-shared",
"version": "1.0.0",
"description": "Shared components, styles and utilities for Hypothesis projects",
"license": "BSD-2-Clause",
"repository": "hypothesis/client",
"devDependencies": {
"@babel/cli": "^7.1.6",
"@babel/core": "^7.1.6",
"@babel/preset-env": "^7.1.6",
"@babel/preset-react": "^7.0.0"
},
"peerDependencies": {
"preact": "^10.4.0"
},
"scripts": {
"build": "npx babel src --out-dir lib --source-maps --ignore '**/test'"
},
"files": [
"lib",
"styles",
"images"
],
"main": "./lib/index.js",
"browserify": {
"transform": [
[
"stringify",
{
"appliesTo": {
"includeExtensions": [
".html",
".svg"
]
}
}
]
]
}
}
/**
* Normalize a keyboard event key name.
*
* Some old Microsoft browsers, such as IE 11 and Edge Legacy [1], use non-standard
* names for some keys. If any abnormal keys are used, this method returns the
* normalized name so our UI components don't require a special case.
*
* [1] https://caniuse.com/keyboardevent-key
*
* @param {string} key - The keyboard event `key` name
* @return {string} - Normalized `key` name
*/
export function normalizeKeyName(key) {
const mappings = {
Left: 'ArrowLeft',
Up: 'ArrowUp',
Down: 'ArrowDown',
Right: 'ArrowRight',
Spacebar: ' ',
Del: 'Delete',
};
return mappings[key] ? mappings[key] : key;
}
import classnames from 'classnames';
import { useLayoutEffect, useRef } from 'preact/hooks';
import propTypes from 'prop-types';
/**
* Object mapping icon names to SVG markup.
*
* @typedef {Object.<string,string>} IconMap
*/
/**
* @template T
* @typedef {import("preact/hooks").Ref<T>} Ref
*/
/**
* Map of icon name to SVG data.
*
* @type {IconMap}
*/
let iconRegistry = {};
/**
* @typedef SvgIconProps
* @prop {string} name - The name of the icon to display.
* The name must match a name that has already been registered using the
* `registerIcons` function.
* @prop {string} [className] - A CSS class to apply to the `<svg>` element.
* @prop {boolean} [inline] - Apply a style allowing for inline display of icon wrapper.
* @prop {string} [title] - Optional title attribute to apply to the SVG's containing `span`.
*/
/**
* Component that renders icons using inline `<svg>` elements.
* This enables their appearance to be customized via CSS.
*
* This matches the way we do icons on the website, see
* https://github.com/hypothesis/h/pull/3675
*
* @param {SvgIconProps} props
*/
export function SvgIcon({ name, className = '', inline = false, title = '' }) {
if (!iconRegistry[name]) {
throw new Error(`Icon name "${name}" is not registered`);
}
const markup = iconRegistry[name];
const element = /** @type {Ref<HTMLElement>} */ (useRef());
useLayoutEffect(() => {
const svg = element.current.querySelector('svg');
// The icon should always contain an `<svg>` element, but check here as we
// don't validate the markup when it is registered.
if (svg) {
svg.setAttribute('class', className);
}
}, [
className,
// `markup` is a dependency of this effect because the SVG is replaced if
// it changes.
markup,
]);
const spanProps = {};
if (title) {
spanProps.title = title;
}
return (
<span
className={classnames('SvgIcon', { 'SvgIcon--inline': inline })}
dangerouslySetInnerHTML={{ __html: markup }}
ref={element}
{...spanProps}
/>
);
}
SvgIcon.propTypes = {
name: propTypes.string.isRequired,
className: propTypes.string,
inline: propTypes.bool,
title: propTypes.string,
};
/**
* Register icons for use with the `SvgIcon` component.
*
* @param {IconMap} icons
* @param {Object} options
* @param {boolean} [options.reset] - If `true`, remove existing registered icons.
*/
export function registerIcons(icons, { reset = false } = {}) {
if (reset) {
iconRegistry = {};
}
Object.assign(iconRegistry, icons);
}
/**
* Return the currently available icons.
*
* To register icons, don't mutate this directly but call `registerIcons`
* instead.
*
* @return {IconMap}
*/
export function availableIcons() {
return iconRegistry;
}
import { render } from 'preact';
import { SvgIcon, availableIcons, registerIcons } from '../SvgIcon';
describe('SvgIcon', () => {
// Tests here use DOM APIs rather than Enzyme because SvgIcon uses
// `dangerouslySetInnerHTML` for its content, and that is not visible in the
// Enzyme tree.
// Global icon set that is registered with `SvgIcon` outside of these tests.
let savedIconSet;
beforeEach(() => {
savedIconSet = availableIcons();
registerIcons(
{
'arrow-left': require('../../../images/icons/arrow-left.svg'),
'arrow-right': require('../../../images/icons/arrow-right.svg'),
},
{ reset: true }
);
});
afterEach(() => {
registerIcons(savedIconSet, { reset: true });
});
it("sets the element's content to the content of the SVG", () => {
const container = document.createElement('div');
render(<SvgIcon name="arrow-left" />, container);
assert.ok(container.querySelector('svg'));
});
it('throws an error if the icon name is not registered', () => {
assert.throws(() => {
const container = document.createElement('div');
render(<SvgIcon name="unknown" />, container);
}, 'Icon name "unknown" is not registered');
});
it('does not set the class of the SVG by default', () => {
const container = document.createElement('div');
render(<SvgIcon name="arrow-left" />, container);
const svg = container.querySelector('svg');
assert.equal(svg.getAttribute('class'), '');
});
it('sets the class of the SVG if provided', () => {
const container = document.createElement('div');
render(<SvgIcon name="arrow-left" className="thing__icon" />, container);
const svg = container.querySelector('svg');
assert.equal(svg.getAttribute('class'), 'thing__icon');
});
it('retains the CSS class if the icon changes', () => {
const container = document.createElement('div');
render(<SvgIcon name="arrow-left" className="thing__icon" />, container);
render(<SvgIcon name="arrow-right" className="thing__icon" />, container);
const svg = container.querySelector('svg');
assert.equal(svg.getAttribute('class'), 'thing__icon');
});
it('sets a default class on the wrapper element', () => {
const container = document.createElement('div');
render(<SvgIcon name="arrow-left" />, container);
const wrapper = container.querySelector('span');
assert.isTrue(wrapper.classList.contains('SvgIcon'));
assert.isFalse(wrapper.classList.contains('SvgIcon--inline'));
});
it('appends an inline class to wrapper if `inline` prop is `true`', () => {
const container = document.createElement('div');
render(<SvgIcon name="arrow-left" inline={true} />, container);
const wrapper = container.querySelector('span');
assert.isTrue(wrapper.classList.contains('SvgIcon'));
assert.isTrue(wrapper.classList.contains('SvgIcon--inline'));
});
it('sets a title to the containing `span` element if `title` is present', () => {
const container = document.createElement('div');
render(<SvgIcon name="arrow-left" title="Open menu" />, container);
const wrapper = container.querySelector('span');
assert.equal(wrapper.getAttribute('title'), 'Open menu');
});
it('sets does not set a title on the containing `span` element if `title` not present', () => {
const container = document.createElement('div');
render(<SvgIcon name="arrow-left" />, container);
const wrapper = container.querySelector('span');
assert.notOk(wrapper.getAttribute('title'));
});
});
import { mount } from 'enzyme';
import { useRef } from 'preact/hooks';
import { act } from 'preact/test-utils';
import { useElementShouldClose } from '../use-element-should-close';
describe('useElementShouldClose', () => {
let handleClose;
const createEvent = (name, props) => {
const event = new Event(name);
Object.assign(event, props);
return event;
};
const events = [
new Event('mousedown'),
new Event('click'),
createEvent('keydown', { key: 'Escape' }),
new Event('focus'),
];
// Create a fake component to mount in tests that uses the hook
// eslint-disable-next-line react/prop-types
function FakeComponent({ isOpen = true }) {
const myRef = useRef();
useElementShouldClose(myRef, isOpen, handleClose);
return (
<div ref={myRef}>
<button>Hi</button>
</div>
);
}
function createComponent(props) {
return mount(<FakeComponent isOpen={true} {...props} />);
}
beforeEach(() => {
handleClose = sinon.stub();
});
events.forEach(event => {
it(`should invoke close callback once for events outside of element (${event.type})`, () => {
const wrapper = createComponent();
act(() => {
document.body.dispatchEvent(event);
});
wrapper.update();
assert.calledOnce(handleClose);
// Update the component to change it and re-execute the hook
wrapper.setProps({ isOpen: false });
act(() => {
document.body.dispatchEvent(event);
});
// Cleanup of hook should have removed eventListeners, so the callback
// is not called again
assert.calledOnce(handleClose);
});
});
events.forEach(event => {
it(`should not invoke close callback on events outside of element if element closed (${event.type})`, () => {
const wrapper = createComponent({ isOpen: false });
act(() => {
document.body.dispatchEvent(event);
});
wrapper.update();
assert.equal(handleClose.callCount, 0);
});
});
events.forEach(event => {
it(`should not invoke close callback on events inside of element (${event.type})`, () => {
const wrapper = createComponent();
const button = wrapper.find('button');
act(() => {
button.getDOMNode().dispatchEvent(event);
});
wrapper.update();
assert.equal(handleClose.callCount, 0);
});
});
});
import { useEffect } from 'preact/hooks';
import { normalizeKeyName } from '../browser-compatibility-utils';
/**
* Attach listeners for one or multiple events to an element and return a
* function that removes the listeners.
*
* @param {HTMLElement} element
* @param {string[]} events
* @param {EventListener} listener
* @param {Object} options
* @param {boolean} [options.useCapture]
* @return {() => void} Function which removes the event listeners.
*/
function listen(element, events, listener, { useCapture = false } = {}) {
events.forEach(event =>
element.addEventListener(event, listener, useCapture)
);
return () => {
events.forEach(event =>
element.removeEventListener(event, listener, useCapture)
);
};
}
/**
* @template T
* @typedef {import("preact/hooks").Ref<T>} Ref
*/
/**
* This hook provides a way to close or hide an element when a user interacts
* with elements outside of it or presses the Esc key. It can be used to
* create non-modal popups (eg. for menus, autocomplete lists and non-modal dialogs)
* that automatically close when appropriate.
*
* When the element is visible/open, this hook monitors for document interactions
* that should close it - such as clicks outside the element or Esc key presses.
* When such an interaction happens, the `handleClose` callback is invoked.
*
* @param {Ref<HTMLElement>} closeableEl - Outer DOM element for the popup
* @param {boolean} isOpen - Whether the popup is currently visible/open
* @param {() => void} handleClose - Callback invoked to close the popup
*/
export function useElementShouldClose(closeableEl, isOpen, handleClose) {
useEffect(() => {
if (!isOpen) {
return () => {};
}
// Close element when user presses Escape key, regardless of focus.
const removeKeyDownListener = listen(document.body, ['keydown'], event => {
const keyEvent = /** @type {KeyboardEvent} */ (event);
if (normalizeKeyName(keyEvent.key) === 'Escape') {
handleClose();
}
});
// Close element if user focuses an element outside of it via any means
// (key press, programmatic focus change).
const removeFocusListener = listen(
document.body,
['focus'],
event => {
if (!closeableEl.current.contains(/** @type {Node} */ (event.target))) {
handleClose();
}
},
{ useCapture: true }
);
// Close element if user clicks outside of it, even if on an element which
// does not accept focus.
const removeClickListener = listen(
document.body,
['mousedown', 'click'],
event => {
if (!closeableEl.current.contains(/** @type {Node} */ (event.target))) {
handleClose();
}
},
{ useCapture: true }
);
return () => {
removeKeyDownListener();
removeClickListener();
removeFocusListener();
};
}, [closeableEl, isOpen, handleClose]);
}
// Components
export { SvgIcon, registerIcons } from './components/SvgIcon';
// Hooks
export { useElementShouldClose } from './hooks/use-element-should-close';
// Utilities
export { normalizeKeyName } from './browser-compatibility-utils';
import { normalizeKeyName } from '../browser-compatibility-utils';
describe('shared/browser-compatibility-utils', () => {
describe('normalizeKeyName', () => {
[
{
from: 'Left',
to: 'ArrowLeft',
},
{
from: 'Up',
to: 'ArrowUp',
},
{
from: 'Down',
to: 'ArrowDown',
},
{
from: 'Right',
to: 'ArrowRight',
},
{
from: 'Spacebar',
to: ' ',
},
{
from: 'Del',
to: 'Delete',
},
].forEach(test => {
it(`changes the key value '${test.from}' to '${test.to}'`, () => {
assert.equal(normalizeKeyName(test.from), test.to);
});
});
});
});
// To use frontend-shared styles, include this file in the sass entry file(s) for parent app
//
// e.g.
// @use '@hypothesis/frontend-shared/styles'
@use './components/SvgIcon';
/* Make the wrapper element's size match the contained `svg` element */
.SvgIcon {
display: flex;
&--inline {
display: inline;
}
}
@use '../variables' as var;
/**
* Add stylized focus to interactive elements such <input> or <textarea>
*
* @param {boolean} $inset -
* The focus style is implemented with a box-shadow and this parameter determines whether
* the box-shadow should be inset on the element. Set this to true when the element
* may be adjacent to another in order to prevent part of the outline from being obscured.
*/
@mixin outline($inset: false) {
&:focus {
outline: none;
box-shadow: 0 0 0 2px var.$color-focus-outline if($inset, inset, null);
// In the case where $inset is false, a z-index > 0 ensures that adjacent sibling
// items don't obscure the box-shadow outline.
z-index: if($inset, inherit, 1);
}
}
@mixin outline--hide {
outline: none;
box-shadow: none;
}
/**
* Display an outline on an element only when it has keyboard focus.
*
* This requires the browser to support the :focus-visible pseudo-selector [1]
* or for the JS polyfill [2] to be loaded.
*
* [1] https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible
* [2] https://github.com/WICG/focus-visible
*
* @param {boolean} $inset - Does the outline render inset or not
*/
@mixin outline-on-keyboard-focus($inset: false) {
@include outline($inset);
// Selector for browsers using JS polyfill, which adds the "focus-visible"
// class to a keyboard-focused element.
&:focus:not(.focus-visible) {
@include outline--hide;
}
// Selector for browsers with native :focus-visible support.
// (Do not combine with above, as an unsupported pseudo-class disables the
// entire selector)
&:focus:not(:focus-visible) {
@include outline--hide;
}
}
// Other colors
$color-focus-outline: #51a7e8;
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -15,11 +15,6 @@ const through = require('through2');
const createBundle = require('./scripts/gulp/create-bundle');
const createStyleBundle = require('./scripts/gulp/create-style-bundle');
const {
buildFrontendSharedJs,
buildFrontendSharedTypes,
linkFrontendShared,
} = require('./scripts/gulp/frontend-shared');
const manifest = require('./scripts/gulp/manifest');
const serveDev = require('./dev-server/serve-dev');
const servePackage = require('./dev-server/serve-package');
......@@ -161,35 +156,13 @@ gulp.task(
})
);
gulp.task(
'build-frontend-shared-js',
gulp.parallel(buildFrontendSharedJs, buildFrontendSharedTypes)
);
gulp.task('link-frontend-shared', linkFrontendShared);
gulp.task(
'build-frontend-shared',
gulp.series('build-frontend-shared-js', 'link-frontend-shared')
);
gulp.task('watch-frontend-shared', () => {
gulp.watch(
'./frontend-shared/src/**',
gulp.series('build-frontend-shared-js')
);
});
gulp.task(
'watch-js',
gulp.series(
gulp.parallel('build-vendor-js', 'build-frontend-shared-js'),
function watchJS() {
appBundleConfigs.forEach(function (config) {
createBundle(config, { watch: true });
});
}
)
gulp.series('build-vendor-js', function watchJS() {
appBundleConfigs.forEach(function (config) {
createBundle(config, { watch: true });
});
})
);
const cssBundles = [
......@@ -221,11 +194,7 @@ gulp.task(
'watch-css',
gulp.series('build-css', function watchCSS() {
const vendorCSS = cssBundles.filter(path => path.endsWith('.css'));
const frontendSharedGlobs = vendorCSS.concat(
'./frontend-shared/styles/**/*.scss'
);
const styleFileGlobs = frontendSharedGlobs.concat('./src/styles/**/*.scss');
const styleFileGlobs = vendorCSS.concat('./src/styles/**/*.scss');
gulp.watch(styleFileGlobs, gulp.task('build-css'));
})
);
......@@ -363,18 +332,14 @@ gulp.task('build', gulp.series(buildAssets, generateManifest));
gulp.task(
'watch',
gulp.series(
'link-frontend-shared',
gulp.parallel(
'serve-package',
'serve-test-pages',
'watch-js',
'watch-css',
'watch-fonts',
'watch-images',
'watch-manifest',
'watch-frontend-shared'
)
gulp.parallel(
'serve-package',
'serve-test-pages',
'watch-js',
'watch-css',
'watch-fonts',
'watch-images',
'watch-manifest'
)
);
......
......@@ -123,13 +123,12 @@
"main": "./build/boot.js",
"scripts": {
"build": "cross-env NODE_ENV=production gulp build",
"lint": "eslint . .github",
"lint": "eslint .",
"checkformatting": "prettier --check '**/*.{js,scss}'",
"format": "prettier --list-different --write '**/*.{js,scss}'",
"test": "gulp test",
"typecheck": "tsc --build src/tsconfig.json",
"report-coverage": "codecov -f coverage/coverage-final.json",
"version": "make clean build",
"setup-frontend-shared": "gulp build-frontend-shared"
"version": "make clean build"
}
}
'use strict';
const gulp = require('gulp');
const babel = require('gulp-babel');
const sourcemaps = require('gulp-sourcemaps');
const { run } = require('./run');
const buildFrontendSharedJs = () => {
// There does not appear to be a simple way of forcing gulp-babel to use a config
// file. Load it up and pass it in manually.
const babelConfig = require('../../frontend-shared/.babelrc.cjs');
return (
gulp
.src(['frontend-shared/src/**/*.js', '!**/test/*.js'])
// Transpile the js source files and write the output in the frontend-shared/lib dir.
// Additionally, add the sourcemaps into the same dir.
.pipe(sourcemaps.init())
.pipe(babel(babelConfig))
.pipe(sourcemaps.write('.'))
.pipe(gulp.dest('frontend-shared/lib'))
);
};
const buildFrontendSharedTypes = async () => {
// nb. If the options get significantly more complex, they should be moved to
// a `tsconfig.json` file.
await run('node_modules/.bin/tsc', [
'--allowJs',
'--declaration',
'--emitDeclarationOnly',
'--outDir',
'frontend-shared/lib',
'frontend-shared/src/index.js',
]);
};
const linkFrontendShared = async () => {
// Make @hypothesis/frontend-shared available for linking in other projects.
await run('yarn', ['link'], { cwd: './frontend-shared' });
// Link it in the parent `client` repo.
await run('yarn', ['link', '@hypothesis/frontend-shared']);
};
module.exports = {
buildFrontendSharedJs,
buildFrontendSharedTypes,
linkFrontendShared,
};
'use strict';
const { spawn } = require('child_process');
/**
* Run a command and return a promise for when it completes.
*
* Output and environment is forwarded as if running a CLI command in the terminal
* or make.
*
* This function is useful for running CLI tools as part of a gulp command.
*
* @param {string} cmd - Command to run
* @param {string[]} args - Command arguments
* @param {object} options - Options to forward to `spawn`
* @return {Promise<void>}
*/
function run(cmd, args, options = {}) {
return new Promise((resolve, reject) => {
spawn(cmd, args, { env: process.env, stdio: 'inherit', ...options }).on(
'exit',
code => {
if (code === 0) {
resolve();
} else {
reject(new Error(`${cmd} exited with status ${code}`));
}
}
);
});
}
module.exports = { run };
......@@ -30,11 +30,7 @@ if (process.env.RUNNING_IN_DOCKER) {
}
module.exports = function (config) {
let testFiles = [
'**/test/*-test.js',
'**/integration/*-test.js',
'../frontend-shared/**/test/*-test.js',
];
let testFiles = ['**/test/*-test.js', '**/integration/*-test.js'];
if (config.grep) {
const allFiles = testFiles
......@@ -89,7 +85,6 @@ module.exports = function (config) {
'./boot/polyfills/*.js': ['browserify'],
'./sidebar/test/bootstrap.js': ['browserify'],
'**/*-test.js': ['browserify'],
'../frontend-shared/**/*-test.js': ['browserify'],
'**/*-it.js': ['browserify'],
},
......@@ -99,21 +94,13 @@ module.exports = function (config) {
[
'babelify',
{
// The existence of this preset option is due to a config issue with frontend-shared/
// where jsx modules are not transpiled to js.
// See https://github.com/hypothesis/client/issues/2929
presets: require('../frontend-shared/.babelrc.cjs').presets,
extensions: ['.js'],
plugins: [
'mockable-imports',
[
'babel-plugin-istanbul',
{
exclude: [
'**/test/**/*.js',
'**/test-util/**',
'frontend-shared/lib',
],
exclude: ['**/test/**/*.js', '**/test-util/**'],
},
],
],
......
......@@ -11,12 +11,11 @@
"noImplicitAny": false,
"target": "ES2020"
},
"include": ["**/*.js", "../frontend-shared/src/**/*.js"],
"include": ["**/*.js"],
"exclude": [
// Tests are not checked.
"**/test/**/*.js",
"test-util/**/*.js",
"karma.config.js",
"../frontend-shared/src/**/test/*.js"
"karma.config.js"
]
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment