@geogirafe/lib-geoportal
Version:
GeoGirafe is a flexible application to build online geoportals.
403 lines (349 loc) • 13 kB
JavaScript
// SPDX-License-Identifier: Apache-2.0
import MagicString from 'magic-string';
import fs from 'fs-extra';
import path from 'path';
import { minify } from 'minify';
import fg from 'fast-glob';
export function findFilesRecursive(sourceDir, allowedExtensions, fileList = []) {
const childs = fs.readdirSync(sourceDir);
childs.forEach((filename) => {
const src = path.join(sourceDir, filename);
if (fs.statSync(src).isDirectory()) {
findFilesRecursive(src, allowedExtensions, fileList);
} else {
const extension = path.extname(filename).toLowerCase();
if (allowedExtensions.includes(extension)) {
fileList.push(src);
}
}
});
return fileList;
}
export function deleteFilesRecursive(sourceDir, extensionsToDelete, fileList = []) {
const childs = fs.readdirSync(sourceDir);
childs.forEach((filename) => {
const src = path.join(sourceDir, filename);
if (fs.statSync(src).isDirectory()) {
deleteFilesRecursive(src, extensionsToDelete, fileList);
} else if (extensionsToDelete.some((ext) => filename.endsWith(ext))) {
console.info(`Deleting file ${src}`);
fileList.push(src);
fs.unlinkSync(src);
}
});
}
export function deleteDirectory(sourceDir) {
if (fs.existsSync(sourceDir)) {
console.info(`Deleting directory ${sourceDir} recursively`);
fs.rmSync(sourceDir, { recursive: true });
}
}
export function deleteFiles(patterns) {
for (const pattern of patterns) {
const filesToDelete = fg.globSync(pattern, { absolute: true });
for (const fileToDelete of filesToDelete) {
console.info(`Deleting file ${fileToDelete}`);
fs.rmSync(fileToDelete);
}
}
}
export function copy(filename, sourceDir, targetDir, targetFilename = null) {
if (targetFilename === null) {
targetFilename = filename;
}
const source = path.join(sourceDir, filename);
const target = path.join(targetDir, targetFilename);
if (fs.existsSync(source)) {
// First create destination directory
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir);
}
// Then copy file
console.info(`Copying ${filename} to ${target}`);
fs.copySync(source, target);
} else {
console.error(`{source} does not exist.`);
}
}
export function replaceInFile(filename, searchPattern, replacement) {
try {
const content = fs.readFileSync(filename, 'utf8');
const newContent = content.replace(searchPattern, replacement);
fs.writeFileSync(filename, newContent, 'utf8');
} catch (error) {
console.error(`Error replacing content in ${filename}: ${error}`);
}
}
async function getStyleCode(currentFilename, relativeCssPath) {
const styleFilePath = path.join(path.dirname(currentFilename), relativeCssPath.trim());
let styleCode = await readStyleCodeFromFile(styleFilePath);
styleCode = `<style>\n${styleCode}\n</style>`;
return styleCode;
}
async function readStyleCodeFromFile(styleFilePath) {
try {
const styleFileContent = fs.readFileSync(styleFilePath, 'utf8');
// Convert css notation (for ex \002a) to javascript notation (\u002a)
let styleCode = styleFileContent.replace(/\\([0-9a-fA-F]{4})/g, '\\u$1');
styleCode = await minify.css(styleCode);
return styleCode;
} catch (error) {
console.error(`Error reading style file for ${styleFilePath}: ${error}`);
throw error;
}
}
async function getHtmlCode(currentFilename, relativeHtmlPath, styleCode) {
const htmlFilePath = path.join(path.dirname(currentFilename), relativeHtmlPath.trim());
try {
let htmlCode = fs.readFileSync(htmlFilePath, 'utf8');
htmlCode = inlineImageSource(htmlCode);
htmlCode = await minify.html(htmlCode, {
html: {
minifyCSS: false,
collapseBooleanAttributes: false,
removeAttributeQuotes: false
}
});
const customStyleCode = '<style>${this.customStyle}</style>';
const feedbackHtmlCode = "${this.htmlUnsafe(this.feedbackTemplateHtml ?? '')}";
htmlCode = `
protected override templateUrl: string | null = null;
protected override styleUrls: string[] | null = null;
template = () => { return uHtml\`${styleCode}\n${customStyleCode}\n${htmlCode}\n${feedbackHtmlCode}\`; }`;
return htmlCode;
} catch (error) {
console.error(`Error reading html file for ${currentFilename}: ${error}`);
throw error;
}
}
export function isLineCommented(regExpMatch, code) {
if (!regExpMatch) {
return false;
}
const lineStart = code.substring(0, regExpMatch.index);
const lastNewlineIndex = lineStart.lastIndexOf('\n');
const lineBeforeMatch = lineStart.substring(lastNewlineIndex + 1);
// The line is commented
if (lineBeforeMatch.trim().startsWith('//')) {
return true;
}
// Test if we are in a commented block
let commentedBlock = 0;
for (let i = 0; i < regExpMatch.index; i++) {
if (code[i] === '/' && code[i + 1] === '*') {
commentedBlock++;
i++;
} else if (code[i] === '*' && code[i + 1] === '/') {
commentedBlock--;
i++;
}
}
if (commentedBlock > 0) {
return true;
}
return false;
}
function isStringCommented(line) {
if (!line) {
return false;
}
if (line.trim().startsWith('//')) {
return true;
}
if (line.trim().startsWith('/*')) {
return true;
}
return false;
}
// Regex definitions
export const styleRegex = /(?:(?:public|private|protected)\s+)?(?:override\s+)?styleUrl *= *['"](.*)['"] *;?/g;
export const stylesRegex = /(?:(?:public|private|protected)\s+)?(?:override\s+)?styleUrls *= *\[([\s\S]*?)\] *;?/gs;
export const htmlRegex = /(?:(?:public|private|protected)\s+)?(?:override\s+)?templateUrl *= *['"](.*)['"] *;?/g;
export async function inlineTemplate(filename) {
// Read the file
const code = fs.readFileSync(filename, 'utf8');
const magicString = new MagicString(code);
// We integrate HTML and CSS only if there is an HTML Template
const htmlFounds = code.matchAll(htmlRegex);
for (const htmlFound of htmlFounds) {
if (htmlFound && !isLineCommented(htmlFound, code) && htmlFound[1]) {
let styleCode = '';
// Unique style Url
const styleFounds = code.matchAll(styleRegex);
for (const styleFound of styleFounds) {
if (styleFound && !isLineCommented(styleFound, code) && styleFound[1]) {
styleCode += await getStyleCode(filename, styleFound[1]);
magicString.overwrite(styleFound.index, styleFound.index + styleFound[0].length, '');
}
}
// Multiple style Urls
const stylesFounds = code.matchAll(stylesRegex);
for (const stylesFound of stylesFounds) {
if (stylesFound && !isLineCommented(stylesFound, code) && stylesFound[1]) {
const stylePaths = stylesFound[1]
.split(',')
.map((p) => p.trim().replace(/['"]/g, ''))
.filter((p) => p.length > 0 && !isStringCommented(p));
for (const stylePath of stylePaths) {
styleCode += await getStyleCode(filename, stylePath);
}
magicString.overwrite(stylesFound.index, stylesFound.index + stylesFound[0].length, '');
}
}
// HTML template
const htmlCode = await getHtmlCode(filename, htmlFound[1], styleCode);
magicString.overwrite(htmlFound.index, htmlFound.index + htmlFound[0].length, htmlCode);
// Add missing import (uHtml)
magicString.prepend(`import { html as uHtml } from 'uhtml';\n`);
if (htmlCode.includes('uHtmlFor')) {
// Include uHtmlFor if it is used in the template
magicString.prepend(`import { htmlFor as uHtmlFor } from 'uhtml/keyed';\n`);
}
}
}
// Return the code and the corresponding sourcemap
const sourcemap = magicString.generateMap({
source: filename,
file: filename + '.map',
includeContent: true,
hires: true
});
return {
code: magicString.toString(),
map: sourcemap.toString()
};
}
export async function getCustomStylesAsText(cssOverrides, projectRootPath) {
const textCssOverrides = {};
for (const [componentName, cssFile] of Object.entries(cssOverrides)) {
const styleFilePath = path.join(projectRootPath, cssFile);
const styleCode = await readStyleCodeFromFile(styleFilePath);
textCssOverrides[componentName] = styleCode;
}
return textCssOverrides;
}
export const customStyleRegex = /initializeGeoGirafeCustomStyles\(\) *;/g;
export function inlineCustomStyles(filename, cssOverrides, code) {
const magicString = new MagicString(code);
// We integrate HTML and CSS only if there is an HTML Template
const founds = code.matchAll(customStyleRegex);
for (const found of founds) {
if (found && !isLineCommented(found, code)) {
const overridesAsCode = `initializeGeoGirafeCustomStyles(${JSON.stringify(cssOverrides)});`;
magicString.overwrite(found.index, found.index + found[0].length, overridesAsCode);
}
}
const sourcemap = magicString.generateMap({
source: filename,
file: filename + '.map',
includeContent: true,
hires: true
});
return {
code: magicString.toString(),
map: sourcemap.toString()
};
}
export function extractDefaultExportTypeName(code) {
const regex = /(?:^|\n)\s*export\s+default\s+(interface|type|enum)\s+(\w+)/;
const match = code.match(regex);
if (match) {
return match[2];
}
return null;
}
export function extractDefaultExportValueName(code) {
const regex = /(?:^|\n)\s*export\s+default\s+(?:async\s+)?(?:abstract\s+)?(class|function|const)\s+(\w+)/;
const match = code.match(regex);
if (match) {
return match[2];
}
return null;
}
export function extractDefaultGlobalExportName(code) {
let regex = /(?:^|\n)\s*export\s+default\s+(?!async|abstract|class|function|const|interface|type|enum\b)(\w+)/;
let match = code.match(regex);
if (match) {
const name = match[1];
regex = new RegExp(`\\b(class|function|const|interface|type|enum)\\s+${name}\\b`);
match = code.match(regex);
if (!match) {
throw new Error(`Cannot export : Unable to identify the object type of ${name}`);
}
const objectType = match[0].split(' ')[0].trim();
return {
objectType: objectType,
objectName: name
};
}
// No default export found
return null;
}
export function extractNotDefaultExportTypeNames(code) {
const regex = /(?:^|\n)\s*export\s+(?!default\b)(interface|type|enum)\s+(\w+)/g;
const exportNames = [];
const matches = code.matchAll(regex);
for (const match of matches) {
exportNames.push(match[2]);
}
return exportNames;
}
export function extractNotDefaultExportValueNames(code) {
const exportNames = [];
let regex = /(?:^|\n)\s*export\s+(?!default\b)(?:async\s+)?(?:abstract\s+)?(class|function|const)\s+(\w+)/g;
let matches = code.matchAll(regex);
for (const match of matches) {
exportNames.push(match[2]);
}
regex = /(?:^|\n)\s*export\s+(?!default\b)(?!abstract|async|class|function|const|interface|type|enum\b)(\w+)/g;
matches = code.matchAll(regex);
for (const match of matches) {
exportNames.push(match[1]);
}
return exportNames;
}
export function appendImportExtension(code, extension) {
if (!extension.startsWith('.')) {
extension = `.${extension}`;
}
// Replace all relative imports to add the extension
let importRegex = /from\s+['"](\.[^'"]*)['"]/g;
let updatedCode = appendExtensionToMatchingImports(code, importRegex, extension);
// Replace all lib imports to add the extension
// Except:
// - uhtml/keyed
// - packages starting with @
importRegex = /from\s+['"]((?!(?:uhtml\/keyed)['"])([^.@'"]*\/[^'"]*))['"]/g;
updatedCode = appendExtensionToMatchingImports(updatedCode, importRegex, extension);
return updatedCode;
}
function appendExtensionToMatchingImports(code, importRegex, extension) {
const updatedCode = code.replaceAll(importRegex, (match, importPath) => {
if (path.extname(importPath)) {
return match;
}
// Add the extension to the import path
const newImportPath = importPath + extension;
return match.replace(importPath, newImportPath);
});
return updatedCode;
}
const imgRegex = /<img\b[^>]*\bsrc\s*=\s*["']([^"']+)["'][^>]*\/?>/gis;
function inlineImageSource(htmlCode) {
const imagesFound = htmlCode.matchAll(imgRegex);
for (const imageFound of imagesFound) {
const imageSrc = imageFound[1];
if (imageSrc.startsWith('http') || !imageSrc.endsWith('.svg?girafeInline')) {
// We do not want to inline the images if they have an absolute path, if they are not svg images
// And we only want to inline them if they have the tag '?girafeInline'
continue;
}
const imagePath = path.join('./src/assets', imageSrc.trim().replace('?girafeInline', ''));
const imageContent = fs.readFileSync(imagePath, 'utf8');
htmlCode = htmlCode.replace(
imageFound[1],
'data:image/svg+xml;base64,' + Buffer.from(imageContent).toString('base64')
);
}
return htmlCode;
}