diff --git a/.gitignore b/.gitignore index b6a211def23b..dbe454cae87e 100644 --- a/.gitignore +++ b/.gitignore @@ -122,6 +122,9 @@ test/functional/*.png # Built asset files /ghost/core/core/built /ghost/core/core/frontend/public/ghost.min.css +/ghost/core/core/frontend/public/comment-counts.min.js +/ghost/core/core/frontend/public/member-attribution.min.js +/ghost/core/core/frontend/public/admin-auth/admin-auth.min.js # Caddyfile - for local development with ssl + caddy Caddyfile diff --git a/ghost/core/core/bridge.js b/ghost/core/core/bridge.js index 0ace590f5c01..e13f61e0951d 100644 --- a/ghost/core/core/bridge.js +++ b/ghost/core/core/bridge.js @@ -16,7 +16,7 @@ const logging = require('@tryghost/logging'); const tpl = require('@tryghost/tpl'); const themeEngine = require('./frontend/services/theme-engine'); const appService = require('./frontend/services/apps'); -const {adminAuthAssets, cardAssets,commentCountsAssets, memberAttributionAssets} = require('./frontend/services/assets-minification'); +const {cardAssets} = require('./frontend/services/assets-minification'); const routerManager = require('./frontend/services/routing').routerManager; const settingsCache = require('./shared/settings-cache'); const urlService = require('./server/services/url'); @@ -51,10 +51,6 @@ class Bridge { return themeEngine.getActive(); } - ensureAdminAuthAssetsMiddleware() { - return adminAuthAssets.serveMiddleware(); - } - async activateTheme(loadedTheme, checkedTheme) { let settings = { locale: settingsCache.get('locale') @@ -69,12 +65,6 @@ class Bridge { const cardAssetConfig = this.getCardAssetConfig(); debug('reload card assets config', cardAssetConfig); cardAssets.invalidate(cardAssetConfig); - - // TODO: is this in the right place? - // rebuild asset files - commentCountsAssets.invalidate(); - adminAuthAssets.invalidate(); - memberAttributionAssets.invalidate(); } catch (err) { logging.error(new errors.InternalServerError({ message: tpl(messages.activateFailed, {theme: loadedTheme.name}), diff --git a/ghost/core/core/frontend/src/admin-auth/index.html b/ghost/core/core/frontend/public/admin-auth/index.html similarity index 100% rename from ghost/core/core/frontend/src/admin-auth/index.html rename to ghost/core/core/frontend/public/admin-auth/index.html diff --git a/ghost/core/core/frontend/src/admin-auth/message-handler.js b/ghost/core/core/frontend/public/admin-auth/message-handler.js similarity index 100% rename from ghost/core/core/frontend/src/admin-auth/message-handler.js rename to ghost/core/core/frontend/public/admin-auth/message-handler.js diff --git a/ghost/core/core/frontend/src/comment-counts/js/comment-counts.js b/ghost/core/core/frontend/public/comment-counts.js similarity index 100% rename from ghost/core/core/frontend/src/comment-counts/js/comment-counts.js rename to ghost/core/core/frontend/public/comment-counts.js diff --git a/ghost/core/core/frontend/src/member-attribution/member-attribution.js b/ghost/core/core/frontend/public/member-attribution.js similarity index 100% rename from ghost/core/core/frontend/src/member-attribution/member-attribution.js rename to ghost/core/core/frontend/public/member-attribution.js diff --git a/ghost/core/core/frontend/services/assets-minification/AdminAuthAssets.js b/ghost/core/core/frontend/services/assets-minification/AdminAuthAssets.js deleted file mode 100644 index df9c52e5fa61..000000000000 --- a/ghost/core/core/frontend/services/assets-minification/AdminAuthAssets.js +++ /dev/null @@ -1,68 +0,0 @@ -// const debug = require('@tryghost/debug')('comments-counts-assets'); -const Minifier = require('@tryghost/minifier'); -const path = require('path'); -const fs = require('fs'); -const logging = require('@tryghost/logging'); -const config = require('../../../shared/config'); -const urlUtils = require('../../../shared/url-utils'); -const AssetsMinificationBase = require('./AssetsMinificationBase'); - -module.exports = class AdminAuthAssets extends AssetsMinificationBase { - constructor(options = {}) { - super(options); - - this.src = options.src || path.join(config.get('paths').assetSrc, 'admin-auth'); - /** @private */ - this.dest = options.dest || path.join(config.getContentPath('public'), 'admin-auth'); - - this.minifier = new Minifier({src: this.src, dest: this.dest}); - - try { - // TODO: don't do this synchronously - fs.mkdirSync(this.dest, {recursive: true}); - fs.copyFileSync(path.join(this.src, 'index.html'), path.join(this.dest, 'index.html')); - } catch (error) { - if (error.code === 'EACCES') { - logging.error('Ghost was not able to write admin-auth asset files due to permissions.'); - return; - } - - throw error; - } - } - - /** - * @override - */ - generateGlobs() { - return { - 'admin-auth.min.js': '*.js' - }; - } - - /** - * @private - */ - generateReplacements() { - // Clean the URL, only keep schema, host and port (without trailing slashes or subdirectory) - const url = new URL(urlUtils.getSiteUrl()); - const origin = url.origin; - - return { - // Properly encode the origin - '\'{{SITE_ORIGIN}}\'': JSON.stringify(origin) - }; - } - - /** - * Minify, move into the destination directory, and clear existing asset files. - * - * @override - * @returns {Promise} - */ - async load() { - const globs = this.generateGlobs(); - const replacements = this.generateReplacements(); - await this.minify(globs, {replacements}); - } -}; diff --git a/ghost/core/core/frontend/services/assets-minification/CommentCountsAssets.js b/ghost/core/core/frontend/services/assets-minification/CommentCountsAssets.js deleted file mode 100644 index 422b0b305fc5..000000000000 --- a/ghost/core/core/frontend/services/assets-minification/CommentCountsAssets.js +++ /dev/null @@ -1,33 +0,0 @@ -const Minifier = require('@tryghost/minifier'); -const path = require('path'); -const config = require('../../../shared/config'); -const AssetsMinificationBase = require('./AssetsMinificationBase'); - -module.exports = class CommentCountsAssets extends AssetsMinificationBase { - constructor(options = {}) { - super(options); - - this.src = options.src || path.join(config.get('paths').assetSrc, 'comment-counts'); - this.dest = options.dest || config.getContentPath('public'); - this.minifier = new Minifier({src: this.src, dest: this.dest}); - - this.files = []; - } - - /** - * @override - */ - generateGlobs() { - return { - 'comment-counts.min.js': 'js/*.js' - }; - } - - /** - * @override - */ - async load() { - const globs = this.generateGlobs(); - this.files = await this.minify(globs); - } -}; diff --git a/ghost/core/core/frontend/services/assets-minification/MemberAttributionAssets.js b/ghost/core/core/frontend/services/assets-minification/MemberAttributionAssets.js deleted file mode 100644 index 33c11e937ef2..000000000000 --- a/ghost/core/core/frontend/services/assets-minification/MemberAttributionAssets.js +++ /dev/null @@ -1,45 +0,0 @@ -const Minifier = require('@tryghost/minifier'); -const path = require('path'); -const config = require('../../../shared/config'); -const AssetsMinificationBase = require('./AssetsMinificationBase'); - -module.exports = class MemberAttributionAssets extends AssetsMinificationBase { - constructor(options = {}) { - super(options); - - /** @private */ - this.src = options.src || path.join(config.get('paths').assetSrc, 'member-attribution'); - /** @private */ - this.dest = options.dest || config.getContentPath('public'); - - this.minifier = new Minifier({src: this.src, dest: this.dest}); - } - - /** - * @override - */ - generateGlobs() { - return { - 'member-attribution.min.js': '*.js' - }; - } - - /** - * @private - */ - generateReplacements() { - return {}; - } - - /** - * Minify, move into the destination directory, and clear existing asset files. - * - * @override - * @returns {Promise} - */ - async load() { - const globs = this.generateGlobs(); - const replacements = this.generateReplacements(); - await this.minify(globs, {replacements}); - } -}; diff --git a/ghost/core/core/frontend/services/assets-minification/index.js b/ghost/core/core/frontend/services/assets-minification/index.js index f69e5352fef1..0af4c9c1153e 100644 --- a/ghost/core/core/frontend/services/assets-minification/index.js +++ b/ghost/core/core/frontend/services/assets-minification/index.js @@ -1,16 +1,7 @@ -const AdminAuthAssets = require('./AdminAuthAssets'); const CardAssets = require('./CardAssets'); -const CommentCountsAssets = require('./CommentCountsAssets'); -const MemberAttributionAssets = require('./MemberAttributionAssets'); -const adminAuthAssets = new AdminAuthAssets(); const cardAssets = new CardAssets(); -const commentCountsAssets = new CommentCountsAssets(); -const memberAttributionAssets = new MemberAttributionAssets(); module.exports = { - adminAuthAssets, - cardAssets, - commentCountsAssets, - memberAttributionAssets -}; \ No newline at end of file + cardAssets +}; diff --git a/ghost/core/core/frontend/src/member-attribution/.eslintrc b/ghost/core/core/frontend/src/member-attribution/.eslintrc deleted file mode 100644 index 097425f5c3ed..000000000000 --- a/ghost/core/core/frontend/src/member-attribution/.eslintrc +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../../../.eslintrc.js", - "env": { - "browser": true, - "node": false - }, - "rules": { - "no-console": "off" - } -} diff --git a/ghost/core/core/frontend/web/site.js b/ghost/core/core/frontend/web/site.js index 637e3a8f833e..24043fc9e0b4 100644 --- a/ghost/core/core/frontend/web/site.js +++ b/ghost/core/core/frontend/web/site.js @@ -16,7 +16,7 @@ const membersService = require('../../server/services/members'); const offersService = require('../../server/services/offers'); const customRedirects = require('../../server/services/custom-redirects'); const linkRedirects = require('../../server/services/link-redirection'); -const {cardAssets, commentCountsAssets, memberAttributionAssets} = require('../services/assets-minification'); +const {cardAssets} = require('../services/assets-minification'); const siteRoutes = require('./routes'); const shared = require('../../server/web/shared'); const errorHandler = require('@tryghost/mw-error-handler'); @@ -73,14 +73,34 @@ module.exports = function setupSiteApp(routerConfig) { siteApp.use(mw.servePublicFile('static', 'public/ghost.min.css', 'text/css', config.get('caching:publicAssets:maxAge'))); // Card assets - siteApp.use(cardAssets.serveMiddleware(), mw.servePublicFile('built', 'public/cards.min.css', 'text/css', config.get('caching:publicAssets:maxAge'))); - siteApp.use(cardAssets.serveMiddleware(), mw.servePublicFile('built', 'public/cards.min.js', 'application/javascript', config.get('caching:publicAssets:maxAge'))); + const cardsCssPath = 'public/cards.min.css'; + const cardsJsPath = 'public/cards.min.js'; + siteApp.use( + function serveCardsCSSMiddleware(req, res, next) { + if (req.path === `/${cardsCssPath}`) { + return cardAssets.serveMiddleware()(req, res, next); + } + + return next(); + }, + mw.servePublicFile('built', cardsCssPath, 'text/css', config.get('caching:publicAssets:maxAge')) + ); + siteApp.use( + function serveCardsJSMiddleware(req, res, next) { + if (req.path === `/${cardsJsPath}`) { + return cardAssets.serveMiddleware()(req, res, next); + } + + return next(); + }, + mw.servePublicFile('built', cardsJsPath, 'application/javascript', config.get('caching:publicAssets:maxAge')) + ); // Comment counts - siteApp.use(commentCountsAssets.serveMiddleware(), mw.servePublicFile('built', 'public/comment-counts.min.js', 'application/javascript', config.get('caching:publicAssets:maxAge'))); + siteApp.use(mw.servePublicFile('static', 'public/comment-counts.min.js', 'application/javascript', config.get('caching:publicAssets:maxAge'))); // Member attribution - siteApp.use(memberAttributionAssets.serveMiddleware(), mw.servePublicFile('built', 'public/member-attribution.min.js', 'application/javascript', config.get('caching:publicAssets:maxAge'))); + siteApp.use(mw.servePublicFile('static', 'public/member-attribution.min.js', 'application/javascript', config.get('caching:publicAssets:maxAge'))); // Serve site images using the storage adapter siteApp.use(STATIC_IMAGE_URL_PREFIX, mw.handleImageSizes, storage.getStorage('images').serve()); diff --git a/ghost/core/core/server/web/admin/app.js b/ghost/core/core/server/web/admin/app.js index 090e3eb16d13..837f872be567 100644 --- a/ghost/core/core/server/web/admin/app.js +++ b/ghost/core/core/server/web/admin/app.js @@ -9,7 +9,7 @@ const shared = require('../shared'); const errorHandler = require('@tryghost/mw-error-handler'); const sentry = require('../../../shared/sentry'); const redirectAdminUrls = require('./middleware/redirect-admin-urls'); -const bridge = require('../../../bridge'); +const createServeAuthFrameFileMw = require('./middleware/serve-admin-auth-frame-file'); /** * @@ -39,7 +39,7 @@ module.exports = function setupAdminApp() { // request to the Admin API /users/me/ endpoint to check if the user is logged in. // // Used by comments-ui to add moderation options to front-end comments when logged in. - adminApp.use('/auth-frame', bridge.ensureAdminAuthAssetsMiddleware(), function authFrameMw(req, res, next) { + adminApp.use('/auth-frame', function authFrameMw(req, res, next) { // only render content when we have an Admin session cookie, // otherwise return a 204 to avoid JS and API requests being made unnecessarily try { @@ -52,9 +52,7 @@ module.exports = function setupAdminApp() { } catch (err) { next(err); } - }, serveStatic( - path.join(config.getContentPath('public'), 'admin-auth') - )); + }, createServeAuthFrameFileMw(config, urlUtils)); // Ember CLI's live-reload script if (config.get('env') === 'development') { diff --git a/ghost/core/core/server/web/admin/middleware/serve-admin-auth-frame-file.js b/ghost/core/core/server/web/admin/middleware/serve-admin-auth-frame-file.js new file mode 100644 index 000000000000..a80ff474ca13 --- /dev/null +++ b/ghost/core/core/server/web/admin/middleware/serve-admin-auth-frame-file.js @@ -0,0 +1,34 @@ +const path = require('node:path'); +const fs = require('node:fs/promises'); + +function createServeAuthFrameFileMw(config, urlUtils) { + const placeholders = { + '{{SITE_ORIGIN}}': new URL(urlUtils.getSiteUrl()).origin + }; + + return function serveAuthFrameFileMw(req, res, next) { + const filename = path.parse(req.url).base; + let basePath = path.join(config.get('paths').publicFilePath, 'admin-auth'); + let filePath; + + if (filename === '') { + filePath = path.join(basePath, 'index.html'); + } else { + filePath = path.join(basePath, filename); + } + + fs.readFile(filePath).then((data) => { + let dataString = data.toString(); + + for (const [key, value] of Object.entries(placeholders)) { + dataString = dataString.replace(key, value); + } + + res.end(dataString); + }).catch((err) => { + next(err); // TODO + }); + }; +} + +module.exports = createServeAuthFrameFileMw; diff --git a/ghost/core/core/shared/config/helpers.js b/ghost/core/core/shared/config/helpers.js index 2644ae4ee399..f5b117e415a6 100644 --- a/ghost/core/core/shared/config/helpers.js +++ b/ghost/core/core/shared/config/helpers.js @@ -1,5 +1,3 @@ -const crypto = require('crypto'); -const os = require('os'); const path = require('path'); const {URL} = require('url'); @@ -66,24 +64,6 @@ const isPrivacyDisabled = function isPrivacyDisabled(privacyFlag) { return this.get('privacy')[privacyFlag] === false; }; -/** @type {string|null} */ -let processTmpDirPath = null; - -/** - * Get a tmp dir path for the current process - * - * @returns {string} - tmp dir path for the current process - */ -function getProcessTmpDirPath() { - // Memoize the computed path to avoid re-computing it on each call - The - // value should not change during the lifetime of the process. - if (processTmpDirPath === null) { - processTmpDirPath = path.join(os.tmpdir(), `ghost_${crypto.randomUUID()}`); - } - - return processTmpDirPath; -} - /** * @callback getContentPathFn * @param {string} type - the type of context you want the path for @@ -108,7 +88,7 @@ const getContentPath = function getContentPath(type) { case 'settings': return path.join(this.get('paths:contentPath'), 'settings/'); case 'public': - return path.join(getProcessTmpDirPath(this), 'public/'); + return path.join(this.get('paths:contentPath'), 'public/'); default: // new Error is allowed here, as we do not want config to depend on @tryghost/error // @TODO: revisit this decision when @tryghost/error is no longer dependent on all of ghost-ignition diff --git a/ghost/core/package.json b/ghost/core/package.json index 9bd34c8d64a7..eaae3884a5be 100644 --- a/ghost/core/package.json +++ b/ghost/core/package.json @@ -22,7 +22,9 @@ "scripts": { "archive": "npm pack", "dev": "node --watch index.js", - "build:assets": "postcss core/frontend/public/ghost.css --no-map --use cssnano -o core/frontend/public/ghost.min.css", + "build:assets:css": "postcss core/frontend/public/ghost.css --no-map --use cssnano -o core/frontend/public/ghost.min.css", + "build:assets:js": "node ../minifier/bin/single.js core/frontend/public/comment-counts.js core/frontend/public/comment-counts.min.js && node ../minifier/bin/single.js core/frontend/public/member-attribution.js core/frontend/public/member-attribution.min.js && node ../minifier/bin/single.js core/frontend/public/admin-auth/message-handler.js core/frontend/public/admin-auth/admin-auth.min.js", + "build:assets": "yarn build:assets:css && yarn build:assets:js", "test": "yarn test:unit", "test:base": "mocha --reporter dot --require=./test/utils/overrides.js --exit --trace-warnings --recursive --extension=test.js", "test:single": "yarn test:base --timeout=60000", diff --git a/ghost/core/test/unit/shared/config/helpers.test.js b/ghost/core/test/unit/shared/config/helpers.test.js index a1210006ce3e..ecafc722a3e3 100644 --- a/ghost/core/test/unit/shared/config/helpers.test.js +++ b/ghost/core/test/unit/shared/config/helpers.test.js @@ -1,10 +1,7 @@ -const os = require('os'); const should = require('should'); const configUtils = require('../../../utils/configUtils'); -const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/; - describe('vhost utils', function () { beforeEach(function () { configUtils.set('url', 'http://ghost.blog'); @@ -56,29 +53,3 @@ describe('vhost utils', function () { }); }); }); - -describe('getContentPath', function () { - it('should return the correct path for type: public', function () { - const publicPath = configUtils.config.getContentPath('public'); - - // Path should be in the tmpdir - const tmpdir = os.tmpdir(); - - publicPath.startsWith(tmpdir).should.be.true(); - - // Path should end with /public/ - publicPath.endsWith('/public/').should.be.true(); - - // Path should include /ghost_ - publicPath.includes('/ghost_').should.be.true(); - - // Path should contain a uuid at the correct location - const publicPathParts = publicPath.split('/'); - const uuidPart = publicPathParts[publicPathParts.length - 3].replace('ghost_', ''); - - UUID_REGEX.test(uuidPart).should.be.true(); - - // Path should be memoized - configUtils.config.getContentPath('public').should.eql(publicPath); - }); -}); diff --git a/ghost/minifier/bin/single.js b/ghost/minifier/bin/single.js new file mode 100644 index 000000000000..a05bba4d002f --- /dev/null +++ b/ghost/minifier/bin/single.js @@ -0,0 +1,17 @@ +/** + * Script to minify a single file + * + * Usage: node bin/single.js + */ + +const path = require('path'); +const Minifier = require('../index'); + +const src = path.parse(process.argv[2]); +const dest = path.parse(process.argv[3]); + +const minifier = new Minifier({src: src.dir, dest: dest.dir}); + +minifier.minify({ + [dest.base]: src.base +});