UNPKG

@geogirafe/lib-geoportal

Version:

GeoGirafe is a flexible application to build online geoportals.

403 lines (349 loc) 13 kB
// 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; }