diff --git a/Makefile b/Makefile index 6cc771a895..811761fb7e 100644 --- a/Makefile +++ b/Makefile @@ -466,7 +466,7 @@ endif netlify-prod: clean docs-clean build docs-production-build docs-new-site .PHONY: netlify-preview -netlify-preview: clean docs-clean build docs-live-blocks-install-deps docs-dev-generate docs-preview-build docs-new-site +netlify-preview: clean docs-clean build docs-dev-generate docs-preview-build docs-new-site # Kept for compatibility. Use `make fuzz` instead. .PHONY: check-fuzz diff --git a/docs/Makefile b/docs/Makefile index 2d4fb91ab2..0175b0cb32 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -13,7 +13,6 @@ clean: rm -rf $(CURDIR)/website/generated rm -rf $(CURDIR)/website/public rm -rf $(CURDIR)/website/resources - rm -rf $(CURDIR)/website/scripts/live-blocks/opa_versions/latest-* .PHONY: generate-cli-docs generate-cli-docs: @@ -32,20 +31,6 @@ copy-hugo-static-content: dev-generate: generate-cli-docs copy-hugo-static-content DEV=true $(CURDIR)/website/scripts/load-docs.sh -# The website has some npm dependencies saved in ./website/node_modules -# As well as dependencies required for the "live-blocks". -# Use this target to update them as needed. -.PHONY: install-deps -install-deps: live-blocks-install-deps - npm install - -# The live-blocks-% pattern target will shim to the -# npm scripts in ./website/scripts/live-blocks -# NOTE(sr): we'll avoid an API call if we make use of releases.yaml to determine the latest release -.PHONY: live-blocks-% -live-blocks-%: - cd $(CURDIR)/website/scripts/live-blocks && LATEST=$(shell sed -n '2s/- //p' $(CURDIR)/website/data/releases.yaml) npm run $* - .PHONY: serve-local serve-local: dev-build # must be run from root of repo for @@ -59,7 +44,7 @@ serve-remote: production-build cd $(CURDIR)/.. && netlify deploy .PHONY: dev-build -dev-build: clean dev-generate hugo-production-build live-blocks-inject +dev-build: clean dev-generate hugo-production-build ###################################################### # @@ -77,7 +62,6 @@ hugo-production-build: .PHONY: production-build production-build: clean generate hugo-production-build - make live-blocks-inject .PHONY: preview-build preview-build: @@ -88,8 +72,6 @@ preview-build: --buildDrafts \ --buildFuture \ --ignoreCache - # this is not compatible with node 22 - # make live-blocks-inject .PHONY: new-site new-site: diff --git a/docs/website/scripts/live-blocks/.eslintignore b/docs/website/scripts/live-blocks/.eslintignore deleted file mode 100644 index 1eae0cf670..0000000000 --- a/docs/website/scripts/live-blocks/.eslintignore +++ /dev/null @@ -1,2 +0,0 @@ -dist/ -node_modules/ diff --git a/docs/website/scripts/live-blocks/.eslintrc.json b/docs/website/scripts/live-blocks/.eslintrc.json deleted file mode 100644 index 72b149854e..0000000000 --- a/docs/website/scripts/live-blocks/.eslintrc.json +++ /dev/null @@ -1,180 +0,0 @@ -{ - "env": { - "browser": true, - "es6": true, - "node": true - }, - "extends": [ - "eslint:recommended", - "plugin:import/errors" - ], - "parser": "babel-eslint", - "parserOptions": { - "ecmaFeatures": { - "jsx": true - }, - "ecmaVersion": 2018, - "sourceType": "module" - }, - "plugins": [ - "import", - "sort-imports-es6-autofix", - "sort-requires" - ], - "rules": { - "array-bracket-spacing": "error", - "arrow-parens": "error", - "arrow-spacing": [ - "error", - { - "before": true, - "after": true - } - ], - "brace-style": "error", - "camelcase": [ - "error", - { - "ignoreDestructuring": true, - "properties": "never" - } - ], - "comma-spacing": "error", - "comma-style": "error", - "computed-property-spacing": "error", - "curly": "error", - "dot-location": [ - "error", - "property" - ], - "dot-notation": "error", - "func-call-spacing": "error", - "generator-star-spacing": [ - "error", - { - "after": true, - "before": false, - "method": { - "after": true, - "before": true - } - } - ], - "import/namespace": [ - "error", - { - "allowComputed": true - } - ], - "import/no-unresolved": [ - "error", - { - "ignore": [ - "src/spritesheet\\.svg" - ] - } - ], - "import/order": [ - "error", - { - "groups": [ - "builtin", - "external", - "internal", - "parent", - "sibling", - "index" - ], - "newlines-between": "always-and-inside-groups" - } - ], - "indent": [ - "error", - 2, - { - "MemberExpression": "off", - "SwitchCase": 1 - } - ], - "jsx-quotes": [ - "error", - "prefer-double" - ], - "key-spacing": "error", - "keyword-spacing": "error", - "linebreak-style": [ - "error", - "unix" - ], - "new-parens": "error", - "no-duplicate-imports": "error", - "no-else-return": "error", - "no-multi-spaces": [ - "error", - { - "ignoreEOLComments": true - } - ], - "no-multiple-empty-lines": [ - "error", - { - "max": 1, - "maxBOF": 0 - } - ], - "no-new-wrappers": "error", - "no-template-curly-in-string": "error", - "no-trailing-spaces": "error", - "no-unused-vars": [ - "error", - { - "varsIgnorePattern": "^_+$" - } - ], - "no-whitespace-before-property": "error", - "object-curly-spacing": "error", - "prefer-arrow-callback": "error", - "quotes": [ - "error", - "single", - { - "avoidEscape": true - } - ], - "rest-spread-spacing": "error", - "semi": [ - "error", - "never" - ], - "sort-imports-es6-autofix/sort-imports-es6": [ - "error", - { - "ignoreCase": true, - "ignoreMemberSort": false, - "memberSyntaxSortOrder": [ - "none", - "single", - "all", - "multiple" - ] - } - ], - "sort-requires/sort-requires": "error", - "space-before-function-paren": [ - "error", - { - "anonymous": "always", - "asyncArrow": "always", - "named": "never" - } - ], - "space-in-parens": "error", - "space-infix-ops": "error", - "space-unary-ops": "error", - "spaced-comment": "error", - "switch-colon-spacing": "error", - "template-curly-spacing": "error", - "template-tag-spacing": "error", - "yield-star-spacing": "error" - } -} diff --git a/docs/website/scripts/live-blocks/.gitignore b/docs/website/scripts/live-blocks/.gitignore deleted file mode 100644 index cf1d528d69..0000000000 --- a/docs/website/scripts/live-blocks/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules/ -dist/ -opa_versions/ \ No newline at end of file diff --git a/docs/website/scripts/live-blocks/README.md b/docs/website/scripts/live-blocks/README.md deleted file mode 100644 index 2b69d36862..0000000000 --- a/docs/website/scripts/live-blocks/README.md +++ /dev/null @@ -1,144 +0,0 @@ -# Live Blocks - -This is the high-level code documentation for the live blocks postprocessor. -Whoever you are, you should probably read the "Live Code Blocks" section of -`docs/README.md` first (it should be all you need if you just want to use -live blocks when writing docs). - -`scripts/live-blocks/` acts as a completely standalone postprocessor, editing and -adding files in `docs/website/public` after the rest of a complete -build has finished and then hydrating the code blocks -on page load. - -## Scripts -`package.json` contains a bunch of scripts, in some cases it calls things in the `live-blocks/scripts/` folder. -The most important of these is `inject` which calls a bunch of other scripts to add live docs functionality -to an otherwise fully built site. All of these scripts can be run from the `docs/` folder using `make live-blocks-`) -} \ No newline at end of file diff --git a/docs/website/scripts/live-blocks/src/preprocess/localEval.js b/docs/website/scripts/live-blocks/src/preprocess/localEval.js deleted file mode 100644 index dfa8e87783..0000000000 --- a/docs/website/scripts/live-blocks/src/preprocess/localEval.js +++ /dev/null @@ -1,124 +0,0 @@ -import cp from 'child_process' -import util from 'util' -import {promises as promFS} from 'fs' - -import tmp from 'tmp' -import semver from 'semver' - -import {ChainedError, OPAErrors} from '../errors' -import {EVAL_MODULE_NAME, VERSION_EDGE, VERSION_LATEST} from '../constants' -import {getGroupData} from '../helpers' - -import {acquireOPAVersion} from './acquireOPAVersion' - -// Clean up temporary files on exit -tmp.setGracefulCleanup() - -// Promisify non-fs async file operations -const execFile = util.promisify(cp.execFile) -const tmpFile = util.promisify(tmp.file) - -// Evaluate the group using a local OPA binary. Either returns a string suitable for a code block or throws an error whose message is user-friendly. If the OPA evaluation itself failed, throws an OPAErrors instance. -export default async function localEval(groups, groupName, opaVersion) { - const opa = await acquireOPAVersion(opaVersion) // Get the path (and download if necessary) the required version of OPA. - - const [args, moduleFilenameMap] = await prepEval(groups, groupName, opaVersion) // Save the group data to temporary files - - try { - return (await execFile(opa, args('pretty'))).stdout.replace(/\n$/, '') // Evaluate, retrieving the pretty-formatted output - } catch (e) { - if (e.stdout) { // OPA returned an error message - let pretty = e.stdout.replace(/\n$/, '') - // Replace the module file names with what the playground will use. - for (let [tmp, replacement] of Object.entries(moduleFilenameMap)) { - pretty = pretty.replace(new RegExp(tmp, 'g'), replacement) - } - - if (pretty === 'undefined') { // Special undefined case, use the same message as the playground. - throw new OPAErrors('undefined decision', undefined) - - } else { // Some other error occurred, reevaluate to get the JSON-formatted version of the errors so that they can be compared against what the block expects. - try { - await execFile(opa, args('json')) - throw new Error('subsequent eval of failing evaluation did not fail') - } catch (e2) { - if (e2.stdout) { // Reevaluation seems to have worked - let opaErrors - try { // Parse the reevaluation result into an OPAErrors object to be thrown. - let parsed = JSON.parse(e2.stdout) - if (parsed.errors) { - opaErrors = new OPAErrors(pretty, parsed.errors) - } else { - opaErrors = new OPAErrors(pretty, parsed.error) - } - } catch (e3) { - throw new ChainedError('invalid response while trying to get details about an evaluation failure', e3) - } - throw opaErrors - } else { // Reevaluation didn't produce output - throw new ChainedError('an error occurred while trying to get details about an evaluation failure', e2) - } - } - } - } else { // OPA didn't return an error message, probably some problem calling it. - throw new ChainedError(e.stderr ? `OPA: ${e.stderr}` : 'unable to perform local evaluation', e) // Some OPA error. This particularly applies to parse errors, they occur at a different point internally and are not reported correctly; it's fine though, doc examples probably shouldn't cause them. - } - } -} - -// Returns 1st a function that consumes a string for the output format you want and produces an array of arguments and 2nd a map of module file names to strings that should be replaced in error messages. May throw a user-friendly error. -async function prepEval(groups, groupName, opaVersion) { - const {module, package: pkg, query, input, included} = getGroupData(groups, groupName) - const base = ['eval', '--fail'] // Fail on undefined - const rest = [] - const moduleFilenameMap = {} - - try { - let tmpModule = await tmpFile({postfix: '.rego'}) - await promFS.writeFile(tmpModule, module) - rest.push('-d', tmpModule, '--package', pkg) - moduleFilenameMap[tmpModule] = EVAL_MODULE_NAME - - if (input) { - let stringified = '' - try { - stringified = JSON.stringify(input) - } catch (e) { - throw new ChainedError('input cannot be stringified', e) - } - const tmpInput = await tmpFile({postfix: '.json'}) - await promFS.writeFile(tmpInput, stringified) - rest.push('--input', tmpInput) - } - - if (query) { - // NOTE(sr): Only do this if the version is recent enough to understand it, - // and if we're rendering docs of a version where we actually started using - // queries that need this import. - if (opaVersion === VERSION_LATEST || opaVersion == VERSION_EDGE || - semver.satisfies(semver.coerce(opaVersion), '>=0.42.0')) { - rest.push('--import', 'future.keywords') // queries may contain them - } - rest.push(query) - } else { // Simulate playground default behavior - if (included) { - rest.push('data') - } else { - rest.push(`data.${pkg}`) - } - } - - if (included) { - for (let [incName, incModule] of Object.entries(included)) { - let tmpIncluded = await tmpFile({postfix: '.rego'}) - await promFS.writeFile(tmpIncluded, incModule) - rest.push('-d', tmpIncluded) - moduleFilenameMap[tmpIncluded] = incName - } - } - - return [(format) => [...base, `--format=${format}`, ...rest], moduleFilenameMap] - } catch (e) { - throw new ChainedError('a problem occurred while preparing to evaluate', e) - } -} diff --git a/docs/website/scripts/live-blocks/src/web/index.js b/docs/website/scripts/live-blocks/src/web/index.js deleted file mode 100644 index aa80d7a431..0000000000 --- a/docs/website/scripts/live-blocks/src/web/index.js +++ /dev/null @@ -1,347 +0,0 @@ -import 'core-js/stable' -import 'regenerator-runtime/runtime' - -import CodeMirror from 'codemirror' -// eslint-disable-next-line -import 'codemirror/lib/codemirror.css' -import 'codemirror/mode/javascript/javascript' - -import './style.css' -import {BASE_EDITOR_OPTS, BLOCK_SELECTOR, BLOCK_TYPES, CLASSES, EDITOR_MODES, HYDRATION_QUEUE_SORT_PERIOD, ICONS, INTERACTIVE_PATH, LINE_NUMBERS_EDITOR_OPTS, OPENING_IN_PLAYGROUND_PATH, READ_ONLY_EDITOR_OPTS, STATIC_TAG_TYPES} from '../constants' -import {batchProcess, delay, getAllGroupModules, getGroupField, handleLater, includedGroupNames, infoFromLabel, report} from '../helpers' -import {OPAErrors} from '../errors' - -import {playgroundEval, shareToPlayground} from './playground' - -import 'codemirror-rego/key-map' -import 'codemirror-rego/mode' -import 'codemirror/addon/comment/comment' -import 'codemirror/addon/edit/matchbrackets' - -// --- MAIN --- -start() // Enter async. -async function start() { - const codeElts = document.querySelectorAll(BLOCK_SELECTOR) - if (!codeElts.length) { - return // Exit! No live blocks. - } - - const groups = constructGroups(codeElts) - registerChangeHandlers(groups) - await hydrate(groups) -} - -// --- HYDRATION HELPERS --- -// Constructs an object of the following form: -// groupName: { -// blockType (module/query/input/output): { -// tags: [string], -// get: () => string, -// set: (string value[, boolean error]) => undefined, -// restore: () => undefined // Restore the contents and error-state of the container. -// changeHandlers: [0+ functions], -// codeElt, -// iconBar, -// container, -// codeMirror: initially, sometimes always, undefined -// openButton: initially, sometimes always, undefined -// }, -// output: { -// ... same as above -// current: initially undefined // Used to discard out of date responses from the playground -// timeout: initially undefined // Used to debounce changes to dependencies -// } -// } -function constructGroups(codeElts) { - const groups = {} - - for (let codeElt of codeElts) { - const container = codeElt.parentNode.parentNode - - const label = codeElt.dataset.lang - let info = infoFromLabel(label) - - if (!groups[info.group]) { // Populate second level of the groups object structure - groups[info.group] = {} - } - - const initText = codeElt.textContent - const initErr = container.classList.contains(CLASSES.BLOCK_CONTAINER_ERROR) - groups[info.group][info.type] = { - tags: info.tags, - get: () => codeElt.textContent, - set: (value, error) => { - codeElt.textContent = value - if (typeof error === 'boolean') { - if (error) { - container.classList.add(CLASSES.BLOCK_CONTAINER_ERROR) - } else { - container.classList.remove(CLASSES.BLOCK_CONTAINER_ERROR) - } - } - runHandlers(groups[info.group][info.type].changeHandlers) - }, - restore: () => { - groups[info.group][info.type].set(initText, initErr) - }, - changeHandlers: [], - codeElt, - iconBar: container.firstElementChild.firstElementChild, - container, - } - } - - return groups -} - -function registerChangeHandlers(groups) { - for (let [groupName, group] of Object.entries(groups)) { - const output = group.output - if (!output) { // Not all groups have outputs - continue - } - // Output contents depend on non-undefined query and input blocks in this group/a parent, this group's chain of inherited module blocks, as well as any included module blocks. - const dependencies = [BLOCK_TYPES.QUERY, BLOCK_TYPES.INPUT].map((type) => getGroupField(groups, groupName, type)).filter((block) => !!block) - dependencies.push(...getAllGroupModules(groups, groupName)) - dependencies.push(...(includedGroupNames(output.tags).reduce((iMs, iGN) => iMs.concat(getAllGroupModules(groups, iGN)), []))) - - for (let dependency of dependencies) { - dependency.changeHandlers.push(() => updateOutput(groups, groupName, output)) - } - } -} - -// Hydrate the all the blocks as applicable -async function hydrate(groups) { - let queue = Object.entries(groups).reduce((arr, [groupName, group]) => arr.concat(Object.entries(group).map(([type, block]) => ({groupName, group, type, block}))), []) - function sort() { // Sort based on distance to the top of the screen - queue.sort((a, b) => Math.abs(b.block.container.getBoundingClientRect().top) - Math.abs(a.block.container.getBoundingClientRect().top)) - } - sort() - - for (let next = queue.pop(); next; next = queue.pop()) { - const {groupName, type, block} = next - - // Hydrate - hydrateEditor(type, block) - hydrateButtons(groups, groupName, type, block) - - // Occasionally re-sort the queue in case the user scrolls. - if (!(queue.length % HYDRATION_QUEUE_SORT_PERIOD)) { - sort() - } - - // Don't freeze the browser :) - await delay(1) - } -} - -// Hydrate visible editors -function hydrateEditor(type, block) { - if (!block.tags.includes(STATIC_TAG_TYPES.HIDDEN)) { // Only bother adding the editor to visible blocks - // Replace intermediate container and hydrate it - const cmDiv = document.createElement('div') - block.codeElt.parentNode.replaceWith(cmDiv) - const codeMirror = CodeMirror(cmDiv, Object.assign({ - value: block.get() - }, constructEditorOptions(type, block.tags))) - fixScroll(codeMirror) - codeMirror.on('change', () => { - runHandlers(block.changeHandlers) - }) - - // Add class to trigger shadows - if (isEditable(type, block.tags)) { - block.container.classList.add(CLASSES.BLOCK_CONTAINER_EDITABLE) - } - - // Update block object - Object.assign(block, { - get: () => codeMirror.getValue(), - set: (value, error) => { - codeMirror.setValue(value) - if (typeof error === 'boolean') { - if (error) { - block.container.classList.add(CLASSES.BLOCK_CONTAINER_ERROR) - } else { - block.container.classList.remove(CLASSES.BLOCK_CONTAINER_ERROR) - } - } - fixScroll(codeMirror) - }, - codeMirror - }) - } -} - -// Hydrate the restore and share buttons as applicable -function hydrateButtons(groups, groupName, type, block) { - if (isEditable(type, block.tags)) { - const restoreButton = createButton(ICONS.RESTORE) - restoreButton.addEventListener('click', () => block.restore()) - block.iconBar.appendChild(restoreButton) - } - - if (isInteractive(block.tags) && block.tags.includes(STATIC_TAG_TYPES.OPENABLE)) { - // The three versions of the "Open in Playground" button. - const readyButton = createButton(ICONS.PLAYGROUND.READY) - const workingButton = createButton(ICONS.PLAYGROUND.WORKING) - const errorButton = createButton(ICONS.PLAYGROUND.ERROR) - - // Handler to open in the playground. - const open = createOpenInPlaygroundHandler(groups, groupName, block, readyButton, workingButton, errorButton) - - // Only ready and error trigger an open, if "working", forces user to wait until it completes. - readyButton.addEventListener('click', open) - errorButton.addEventListener('click', open) - - // Hydrate - putOpenInPlaygroundButton(block, readyButton) - } -} - -// Create a new button in the DOM based on the given icon constant. -function createButton(icon) { - const image = document.createElement('img') - image.src = icon.src - image.classList.add(CLASSES.ICON_IMG) - if (icon.spinning) { - image.classList.add(CLASSES.ICON_IMG_SPINNING) - } - const container = document.createElement('div') - container.title = icon.title - container.classList.add(CLASSES.ICON_CONTAINER) - container.classList.add(CLASSES.ICON_CONTAINER_BUTTON) - container.appendChild(image) - return container -} - -// Update the contents of an output block from a group. -function updateOutput(groups, groupName, output) { - output.set('...' + '\n'.repeat((output.get().match(/\n/g) || []).length), false) // Show that it's in progress without changing size - - clearTimeout(output.timeout) // Debounce: If update is called rapidly, cancel last one. - - output.timeout = setTimeout((async () => { - const promise = handleLater(() => playgroundEval(groups, groupName)) // Wrap with handleLater so that immediate rejection isn't a problem - output.current = promise // Store the promise to the playground's response globally (tied to this output) - - const [text, error] = await promise // Time passes... - - if (output.current === promise) { // Is the global current promise (for this output block) still the one created by this call? - if (!error) { - output.set(text, false) - } else { - if (!(error instanceof OPAErrors) || !error.matchesExpected(output.tags)) { - report(error) // Some extra logging for unexpected errors. - } - output.set(error.message, true) - } - } else { // A new request was made while waiting for this response, discard it. - return - } - }), 500) // Wait 0.5s before fetching in case they're still typing, will be canceled above if they do. -} - -// Padding was causing issues where text on the right side got cut off, this fixes it for a given CodeMirror instance. -function fixScroll(editor) { - // Below is copied from the CodeMirror window resize handler, codeMirror.refresh() didn't work... - const d = editor.display - d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null - d.scrollbarsClipped = false - editor.setSize() -} - -// Runs every potentially async handler of handlers, logging unexpected errors. Guaranteed to not reject so can be run without catching. -async function runHandlers(handlers) { - try { - await batchProcess(0, handlers) - } catch (errs) { - report('one or more errors occurred in change handlers', handlers, errs) - } -} - -// Determines whether a block can be interacted with (a lower bar than editability) based on its tags and the current page's path. -function isInteractive(tags) { - if (tags.includes(STATIC_TAG_TYPES.HIDDEN)) { - return false - } - return INTERACTIVE_PATH.test(window.location.pathname); - -} - -// Determines whether a block should be editable based on its type, tags, and the current page's path. -function isEditable(type, tags) { - if (!isInteractive(tags)) { - return false - } - if (type === BLOCK_TYPES.OUTPUT) { - return false - } - if (tags.includes(STATIC_TAG_TYPES.READ_ONLY)) { - return false - } - return true -} - -// Returns the options to use with a given block type that has the given tags. -function constructEditorOptions(type, tags) { - const out = Object.assign({}, BASE_EDITOR_OPTS) - - out.mode = EDITOR_MODES[type] - - if (!isEditable(type, tags)) { - Object.assign(out, READ_ONLY_EDITOR_OPTS) - } - - if (tags.includes(STATIC_TAG_TYPES.LINE_NUMBERS)) { - Object.assign(out, LINE_NUMBERS_EDITOR_OPTS) - } - - return out -} - -// Add/ replace the "Open in Playground" button. -function putOpenInPlaygroundButton(block, button) { - if (!block.openButton) { - block.iconBar.appendChild(button) - } else { - block.openButton.replaceWith(button) - } - block.openButton = button -} - -// Returns a function that when called attempts to open the given group in the playground. Puts the appropriate button for the given block as it works. -// NOTE: The user should not be able to trigger the returned function when the button is set to `working`. -function createOpenInPlaygroundHandler(groups, groupName, block, ready, working, error) { - return function () { - // Open window (really a tab) immediately in this bog-standard, non-async, non-arrow handler to minimize the chance that browsers will block it/ ask permission. - const pg = window.open(OPENING_IN_PLAYGROUND_PATH, '_blank') - - try { - // Show that it's working - putOpenInPlaygroundButton(block, working) - - // Try to maintain focus, this generally won't work due to malicious popup mitigations in browsers. - pg.blur() - window.focus() - } catch (e) { - report(e) - // If any of those not only didn't work but actually errored, it's still not a problem. Continue... - } - - // Asyncronously try to perform the share and update the new window accordingly. - (async () => { - try { - const url = await shareToPlayground(groups, groupName) - pg.location.replace(url) - pg.focus() - putOpenInPlaygroundButton(block, ready) - } catch (e) { - report('unable to open in playground', e) - pg.close() - putOpenInPlaygroundButton(block, error) - } - })() - } -} \ No newline at end of file diff --git a/docs/website/scripts/live-blocks/src/web/playground.js b/docs/website/scripts/live-blocks/src/web/playground.js deleted file mode 100644 index b86855d0dc..0000000000 --- a/docs/website/scripts/live-blocks/src/web/playground.js +++ /dev/null @@ -1,90 +0,0 @@ -import {ChainedError, OPAErrors} from '../errors' -import {EVAL_MODULE_NAME, PLAYGROUND} from '../constants' -import {getGroupData} from '../helpers' - -// Evaluate the group using the playground. Either returns a string suitable for a code block or throws an error whose message is user-friendly. If the OPA evaluation itself failed, throws an OPAErrors instance. -export async function playgroundEval(groups, groupName) { - // Get fetch options - const opts = createDataRequestOpts(groups, groupName) - - // Make request - let resp - try { - resp = await fetch(`${PLAYGROUND}/v1/data`, opts) - } catch (e) { - throw new ChainedError("can't contact server", e) - } - - // Parse response and return - let parsed - try { - parsed = await resp.json() - } catch (e) { - throw new ChainedError('invalid response from server', e) - } - - if (resp.status === 200) { - if (parsed.pretty) { - return parsed.pretty.replace(/\n$/, '') // Some responses may have trailing newlines - } - if (parsed.result === null) { - throw new OPAErrors("undefined decision", undefined) - } - // Else throw below - } else if (parsed.message) { // The server returned a non-200 code and a message - const message = parsed.message.replace(/\n$/, '') // Some messages may have trailing newlines - if (parsed.error || message === 'undefined decision') { // The error was related to evaluation - let opaErrors - try { // Try validating the errors by creating an OPAErrors object to throw - opaErrors = new OPAErrors(message, parsed.error) - } catch (e) { - throw new ChainedError(message, e) // Errors seem to be invalid but use the message it sent anyway, it's more informative than anything else. - } - throw opaErrors - } else { - throw new Error(message) - } - } - - // None of the return or throw cases matched, that's unexpected - throw new Error('invalid response from server') -} - -// Either resolves to a playground url with the group's content or rejects with a (non-user-friendly) error. -export async function shareToPlayground(groups, groupName) { - let url = (await (await fetch(`${PLAYGROUND}/v1/share`, createDataRequestOpts(groups, groupName))).json()).result - if (!url) { - throw new Error("Unable to get URL from the playground's response.") - } - return url -} - -// Constructs a the fetch options for querying the playground with a DataRequest. Throws an error with a user-friendly message if it can't. -function createDataRequestOpts(groups, groupName) { - const {module, package: pkg, query, input, included} = getGroupData(groups, groupName) // Throws user-friendly messages. - - try { - const body = {} // In the format accepted by playground backend - body.rego_modules = {[EVAL_MODULE_NAME]: module, ...included} - body.rego_version = 1 - body.query_package = pkg - if (query) { - body.rego = query - } - if (input) { - body.input = input - } - body.query_imports = ["rego.v1"] - - return { - body: JSON.stringify(body), - // Both types of requests are POSTing JSON - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - } - } catch (e) { // Presumably stringifying the body failed, that shouldn't happen - throw new ChainedError('unexpected error', e) - } -} \ No newline at end of file diff --git a/docs/website/scripts/live-blocks/src/web/style.css b/docs/website/scripts/live-blocks/src/web/style.css deleted file mode 100644 index 8c4fefa064..0000000000 --- a/docs/website/scripts/live-blocks/src/web/style.css +++ /dev/null @@ -1,124 +0,0 @@ -/* NOTE: Update constants.js with any changes to class names. */ -.live--block-container { - margin-bottom: 1.5rem; - /* Ensure shadows appear over below elements correctly */ - position: relative; - z-index: 0; -} - -.live--block-container--merged-down { - margin-bottom: 0 !important; - border-bottom: solid #cecece 1px; -} - -.live--block-container--editable.live--block-container--merged-down { - /* Ensure shadows appear over below elements correctly */ - z-index: 1; -} - -.live--block-container--error span { - color: #ff1b00 !important; -} - -.live--icon-bar--outer { - height: 0; - position: relative; -} - -.live--icon-bar--inner { - height: 1.25em; - padding: 0.0625em 0; - flex-direction: row-reverse; - display: flex; - justify-content: flex-start; - font-size: 1.275em; /*TODO set for both this and codemirror, try setting it on container*/ - align-items: center; -} - -.live--icon--container { - width: 1.0625em; - height: 1.0625em; - margin-right: 0.1875em; - display: flex; - z-index: 1; -} - -.live--icon--container--indicator { - opacity: 0.25; -} - -.live--icon--container--button { - border-radius: 0.25em; - cursor: pointer; - background-color: #363636; -} - -.live--icon--img { - padding: 0.0625em; -} - -.live--icon--img--spinning { - animation: spin 5000ms linear infinite; -} - -@keyframes spin { - from {transform:rotate(360deg);} - to {transform:rotate(0deg);} -} - -/* --- SHADOWS --- */ - -.live--block-container .CodeMirror { - transition: box-shadow 0.2s ease-out; -} - -.live--block-container--editable .CodeMirror { - box-shadow: 0 0 3px #aaa; -} - -/* apply to CodeMirror instead of container using :focus-within because Edge doesn't support it */ -.live--block-container--editable .CodeMirror-focused { - box-shadow: 0 0 6px #aaa !important; -} - -/* --- CODEMIRROR OVERRIDES --- */ - -.live--block-container .CodeMirror { - height: auto; - background-color: #f8f8f8; - font-weight: 400; - line-height: 1.5; - font-size: 0.875em; -} - -.live--block-container .CodeMirror-scroll { - overflow-y: hidden !important; - overflow-x: auto !important; -} - -.live--block-container .CodeMirror-hscrollbar { - pointer-events: auto !important; /* Fixes weird bug where with no vertical scroll it was getting set to none... */ - outline: none; -} - -.live--block-container .CodeMirror-sizer { - padding: 1.25em 1.5em 1.25em 1.5em !important; - min-height: 0 !important; -} - -.live--block-container .CodeMirror-lines { - margin-left: -4px !important; /* Removing the 4px of left padding on .CodeMirror-line breaks highlighting and cursor, this corrects for it without removing it. Otherwise, the code jumps to the right when hydrated. */ - padding: 0 !important; -} - -.live--block-container .CodeMirror-line { - margin: 0 !important; -} - -.live--block-container .CodeMirror-gutter-wrapper { - transform: translateX(-1.5em); /* Undo positioning from sizer padding */ -} - -.live--block-container .cm-variable { - color: #003c66; /* Just overriding the default theme for variables (otherwise black), we may want a custom theme later. */ -} \ No newline at end of file diff --git a/docs/website/scripts/live-blocks/static/icons/LICENSE b/docs/website/scripts/live-blocks/static/icons/LICENSE deleted file mode 100644 index 92424b805c..0000000000 --- a/docs/website/scripts/live-blocks/static/icons/LICENSE +++ /dev/null @@ -1,204 +0,0 @@ -The icons in this folder are provided by Google under the Apache 2.0 license (https://github.com/google/material-design-icons), a copy of which is included below: - - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. \ No newline at end of file diff --git a/docs/website/scripts/live-blocks/static/icons/input.svg b/docs/website/scripts/live-blocks/static/icons/input.svg deleted file mode 100644 index 3ece8312b3..0000000000 --- a/docs/website/scripts/live-blocks/static/icons/input.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/website/scripts/live-blocks/static/icons/module.svg b/docs/website/scripts/live-blocks/static/icons/module.svg deleted file mode 100644 index aefea22097..0000000000 --- a/docs/website/scripts/live-blocks/static/icons/module.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/website/scripts/live-blocks/static/icons/opening.svg b/docs/website/scripts/live-blocks/static/icons/opening.svg deleted file mode 100644 index 38795a1ca5..0000000000 --- a/docs/website/scripts/live-blocks/static/icons/opening.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/website/scripts/live-blocks/static/icons/output.svg b/docs/website/scripts/live-blocks/static/icons/output.svg deleted file mode 100644 index 2252c75714..0000000000 --- a/docs/website/scripts/live-blocks/static/icons/output.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/website/scripts/live-blocks/static/icons/playground-error.svg b/docs/website/scripts/live-blocks/static/icons/playground-error.svg deleted file mode 100644 index b4bdab950c..0000000000 --- a/docs/website/scripts/live-blocks/static/icons/playground-error.svg +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - image/svg+xml - - - - - - - - - diff --git a/docs/website/scripts/live-blocks/static/icons/playground-open.svg b/docs/website/scripts/live-blocks/static/icons/playground-open.svg deleted file mode 100644 index 788f40e011..0000000000 --- a/docs/website/scripts/live-blocks/static/icons/playground-open.svg +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - image/svg+xml - - - - - - - - - diff --git a/docs/website/scripts/live-blocks/static/icons/playground-working.svg b/docs/website/scripts/live-blocks/static/icons/playground-working.svg deleted file mode 100644 index e5984a8e30..0000000000 --- a/docs/website/scripts/live-blocks/static/icons/playground-working.svg +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - image/svg+xml - - - - - - - - - diff --git a/docs/website/scripts/live-blocks/static/icons/query.svg b/docs/website/scripts/live-blocks/static/icons/query.svg deleted file mode 100644 index 6320581532..0000000000 --- a/docs/website/scripts/live-blocks/static/icons/query.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/website/scripts/live-blocks/static/icons/restore.svg b/docs/website/scripts/live-blocks/static/icons/restore.svg deleted file mode 100644 index 035077ec77..0000000000 --- a/docs/website/scripts/live-blocks/static/icons/restore.svg +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - image/svg+xml - - - - - - - - - diff --git a/docs/website/scripts/live-blocks/static/opening-in-playground.html b/docs/website/scripts/live-blocks/static/opening-in-playground.html deleted file mode 100644 index fc01f0f1b5..0000000000 --- a/docs/website/scripts/live-blocks/static/opening-in-playground.html +++ /dev/null @@ -1,24 +0,0 @@ - - - Opening... - - - - - - \ No newline at end of file diff --git a/docs/website/scripts/live-blocks/test/errors.test.js b/docs/website/scripts/live-blocks/test/errors.test.js deleted file mode 100644 index 43cb6bada5..0000000000 --- a/docs/website/scripts/live-blocks/test/errors.test.js +++ /dev/null @@ -1,100 +0,0 @@ -import {describe, it} from 'mocha' -import {expect} from 'chai' - -import {ChainedError, OPAErrors} from '../src/errors' -import {STATIC_TAG_TYPES} from '../src/constants' - -describe('ChainedError', () => { - it('uses a new message', () => { - expect((new ChainedError('b', new Error('a'))).message).to.equal('b') - }) - - it('preserves old stack information', () => { - const e = new Error() - e.stack = 'foobar' - expect((new ChainedError('', e)).stack).to.contain('foobar') - }) - - it('won\'t throw an unintelligible error if you forget the cause', () => { - expect((new ChainedError('')).stack).to.contain('no cause') - }) -}) - -describe('OPAErrors', () => { - const oe = (code) => ({code}) - - it('message is the given pretty string', () => { - expect((new OPAErrors('foobar', oe(''))).message).to.equal('foobar') - }) - - it('can be constructed with a single OPA error', () => { - expect((new OPAErrors('', oe('rego_recursion_error'))).matchesExpected([STATIC_TAG_TYPES.EXPECT_RECURSION])).to.be.true - expect((new OPAErrors('', oe('rego_recursion_error'))).matchesExpected([STATIC_TAG_TYPES.EXPECT_BUILTIN_ERROR])).to.be.false - }) - - it('can be constructed with an array of OPA errors', () => { - expect((new OPAErrors('', [oe('rego_recursion_error'), oe('rego_compile_error')])).matchesExpected([STATIC_TAG_TYPES.EXPECT_RECURSION, STATIC_TAG_TYPES.EXPECT_COMPILE_ERROR])).to.be.true - expect((new OPAErrors('', [oe('rego_recursion_error'), oe('rego_compile_error')])).matchesExpected([STATIC_TAG_TYPES.EXPECT_RECURSION, STATIC_TAG_TYPES.EXPECT_UNSAFE_VAR])).to.be.false - }) - - it('can be constructed with undefined as the error', () => { - expect((new OPAErrors('', undefined)).matchesExpected([STATIC_TAG_TYPES.EXPECT_UNDEFINED])).to.be.true - expect((new OPAErrors('', undefined)).matchesExpected([STATIC_TAG_TYPES.EXPECT_BUILTIN_ERROR])).to.be.false - - expect((new OPAErrors('', [undefined])).matchesExpected([STATIC_TAG_TYPES.EXPECT_UNDEFINED])).to.be.true - expect((new OPAErrors('', [undefined])).matchesExpected([STATIC_TAG_TYPES.EXPECT_BUILTIN_ERROR])).to.be.false - }) - - it('can\'t be constructed with improperly formatted OPA errors', () => { - expect(() => new OPAErrors('', 'foobar')).to.throw() - expect(() => new OPAErrors('', {message: 'foobar'})).to.throw() - expect(() => new OPAErrors('', [oe('rego_recursion_error'), {message: 'foobar'}])).to.throw() - }) - - it('can\'t be constructed with empty array of OPA errors', () => { - expect(() => new OPAErrors('', [])).to.throw() - }) - - it('can match against no tags', () => { - expect((new OPAErrors('', oe('rego_recursion_error'))).matchesExpected([])).to.be.false - }) - - it('can match with duplicate errors', () => { - expect((new OPAErrors('', [oe('rego_recursion_error'), oe('rego_recursion_error')])).matchesExpected([STATIC_TAG_TYPES.EXPECT_RECURSION])).to.be.true - }) - - it('can match with duplicate tags', () => { - expect((new OPAErrors('', oe('rego_recursion_error'))).matchesExpected([STATIC_TAG_TYPES.EXPECT_RECURSION, STATIC_TAG_TYPES.EXPECT_RECURSION])).to.be.true - }) - - it('can match with non-error tags', () => { - expect((new OPAErrors('', oe('rego_recursion_error'))).matchesExpected([STATIC_TAG_TYPES.HIDDEN, STATIC_TAG_TYPES.EXPECT_RECURSION])).to.be.true - }) - - it('requires all errors to be matched', () => { - expect((new OPAErrors('', oe('rego_recursion_error'))).matchesExpected([STATIC_TAG_TYPES.HIDDEN])).to.be.false - expect((new OPAErrors('', [oe('rego_recursion_error'), oe('rego_compile_error')])).matchesExpected([STATIC_TAG_TYPES.HIDDEN, STATIC_TAG_TYPES.EXPECT_RECURSION])).to.be.false - }) - - it('requires all error tags to match an error', () => { - expect((new OPAErrors('', oe('rego_recursion_error'))).matchesExpected([STATIC_TAG_TYPES.HIDDEN])).to.be.false - expect((new OPAErrors('', [oe('rego_recursion_error'), oe('rego_compile_error')])).matchesExpected([STATIC_TAG_TYPES.EXPECT_RECURSION, STATIC_TAG_TYPES.EXPECT_COMPILE_ERROR, STATIC_TAG_TYPES.EXPECT_ASSIGNED_ABOVE])).to.be.false - }) - - it('child errors can be matched precisely', () => { - expect((new OPAErrors('', [{code: 'rego_compile_error', message: 'assigned above'}])).matchesExpected([STATIC_TAG_TYPES.EXPECT_ASSIGNED_ABOVE])).to.be.true - expect((new OPAErrors('', [{code: 'rego_compile_error', message: 'assigned above'}, {code: 'rego_compile_error', message: 'referenced above'}])).matchesExpected([STATIC_TAG_TYPES.EXPECT_ASSIGNED_ABOVE])).to.be.false - }) - - it('parent tags match children', () => { - expect((new OPAErrors('', [undefined, oe('rego_type_error'), oe('eval_type_error'), oe('fake_error')])).matchesExpected([STATIC_TAG_TYPES.EXPECT_ERROR])).to.be.true - expect((new OPAErrors('', [oe('rego_type_error')])).matchesExpected([STATIC_TAG_TYPES.EXPECT_REGO_ERROR])).to.be.true - expect((new OPAErrors('', [oe('eval_type_error')])).matchesExpected([STATIC_TAG_TYPES.EXPECT_EVAL_ERROR])).to.be.true - expect((new OPAErrors('', [{code: 'rego_compile_error', message: 'assigned above'}])).matchesExpected([STATIC_TAG_TYPES.EXPECT_COMPILE_ERROR])).to.be.true - }) - - it('undefined is only matched by its own tag and expect_error', () => { - expect((new OPAErrors('', undefined).matchesExpected([STATIC_TAG_TYPES.EXPECT_ERROR, STATIC_TAG_TYPES.EXPECT_UNDEFINED]))).to.be.true - expect((new OPAErrors('', undefined).matchesExpected([STATIC_TAG_TYPES.EXPECT_EVAL_ERROR, STATIC_TAG_TYPES.EXPECT_UNDEFINED]))).to.be.false - }) -}) \ No newline at end of file diff --git a/docs/website/scripts/live-blocks/test/helpers.test.js b/docs/website/scripts/live-blocks/test/helpers.test.js deleted file mode 100644 index 13b28fb274..0000000000 --- a/docs/website/scripts/live-blocks/test/helpers.test.js +++ /dev/null @@ -1,422 +0,0 @@ -import chai, {expect} from 'chai' -import chaiAsPromised from 'chai-as-promised' -import sinon from 'sinon' -import {describe, it} from 'mocha' - -import {batchMapFold, batchProcess, delay, expectedErrorTags, getAllGroupModules, getGroupData, getGroupField, handleLater, includedGroupNames, infoFromLabel, toArray} from '../src/helpers' -import {BLOCK_TYPES, STATIC_TAG_TYPES} from '../src/constants' - -chai.use(chaiAsPromised) - -describe('infoFromLabel', () => { - it('parses a label', () => { - const res = infoFromLabel('live:eg/pi:output:merge_down,expect_undefined,include(foobar/barfoo)') - expect(res).has.keys(['group', 'type', 'tags']) - expect(res.group).equals('eg/pi') - expect(res.type).equals(BLOCK_TYPES.OUTPUT) - expect(res.tags).lengthOf(3).and.contains('merge_down').and.contains('expect_undefined').and.contains('include(foobar/barfoo)') - }) - - it('parses a label without tags', () => { - const res = infoFromLabel('live:eg/pi:output') - expect(res).has.keys(['group', 'type', 'tags']) - expect(res.group).equals('eg/pi') - expect(res.type).equals('output') - expect(res.tags).lengthOf(0) - }) - - it('errors on non-live block labels', () => { - expect(() => infoFromLabel('json')).to.throw() - }) - - it('errors on malformed block labels', () => { - expect(() => infoFromLabel('live:eg:modle')).to.throw() - expect(() => infoFromLabel('live::module')).to.throw() - expect(() => infoFromLabel('live:eg:module:')).to.throw() - expect(() => infoFromLabel('live:eg:module:foobar')).to.throw() - expect(() => infoFromLabel('live:eg:module:expect_undefined')).to.throw() - expect(() => infoFromLabel('live:eg:module:include(foobar/barfoo)')).to.throw() - }) -}) - -describe('expectedErrorTags', () => { - it('accepts an empty array', () => { - expect(expectedErrorTags([])).deep.equals([]) - }) - - it('can consume only non-expectations', () => { - expect(expectedErrorTags([STATIC_TAG_TYPES.HIDDEN, STATIC_TAG_TYPES.MERGE_DOWN, 'include(foobar)'])).deep.equals([]) - }) - - it('can consume only expectations', () => { - expect(expectedErrorTags([STATIC_TAG_TYPES.EXPECT_BUILTIN_ERROR, STATIC_TAG_TYPES.EXPECT_ERROR])) - .has.lengthOf(2) - .and.contains(STATIC_TAG_TYPES.EXPECT_BUILTIN_ERROR) - .and.contains(STATIC_TAG_TYPES.EXPECT_ERROR) - }) - - it('can consume a mixture', () => { - expect(expectedErrorTags([STATIC_TAG_TYPES.HIDDEN, STATIC_TAG_TYPES.EXPECT_BUILTIN_ERROR, STATIC_TAG_TYPES.MERGE_DOWN, STATIC_TAG_TYPES.EXPECT_ERROR])) - .has.lengthOf(2) - .and.contains(STATIC_TAG_TYPES.EXPECT_BUILTIN_ERROR) - .and.contains(STATIC_TAG_TYPES.EXPECT_ERROR) - }) - - it('doesn\'t mutate argument', () => { - const tags = [STATIC_TAG_TYPES.HIDDEN, STATIC_TAG_TYPES.EXPECT_BUILTIN_ERROR, STATIC_TAG_TYPES.MERGE_DOWN, STATIC_TAG_TYPES.EXPECT_ERROR] - const args = tags.slice() - - expectedErrorTags([STATIC_TAG_TYPES.HIDDEN, STATIC_TAG_TYPES.EXPECT_BUILTIN_ERROR, STATIC_TAG_TYPES.MERGE_DOWN, STATIC_TAG_TYPES.EXPECT_ERROR]) - expect(args).to.deep.equal(tags) - }) -}) - -describe('includedGroupNames', () => { - it('accepts an empty array', () => { - expect(includedGroupNames([])).deep.equals([]) - }) - - it('can consume only non-includes', () => { - expect(includedGroupNames([STATIC_TAG_TYPES.HIDDEN, STATIC_TAG_TYPES.MERGE_DOWN, STATIC_TAG_TYPES.EXPECT_ASSIGNED_ABOVE])).deep.equals([]) - }) - - it('can consume only includes', () => { - expect(includedGroupNames(['include(foo/bar)', 'include(eg)'])) - .has.lengthOf(2) - .and.contains('foo/bar') - .and.contains('eg') - }) - - it('can consume a mixture', () => { - expect(includedGroupNames([STATIC_TAG_TYPES.HIDDEN, 'include(foo/bar)', STATIC_TAG_TYPES.EXPECT_BUILTIN_ERROR, STATIC_TAG_TYPES.MERGE_DOWN, 'include(eg)', STATIC_TAG_TYPES.EXPECT_ERROR])) - .has.lengthOf(2) - .and.contains('foo/bar') - .and.contains('eg') - }) - - it('doesn\'t mutate argument', () => { - const tags = [STATIC_TAG_TYPES.HIDDEN, 'include(foo/bar)', STATIC_TAG_TYPES.EXPECT_BUILTIN_ERROR, STATIC_TAG_TYPES.MERGE_DOWN, 'include(eg)', STATIC_TAG_TYPES.EXPECT_ERROR] - const args = tags.slice() - - includedGroupNames([STATIC_TAG_TYPES.HIDDEN, STATIC_TAG_TYPES.EXPECT_BUILTIN_ERROR, STATIC_TAG_TYPES.MERGE_DOWN, STATIC_TAG_TYPES.EXPECT_ERROR]) - expect(args).to.deep.equal(tags) - }) - - it('only accepts properly formated tags', () => { - expect(includedGroupNames(['include(foo-bar)', 'include', 'include()', 'include(foo/bar/)'])) - .has.lengthOf(0) - }) -}) - -describe('getGroupField', () => { - it('gets field in group if there', () => { - expect(getGroupField({foo: {bar: 1}, 'foo/two': {bar: 2}}, 'foo/two', 'bar')).to.equal(2) - }) - - it('gets parents\' fields when field is missing', () => { - expect(getGroupField({foo: {bar: 7}, 'foo/two': {baz: 42}}, 'foo/two', 'bar')).to.equal(7) - }) - - it('gets parents\' fields when group is missing', () => { - expect(getGroupField({foo: {bar: 7}}, 'foo/two', 'bar')).to.equal(7) - }) - - it('gets field from deepest parent', () => { - const groups = {foo: {bar: 1}, 'foo/two': {bar: 2}, 'foo/two/child': {}} - expect(getGroupField(groups, 'foo/two/child', 'bar')).to.equal(2) - }) - - it('skips missing parents', () => { - expect(getGroupField({foo: {bar: 7}, 'foo/two/three': {baz: 42}}, 'foo/two/three', 'bar')).to.equal(7) - }) - - it('returns undefined when hierarchy does not exist', () => { - expect(getGroupField({foo: {bar: 7}, 'foo/two/three': {baz: 42}}, 'a/b/c', 'bar')).to.equal(undefined) - }) - - it('returns undefined when out of parents', () => { - expect(getGroupField({foo: {bar: 7}, 'foo/two': {baz: 42}}, 'foo/two', 'unset')).to.equal(undefined) - }) - - it('doesn\'t mutate groups', () => { - const groups = {foo: {bar: 7}, 'foo/two': {baz: 42}} - const arg = JSON.parse(JSON.stringify(groups)) - getGroupField(arg, 'foo/two', 'bar') - expect(arg).to.deep.equal(groups) - getGroupField(arg, 'foo/two', 'unset') - expect(arg).to.deep.equal(groups) - }) -}) - -describe('getAllGroupModules', () => { - it('can get no modules', () => { - expect(getAllGroupModules({}, 'foo')).to.deep.equal([]) - expect(getAllGroupModules({foo: {bar: 7}, 'foo/two': {baz: 42}}, 'foo/two')).to.deep.equal([]) - }) - - it('gets all the present modules from shallowest to deepest', () => { - expect(getAllGroupModules({ - 'a': {module: 1}, - 'a/b/c': {module: 2} - }, 'a/b/c/d')).to.deep.equal([1, 2]) - }) - - it('doesn\'t mutate groups', () => { - const groups = { - 'a': {module: 1}, - 'a/b/c': {module: 2} - } - const arg = JSON.parse(JSON.stringify(groups)) - getAllGroupModules(arg, 'a/b/c/d') - expect(arg).to.deep.equal(groups) - }) -}) - -describe('getGroupData', () => { - it('concatenates module blocks', () => { - expect(getGroupData({ - 'a': {module: {get: () => 'package foo\na'}}, - 'a/b/c': {module: {get: () => 'c'}} - }, 'a/b/c/d').module).to.equal('package foo\na\n\nc') - }) - - it('errors when there are no module blocks', () => { - expect(() => getGroupData({ - 'a': {query: {get: () => 'a'}}, - 'a/b/c': {query: {get: () => 'c'}} - }, 'a/b/c/d')).to.throw('no module') - }) - - it('get\'s the module package', () => { - expect(getGroupData({ - 'a': {module: {get: () => 'package foo\na'}}, - 'a/b/c': {module: {get: () => 'c'}} - }, 'a/b/c/d').package).to.equal('foo') - }) - - it('errors when there is no package declaration', () => { - expect(() => getGroupData({ - 'a': {module: {get: () => 'a'}}, - 'a/b/c': {module: {get: () => 'c'}} - }, 'a/b/c/d')).to.throw(/package/) - }) - - const egInput = ['foo', {bar: 'baz'}] - const strInput = JSON.stringify(egInput) - - it('parses input from block', () => { - expect(getGroupData({ - 'a': {input: {get: () => 'foobar'}, module: {get: () => 'package foo\na'}}, // Will error if gets from this parent - 'a/b/c': {input: {get: () => strInput}, module: {get: () => 'c'}} - }, 'a/b/c').input).to.deep.equal(egInput) - }) - - it('can get input from parents', () => { - expect(getGroupData({ - 'a': {input: {get: () => 'foobar'}, module: {get: () => 'package foo\na'}}, // Will error if gets from this parent - 'a/b/c': {input: {get: () => strInput}, module: {get: () => 'c'}} - }, 'a/b/c/d').input).to.deep.equal(egInput) - }) - - it('errors when input cannot be parsed', () => { - expect(() => getGroupData({ - 'a': {input: {get: () => 'foobar'}, module: {get: () => 'package foo\na'}}, - }, 'a')).to.throw('can\'t parse input') - }) - - it('gets the query', () => { - expect(getGroupData({ - 'a': {query: {get: () => { - throw new Error() - }}, module: {get: () => 'package foo\na'}}, // Will error if gets from this parent - 'a/b/c': {query: {get: () => 'foobar'}, module: {get: () => 'c'}} - }, 'a/b/c').query).to.equal('foobar') - }) - - it('can get the query from parents', () => { - expect(getGroupData({ - 'a': {query: {get: () => { - throw new Error() - }}, module: {get: () => 'package foo\na'}}, // Will error if gets from this parent - 'a/b/c': {query: {get: () => 'foobar'}, module: {get: () => 'c'}} - }, 'a/b/c/d').query).to.equal('foobar') - }) - - it('includes other modules', () => { - expect(getGroupData({ - 'a': {module: {get: () => 'package foo\na'}}, - 'a/b/c': {module: {get: () => 'c'}, output: {tags: ['include(d)']}}, - 'd': {module: {get: () => 'd'}} - }, 'a/b/c').included["d.rego"]).to.equal('d') - }) -}) - -describe('handleLater', () => { - it('resolves with the resolved value', async () => { - expect(await handleLater(() => Promise.resolve('foo'))).to.deep.equal(['foo', undefined]) - }) - - it('resolves with the rejected value', async () => { - expect(await handleLater(() => Promise.reject('foo'))).to.deep.equal([undefined, 'foo']) - }) - - it('resolves with resolved undefined', async () => { - expect(await handleLater(() => Promise.resolve(undefined))).to.deep.equal([undefined, undefined]) - }) - - it('resolves with rejected undefined', async () => { - expect(await handleLater(() => Promise.reject(undefined))).to.deep.equal([undefined, undefined]) - }) -}) - -describe('batchProcess', () => { - it('can run no jobs', async () => { - expect(await batchProcess(0, [])).to.deep.equal([]) - }) - - it('runs only maxC jobs at a time', async () => { - expect(await time(() => - batchProcess(1, [ - async () => await delay(100), - async () => await delay(100), - ]) - )).to.equal(200) - - expect(await time(() => - batchProcess(2, [ - async () => await delay(100), - async () => await delay(100), - async () => await delay(100), - async () => await delay(100), - ]) - )).to.equal(200) - }) - - it('runs all of them simultaneously with maxC = 0', async () => { - expect(await time(() => - batchProcess(0, [ - async () => await delay(100), - async () => await delay(100), - ]) - )).to.equal(100) - }) - - it('resolves to an array of the results', async () => { - expect(await batchProcess(0, [() => 1, () => 2])).to.have.a.lengthOf(2).and.contain(1).and.contain(2) - }) - - it('rejects once all are run with a flat array of errors', async () => { - const [_, e] = await handleLater(() => batchProcess(0, [() => { - throw [1, 2] - }, () => { - throw 3 - }])) - expect(e).to.have.a.lengthOf(3).and.contain(1).and.contain(2).and.contain(3) - }) - - it('doesn\'t mutate jobs', async () => { - const jobs = [() => 1, () => 2] - const arg = jobs.slice() - await batchProcess(0, arg) - expect(arg).to.deep.equal(jobs) - }) -}) - -describe('batchMapFold', () => { - it('can handle no input', async () => { - expect(await batchMapFold(0, [], () => { - throw '' - }, () => { - throw '' - }, 'foo')).to.equal('foo') - }) - - it('handles only maxC inputs at a time', async () => { - expect(await time(() => - batchMapFold(1, [0, 0], () => delay(100), () => {}, 0) - )).to.equal(200) - - expect(await time(() => - batchMapFold(2, [0, 0, 0, 0], () => delay(100), () => {}, 0) - )).to.equal(200) - }) - - it('handles all of them simultaneously with maxC = 0', async () => { - expect(await time(() => - batchMapFold(0, [0, 0, 0, 0], () => delay(100), () => {}, 0) - )).to.equal(100) - }) - - it('resolves to the folded results', async () => { - expect(await batchMapFold(0, [1, 2, 4, 8], (n) => n, (o, {output: [n]}) => o + n, 0)).to.equal(15) - }) - - it('reports map errors to the folder', async () => { - expect(await batchMapFold(0, [1, 2, 4, 8], (n) => { - throw n - }, (o, {output: [, n]}) => o + n, 0)).to.equal(15) - }) - - it('folds synchronously', async () => { - expect(await batchMapFold(0, [1, 2, 4, 8], (n) => n, async (o, {output: [n]}) => o + n, 0)).to.equal('[object Promise]1') - }) - - it('doesn\'t mutate inputs', async () => { - const inputs = [1, 2, 4, 8] - const arg = inputs.slice() - await batchMapFold(0, arg, (n) => n, (o, {output: [n]}) => o + n, 0) - expect(arg).to.deep.equal(inputs) - }) - - it('can mutate the out argument', async () => { - const out = {val: 0} - await batchMapFold(0, [1, 2, 4, 8], (n) => n, (o, {output: [n]}) => { - o.val += n; return o - }, out) - expect(out.val).to.equal(15) - }) -}) - -describe('delay', () => { - it('delays the given amount', async () => { - expect(await time(() => delay(100))).to.equal(100) - }) -}) - -describe('toArray', () => { - it('returns arrays', () => { - const empty = [] - expect(toArray(empty)).to.equal(empty) // Not just deep; actually equal - const full = [1, 2, 3] - expect(toArray(full)).to.equal(full) - }) - - it('wraps non-arrays', () => { - expect(toArray(undefined)).to.deep.equal([undefined]) - expect(toArray('a')).to.deep.equal(['a']) - expect(toArray({a: 'a'})).to.deep.equal([{a: 'a'}]) - }) -}) - -// Time how long it takes an async function to complete without actually waiting that long. Returns a promise that resolves to the # of ms. -async function time(f) { - const clock = sinon.useFakeTimers() // Will only advance automatically if runAll() doesn't work. - - // Run the function - let done = false; - (async () => { - await handleLater(f) // await f to either resolve or reject - done = true - })() - - // Run the clock, allowing the function's delays to complete - while (!done) { - clock.runAll() // Even though it's called 'runAll', calling it multiple times is necessary - await Promise.resolve() // Yield to the function that's running - } - - // Finish up - const end = new Date().getTime() - clock.restore() - return end -} \ No newline at end of file