Reverted custom fonts flag (#21645)

ref https://ghost.slack.com/archives/C025584CA/p1731950126867179

- moved custom fonts functionality back to a labs flag
- reverted gscan version to 4.45 which doesn't include the custom fonts
checks/warnings
This commit is contained in:
Steve Larson 2024-11-18 11:45:03 -06:00
parent 254acb3b86
commit 34d267decc
13 changed files with 143 additions and 101 deletions

View File

@ -63,6 +63,10 @@ const features = [{
title: 'Staff 2FA',
description: 'Enables email verification for staff logins',
flag: 'staff2fa'
}, {
title: 'Custom Fonts',
description: 'Enables new custom font settings',
flag: 'customFonts'
}];
const AlphaFeatures: React.FC = () => {

View File

@ -1,3 +1,4 @@
import BehindFeatureFlag from '../../../BehindFeatureFlag';
import React, {useState} from 'react';
import UnsplashSelector from '../../../selectors/UnsplashSelector';
import clsx from 'clsx';
@ -318,50 +319,52 @@ const GlobalSettings: React.FC<{ values: GlobalSettingValues, updateSetting: (ke
}
</div>
</Form>
<Form className='-mt-4' gap='sm' margins='lg' title='Typography'>
<Select
className={selectFont(selectedHeadingFont.label, true)}
components={{Option, SingleValue}}
controlClasses={{control: '!min-h-16 !pl-2', option: '!pl-2'}}
hint={''}
menuShouldScrollIntoView={true}
options={customHeadingFonts}
selectedOption={selectedHeadingFont}
testId='heading-font-select'
title={'Heading font'}
onSelect={(option) => {
if (option?.value === DEFAULT_FONT) {
setHeadingFont({name: DEFAULT_FONT, creator: themeNameVersion});
updateSetting('heading_font', '');
} else {
setHeadingFont({name: option?.value || '', creator: CUSTOM_FONTS.heading.find(f => f.name === option?.value)?.creator || ''});
updateSetting('heading_font', option?.value || '');
}
}}
/>
<Select
className={selectFont(selectedBodyFont.label, false)}
components={{Option, SingleValue}}
controlClasses={{control: '!min-h-16 !pl-2', option: '!pl-2'}}
hint={''}
maxMenuHeight={200}
menuPosition='fixed'
menuShouldScrollIntoView={true}
options={customBodyFonts}
selectedOption={selectedBodyFont}
testId='body-font-select'
title={'Body font'}
onSelect={(option) => {
if (option?.value === DEFAULT_FONT) {
setBodyFont({name: DEFAULT_FONT, creator: themeNameVersion});
updateSetting('body_font', '');
} else {
setBodyFont({name: option?.value || '', creator: CUSTOM_FONTS.body.find(f => f.name === option?.value)?.creator || ''});
updateSetting('body_font', option?.value || '');
}
}}
/>
</Form>
<BehindFeatureFlag flag="customFonts">
<Form className='-mt-4' gap='sm' margins='lg' title='Typography'>
<Select
className={selectFont(selectedHeadingFont.label, true)}
components={{Option, SingleValue}}
controlClasses={{control: '!min-h-16 !pl-2', option: '!pl-2'}}
hint={''}
menuShouldScrollIntoView={true}
options={customHeadingFonts}
selectedOption={selectedHeadingFont}
testId='heading-font-select'
title={'Heading font'}
onSelect={(option) => {
if (option?.value === DEFAULT_FONT) {
setHeadingFont({name: DEFAULT_FONT, creator: themeNameVersion});
updateSetting('heading_font', '');
} else {
setHeadingFont({name: option?.value || '', creator: CUSTOM_FONTS.heading.find(f => f.name === option?.value)?.creator || ''});
updateSetting('heading_font', option?.value || '');
}
}}
/>
<Select
className={selectFont(selectedBodyFont.label, false)}
components={{Option, SingleValue}}
controlClasses={{control: '!min-h-16 !pl-2', option: '!pl-2'}}
hint={''}
maxMenuHeight={200}
menuPosition='fixed'
menuShouldScrollIntoView={true}
options={customBodyFonts}
selectedOption={selectedBodyFont}
testId='body-font-select'
title={'Body font'}
onSelect={(option) => {
if (option?.value === DEFAULT_FONT) {
setBodyFont({name: DEFAULT_FONT, creator: themeNameVersion});
updateSetting('body_font', '');
} else {
setBodyFont({name: option?.value || '', creator: CUSTOM_FONTS.body.find(f => f.name === option?.value)?.creator || ''});
updateSetting('body_font', option?.value || '');
}
}}
/>
</Form>
</BehindFeatureFlag>
</>
);
};

View File

@ -1,5 +1,6 @@
import React from 'react';
import ThemeSetting from './ThemeSetting';
import useFeatureFlag from '../../../../hooks/useFeatureFlag';
import {CustomThemeSetting} from '@tryghost/admin-x-framework/api/customThemeSettings';
import {Form} from '@tryghost/admin-x-design-system';
import {Theme, useBrowseThemes} from '@tryghost/admin-x-framework/api/themes';
@ -43,6 +44,7 @@ const ThemeSettings: React.FC<ThemeSettingsProps> = ({sections, updateSetting})
const {data: themesData} = useBrowseThemes();
const activeTheme = themesData?.themes.find((theme: Theme) => theme.active);
const activeThemeName = activeTheme?.package.name?.toLowerCase() || '';
const hasCustomFonts = useFeatureFlag('customFonts');
return (
<>
@ -65,9 +67,11 @@ const ThemeSettings: React.FC<ThemeSettingsProps> = ({sections, updateSetting})
// hides typography related theme settings from official themes
// should be removed once we remove the settings from the themes in 6.0
const hidingSettings = themeSettingsMap[activeThemeName];
if (hidingSettings && hidingSettings.includes(setting.key)) {
spaceClass += ' hidden';
if (hasCustomFonts) {
const hidingSettings = themeSettingsMap[activeThemeName];
if (hidingSettings && hidingSettings.includes(setting.key)) {
spaceClass += ' hidden';
}
}
previousType = setting.type;

View File

@ -3,6 +3,7 @@ import {
mockApi,
mockSitePreview,
responseFixtures,
toggleLabsFlag,
updatedSettingsResponse
} from '@tryghost/admin-x-framework/test/acceptance';
import {expect, test} from '@playwright/test';
@ -305,6 +306,7 @@ test.describe('Design settings', async () => {
});
test('Custom fonts', async ({page}) => {
toggleLabsFlag('customFonts', true);
await mockApi({page, requests: {
...globalDataRequests,
browseCustomThemeSettings: {method: 'GET', path: '/custom_theme_settings/', response: {
@ -350,6 +352,7 @@ test.describe('Design settings', async () => {
});
test('Custom fonts setting back to default', async ({page}) => {
toggleLabsFlag('customFonts', true);
await mockApi({page, requests: {
...globalDataRequests,
browseSettings: {...globalDataRequests.browseSettings, response: updatedSettingsResponse([

View File

@ -2,7 +2,7 @@
// Usage: `{{body_class}}`
//
// Output classes for the body element
const {settingsCache} = require('../services/proxy');
const {labs, settingsCache} = require('../services/proxy');
const {generateCustomFontBodyClass, isValidCustomFont, isValidCustomHeadingFont} = require('@tryghost/custom-fonts');
const {SafeString} = require('../services/handlebars');
@ -45,28 +45,30 @@ module.exports = function body_class(options) { // eslint-disable-line camelcase
classes.push('paged');
}
// Check if if the request is for a site preview, in which case we **always** use the custom font values
// from the passed in data, even when they're empty strings or settings cache has values.
const isSitePreview = options.data?.site?._preview ?? false;
// Taking the fonts straight from the passed in data, as they can't be used from the
// settings cache for the theme preview until the settings are saved. Once saved,
// we need to use the settings cache to provide the correct CSS injection.
const headingFont = isSitePreview ? options.data?.site?.heading_font : settingsCache.get('heading_font');
const bodyFont = isSitePreview ? options.data?.site?.body_font : settingsCache.get('body_font');
if (labs.isSet('customFonts')) {
// Check if if the request is for a site preview, in which case we **always** use the custom font values
// from the passed in data, even when they're empty strings or settings cache has values.
const isSitePreview = options.data.site._preview;
// Taking the fonts straight from the passed in data, as they can't be used from the
// settings cache for the theme preview until the settings are saved. Once saved,
// we need to use the settings cache to provide the correct CSS injection.
const headingFont = isSitePreview ? options.data.site.heading_font : settingsCache.get('heading_font');
const bodyFont = isSitePreview ? options.data.site.body_font : settingsCache.get('body_font');
if ((typeof headingFont === 'string' && isValidCustomHeadingFont(headingFont)) ||
(typeof bodyFont === 'string' && isValidCustomFont(bodyFont))) {
/** @type FontSelection */
const fontSelection = {};
if ((typeof headingFont === 'string' && isValidCustomHeadingFont(headingFont)) ||
(typeof bodyFont === 'string' && isValidCustomFont(bodyFont))) {
/** @type FontSelection */
const fontSelection = {};
if (headingFont) {
fontSelection.heading = headingFont;
if (headingFont) {
fontSelection.heading = headingFont;
}
if (bodyFont) {
fontSelection.body = bodyFont;
}
const customBodyClasses = generateCustomFontBodyClass(fontSelection);
classes.push(new SafeString(customBodyClasses));
}
if (bodyFont) {
fontSelection.body = bodyFont;
}
const customBodyClasses = generateCustomFontBodyClass(fontSelection);
classes.push(new SafeString(customBodyClasses));
}
classes = classes.join(' ').trim();

View File

@ -353,27 +353,29 @@ module.exports = async function ghost_head(options) { // eslint-disable-line cam
head.push(getTinybirdTrackerScript(dataRoot));
}
// Check if if the request is for a site preview, in which case we **always** use the custom font values
// from the passed in data, even when they're empty strings or settings cache has values.
const isSitePreview = options.data?.site?._preview ?? false;
// Taking the fonts straight from the passed in data, as they can't be used from the
// settings cache for the theme preview until the settings are saved. Once saved,
// we need to use the settings cache to provide the correct CSS injection.
const headingFont = isSitePreview ? options.data?.site?.heading_font : settingsCache.get('heading_font');
const bodyFont = isSitePreview ? options.data?.site?.body_font : settingsCache.get('body_font');
if ((typeof headingFont === 'string' && isValidCustomHeadingFont(headingFont)) ||
(typeof bodyFont === 'string' && isValidCustomFont(bodyFont))) {
/** @type FontSelection */
const fontSelection = {};
if (labs.isSet('customFonts')) {
// Check if if the request is for a site preview, in which case we **always** use the custom font values
// from the passed in data, even when they're empty strings or settings cache has values.
const isSitePreview = options.data.site._preview;
// Taking the fonts straight from the passed in data, as they can't be used from the
// settings cache for the theme preview until the settings are saved. Once saved,
// we need to use the settings cache to provide the correct CSS injection.
const headingFont = isSitePreview ? options.data.site.heading_font : settingsCache.get('heading_font');
const bodyFont = isSitePreview ? options.data.site.body_font : settingsCache.get('body_font');
if ((typeof headingFont === 'string' && isValidCustomHeadingFont(headingFont)) ||
(typeof bodyFont === 'string' && isValidCustomFont(bodyFont))) {
/** @type FontSelection */
const fontSelection = {};
if (headingFont) {
fontSelection.heading = headingFont;
if (headingFont) {
fontSelection.heading = headingFont;
}
if (bodyFont) {
fontSelection.body = bodyFont;
}
const customCSS = generateCustomFontCss(fontSelection);
head.push(new SafeString(customCSS));
}
if (bodyFont) {
fontSelection.body = bodyFont;
}
const customCSS = generateCustomFontCss(fontSelection);
head.push(new SafeString(customCSS));
}
}

View File

@ -46,7 +46,8 @@ const ALPHA_FEATURES = [
'adminXDemo',
'contentVisibility',
'commentImprovements',
'staff2fa'
'staff2fa',
'customFonts'
];
module.exports.GA_KEYS = [...GA_FEATURES];

View File

@ -192,7 +192,7 @@
"ghost-storage-base": "1.0.0",
"glob": "8.1.0",
"got": "11.8.6",
"gscan": "4.46.0",
"gscan": "4.45.0",
"human-number": "2.0.4",
"image-size": "1.1.1",
"intl": "1.2.5",

View File

@ -20,6 +20,7 @@ Object {
"collectionsCard": true,
"commentImprovements": true,
"contentVisibility": true,
"customFonts": true,
"editorExcerpt": true,
"emailCustomization": true,
"i18n": true,

View File

@ -1032,7 +1032,7 @@ Object {
<meta name=\\"generator\\" content=\\"Ghost 0.3\\">
<link rel=\\"alternate\\" type=\\"application/rss+xml\\" title=\\"Ghost\\" href=\\"http://localhost:65530/rss/\\">
<script defer src=\\"https://cdn.jsdelivr.net/ghost/sodo-search@~[[VERSION]]/umd/sodo-search.min.js\\" data-key=\\"xyz\\" data-styles=\\"https://cdn.jsdelivr.net/ghost/sodo-search@~[[VERSION]]/umd/main.css\\" data-sodo-search=\\"http://127.0.0.1:2369/\\" data-locale=\\"en\\" crossorigin=\\"anonymous\\"></script>
<script defer src=\\"https://cdn.jsdelivr.net/ghost/sodo-search@~[[VERSION]]/umd/sodo-search.min.js\\" data-key=\\"xyz\\" data-styles=\\"https://cdn.jsdelivr.net/ghost/sodo-search@~[[VERSION]]/umd/main.css\\" data-sodo-search=\\"http://127.0.0.1:2369/\\" crossorigin=\\"anonymous\\"></script>
<link href=\\"http://127.0.0.1:2369/webmentions/receive/\\" rel=\\"webmention\\">",
}
@ -1106,7 +1106,7 @@ Object {
<meta name=\\"generator\\" content=\\"Ghost 0.3\\">
<link rel=\\"alternate\\" type=\\"application/rss+xml\\" title=\\"Ghost\\" href=\\"http://localhost:65530/rss/\\">
<script defer src=\\"https://cdn.jsdelivr.net/ghost/sodo-search@~[[VERSION]]/umd/sodo-search.min.js\\" data-key=\\"xyz\\" data-styles=\\"https://cdn.jsdelivr.net/ghost/sodo-search@~[[VERSION]]/umd/main.css\\" data-sodo-search=\\"http://127.0.0.1:2369/\\" data-locale=\\"en\\" crossorigin=\\"anonymous\\"></script>
<script defer src=\\"https://cdn.jsdelivr.net/ghost/sodo-search@~[[VERSION]]/umd/sodo-search.min.js\\" data-key=\\"xyz\\" data-styles=\\"https://cdn.jsdelivr.net/ghost/sodo-search@~[[VERSION]]/umd/main.css\\" data-sodo-search=\\"http://127.0.0.1:2369/\\" crossorigin=\\"anonymous\\"></script>
<link href=\\"http://127.0.0.1:2369/webmentions/receive/\\" rel=\\"webmention\\">",
}
@ -1180,7 +1180,7 @@ Object {
<meta name=\\"generator\\" content=\\"Ghost 0.3\\">
<link rel=\\"alternate\\" type=\\"application/rss+xml\\" title=\\"Ghost\\" href=\\"http://localhost:65530/rss/\\">
<script defer src=\\"https://cdn.jsdelivr.net/ghost/sodo-search@~[[VERSION]]/umd/sodo-search.min.js\\" data-key=\\"xyz\\" data-styles=\\"https://cdn.jsdelivr.net/ghost/sodo-search@~[[VERSION]]/umd/main.css\\" data-sodo-search=\\"http://127.0.0.1:2369/\\" data-locale=\\"en\\" crossorigin=\\"anonymous\\"></script>
<script defer src=\\"https://cdn.jsdelivr.net/ghost/sodo-search@~[[VERSION]]/umd/sodo-search.min.js\\" data-key=\\"xyz\\" data-styles=\\"https://cdn.jsdelivr.net/ghost/sodo-search@~[[VERSION]]/umd/main.css\\" data-sodo-search=\\"http://127.0.0.1:2369/\\" crossorigin=\\"anonymous\\"></script>
<link href=\\"http://127.0.0.1:2369/webmentions/receive/\\" rel=\\"webmention\\">",
}
@ -1254,7 +1254,7 @@ Object {
<meta name=\\"generator\\" content=\\"Ghost 0.3\\">
<link rel=\\"alternate\\" type=\\"application/rss+xml\\" title=\\"Ghost\\" href=\\"http://localhost:65530/rss/\\">
<script defer src=\\"https://cdn.jsdelivr.net/ghost/sodo-search@~[[VERSION]]/umd/sodo-search.min.js\\" data-key=\\"xyz\\" data-styles=\\"https://cdn.jsdelivr.net/ghost/sodo-search@~[[VERSION]]/umd/main.css\\" data-sodo-search=\\"http://127.0.0.1:2369/\\" data-locale=\\"en\\" crossorigin=\\"anonymous\\"></script>
<script defer src=\\"https://cdn.jsdelivr.net/ghost/sodo-search@~[[VERSION]]/umd/sodo-search.min.js\\" data-key=\\"xyz\\" data-styles=\\"https://cdn.jsdelivr.net/ghost/sodo-search@~[[VERSION]]/umd/main.css\\" data-sodo-search=\\"http://127.0.0.1:2369/\\" crossorigin=\\"anonymous\\"></script>
<link href=\\"http://127.0.0.1:2369/webmentions/receive/\\" rel=\\"webmention\\">",
}
@ -1403,7 +1403,7 @@ Object {
<meta name=\\"generator\\" content=\\"Ghost 0.3\\">
<link rel=\\"alternate\\" type=\\"application/rss+xml\\" title=\\"Ghost\\" href=\\"http://localhost:65530/rss/\\">
<script defer src=\\"https://cdn.jsdelivr.net/ghost/sodo-search@~[[VERSION]]/umd/sodo-search.min.js\\" data-key=\\"xyz\\" data-styles=\\"https://cdn.jsdelivr.net/ghost/sodo-search@~[[VERSION]]/umd/main.css\\" data-sodo-search=\\"http://127.0.0.1:2369/\\" data-locale=\\"en\\" crossorigin=\\"anonymous\\"></script>
<script defer src=\\"https://cdn.jsdelivr.net/ghost/sodo-search@~[[VERSION]]/umd/sodo-search.min.js\\" data-key=\\"xyz\\" data-styles=\\"https://cdn.jsdelivr.net/ghost/sodo-search@~[[VERSION]]/umd/main.css\\" data-sodo-search=\\"http://127.0.0.1:2369/\\" crossorigin=\\"anonymous\\"></script>
<link href=\\"http://127.0.0.1:2369/webmentions/receive/\\" rel=\\"webmention\\">
<link rel=\\"preconnect\\" href=\\"https://fonts.bunny.net\\"><link rel=\\"stylesheet\\" href=\\"https://fonts.bunny.net/css?family=space-grotesk:700|poppins:400,500,600\\"><style>:root {--gh-font-heading: Space Grotesk;--gh-font-body: Poppins;}</style>",
@ -1553,7 +1553,7 @@ Object {
<meta name=\\"generator\\" content=\\"Ghost 0.3\\">
<link rel=\\"alternate\\" type=\\"application/rss+xml\\" title=\\"Ghost\\" href=\\"http://localhost:65530/rss/\\">
<script defer src=\\"https://cdn.jsdelivr.net/ghost/sodo-search@~[[VERSION]]/umd/sodo-search.min.js\\" data-key=\\"xyz\\" data-styles=\\"https://cdn.jsdelivr.net/ghost/sodo-search@~[[VERSION]]/umd/main.css\\" data-sodo-search=\\"http://127.0.0.1:2369/\\" data-locale=\\"en\\" crossorigin=\\"anonymous\\"></script>
<script defer src=\\"https://cdn.jsdelivr.net/ghost/sodo-search@~[[VERSION]]/umd/sodo-search.min.js\\" data-key=\\"xyz\\" data-styles=\\"https://cdn.jsdelivr.net/ghost/sodo-search@~[[VERSION]]/umd/main.css\\" data-sodo-search=\\"http://127.0.0.1:2369/\\" crossorigin=\\"anonymous\\"></script>
<link href=\\"http://127.0.0.1:2369/webmentions/receive/\\" rel=\\"webmention\\">
<link rel=\\"preconnect\\" href=\\"https://fonts.bunny.net\\"><link rel=\\"stylesheet\\" href=\\"https://fonts.bunny.net/css?family=undefined|lora:400,700\\"><style>:root {--gh-font-heading: Playfair Display;--gh-font-body: Lora;}</style>",

View File

@ -7,7 +7,7 @@ const body_class = require('../../../../core/frontend/helpers/body_class');
// Stubs
const proxy = require('../../../../core/frontend/services/proxy');
const {settingsCache} = proxy;
const {settingsCache, labs} = proxy;
describe('{{body_class}} helper', function () {
let options = {};
@ -30,8 +30,7 @@ describe('{{body_class}} helper', function () {
root: {
context: [],
settings: {active_theme: 'casper'}
},
site: {}
}
}
};
});
@ -164,6 +163,7 @@ describe('{{body_class}} helper', function () {
describe('custom fonts', function () {
let settingsCacheStub;
let labsStub;
function callBodyClassWithContext(context, self) {
options.data.root.context = context;
@ -174,6 +174,7 @@ describe('{{body_class}} helper', function () {
}
beforeEach(function () {
labsStub = sinon.stub(labs, 'isSet').withArgs('customFonts').returns(true);
settingsCacheStub = sinon.stub(settingsCache, 'get');
options = {
data: {
@ -215,6 +216,17 @@ describe('{{body_class}} helper', function () {
rendered.string.should.equal('post-template tag-foo tag-bar gh-font-heading-space-grotesk gh-font-body-noto-sans');
});
it('does not include custom font classes when custom fonts are not enabled', function () {
labsStub.withArgs('customFonts').returns(false);
const rendered = callBodyClassWithContext(
['post'],
{relativeUrl: '/my-awesome-post/', post: {tags: [{slug: 'foo'}, {slug: 'bar'}]}}
);
rendered.string.should.equal('post-template tag-foo tag-bar');
});
it('includes custom font classes for home page when set in options data object', function () {
options.data.site.heading_font = 'Space Grotesk';
options.data.site.body_font = '';

View File

@ -1128,6 +1128,8 @@ describe('{{ghost_head}} helper', function () {
describe('custom fonts', function () {
it('includes custom font when set in options data object and preview is set', async function () {
sinon.stub(labs, 'isSet').withArgs('customFonts').returns(true);
const renderObject = {
post: posts[1]
};
@ -1152,6 +1154,7 @@ describe('{{ghost_head}} helper', function () {
});
it('includes custom font when set in settings cache and no preview', async function () {
sinon.stub(labs, 'isSet').withArgs('customFonts').returns(true);
settingsCache.get.withArgs('heading_font').returns('Playfair Display');
settingsCache.get.withArgs('body_font').returns('Lora');
@ -1171,6 +1174,8 @@ describe('{{ghost_head}} helper', function () {
});
it('does not include custom font when not set', async function () {
sinon.stub(labs, 'isSet').withArgs('customFonts').returns(true);
settingsCache.get.withArgs('heading_font').returns(null);
settingsCache.get.withArgs('body_font').returns('');
@ -1190,6 +1195,8 @@ describe('{{ghost_head}} helper', function () {
});
it('does not include custom font when invalid', async function () {
sinon.stub(labs, 'isSet').withArgs('customFonts').returns(true);
settingsCache.get.withArgs('heading_font').returns(null);
settingsCache.get.withArgs('body_font').returns('Wendy Sans');
@ -1216,6 +1223,7 @@ describe('{{ghost_head}} helper', function () {
});
it('does not inject custom fonts when preview is set and default font was selected (empty string)', async function () {
sinon.stub(labs, 'isSet').withArgs('customFonts').returns(true);
// The site has fonts set up, but we override them with Theme default fonts (empty string)
settingsCache.get.withArgs('heading_font').returns('Playfair Display');
settingsCache.get.withArgs('body_font').returns('Lora');
@ -1240,6 +1248,8 @@ describe('{{ghost_head}} helper', function () {
});
it('can handle preview being set and custom font keys missing', async function () {
sinon.stub(labs, 'isSet').withArgs('customFonts').returns(true);
// The site has fonts set up, but we override them with Theme default fonts (empty string)
settingsCache.get.withArgs('heading_font').returns('Playfair Display');
settingsCache.get.withArgs('body_font').returns('Lora');

View File

@ -18792,10 +18792,10 @@ growly@^1.3.0:
resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
integrity sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw==
gscan@4.46.0:
version "4.46.0"
resolved "https://registry.yarnpkg.com/gscan/-/gscan-4.46.0.tgz#682e5388061e35518e0906ca1285734347cbbe60"
integrity sha512-SHsvld0EbVW7X9aHqL6P2CG31yjvrX1IxrVePZTm4yKxH7jjD1QmSmL1Ol3sFAh5MRqtGBuj9mGfDC1nJI0e7w==
gscan@4.45.0:
version "4.45.0"
resolved "https://registry.yarnpkg.com/gscan/-/gscan-4.45.0.tgz#8f033793b80ac65b64d666aac0891287c1e74909"
integrity sha512-d7yu7eGJSv7Xrd8lcBr7Bq76xeKe/LYkrRsRgE8vPRlmHxLPx7AlFb585vbS1Q64cQGwQhlAGusIeKesrfOa6w==
dependencies:
"@sentry/node" "^7.73.0"
"@tryghost/config" "^0.2.18"