require('make-promises-safe') const argv = require('minimist')(process.argv.slice(2)) const path = require('path') const i18n = require('./lib/i18n') const express = require('express') const lobars = require('lobars') // Middleware const hbs = require('express-hbs') const useragent = require('express-useragent') const compression = require('compression') const slashes = require('connect-slashes') const browsersync = require('./middleware/browsersync') const requestLanguage = require('express-request-language') const cookieParser = require('cookie-parser') const sass = require('./middleware/sass') const helmet = require('helmet') const langResolver = require('./middleware/lang-resolver') const contextBuilder = require('./middleware/context-builder') const getOcticons = require('./middleware/register-octicons') const feedback = require('./middleware/feedback') const port = Number(process.env.PORT) || argv.p || argv.port || 5000 const app = express() const appImgDir = path.resolve(require.resolve('electron-apps'), '..', 'apps') const isProduction = process.env.NODE_ENV === 'production' const staticSettings = { redirect: false, maxAge: isProduction ? 31557600000 : 0, } // Handlebars Templates hbs.registerHelper(lobars) /** * Handlebars helper that accepts options from the `{{octicon}}` tag, * parses with `getOcticons()` function and returns this to user. * * @param {string[]} data The data of hbs helper. * @param {void} cb Async callback. */ hbs.registerAsyncHelper('octicon', async (data, cb) => { const { name, className, ariaLabel, width, height } = data.hash if (data.hash.class) { return console.error( `ERROR(Octicons Helper): Use 'className' instead of 'class'.` ) } if (name === undefined) { return console.error( 'ERROR(Octicons Helper): Name is required field in octicon helper.' ) } const htmlSVG = await getOcticons(name, className, width, height, ariaLabel) return cb(new hbs.SafeString(htmlSVG)) }) app.engine( 'hbs', hbs.express4({ defaultLayout: path.join(__dirname, '/views/layouts/main.hbs'), extname: '.hbs', layoutsDir: path.join(__dirname, '/views/layouts'), partialsDir: path.join(__dirname, '/views/partials'), onCompile: function (exhbs, source, filename) { var options = { preventIndent: true } return exhbs.handlebars.compile(source, options) }, }) ) // Middleware app.set('view engine', 'hbs') app.set('views', path.join(__dirname, '/views')) app.use(compression()) app.use( helmet({ contentSecurityPolicy: false, referrerPolicy: false, }) ) // Helper to generate the redirects to the right document in the new docs paths hbs.registerHelper('new-docs', (currentPage) => { // This particular page is the root for the docs in the new site if (!currentPage || currentPage.endsWith('tutorial/introduction')) { return '/docs/latest/' } else { return currentPage.replace('docs/', 'docs/latest/') } }) /** * This helper "transforms" a locale in the form of xx-XX to the 2 char code * used by Crowdin. */ hbs.registerHelper('to2CharLocale', (locale) => { // Because of the current supported languages, we do not have any edge cases const [language] = locale.toLowerCase().split('-') if (locale === 'en') { return '' } else { return language } }) hbs.registerHelper('replace', function (options) { const string = options.fn(this) return string.replace( new RegExp(`\\bhttps:\/\/discord\.gg/electron\\b`, 'g'), 'https://discord.gg/electronjs' ) }) if (isProduction) { const jsManifest = require(path.join( __dirname, 'precompiled', 'scripts', 'manifest.json' )) const cssManifest = require(path.join( __dirname, 'precompiled', 'styles', 'manifest.json' )) const imagesManifest = require(path.join( __dirname, 'precompiled', 'images', 'manifest.json' )) hbs.registerHelper('static-asset', (type, ...parts) => { // `parts` should be at minimum [name, function] // but it could also be [part1, part2, part3, function ] // if we want to link to dynamic images const name = parts.length === 2 ? parts[0] : parts.slice(0, -1).join('') if (type === 'js') { return jsManifest[name] || 'unknown.name' } if (type === 'css') { return cssManifest[name] || 'unknown.name' } if (type === 'image') { return imagesManifest[name] || 'unknown.name' } return 'unknown.type' }) } else { hbs.registerHelper('static-asset', (type, ...parts) => { const name = parts.length === 2 ? parts[0] : parts.slice(0, -1).join('') if (type === 'js') { return `/scripts/${name}` } if (type === 'css') { return `/styles/${name}` } if (type === 'image') { return `/images${name}` } return 'unknown.type' }) } if (isProduction) { console.log('Production app detected; serving JS and CSS from disk') app.use(express.static(path.join(__dirname, 'precompiled'), staticSettings)) } else if (process.env.NODE_ENV === 'development') { console.log('Dev app detected; compiling JS and CSS in memory') app.use(sass()) const webpack = require('./middleware/webpack') app.use(webpack()) } else { app.use(sass()) } app.get('/service-worker.js', (req, res) => res.sendFile(path.resolve(__dirname, 'scripts', 'service-worker.js')) ) app.use(cookieParser()) app.use( requestLanguage({ languages: Object.keys(i18n.locales), cookie: { name: 'language', options: { maxAge: 30 * 24 * 60 * 60 * 1000 }, url: '/languages/{language}', }, }) ) app.use(express.static(path.join(__dirname, 'public'), staticSettings)) app.use('/images/app-img', express.static(appImgDir, staticSettings)) app.use(slashes(false)) app.use(langResolver) app.use(contextBuilder) app.use(browsersync()) app.use(useragent.express()) // Routes const routes = require('./routes') app.get('/', routes.home) app.get('/app/:slug', (req, res) => res.redirect(`/apps/${req.params.slug}`)) app.get('/apps', routes.apps.index) app.get('/apps/:slug', routes.apps.show) app.use('/blacklivesmatter', routes.blacklivesmatter) app.get('/community', routes.community) app.get('/contact', (req, res) => res.redirect(301, '/community')) app.use('/crowdin', routes.languages.proxy) app.get('/devtron', routes.devtron) app.use('/docs', feedback) // The documentation is served elsewhere, see electron/electronjs.org-new // Moving all users landing directly in an old "docs" route to the new one app.get('/docs', (req, res) => res.redirect(301, '/docs/latest')) app.get('/docs/*', (req, res, next) => { const route = req.params[0] if (!route.includes('latest')) { res.redirect(301, `/docs/latest/${route}`) } else { next() } }) // The requests to the following docs routes should be intercepted by Fastly and never reach app.get('/docs/versions', (req, res) => res.redirect(301, '/releases/stable')) app.get('/docs/:category', routes.docs.category) app.get('/docs/api/structures', routes.docs.structures) app.get('/docs/*/history', routes.docs.history) app.get('/docs/:category/*', routes.docs.show) app.use('/donors', routes.donors) app.get('/fiddle', routes.fiddle) app.get('/governance', routes.governance.index) app.use('/headers/*', routes.headers) app.get('/languages', routes.languages.index) app.get('/releases', (req, res) => res.redirect(301, '/releases/stable')) app.get('/releases/stable', routes.releases.index('stable')) app.get('/releases/beta', routes.releases.index('beta')) app.get('/releases/alpha', routes.releases.index('alpha')) app.get('/releases/nightly', routes.releases.index('nightly')) app.get('/releases.json', routes.feed.releases) app.get('/releases.xml', routes.feed.releases) app.get('/search/:searchIn*?*', (req, res) => res.redirect(req.query.q ? `/?query=${req.query.q}` : `/`) ) app.get('/userland', routes.userland.index) app.get('/userland/*', routes.userland.show) // External redirects app.get('/issues', (req, res) => res.redirect(301, 'https://github.com/electron/electronjs.org/issues') ) app.get('/issues/new', (req, res) => res.redirect(301, 'https://github.com/electron/electronjs.org/issues/new') ) app.get('/maintainers/join', (req, res) => res.redirect('https://airtable.com/shrNrpaXIJiRZj6bS') ) app.get('/pulls', (req, res) => res.redirect(301, 'https://github.com/electron/electronjs.org/pulls') ) // Redirected old paths app.get('/awesome', (req, res) => res.redirect('/community')) app.get('/docs/v0*', (req, res) => res.redirect(req.path.replace(/^\/docs\/v0\.\d+\.\d+/gi, '/docs')) ) app.get('/docs/api/breaking-changes', (req, res) => res.redirect(301, '/docs/breaking-changes') ) app.get('/docs/tutorial/faq', (req, res) => res.redirect('/docs/faq')) app.get('/docs/tutorial/first-app', (_, res) => { res.redirect(301, '/docs/tutorial/quick-start') }) app.get('/docs/tutorial/application-architecture', (_, res) => { res.redirect(301, '/docs/tutorial/quick-start') }) // Generic 404 handler app.use(routes._404) if (!module.parent) { app.listen(port, () => { console.log(`app running on http://localhost:${port}`) if (isProduction) { console.log(`If you're developing, you probably want \`yarn dev\`\n\n`) } }) } module.exports = app