Spaces:
Sleeping
Sleeping
| /* | |
| * Copyright (C) 2024-present Puter Technologies Inc. | |
| * | |
| * This file is part of Puter. | |
| * | |
| * Puter is free software: you can redistribute it and/or modify | |
| * it under the terms of the GNU Affero General Public License as published | |
| * by the Free Software Foundation, either version 3 of the License, or | |
| * (at your option) any later version. | |
| * | |
| * This program is distributed in the hope that it will be useful, | |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| * GNU Affero General Public License for more details. | |
| * | |
| * You should have received a copy of the GNU Affero General Public License | |
| * along with this program. If not, see <https://www.gnu.org/licenses/>. | |
| */ | |
| import { encode } from 'html-entities'; | |
| import fs from 'fs'; | |
| import path from 'path'; | |
| import webpack from 'webpack'; | |
| import CleanCSS from 'clean-css'; | |
| import uglifyjs from 'uglify-js'; | |
| import { lib_paths, css_paths, js_paths } from './src/static-assets.js'; | |
| import { fileURLToPath } from 'url'; | |
| import BaseConfig from './webpack/BaseConfig.cjs'; | |
| // Polyfill __dirname, which doesn't exist in modules mode | |
| const __dirname = path.dirname(fileURLToPath(import.meta.url)); | |
| /** | |
| * Builds the application by performing various tasks such as cleaning the distribution directory, | |
| * merging and minifying JavaScript and CSS files, bundling GUI core files, handling images, | |
| * and preparing the final distribution package. The process involves concatenating library | |
| * scripts, optimizing them for production, and copying essential assets to the distribution | |
| * directory. The function also supports a verbose mode for logging detailed information during | |
| * the build process. | |
| * | |
| * @param {Object} [options] - Optional parameters to customize the build process. | |
| * @param {boolean} [options.verbose=false] - Specifies whether to log detailed information during the build. | |
| * | |
| * @async | |
| * @returns {Promise<void>} A promise that resolves when the build process is complete. | |
| * | |
| * @example | |
| * build({ verbose: true }).then(() => { | |
| * console.log('Build process completed successfully.'); | |
| * }).catch(error => { | |
| * console.error('Build process failed:', error); | |
| * }); | |
| */ | |
| async function build (options) { | |
| const PUTER_ENV = process.env.PUTER_ENV || 'prod'; | |
| // ----------------------------------------------- | |
| // Delete ./dist/ directory if it exists and create a new one | |
| // ----------------------------------------------- | |
| if ( fs.existsSync(path.join(__dirname, 'dist')) ) { | |
| fs.rmSync(path.join(__dirname, 'dist'), { recursive: true }); | |
| } | |
| fs.mkdirSync(path.join(__dirname, 'dist')); | |
| // ----------------------------------------------- | |
| // Concat/merge the JS libraries and save them to ./dist/libs.js | |
| // ----------------------------------------------- | |
| let js = ''; | |
| for ( let i = 0; i < lib_paths.length; i++ ) { | |
| const file = path.join(__dirname, 'src', lib_paths[i]); | |
| // js | |
| if ( file.endsWith('.js') && !file.endsWith('.min.js') ) { | |
| let minified_code = await uglifyjs.minify(fs.readFileSync(file).toString(), { mangle: false }); | |
| if ( minified_code && minified_code.code ) { | |
| js += minified_code.code; | |
| if ( options?.verbose ) | |
| { | |
| console.log('minified: ', file); | |
| } | |
| } | |
| } else { | |
| js += fs.readFileSync(file); | |
| if ( options?.verbose ) | |
| { | |
| console.log('skipped minification: ', file); | |
| } | |
| } | |
| js += '\n\n\n'; | |
| } | |
| // ----------------------------------------------- | |
| // Combine all images into a single js file | |
| // ----------------------------------------------- | |
| let icons = 'window.icons = [];\n\n\n'; | |
| fs.readdirSync(path.join(__dirname, 'src/icons')).forEach(file => { | |
| // skip dotfiles | |
| if ( file.startsWith('.') ) | |
| { | |
| return; | |
| } | |
| // load image | |
| let buff = new Buffer.from(fs.readFileSync(`${path.join(__dirname, 'src/icons') }/${ file}`)); | |
| // convert to base64 | |
| let base64data = buff.toString('base64'); | |
| // add to `window.icons` | |
| if ( file.endsWith('.png') ) | |
| { | |
| icons += `window.icons['${file}'] = "data:image/png;base64,${base64data}";\n`; | |
| } | |
| else if ( file.endsWith('.svg') ) | |
| { | |
| icons += `window.icons['${file}'] = "data:image/svg+xml;base64,${base64data}";\n`; | |
| } | |
| }); | |
| // ----------------------------------------------- | |
| // concat/merge the CSS files and save them to ./dist/bundle.min.css | |
| // ----------------------------------------------- | |
| let css = ''; | |
| for ( let i = 0; i < css_paths.length; i++ ) { | |
| let fullpath = path.join(__dirname, 'src', css_paths[i]); | |
| // minify CSS files if not already minified, then concatenate | |
| if ( css_paths[i].endsWith('.css') && !css_paths[i].endsWith('.min.css') ) { | |
| const minified_css = new CleanCSS({}).minify(fs.readFileSync(fullpath).toString()); | |
| css += minified_css.styles; | |
| } | |
| // otherwise, just concatenate the file | |
| else | |
| { | |
| css += fs.readFileSync(path.join(__dirname, 'src', css_paths[i])); | |
| } | |
| // add newlines between files | |
| css += '\n\n\n'; | |
| } | |
| fs.writeFileSync(path.join(__dirname, 'dist', 'bundle.min.css'), css); | |
| // ----------------------------------------------- | |
| // Bundle GUI core and merge all files into a single JS file | |
| // ----------------------------------------------- | |
| let main_array = []; | |
| for ( let i = 0; i < js_paths.length; i++ ) { | |
| main_array.push(path.join(__dirname, 'src', js_paths[i])); | |
| } | |
| const webpack_opts = { | |
| ...(await BaseConfig({ | |
| ...options, | |
| env: PUTER_ENV, | |
| })), | |
| mode: 'production', | |
| optimization: { | |
| minimize: true, | |
| }, | |
| }; | |
| console.log('webpack opts', webpack_opts); | |
| await webpack(webpack_opts, (err, stats) => { | |
| if ( err ) { | |
| throw err; | |
| // console.error(err); | |
| // return; | |
| } | |
| //if(options?.verbose) | |
| console.log(stats.toString()); | |
| // write to ./dist/bundle.min.js | |
| // fs.writeFileSync(path.join(__dirname, 'dist', 'bundle.min.js'), fs.readFileSync(path.join(__dirname, 'dist', 'main.js'))); | |
| // remove ./dist/main.js | |
| // fs.unlinkSync(path.join(__dirname, 'dist', 'main.js')); | |
| }); | |
| // Copy index.js to dist/gui.js | |
| // Prepend `window.gui_env="prod";` to `./dist/gui.js` | |
| fs.writeFileSync(path.join(__dirname, 'dist', 'gui.js'), | |
| `window.gui_env="${PUTER_ENV}"; \n\n${ fs.readFileSync(path.join(__dirname, 'src', 'index.js'))}`); | |
| const copy_these = [ | |
| 'images', | |
| 'fonts', | |
| 'favicons', | |
| 'browserconfig.xml', | |
| 'manifest.json', | |
| 'favicon.ico', | |
| 'security.txt', | |
| ]; | |
| const recursive_copy = (src, dest) => { | |
| const stat = fs.statSync(src); | |
| if ( stat.isDirectory() ) { | |
| if ( ! fs.existsSync(dest) ) fs.mkdirSync(dest); | |
| const files = fs.readdirSync(src); | |
| for ( const file of files ) { | |
| recursive_copy(path.join(src, file), path.join(dest, file)); | |
| } | |
| } else { | |
| fs.copyFileSync(src, dest); | |
| } | |
| }; | |
| for ( const to_copy of copy_these ) { | |
| recursive_copy(path.join(__dirname, 'src', to_copy), path.join(__dirname, 'dist', to_copy)); | |
| } | |
| } | |
| /** | |
| * Generates the HTML content for the GUI based on the specified configuration options. The function | |
| * creates a new HTML document with the specified title, description, and other metadata. The function | |
| * also includes the necessary CSS and JavaScript files, as well as the required meta tags for social | |
| * media sharing and search engine optimization. The function is designed to be used in development | |
| * environments to generate the HTML content for the GUI. | |
| * | |
| * @param {Object} options - The configuration options for the GUI. | |
| * @param {string} options.env - The environment in which the GUI is running (e.g., "dev" or "prod"). | |
| * @param {string} options.api_origin - The origin of the API server. | |
| * @param {string} options.title - The title of the GUI. | |
| * @param {string} options.company - The name of the company or organization. | |
| * @param {string} options.description - The description of the GUI. | |
| * @param {string} options.app_description - The description of the application. | |
| * @param {string} options.short_description - The short description of the GUI. | |
| * @param {string} options.origin - The origin of the GUI. | |
| * @param {string} options.social_card - The URL of the social media card image. | |
| * @returns {string} The HTML content for the GUI based on the specified configuration options. | |
| * | |
| */ | |
| function generateDevHtml (options) { | |
| let start_t = Date.now(); | |
| // final html string | |
| let h = ''; | |
| h += '<!DOCTYPE html>'; | |
| h += '<html lang="en">'; | |
| h += '<head>'; | |
| h += `<title>${encode((options.title))}</title>`; | |
| h += `<meta name="author" content="${encode(options.company)}">`; | |
| // description | |
| let description = options.description; | |
| // if app_description is set, use that instead | |
| if ( options.app_description ) { | |
| description = options.app_description; | |
| } | |
| // if no app_description is set, use short_description if set | |
| else if ( options.short_description ) { | |
| description = options.short_description; | |
| } | |
| // description | |
| h += `<meta name="description" content="${encode((description).replace(/\n/g, ' '))}">`; | |
| // facebook domain verification | |
| h += '<meta name="facebook-domain-verification" content="e29w3hjbnnnypf4kzk2cewcdaxym1y" />'; | |
| // canonical url | |
| h += `<link rel="canonical" href="${options.origin}">`; | |
| // DEV: load every CSS file individually | |
| if ( options.env === 'dev' ) { | |
| for ( let i = 0; i < css_paths.length; i++ ) { | |
| h += `<link rel="stylesheet" href="${css_paths[i]}">`; | |
| } | |
| } | |
| // Facebook meta tags | |
| h += `<meta property="og:url" content="https://${options.domain}">`; | |
| h += '<meta property="og:type" content="website">'; | |
| h += `<meta property="og:title" content="${encode(options.title)}">`; | |
| h += `<meta property="og:description" content="${encode((options.short_description).replace(/\n/g, ' '))}">`; | |
| h += `<meta property="og:image" content="${options.social_card}">`; | |
| // Twitter meta tags | |
| h += '<meta name="twitter:card" content="summary_large_image">'; | |
| h += `<meta property="twitter:domain" content="${options.domain}">`; | |
| h += `<meta property="twitter:url" content="https://${options.domain}">`; | |
| h += `<meta name="twitter:title" content="${encode(options.title)}">`; | |
| h += `<meta name="twitter:description" content="${encode((options.short_description).replace(/\n/g, ' '))}">`; | |
| h += `<meta name="twitter:image" content="${options.social_card}">`; | |
| // favicons | |
| h += ` | |
| <link rel="apple-touch-icon" sizes="57x57" href="/favicons/apple-icon-57x57.png"> | |
| <link rel="apple-touch-icon" sizes="60x60" href="/favicons/apple-icon-60x60.png"> | |
| <link rel="apple-touch-icon" sizes="72x72" href="/favicons/apple-icon-72x72.png"> | |
| <link rel="apple-touch-icon" sizes="76x76" href="/favicons/apple-icon-76x76.png"> | |
| <link rel="apple-touch-icon" sizes="114x114" href="/favicons/apple-icon-114x114.png"> | |
| <link rel="apple-touch-icon" sizes="120x120" href="/favicons/apple-icon-120x120.png"> | |
| <link rel="apple-touch-icon" sizes="144x144" href="/favicons/apple-icon-144x144.png"> | |
| <link rel="apple-touch-icon" sizes="152x152" href="/favicons/apple-icon-152x152.png"> | |
| <link rel="apple-touch-icon" sizes="180x180" href="/favicons/apple-icon-180x180.png"> | |
| <link rel="icon" type="image/png" sizes="192x192" href="/favicons/android-icon-192x192.png"> | |
| <link rel="icon" type="image/png" sizes="32x32" href="/favicons/favicon-32x32.png"> | |
| <link rel="icon" type="image/png" sizes="96x96" href="/favicons/favicon-96x96.png"> | |
| <link rel="icon" type="image/png" sizes="16x16" href="/favicons/favicon-16x16.png"> | |
| <link rel="manifest" href="/manifest.json"> | |
| <meta name="msapplication-TileColor" content="#ffffff"> | |
| <meta name="msapplication-TileImage" content="/favicons/ms-icon-144x144.png"> | |
| <meta name="theme-color" content="#ffffff">`; | |
| // preload images when applicable | |
| h += '<link rel="preload" as="image" href="./images/wallpaper.webp">'; | |
| h += '</head>'; | |
| h += '<body>'; | |
| // To indicate that the GUI is running to any 3rd-party scripts that may be running on the page | |
| // specifically, the `puter.js` script | |
| // This line is also present verbatim in `src/index.js` for production builds | |
| h += '<script>window.puter_gui_enabled = true;</script>'; | |
| // DEV: load every JS library individually | |
| if ( options.env === 'dev' ) { | |
| for ( let i = 0; i < lib_paths.length; i++ ) { | |
| h += `<script src="${lib_paths[i]}"></script>`; | |
| } | |
| } | |
| // load images and icons as base64 for performance | |
| if ( options.env === 'dev' ) { | |
| h += '<script>'; | |
| h += 'window.icons = {};'; | |
| fs.readdirSync(path.join(__dirname, 'src/icons')).forEach(file => { | |
| // skip dotfiles | |
| if ( file.startsWith('.') ) | |
| { | |
| return; | |
| } | |
| // load image | |
| let buff = new Buffer.from(fs.readFileSync(`${path.join(__dirname, 'src/icons') }/${ file}`)); | |
| // convert to base64 | |
| let base64data = buff.toString('base64'); | |
| // add to `window.icons` | |
| if ( file.endsWith('.png') ) | |
| { | |
| h += `window.icons['${file}'] = "data:image/png;base64,${base64data}";\n`; | |
| } | |
| else if ( file.endsWith('.svg') ) | |
| { | |
| h += `window.icons['${file}'] = "data:image/svg+xml;base64,${base64data}";\n`; | |
| } | |
| }); | |
| h += '</script>'; | |
| } | |
| // PROD: gui.js | |
| if ( options.env === 'prod' ) { | |
| h += '<script src="/dist/gui.js"></script>'; | |
| } | |
| // DEV: load every JS file individually | |
| else { | |
| for ( let i = 0; i < js_paths.length; i++ ) { | |
| h += `<script type="module" src="${js_paths[i]}"></script>`; | |
| } | |
| // load GUI | |
| h += '<script type="module" src="/index.js"></script>'; | |
| } | |
| // ---------------------------------------- | |
| // Initialize GUI with config options | |
| // ---------------------------------------- | |
| h += ` | |
| <script type="text/javascript"> | |
| window.addEventListener('load', function() {`; | |
| h += 'gui()'; | |
| h += `}); | |
| </script>`; | |
| h += `</body> | |
| </html>`; | |
| console.log(`/index.js: ${ (Date.now() - start_t) / 1000}`); | |
| return h; | |
| } | |
| // export | |
| export { generateDevHtml, build }; | |