diff --git a/src/userlib/js/bootstrap/candid/candid-core.ts b/src/userlib/js/bootstrap/candid/candid-core.ts new file mode 100644 index 0000000000..6d5b3d848b --- /dev/null +++ b/src/userlib/js/bootstrap/candid/candid-core.ts @@ -0,0 +1,229 @@ +import { IDL } from '@internet-computer/userlib'; + +// tslint:disable:max-classes-per-file + +export interface ParseConfig { + random?: boolean; +} + +export interface UIConfig { + input?: HTMLElement; + form?: InputForm; + parse(t: IDL.Type, config: ParseConfig, v: string): any; +} + +export interface FormConfig { + open?: HTMLElement; + event?: string; + container: string; + render(t: IDL.Type): InputBox; +} + +export class InputBox { + public status: HTMLElement; + public label: string | null = null; + public value: any = undefined; + + constructor(public idl: IDL.Type, public ui: UIConfig) { + const status = document.createElement('div'); + status.className = 'status'; + this.status = status; + + if (ui.input) { + ui.input.addEventListener('blur', () => { + if ((ui.input as HTMLInputElement).value === '') { + return; + } + this.parse(); + }); + ui.input.addEventListener('focus', () => { + ui.input!.classList.remove('reject'); + }); + } + } + public isRejected(): boolean { + return this.value === undefined; + } + + public parse(config: ParseConfig = {}): any { + if (this.ui.form) { + const value = this.ui.form.parse(config); + this.value = value; + return value; + } + + if (this.ui.input) { + const input = this.ui.input as HTMLInputElement; + try { + const value = this.ui.parse(this.idl, config, input.value); + if (!this.idl.covariant(value)) { + throw new Error(`${input.value} is not of type ${this.idl.display()}`); + } + this.status.style.display = 'none'; + this.value = value; + return value; + } catch (err) { + input.classList.add('reject'); + this.status.style.display = 'block'; + this.status.innerHTML = 'InputError: ' + err.message; + this.value = undefined; + return undefined; + } + } + return null; + } + public render(dom: HTMLElement): void { + const container = document.createElement('span'); + if (this.label) { + const label = document.createElement('label'); + label.innerText = this.label; + container.appendChild(label); + } + if (this.ui.input) { + container.appendChild(this.ui.input); + container.appendChild(this.status); + } + + if (this.ui.form) { + this.ui.form.render(container); + } + dom.appendChild(container); + } +} + +export abstract class InputForm { + public form: InputBox[] = []; + constructor(public ui: FormConfig) {} + + public abstract parse(config: ParseConfig): any; + public abstract generateForm(): any; + public renderForm(dom: HTMLElement): void { + if (this.form.length === 0) { + return; + } + if (!(this instanceof VecForm) && this.form.length === 1) { + this.form[0].render(dom); + return; + } + const form = document.createElement(this.ui.container); + form.classList.add('popup-form'); + this.form.forEach(e => e.render(form)); + dom.appendChild(form); + } + public render(dom: HTMLElement): void { + if (this.ui.open && this.ui.event) { + dom.appendChild(this.ui.open); + const form = this; + form.ui.open!.addEventListener(form.ui.event!, () => { + while (dom.lastElementChild) { + if (dom.lastElementChild !== form.ui.open) { + dom.removeChild(dom.lastElementChild); + } else { + break; + } + } + // Render form + form.generateForm(); + form.renderForm(dom); + }); + } else { + this.generateForm(); + this.renderForm(dom); + } + } +} + +export class RecordForm extends InputForm { + constructor(public fields: Array<[string, IDL.Type]>, public ui: FormConfig) { + super(ui); + } + public generateForm(): void { + this.form = this.fields.map(([key, type]) => { + const input = this.ui.render(type); + input.label = key + ' '; + return input; + }); + } + public parse(config: ParseConfig): Record | undefined { + const v: Record = {}; + this.fields.forEach(([key, _], i) => { + const value = this.form[i].parse(config); + v[key] = value; + }); + if (this.form.some(input => input.isRejected())) { + return undefined; + } + return v; + } +} + +export class VariantForm extends InputForm { + constructor(public fields: Array<[string, IDL.Type]>, public ui: FormConfig) { + super(ui); + } + public generateForm(): void { + const index = (this.ui.open as HTMLSelectElement).selectedIndex; + const [_, type] = this.fields[index]; + const variant = this.ui.render(type); + this.form = [variant]; + } + public parse(config: ParseConfig): Record | undefined { + const select = this.ui.open as HTMLSelectElement; + const selected = select.options[select.selectedIndex].text; + const value = this.form[0].parse(config); + if (value === undefined) { + return undefined; + } + const v: Record = {}; + v[selected] = value; + return v; + } +} + +export class OptionForm extends InputForm { + constructor(public ty: IDL.Type, public ui: FormConfig) { + super(ui); + } + public generateForm(): void { + if ((this.ui.open as HTMLInputElement).checked) { + const opt = this.ui.render(this.ty); + this.form = [opt]; + } else { + this.form = []; + } + } + public parse(config: ParseConfig): [T] | [] | undefined { + if (this.form.length === 0) { + return []; + } else { + const value = this.form[0].parse(config); + if (value === undefined) { + return undefined; + } + return [value]; + } + } +} + +export class VecForm extends InputForm { + constructor(public ty: IDL.Type, public ui: FormConfig) { + super(ui); + } + public generateForm(): void { + const len = +(this.ui.open as HTMLInputElement).value; + this.form = []; + for (let i = 0; i < len; i++) { + const t = this.ui.render(this.ty); + this.form.push(t); + } + } + public parse(config: ParseConfig): T[] | undefined { + const value = this.form.map(input => { + return input.parse(config); + }); + if (this.form.some(input => input.isRejected())) { + return undefined; + } + return value; + } +} diff --git a/src/userlib/js/bootstrap/candid/candid-ui.ts b/src/userlib/js/bootstrap/candid/candid-ui.ts new file mode 100644 index 0000000000..4cf760a78a --- /dev/null +++ b/src/userlib/js/bootstrap/candid/candid-ui.ts @@ -0,0 +1,159 @@ +import { CanisterId, IDL } from '@internet-computer/userlib'; +import BigNumber from 'bignumber.js'; +import * as UI from './candid-core'; + +// tslint:disable:max-classes-per-file +type InputBox = UI.InputBox; + +const InputConfig: UI.UIConfig = { parse: parsePrimitive }; +const FormConfig: UI.FormConfig = { render: renderInput, container: 'div' }; + +const inputBox = (t: IDL.Type, config: Partial) => { + return new UI.InputBox(t, {...InputConfig, ...config}); +}; +const recordForm = (fields: Array<[string, IDL.Type]>, config: Partial) => { + return new UI.RecordForm(fields, {...FormConfig, ...config}); +}; +const variantForm = (fields: Array<[string, IDL.Type]>, config: Partial) => { + return new UI.VariantForm(fields, {...FormConfig, ...config}); +}; +const optForm = (ty: IDL.Type, config: Partial) => { + return new UI.OptionForm(ty, {...FormConfig, ...config}); +}; +const vecForm = (ty: IDL.Type, config: Partial) => { + return new UI.VecForm(ty, {...FormConfig, ...config}); +}; + +class Render extends IDL.Visitor { + public visitType(t: IDL.Type, d: null): InputBox { + const input = document.createElement('input'); + input.classList.add('argument'); + input.placeholder = t.display(); + return inputBox(t, { input }); + } + public visitNull(t: IDL.NullClass, d: null): InputBox { + return inputBox(t, {}); + } + public visitRecord(t: IDL.RecordClass, fields: Array<[string, IDL.Type]>, d: null): InputBox { + const form = recordForm(fields, {}); + return inputBox(t, { form }); + } + public visitVariant(t: IDL.VariantClass, fields: Array<[string, IDL.Type]>, d: null): InputBox { + const select = document.createElement('select'); + for (const [key, type] of fields) { + const option = document.createElement('option'); + option.innerText = key; + select.appendChild(option); + } + select.selectedIndex = -1; + select.classList.add('open'); + const form = variantForm(fields, { open: select, event: 'change' }); + return inputBox(t, { form }); + } + public visitOpt(t: IDL.OptClass, ty: IDL.Type, d: null): InputBox { + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.classList.add('open'); + const form = optForm(ty, { open: checkbox, event: 'change' }); + return inputBox(t, { form }); + } + public visitVec(t: IDL.VecClass, ty: IDL.Type, d: null): InputBox { + const len = document.createElement('input'); + len.type = 'number'; + len.min = '0'; + len.max = '100'; + len.style.width = '3em'; + len.placeholder = 'len'; + len.classList.add('open'); + const form = vecForm(ty, { open: len, event: 'change' }); + return inputBox(t, { form }); + } + public visitRec(t: IDL.RecClass, ty: IDL.ConstructType, d: null): InputBox { + return renderInput(ty); + } +} + +class Parse extends IDL.Visitor { + public visitNull(t: IDL.NullClass, v: string): null { + return null; + } + public visitBool(t: IDL.BoolClass, v: string): boolean { + if (v === 'true') { + return true; + } + if (v === 'false') { + return false; + } + throw new Error(`Cannot parse ${v} as boolean`); + } + public visitText(t: IDL.TextClass, v: string): string { + return v; + } + public visitInt(t: IDL.IntClass, v: string): BigNumber { + return new BigNumber(v); + } + public visitNat(t: IDL.NatClass, v: string): BigNumber { + return new BigNumber(v); + } + public visitFixedInt(t: IDL.FixedIntClass, v: string): BigNumber { + return new BigNumber(v); + } + public visitFixedNat(t: IDL.FixedNatClass, v: string): BigNumber { + return new BigNumber(v); + } + public visitPrincipal(t: IDL.PrincipalClass, v: string): CanisterId { + return CanisterId.fromText(v); + } + public visitService(t: IDL.ServiceClass, v: string): CanisterId { + return CanisterId.fromText(v); + } + public visitFunc(t: IDL.FuncClass, v: string): [CanisterId, string] { + const x = v.split('.', 2); + return [CanisterId.fromText(x[0]), x[1]]; + } +} + +class Random extends IDL.Visitor { + public visitNull(t: IDL.NullClass, v: string): null { + return null; + } + public visitBool(t: IDL.BoolClass, v: string): boolean { + return Math.random() < 0.5; + } + public visitText(t: IDL.TextClass, v: string): string { + return Math.random().toString(36).substring(6); + } + public visitInt(t: IDL.IntClass, v: string): BigNumber { + return new BigNumber(this.generateNumber(true)); + } + public visitNat(t: IDL.NatClass, v: string): BigNumber { + return new BigNumber(this.generateNumber(false)); + } + public visitFixedInt(t: IDL.FixedIntClass, v: string): BigNumber { + return new BigNumber(this.generateNumber(true)); + } + public visitFixedNat(t: IDL.FixedNatClass, v: string): BigNumber { + return new BigNumber(this.generateNumber(false)); + } + private generateNumber(signed: boolean): number { + const num = Math.floor(Math.random() * 100); + if (signed && Math.random() < 0.5) { + return -num; + } else { + return num; + } + } +} + +export function renderInput(t: IDL.Type): InputBox { + return t.accept(new Render(), null); +} + +function parsePrimitive(t: IDL.Type, config: UI.ParseConfig, d: string) { + if (config.random && d === '') { + return t.accept(new Random(), d); + } else { + return t.accept(new Parse(), d); + } +} + diff --git a/src/userlib/js/bootstrap/candid/candid.css b/src/userlib/js/bootstrap/candid/candid.css new file mode 100644 index 0000000000..e933b5c9d4 --- /dev/null +++ b/src/userlib/js/bootstrap/candid/candid.css @@ -0,0 +1,74 @@ +.signature { + font-size: 15px; + font-weight: 400; + margin: 5px; +} +.argument, .result, .status, .composite { + background-color: #F9F9F9; + border: 1px solid #E5E5E5; + color: #545454; + font-family: monospace; + font-size: 15px; + font-weight: 400; + height: auto; + margin-right: 10px; + margin-bottom: 10px; +} +.open { + margin: 5px; +} +.reject { + border: 1px solid #cc0000; +} +.result { + display: none; +} +.error { + color: #cc0000; +} +.status { + color: #cc0000; + display: none; +} +.left { + text-align: left; + width: 84%; + display: inline-block; + overflow-wrap: break-word; +} +.right { + text-align: right; + width: 15%; + display: inline-block; +} +.btn { + background-color: #02ADEA; + border-color: #02ADEA; + border-radius: .25rem; + color: #FFF; + font-family: sans-serif; + font-size: 16px; + font-weight: 700; + margin: 5px; +} +.popup-form { + border: 1px solid #E5E5E5; + padding-top: 10px; + padding-left: 10px; +} +.console { + display: flex; + flex: 1; + flex-direction: column; + font-family: monospace; + background-color: #F9F9F9; + color: #545454; +} +.console-line { + overflow-wrap: break-word; + flex: 0; + flex-basis: auto; + border: 0; + margin-left: 5px; + margin-bottom: 5px; +} diff --git a/src/userlib/js/bootstrap/candid/candid.js b/src/userlib/js/bootstrap/candid/candid.js index 6e722b7528..4bbba95fd9 100644 --- a/src/userlib/js/bootstrap/candid/candid.js +++ b/src/userlib/js/bootstrap/candid/candid.js @@ -1,4 +1,5 @@ -import * as UI from './idl-ui'; +import * as UI from './candid-ui'; +import './candid.css'; export function render(id, actor, canister) { document.getElementById('title').innerText = `Service ${id}`; diff --git a/src/userlib/js/bootstrap/candid/idl-ui.ts b/src/userlib/js/bootstrap/candid/idl-ui.ts deleted file mode 100644 index e09feb4b0f..0000000000 --- a/src/userlib/js/bootstrap/candid/idl-ui.ts +++ /dev/null @@ -1,380 +0,0 @@ -import { CanisterId, IDL } from '@internet-computer/userlib'; -import BigNumber from 'bignumber.js'; - -// tslint:disable:max-classes-per-file - -class Render extends IDL.Visitor { - public visitPrimitive(t: IDL.PrimitiveType, d: null): InputBox { - return new InputBox(t, null); - } - public visitNull(t: IDL.NullClass, d: null): InputBox { - const input = new InputBox(t, null); - input.input.type = 'hidden'; - return input; - } - public visitRecord(t: IDL.RecordClass, fields: Array<[string, IDL.Type]>, d: null): InputBox { - const form = new RecordForm(fields); - return new InputBox(t, form); - } - public visitVariant(t: IDL.VariantClass, fields: Array<[string, IDL.Type]>, d: null): InputBox { - const form = new VariantForm(fields); - return new InputBox(t, form); - } - public visitOpt(t: IDL.OptClass, ty: IDL.Type, d: null): InputBox { - const form = new OptionForm(ty); - return new InputBox(t, form); - } - public visitVec(t: IDL.VecClass, ty: IDL.Type, d: null): InputBox { - const form = new VecForm(ty); - return new InputBox(t, form); - } - public visitRec(t: IDL.RecClass, ty: IDL.ConstructType, d: null): InputBox { - return renderInput(ty); - } - public visitService(t: IDL.ServiceClass, d: null): InputBox { - return new InputBox(t, null); - } - public visitFunc(t: IDL.FuncClass, d: null): InputBox { - return new InputBox(t, null); - } -} - -class Parse extends IDL.Visitor { - public visitNull(t: IDL.NullClass, v: string): null { - return null; - } - public visitBool(t: IDL.BoolClass, v: string): boolean { - if (v === 'true') { - return true; - } - if (v === 'false') { - return false; - } - throw new Error(`Cannot parse ${v} as boolean`); - } - public visitText(t: IDL.TextClass, v: string): string { - return v; - } - public visitInt(t: IDL.IntClass, v: string): BigNumber { - return new BigNumber(v); - } - public visitNat(t: IDL.NatClass, v: string): BigNumber { - return new BigNumber(v); - } - public visitFixedInt(t: IDL.FixedIntClass, v: string): BigNumber { - return new BigNumber(v); - } - public visitFixedNat(t: IDL.FixedNatClass, v: string): BigNumber { - return new BigNumber(v); - } - public visitPrincipal(t: IDL.PrincipalClass, v: string): CanisterId { - return CanisterId.fromText(v); - } - public visitService(t: IDL.ServiceClass, v: string): CanisterId { - return CanisterId.fromText(v); - } - public visitFunc(t: IDL.FuncClass, v: string): [CanisterId, string] { - const x = v.split('.', 2); - return [CanisterId.fromText(x[0]), x[1]]; - } -} - -class Random extends IDL.Visitor { - public visitNull(t: IDL.NullClass, v: string): null { - return null; - } - public visitBool(t: IDL.BoolClass, v: string): boolean { - return Math.random() < 0.5; - } - public visitText(t: IDL.TextClass, v: string): string { - return Math.random().toString(36).substring(6); - } - public visitInt(t: IDL.IntClass, v: string): BigNumber { - return new BigNumber(this.generateNumber(true)); - } - public visitNat(t: IDL.NatClass, v: string): BigNumber { - return new BigNumber(this.generateNumber(false)); - } - public visitFixedInt(t: IDL.FixedIntClass, v: string): BigNumber { - return new BigNumber(this.generateNumber(true)); - } - public visitFixedNat(t: IDL.FixedNatClass, v: string): BigNumber { - return new BigNumber(this.generateNumber(false)); - } - private generateNumber(signed: boolean): number { - const num = Math.floor(Math.random() * 100); - if (signed && Math.random() < 0.5) { - return -num; - } else { - return num; - } - } -} - -export function renderInput(t: IDL.Type): InputBox { - return t.accept(new Render(), null); -} - -function parsePrimitive(t: IDL.Type, d: string) { - return t.accept(new Parse(), d); -} - -function generatePrimitive(t: IDL.Type) { - // TODO: in the future we may want to take a string to specify how random values are generated - return t.accept(new Random(), ''); -} - -export interface ParseConfig { - random?: boolean; -} - -class InputBox { - public input: HTMLInputElement; - public status: HTMLElement; - public label: string | null = null; - public value: any = undefined; - - constructor(public idl: IDL.Type, public form: InputForm | null = null) { - const status = document.createElement('div'); - status.className = 'status'; - this.status = status; - - const input = document.createElement('input'); - input.className = 'argument'; - input.placeholder = idl.display(); - this.input = input; - - input.addEventListener('blur', () => { - if (input.value === '') { - return; - } - this.parse(); - }); - input.addEventListener('focus', () => { - input.className = 'argument'; - }); - } - public isRejected(): boolean { - return this.value === undefined; - } - - public parse(config: ParseConfig = {}): any { - if (this.form) { - const value = this.form.parse(config); - this.value = value; - return value; - } - - try { - if (config.random && this.input.value === '') { - const v = generatePrimitive(this.idl); - this.value = v; - return v; - } - const value = parsePrimitive(this.idl, this.input.value); - if (!this.idl.covariant(value)) { - throw new Error(`${this.input.value} is not of type ${this.idl.display()}`); - } - this.status.style.display = 'none'; - this.value = value; - return value; - } catch (err) { - this.input.className += ' reject'; - this.status.style.display = 'block'; - this.status.innerHTML = 'InputError: ' + err.message; - this.value = undefined; - return undefined; - } - } - public render(dom: HTMLElement): void { - const container = document.createElement('span'); - if (this.label) { - const label = document.createElement('label'); - label.innerText = this.label; - container.appendChild(label); - } - container.appendChild(this.input); - container.appendChild(this.status); - - if (this.form) { - this.input.type = 'hidden'; - this.form.render(container); - const input = this.input; - } - dom.appendChild(container); - } -} - -abstract class InputForm { - public form: InputBox[] = []; - public open: HTMLElement = document.createElement('button'); - public event: string = 'change'; - - public abstract parse(config: ParseConfig): any; - public abstract generateForm(): any; - public renderForm(dom: HTMLElement): void { - if (this.form.length === 0) { - return; - } - if (this.form.length === 1) { - this.form[0].render(dom); - return; - } - const form = document.createElement('div'); - form.className = 'popup-form'; - this.form.forEach(e => e.render(form)); - dom.appendChild(form); - } - public render(dom: HTMLElement): void { - dom.appendChild(this.open); - const form = this; - form.open.addEventListener(form.event, () => { - while (dom.lastElementChild) { - if (dom.lastElementChild !== form.open) { - dom.removeChild(dom.lastElementChild); - } else { - break; - } - } - // Render form - form.generateForm(); - form.renderForm(dom); - }); - } -} - -class RecordForm extends InputForm { - constructor(public fields: Array<[string, IDL.Type]>) { - super(); - this.open.innerText = '...'; - this.event = 'click'; - } - public generateForm(): void { - this.form = this.fields.map(([key, type]) => { - const input = renderInput(type); - input.label = key + ' '; - return input; - }); - } - public render(dom: HTMLElement): void { - // No open button for record - this.generateForm(); - this.renderForm(dom); - } - public parse(config: ParseConfig): Record | undefined { - const v: Record = {}; - this.fields.forEach(([key, _], i) => { - const value = this.form[i].parse(config); - v[key] = value; - }); - if (this.form.some(input => input.isRejected())) { - return undefined; - } - return v; - } -} - -class VariantForm extends InputForm { - constructor(public fields: Array<[string, IDL.Type]>) { - super(); - const select = document.createElement('select'); - for (const [key, type] of fields) { - const option = document.createElement('option'); - option.innerText = key; - select.appendChild(option); - } - select.selectedIndex = -1; - select.className = 'open'; - this.open = select; - this.event = 'change'; - } - public generateForm(): void { - const index = (this.open as HTMLSelectElement).selectedIndex; - const [_, type] = this.fields[index]; - const variant = renderInput(type); - this.form = [variant]; - } - public parse(config: ParseConfig): Record | undefined { - const select = this.open as HTMLSelectElement; - const selected = select.options[select.selectedIndex].text; - const value = this.form[0].parse(config); - if (value === undefined) { - return undefined; - } - const v: Record = {}; - v[selected] = value; - return v; - } -} - -class OptionForm extends InputForm { - constructor(public ty: IDL.Type) { - super(); - const checkbox = document.createElement('input'); - checkbox.type = 'checkbox'; - checkbox.className = 'open'; - this.open = checkbox; - this.event = 'change'; - } - public generateForm(): void { - if ((this.open as HTMLInputElement).checked) { - const opt = renderInput(this.ty); - this.form = [opt]; - } else { - this.form = []; - } - } - public parse(config: ParseConfig): [T] | [] | undefined { - if (this.form.length === 0) { - return []; - } else { - const value = this.form[0].parse(config); - if (value === undefined) { - return undefined; - } - return [value]; - } - } -} - -class VecForm extends InputForm { - constructor(public ty: IDL.Type) { - super(); - const len = document.createElement('input'); - len.type = 'number'; - len.min = '0'; - len.max = '100'; - len.style.width = '3em'; - len.placeholder = 'length'; - len.className = 'open'; - this.open = len; - this.event = 'change'; - } - public generateForm(): void { - const len = (this.open as HTMLInputElement).valueAsNumber; - this.form = []; - for (let i = 0; i < len; i++) { - const t = renderInput(this.ty); - this.form.push(t); - } - } - public renderForm(dom: HTMLElement): void { - // Same code as parent class except the single length optimization - if (this.form.length === 0) { - return; - } - const form = document.createElement('div'); - form.className = 'popup-form'; - this.form.forEach(e => e.render(form)); - dom.appendChild(form); - } - public parse(config: ParseConfig): T[] | undefined { - const value = this.form.map(input => { - return input.parse(config); - }); - if (this.form.some(input => input.isRejected())) { - return undefined; - } - return value; - } -} diff --git a/src/userlib/js/bootstrap/candid/index.html b/src/userlib/js/bootstrap/candid/index.html index a7a30d2c67..952398d4e7 100644 --- a/src/userlib/js/bootstrap/candid/index.html +++ b/src/userlib/js/bootstrap/candid/index.html @@ -2,82 +2,6 @@ DFINITY Canister Candid UI - diff --git a/src/userlib/js/package-lock.json b/src/userlib/js/package-lock.json index a0277f1bbf..0c27cd8b7b 100644 --- a/src/userlib/js/package-lock.json +++ b/src/userlib/js/package-lock.json @@ -1864,6 +1864,56 @@ "randomfill": "^1.0.3" } }, + "css-loader": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-3.4.2.tgz", + "integrity": "sha512-jYq4zdZT0oS0Iykt+fqnzVLRIeiPWhka+7BqPn+oSIpWJAHak5tmB/WZrJ2a21JhCeFyNnnlroSl8c+MtVndzA==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "cssesc": "^3.0.0", + "icss-utils": "^4.1.1", + "loader-utils": "^1.2.3", + "normalize-path": "^3.0.0", + "postcss": "^7.0.23", + "postcss-modules-extract-imports": "^2.0.0", + "postcss-modules-local-by-default": "^3.0.2", + "postcss-modules-scope": "^2.1.1", + "postcss-modules-values": "^3.0.0", + "postcss-value-parser": "^4.0.2", + "schema-utils": "^2.6.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz", + "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "fast-deep-equal": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", + "dev": true + }, + "schema-utils": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.5.tgz", + "integrity": "sha512-5KXuwKziQrTVHh8j/Uxz+QUbxkaLW9X/86NBlx/gnKgtsZA2GIVMUn17qWhRFwF8jdYb3Dig5hRO/W5mZqy6SQ==", + "dev": true, + "requires": { + "ajv": "^6.12.0", + "ajv-keywords": "^3.4.1" + } + } + } + }, "css-select": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", @@ -1882,6 +1932,12 @@ "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==", "dev": true }, + "cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true + }, "cssom": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", @@ -3767,6 +3823,15 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "icss-utils": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.1.tgz", + "integrity": "sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==", + "dev": true, + "requires": { + "postcss": "^7.0.14" + } + }, "ieee754": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", @@ -3806,6 +3871,12 @@ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true }, + "indexes-of": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", + "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=", + "dev": true + }, "infer-owner": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", @@ -5764,6 +5835,92 @@ "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", "dev": true }, + "postcss": { + "version": "7.0.27", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.27.tgz", + "integrity": "sha512-WuQETPMcW9Uf1/22HWUWP9lgsIC+KEHg2kozMflKjbeUtw9ujvFX6QmIfozaErDkmLWS9WEnEdEe6Uo9/BNTdQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-modules-extract-imports": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz", + "integrity": "sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==", + "dev": true, + "requires": { + "postcss": "^7.0.5" + } + }, + "postcss-modules-local-by-default": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.2.tgz", + "integrity": "sha512-jM/V8eqM4oJ/22j0gx4jrp63GSvDH6v86OqyTHHUvk4/k1vceipZsaymiZ5PvocqZOl5SFHiFJqjs3la0wnfIQ==", + "dev": true, + "requires": { + "icss-utils": "^4.1.1", + "postcss": "^7.0.16", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.0.0" + } + }, + "postcss-modules-scope": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-2.1.1.tgz", + "integrity": "sha512-OXRUPecnHCg8b9xWvldG/jUpRIGPNRka0r4D4j0ESUU2/5IOnpsjfPPmDprM3Ih8CgZ8FXjWqaniK5v4rWt3oQ==", + "dev": true, + "requires": { + "postcss": "^7.0.6", + "postcss-selector-parser": "^6.0.0" + } + }, + "postcss-modules-values": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz", + "integrity": "sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg==", + "dev": true, + "requires": { + "icss-utils": "^4.0.0", + "postcss": "^7.0.6" + } + }, + "postcss-selector-parser": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz", + "integrity": "sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==", + "dev": true, + "requires": { + "cssesc": "^3.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + }, + "postcss-value-parser": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.0.3.tgz", + "integrity": "sha512-N7h4pG+Nnu5BEIzyeaaIYWs0LI5XC40OrRh5L60z0QjFsqGWcHcbkBvpe1WYpcIS9yQ8sOi/vIPt1ejQCrMVrg==", + "dev": true + }, "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", @@ -6794,6 +6951,46 @@ "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", "dev": true }, + "style-loader": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-1.1.3.tgz", + "integrity": "sha512-rlkH7X/22yuwFYK357fMN/BxYOorfnfq0eD7+vqlemSK4wEcejFF1dg4zxP0euBW8NrYx2WZzZ8PPFevr7D+Kw==", + "dev": true, + "requires": { + "loader-utils": "^1.2.3", + "schema-utils": "^2.6.4" + }, + "dependencies": { + "ajv": { + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz", + "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "fast-deep-equal": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", + "dev": true + }, + "schema-utils": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.5.tgz", + "integrity": "sha512-5KXuwKziQrTVHh8j/Uxz+QUbxkaLW9X/86NBlx/gnKgtsZA2GIVMUn17qWhRFwF8jdYb3Dig5hRO/W5mZqy6SQ==", + "dev": true, + "requires": { + "ajv": "^6.12.0", + "ajv-keywords": "^3.4.1" + } + } + } + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -7157,6 +7354,12 @@ "set-value": "^2.0.1" } }, + "uniq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", + "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=", + "dev": true + }, "unique-filename": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", diff --git a/src/userlib/js/package.json b/src/userlib/js/package.json index ff5b758f07..af884b15ea 100644 --- a/src/userlib/js/package.json +++ b/src/userlib/js/package.json @@ -29,11 +29,13 @@ "@types/crc": "^3.4.0", "@types/jest": "^24.0.18", "copy-webpack-plugin": "^5.1.1", + "css-loader": "^3.4.2", "html-webpack-plugin": "^3.2.0", "jest": "^24.9.0", "jest-expect-message": "^1.0.2", "node-fetch": "2.6.0", "prettier": "^1.19.1", + "style-loader": "^1.1.3", "terser-webpack-plugin": "^2.3.2", "text-encoding": "^0.7.0", "ts-jest": "^24.2.0", diff --git a/src/userlib/js/webpack.config.js b/src/userlib/js/webpack.config.js index e97d2cfc5c..cd136849d2 100644 --- a/src/userlib/js/webpack.config.js +++ b/src/userlib/js/webpack.config.js @@ -34,6 +34,12 @@ const bootstrapConfig = { }), ], }, + module: { + rules: [{ + test: /\.css$/, + use: ['style-loader', 'css-loader'] + }] + }, plugins: [ new HtmlWebpackPlugin({ template: 'bootstrap/index.html',