@aire-ux/aire-code-panel
Version:
aire-code-panel
298 lines (266 loc) • 10.6 kB
JavaScript
/*
* Copyright 2000-2022 Vaadin Ltd.
*
* 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.
*/
/**
* This file handles the generation of the '[theme-name].js' to
* the themes/[theme-name] folder according to properties from 'theme.json'.
*/
const glob = require('glob');
const path = require('path');
const fs = require('fs');
const { checkModules } = require('./theme-copy');
// Special folder inside a theme for component themes that go inside the component shadow root
const themeComponentsFolder = 'components';
// The contents of a global CSS file with this name in a theme is always added to
// the document. E.g. @font-face must be in this
const documentCssFile = 'document.css';
// styles.css is the only entrypoint css file with document.css. Everything else should be imported using css @import
const stylesCssFile = 'styles.css';
const headerImport = `import 'construct-style-sheets-polyfill';
`;
const createLinkReferences = `
const createLinkReferences = (css, target) => {
// Unresolved urls are written as '@import url(text);' or '@import "text";' to the css
// media query can be present on @media tag or on @import directive after url
// Note that with Vite production build there is no space between @import and "text"
// [0] is the full match
// [1] matches the media query
// [2] matches the url
// [3] matches the quote char surrounding in '@import "..."'
// [4] matches the url in '@import "..."'
// [5] matches media query on @import statement
const importMatcher = /(?:@media\\s(.+?))?(?:\\s{)?\\@import\\s*(?:url\\(\\s*['"]?(.+?)['"]?\\s*\\)|(["'])((?:\\\\.|[^\\\\])*?)\\3)([^;]*);(?:})?/g
var match;
var styleCss = css;
// For each external url import add a link reference
while((match = importMatcher.exec(css)) !== null) {
styleCss = styleCss.replace(match[0], "");
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = match[2] || match[4];
const media = match[1] || match[5];
if (media) {
link.media = media;
}
// For target document append to head else append to target
if (target === document) {
document.head.appendChild(link);
} else {
target.appendChild(link);
}
};
return styleCss;
};
`;
const injectGlobalCssMethod = `
// target: Document | ShadowRoot
export const injectGlobalCss = (css, target, first) => {
if(target === document) {
const hash = getHash(css);
if (window.Vaadin.theme.injectedGlobalCss.indexOf(hash) !== -1) {
return;
}
window.Vaadin.theme.injectedGlobalCss.push(hash);
}
const sheet = new CSSStyleSheet();
sheet.replaceSync(createLinkReferences(css,target));
if (first) {
target.adoptedStyleSheets = [sheet, ...target.adoptedStyleSheets];
} else {
target.adoptedStyleSheets = [...target.adoptedStyleSheets, sheet];
}
};
`;
/**
* Generate the [themeName].js file for themeFolder which collects all required information from the folder.
*
* @param {string} themeFolder folder of the theme
* @param {string} themeName name of the handled theme
* @param {JSON} themeProperties content of theme.json
* @param {boolean} productionMode true if making a production build.
* @returns {string} theme file content
*/
function generateThemeFile(themeFolder, themeName, themeProperties, productionMode) {
const styles = path.resolve(themeFolder, stylesCssFile);
const document = path.resolve(themeFolder, documentCssFile);
const componentsFiles = glob.sync('*.css', {
cwd: path.resolve(themeFolder, themeComponentsFolder),
nodir: true
});
let themeFile = headerImport;
if (componentsFiles.length > 0) {
themeFile += "import { unsafeCSS, registerStyles } from '@vaadin/vaadin-themable-mixin/register-styles';\n";
}
if (themeProperties.parent) {
themeFile += `import {applyTheme as applyBaseTheme} from './theme-${themeProperties.parent}.generated.js';`;
}
themeFile += createLinkReferences;
themeFile += injectGlobalCssMethod;
const imports = [];
const globalCssCode = [];
const lumoCssCode = [];
const componentCssCode = [];
const parentTheme = themeProperties.parent ? 'applyBaseTheme(target);\n' : '';
const themeIdentifier = '_vaadintheme_' + themeName + '_';
const lumoCssFlag = '_vaadinthemelumoimports_';
const globalCssFlag = themeIdentifier + 'globalCss';
const componentCssFlag = themeIdentifier + 'componentCss';
if (!fs.existsSync(styles)) {
if (productionMode) {
throw new Error(`styles.css file is missing and is needed for '${themeName}' in folder '${themeFolder}'`);
}
fs.writeFileSync(
styles,
'/* Import your application global css files here or add the styles directly to this file */',
'utf8'
);
}
// styles.css will always be available as we write one if it doesn't exist.
let filename = path.basename(styles);
let variable = camelCase(filename);
imports.push(`import ${variable} from 'themes/${themeName}/${filename}?inline';\n`);
/* Lumo must be first so that custom styles override Lumo styles */
const lumoImports = themeProperties.lumoImports || ['color', 'typography'];
if (lumoImports && lumoImports.length > 0) {
lumoImports.forEach((lumoImport) => {
imports.push(`import { ${lumoImport} } from '@vaadin/vaadin-lumo-styles/${lumoImport}.js';\n`);
});
lumoImports.forEach((lumoImport) => {
lumoCssCode.push(`injectGlobalCss(${lumoImport}.cssText, target, true);\n`);
});
}
globalCssCode.push(`injectGlobalCss(${variable}.toString(), target);\n `);
if (fs.existsSync(document)) {
filename = path.basename(document);
variable = camelCase(filename);
imports.push(`import ${variable} from 'themes/${themeName}/${filename}?inline';\n`);
globalCssCode.push(`injectGlobalCss(${variable}.toString(), document);\n `);
}
let i = 0;
if (themeProperties.documentCss) {
const missingModules = checkModules(themeProperties.documentCss);
if (missingModules.length > 0) {
throw Error(
"Missing npm modules or files '" +
missingModules.join("', '") +
"' for documentCss marked in 'theme.json'.\n" +
"Install or update package(s) by adding a @NpmPackage annotation or install it using 'npm/pnpm i'"
);
}
themeProperties.documentCss.forEach((cssImport) => {
const variable = 'module' + i++;
imports.push(`import ${variable} from '${cssImport}?inline';\n`);
// Due to chrome bug https://bugs.chromium.org/p/chromium/issues/detail?id=336876 font-face will not work
// inside shadowRoot so we need to inject it there also.
globalCssCode.push(`if(target !== document) {
injectGlobalCss(${variable}.toString(), target);
}\n `);
globalCssCode.push(`injectGlobalCss(${variable}.toString(), document);\n `);
});
}
if (themeProperties.importCss) {
const missingModules = checkModules(themeProperties.importCss);
if (missingModules.length > 0) {
throw Error(
"Missing npm modules or files '" +
missingModules.join("', '") +
"' for importCss marked in 'theme.json'.\n" +
"Install or update package(s) by adding a @NpmPackage annotation or install it using 'npm/pnpm i'"
);
}
themeProperties.importCss.forEach((cssPath) => {
const variable = 'module' + i++;
imports.push(`import ${variable} from '${cssPath}';\n`);
globalCssCode.push(`injectGlobalCss(${variable}.toString(), target);\n`);
});
}
componentsFiles.forEach((componentCss) => {
const filename = path.basename(componentCss);
const tag = filename.replace('.css', '');
const variable = camelCase(filename);
imports.push(`import ${variable} from 'themes/${themeName}/${themeComponentsFolder}/${filename}?inline';\n`);
// Don't format as the generated file formatting will get wonky!
const componentString = `registerStyles(
'${tag}',
unsafeCSS(${variable}.toString())
);
`;
componentCssCode.push(componentString);
});
themeFile += imports.join('');
themeFile += `
window.Vaadin = window.Vaadin || {};
window.Vaadin.theme = window.Vaadin.theme || {};
window.Vaadin.theme.injectedGlobalCss = [];
/**
* Calculate a 32 bit FNV-1a hash
* Found here: https://gist.github.com/vaiorabbit/5657561
* Ref.: http://isthe.com/chongo/tech/comp/fnv/
*
* @param {string} str the input value
* @returns {string} 32 bit (as 8 byte hex string)
*/
function hashFnv32a(str) {
/*jshint bitwise:false */
let i, l, hval = 0x811c9dc5;
for (i = 0, l = str.length; i < l; i++) {
hval ^= str.charCodeAt(i);
hval += (hval << 1) + (hval << 4) + (hval << 7) + (hval << 8) + (hval << 24);
}
// Convert to 8 digit hex string
return ("0000000" + (hval >>> 0).toString(16)).substr(-8);
}
/**
* Calculate a 64 bit hash for the given input.
* Double hash is used to significantly lower the collision probability.
*
* @param {string} input value to get hash for
* @returns {string} 64 bit (as 16 byte hex string)
*/
function getHash(input) {
let h1 = hashFnv32a(input); // returns 32 bit (as 8 byte hex string)
return h1 + hashFnv32a(h1 + input);
}
`;
// Don't format as the generated file formatting will get wonky!
// If targets check that we only register the style parts once, checks exist for global css and component css
const themeFileApply = `export const applyTheme = (target) => {
${parentTheme}
${globalCssCode.join('')}
if (!document['${componentCssFlag}']) {
${componentCssCode.join('')}
document['${componentCssFlag}'] = true;
}
${lumoCssCode.join('')}
}
`;
themeFile += themeFileApply;
return themeFile;
}
/**
* Make given string into camelCase.
*
* @param {string} str string to make into cameCase
* @returns {string} camelCased version
*/
function camelCase(str) {
return str
.replace(/(?:^\w|[A-Z]|\b\w)/g, function (word, index) {
return index === 0 ? word.toLowerCase() : word.toUpperCase();
})
.replace(/\s+/g, '')
.replace(/\.|\-/g, '');
}
module.exports = generateThemeFile;