UNPKG

@onereach/webform

Version:

Content Builder includes several views for: - Content builder view itself; - Web Form view; - Slack block-kit builder;

275 lines (242 loc) 8.43 kB
import * as _ from 'lodash'; import * as later from 'later'; import * as espree from 'espree'; import * as moment from 'moment'; import * as momentTZ from 'moment-timezone'; import * as orUi from '@onereach/ui'; import * as stepMessageBus from '../step_message_bus'; import * as timestring from 'timestring'; import * as uuid from 'uuid'; import { utils, validators } from '@onereach/ui'; import timeInterpreter from '@onereach/time-interpreter'; import * as Vue from 'vue'; import * as vuelidators from 'vuelidate/lib/validators'; import * as vueTemplateCompiler from 'vue-template-compiler'; import * as vuex from 'vuex'; import * as d3 from 'd3'; import * as vueColor from 'vue-color'; import CKEditor from '@ckeditor/ckeditor5-vue'; import ClassicEditor from '@onereach/ckeditor5-build-full'; import { compileStyle } from './compile-sfc-style/compileStyle'; import Cache from './cache'; import compiler from './compiler'; import config from '../config'; import Promise from 'bluebird'; import toolboxApi from '../api/toolbox'; export const loaderConfig = { EXTERNAL_STYLES_CONTAINER: 'external-components-styles-container-218673' }; const client = config.CLIENT; const env = config.ENV; const pluginsUrl = config.PLUGINS_URL_ROOT; const isLocal = config.LOCAL; const loader = { load (name, root) { const normName = this._normalize(name); return this._cache.get(normName, () => this._load(normName, root || name)); }, reset () { this._cache.reset(); this._cache.set('bluebird', Promise); this._cache.set('later', later); this._cache.set('lodash', _); this._cache.set('espree', espree); this._cache.set('moment', moment); this._cache.set('moment-timezone', momentTZ); this._cache.set('or-ui', orUi); this._cache.set('@onereach/ui', orUi); this._cache.set('timestring', timestring); this._cache.set('vue', Vue); this._cache.set('vuex', vuex); this._cache.set('vuelidate/lib/validators', vuelidators); this._cache.set('vue-template-compiler', vueTemplateCompiler); this._cache.set('uuid', uuid); this._cache.set('_applyStyles', (...args) => this._applyStyles(...args)); this._cache.set('_validators', validators); this._cache.set('_orUtils', utils); this._cache.set('_stepMessageBus', stepMessageBus); this._cache.set('d3', d3); this._cache.set('loaderConfig', loaderConfig); this._cache.set('vue-color', vueColor); this._cache.set('@ckeditor/ckeditor5-vue', CKEditor); this._cache.set('@onereach/ckeditor5-build-full', ClassicEditor); this._cache.set('@onereach/time-interpreter', timeInterpreter); }, _encapsulateStyles (wrapper, styles) { return `.${loaderConfig.EXTERNAL_STYLES_CONTAINER} {${styles}}`; }, async _applyStyles (ownerPath, wrapperId, vueStyles) { try { const sassOptions = { comments: false, indent: ' ', inputPath: ownerPath }; const stylesMapped = await Promise.map(vueStyles, async (style) => { // use native vue styles compiler OR our custom (as default) const useNativeCompiler = _.get( style, 'attrs.useNativeCompiler', false ); const content = useNativeCompiler ? style.content : style.scoped ? `[data${wrapperId}] {${style.content}}` : style.content; if (_.isEmpty(content)) { return ''; } const styles = this._encapsulateStyles( loaderConfig.EXTERNAL_STYLES_CONTAINER, content ); const cssResult = isLocal ? await compiler.compileSass(wrapperId, styles, sassOptions) : await toolboxApi.compileStyles({ styles, options: sassOptions }); const compiled = _.get(cssResult, 'compiled', {}); if (compiled.status !== 0) { throw new Error(compiled.formatted); } let compiledStyles = compiled.text ? compiled.text : ''; // remove spaces between data attr and selector (example: [data-<hash>]<class/id selector>) // in case of selector that contains only tag name - put tag at the beginning of line (example: <tag>[data-<hash>]) compiledStyles = compiledStyles.replace( new RegExp(`(\\[data${wrapperId}\\])\\s+(\\w*)`, 'g'), '$2$1' ); if (useNativeCompiler) { compiledStyles = _.get( compileStyle({ source: compiledStyles, scoped: style.scoped, id: `data${wrapperId}` }), 'code', '' ); } return compiledStyles; }); const styles = _.join(stylesMapped, ''); let styleNode = document.getElementById(wrapperId); if (styleNode) { styleNode.innerHTML = styles || ''; } else { await new Promise((resolve, reject) => { styleNode = document.createElement('style'); styleNode.onload = resolve; styleNode.onerror = reject; styleNode.setAttribute('id', wrapperId); document.head.appendChild(styleNode); styleNode.innerHTML = styles || ''; }); } } catch (error) { console.warn('failed to compile', error, ownerPath, vueStyles); } }, async _load (moduleName, root) { try { let moduleHref = moduleName; if (!_.includes(moduleName, '://')) { moduleHref = `https://unpkg.com/${moduleName}`; } const result = {}; result.module = { exports: result, imports: {}, root }; const transpiled = await compiler.load(moduleName, root); moduleHref = transpiled.url; const moduleParent = moduleHref.substr( 0, moduleHref.lastIndexOf('/') + 1 ); // eslint-disable-next-line no-unused-vars const define = (imports, fn) => { const importPromise = Promise.map(imports, (name) => { if (name === 'exports') { return result; } if (name === 'module') { return result.module; } if (name.startsWith('./') || name.startsWith('../')) { name = new URL(name, moduleParent).href; } return this.load(name, root); }); return Promise.all(importPromise).then((imported) => { _.range(imports.length, (index) => { result.imports[imports[index]] = imported[index]; }); return Promise.try(() => fn(...imported)); }); }; const scope = { _, define, later, moment, Vue }; // eslint-disable-next-line no-new-func const compiledCode = new Function( ..._.keys(scope), `return ${transpiled.code}` ); await compiledCode(..._.values(scope)); if (transpiled.vue && result.default) { const vueSFC = transpiled.vue; const descriptor = result.default; descriptor.wrapperId = this._generateWrapperId(); if (vueSFC.template) { // adding data wrapper attribute to component tag descriptor.template = `${vueSFC.template.content.replace( /(\s*<\s*[^/\s>]+)(\s|>)/g, `$1 data${descriptor.wrapperId}$2` )}`; } await this._applyStyles( moduleHref, descriptor.wrapperId, vueSFC.styles ); } return result; } catch (error) { console.error('eval', error); this._cache.del(moduleName); throw new Error(`Failed to eval: ${moduleName}\n`); } }, _hasExtension (name) { return /(\/|[.[a-z]{1,4}|@\w+)$/.test(name); }, _normalize (name) { const match = this._normalizer.exec(name); return match[1] .replace(/@default\b/g, pluginsUrl) .replace(/@env\b/g, `@${client}_${env}`) .replace(/\[env_client\]/g, `${env}_${client}`); }, _generateWrapperId () { const maxRadix = 36; this._nextWrapperId += 1; return `-vs-${this._nextWrapperId.toString(maxRadix)}`; }, _cache: new Cache(), _nextWrapperId: 0, _normalizer: /^(?:https:\/\/unpkg.com\/)?(.*)$/ }; loader.reset(); /** * Import package asyncronyously * @param {String} moduleName name of the package to import * @returns {Promise} imported package */ export default function (moduleName) { return loader.load(moduleName); }