diff --git a/app/application.js b/app/application.js index a6d4ed199..96cedfeef 100644 --- a/app/application.js +++ b/app/application.js @@ -2,6 +2,9 @@ import loadPolyfills from './soapbox/load_polyfills'; require.context('./images/', true); +// Load stylesheet +require('./styles/application.scss'); + loadPolyfills().then(() => { require('./soapbox/main').default(); }).catch(e => { diff --git a/app/soapbox/components/__tests__/__snapshots__/autosuggest_emoji-test.js.snap b/app/soapbox/components/__tests__/__snapshots__/autosuggest_emoji-test.js.snap index 1c3727848..6bbb1eb74 100644 --- a/app/soapbox/components/__tests__/__snapshots__/autosuggest_emoji-test.js.snap +++ b/app/soapbox/components/__tests__/__snapshots__/autosuggest_emoji-test.js.snap @@ -20,7 +20,11 @@ exports[` renders native emoji 1`] = ` ๐Ÿ’™ :foobar: diff --git a/app/soapbox/components/__tests__/__snapshots__/emoji_selector-test.js.snap b/app/soapbox/components/__tests__/__snapshots__/emoji_selector-test.js.snap index 06d7764ec..d009a5551 100644 --- a/app/soapbox/components/__tests__/__snapshots__/emoji_selector-test.js.snap +++ b/app/soapbox/components/__tests__/__snapshots__/emoji_selector-test.js.snap @@ -15,7 +15,7 @@ exports[` renders correctly 1`] = ` className="emoji-react-selector__emoji" dangerouslySetInnerHTML={ Object { - "__html": "\\"๐Ÿ‘\\"", + "__html": "\\"๐Ÿ‘\\"", } } onClick={[Function]} @@ -26,7 +26,7 @@ exports[` renders correctly 1`] = ` className="emoji-react-selector__emoji" dangerouslySetInnerHTML={ Object { - "__html": "\\"โค\\"", + "__html": "\\"โค\\"", } } onClick={[Function]} @@ -37,7 +37,7 @@ exports[` renders correctly 1`] = ` className="emoji-react-selector__emoji" dangerouslySetInnerHTML={ Object { - "__html": "\\"๐Ÿ˜†\\"", + "__html": "\\"๐Ÿ˜†\\"", } } onClick={[Function]} @@ -48,7 +48,7 @@ exports[` renders correctly 1`] = ` className="emoji-react-selector__emoji" dangerouslySetInnerHTML={ Object { - "__html": "\\"๐Ÿ˜ฎ\\"", + "__html": "\\"๐Ÿ˜ฎ\\"", } } onClick={[Function]} @@ -59,7 +59,7 @@ exports[` renders correctly 1`] = ` className="emoji-react-selector__emoji" dangerouslySetInnerHTML={ Object { - "__html": "\\"๐Ÿ˜ข\\"", + "__html": "\\"๐Ÿ˜ข\\"", } } onClick={[Function]} @@ -70,7 +70,7 @@ exports[` renders correctly 1`] = ` className="emoji-react-selector__emoji" dangerouslySetInnerHTML={ Object { - "__html": "\\"๐Ÿ˜ฉ\\"", + "__html": "\\"๐Ÿ˜ฉ\\"", } } onClick={[Function]} diff --git a/app/soapbox/components/autosuggest_emoji.js b/app/soapbox/components/autosuggest_emoji.js index 6311061b0..da2df72a3 100644 --- a/app/soapbox/components/autosuggest_emoji.js +++ b/app/soapbox/components/autosuggest_emoji.js @@ -1,8 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import unicodeMapping from '../features/emoji/emoji_unicode_mapping_light'; -import { join } from 'path'; -import { FE_SUBDIRECTORY } from 'soapbox/build_config'; export default class AutosuggestEmoji extends React.PureComponent { @@ -23,7 +21,7 @@ export default class AutosuggestEmoji extends React.PureComponent { return null; } - url = join(FE_SUBDIRECTORY, 'emoji', `${mapping.filename}.svg`); + url = require(`twemoji/assets/svg/${mapping.filename}.svg`); } return ( diff --git a/app/soapbox/features/compose/components/emoji_picker_dropdown.js b/app/soapbox/features/compose/components/emoji_picker_dropdown.js index 2d9f155fd..9823978c9 100644 --- a/app/soapbox/features/compose/components/emoji_picker_dropdown.js +++ b/app/soapbox/features/compose/components/emoji_picker_dropdown.js @@ -7,8 +7,6 @@ import classNames from 'classnames'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { supportsPassiveEvents } from 'detect-passive-events'; import { buildCustomEmojis } from '../../emoji/emoji'; -import { join } from 'path'; -import { FE_SUBDIRECTORY } from 'soapbox/build_config'; const messages = defineMessages({ emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' }, @@ -29,7 +27,7 @@ const messages = defineMessages({ let EmojiPicker, Emoji; // load asynchronously -const backgroundImageFn = () => join(FE_SUBDIRECTORY, 'emoji', 'sheet_13.png'); +const backgroundImageFn = () => require('emoji-datasource/img/twitter/sheets/32.png'); const listenerOptions = supportsPassiveEvents ? { passive: true } : false; const categoriesSort = [ @@ -358,8 +356,8 @@ class EmojiPickerDropdown extends React.PureComponent {
๐Ÿ™‚
diff --git a/app/soapbox/features/crypto_donate/utils/coin_icons.js b/app/soapbox/features/crypto_donate/utils/coin_icons.js index 2c0376bfa..39fe39ded 100644 --- a/app/soapbox/features/crypto_donate/utils/coin_icons.js +++ b/app/soapbox/features/crypto_donate/utils/coin_icons.js @@ -1,20 +1,4 @@ -// Does some trickery to import all the icons into the project -// See: https://stackoverflow.com/questions/42118296/dynamically-import-images-from-a-directory-using-webpack - -const icons = {}; - -function importAll(r) { - const pathRegex = /\.\/(.*)\.svg/i; - - r.keys().forEach((key) => { - const ticker = pathRegex.exec(key)[1]; - return icons[ticker] = r(key).default; - }); -} - -importAll(require.context('cryptocurrency-icons/svg/color/', true, /\.svg$/)); - -export default icons; - // For getting the icon -export const getCoinIcon = ticker => icons[ticker] || icons.generic || null; +export const getCoinIcon = ticker => { + return require(`cryptocurrency-icons/svg/color/${ticker.toLowerCase()}.svg`); +}; diff --git a/app/soapbox/features/emoji/__tests__/emoji-test.js b/app/soapbox/features/emoji/__tests__/emoji-test.js index c8425c4c6..ce8d4e2a8 100644 --- a/app/soapbox/features/emoji/__tests__/emoji-test.js +++ b/app/soapbox/features/emoji/__tests__/emoji-test.js @@ -22,23 +22,23 @@ describe('emoji', () => { it('does unicode', () => { expect(emojify('\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66')).toEqual( - '๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ'); + '๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ'); expect(emojify('๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง')).toEqual( - '๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง'); - expect(emojify('๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆ')).toEqual('๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆ'); + '๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง'); + expect(emojify('๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆ')).toEqual('๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆ'); expect(emojify('\u2757')).toEqual( - 'โ—'); + 'โ—'); }); it('does multiple unicode', () => { expect(emojify('\u2757 #\uFE0F\u20E3')).toEqual( - 'โ— #๏ธโƒฃ'); + 'โ— #๏ธโƒฃ'); expect(emojify('\u2757#\uFE0F\u20E3')).toEqual( - 'โ—#๏ธโƒฃ'); + 'โ—#๏ธโƒฃ'); expect(emojify('\u2757 #\uFE0F\u20E3 \u2757')).toEqual( - 'โ— #๏ธโƒฃ โ—'); + 'โ— #๏ธโƒฃ โ—'); expect(emojify('foo \u2757 #\uFE0F\u20E3 bar')).toEqual( - 'foo โ— #๏ธโƒฃ bar'); + 'foo โ— #๏ธโƒฃ bar'); }); it('ignores unicode inside of tags', () => { @@ -46,16 +46,16 @@ describe('emoji', () => { }); it('does multiple emoji properly (issue 5188)', () => { - expect(emojify('๐Ÿ‘Œ๐ŸŒˆ๐Ÿ’•')).toEqual('๐Ÿ‘Œ๐ŸŒˆ๐Ÿ’•'); - expect(emojify('๐Ÿ‘Œ ๐ŸŒˆ ๐Ÿ’•')).toEqual('๐Ÿ‘Œ ๐ŸŒˆ ๐Ÿ’•'); + expect(emojify('๐Ÿ‘Œ๐ŸŒˆ๐Ÿ’•')).toEqual('๐Ÿ‘Œ๐ŸŒˆ๐Ÿ’•'); + expect(emojify('๐Ÿ‘Œ ๐ŸŒˆ ๐Ÿ’•')).toEqual('๐Ÿ‘Œ ๐ŸŒˆ ๐Ÿ’•'); }); it('does an emoji that has no shortcode', () => { - expect(emojify('๐Ÿ‘โ€๐Ÿ—จ')).toEqual('๐Ÿ‘โ€๐Ÿ—จ'); + expect(emojify('๐Ÿ‘โ€๐Ÿ—จ')).toEqual('๐Ÿ‘โ€๐Ÿ—จ'); }); it('does an emoji whose filename is irregular', () => { - expect(emojify('โ†™๏ธ')).toEqual('โ†™๏ธ'); + expect(emojify('โ†™๏ธ')).toEqual('โ†™๏ธ'); }); it('avoid emojifying on invisible text', () => { @@ -67,16 +67,16 @@ describe('emoji', () => { it('avoid emojifying on invisible text with nested tags', () => { expect(emojify('๐Ÿ˜‡')) - .toEqual('๐Ÿ˜‡'); + .toEqual('๐Ÿ˜‡'); expect(emojify('๐Ÿ˜‡')) - .toEqual('๐Ÿ˜‡'); + .toEqual('๐Ÿ˜‡'); expect(emojify('๐Ÿ˜‡')) - .toEqual('๐Ÿ˜‡'); + .toEqual('๐Ÿ˜‡'); }); it('skips the textual presentation VS15 character', () => { expect(emojify('โœด๏ธŽ')) // This is U+2734 EIGHT POINTED BLACK STAR then U+FE0E VARIATION SELECTOR-15 - .toEqual('โœด'); + .toEqual('โœด'); }); }); }); diff --git a/app/soapbox/features/emoji/emoji.js b/app/soapbox/features/emoji/emoji.js index eb0df79a7..fe1029c59 100644 --- a/app/soapbox/features/emoji/emoji.js +++ b/app/soapbox/features/emoji/emoji.js @@ -1,7 +1,5 @@ import unicodeMapping from './emoji_unicode_mapping_light'; import Trie from 'substring-trie'; -import { join } from 'path'; -import { FE_SUBDIRECTORY } from 'soapbox/build_config'; const trie = new Trie(Object.keys(unicodeMapping)); @@ -62,7 +60,8 @@ const emojify = (str, customEmojis = {}, autoplay = false) => { } else { // matched to unicode emoji const { filename, shortCode } = unicodeMapping[match]; const title = shortCode ? `:${shortCode}:` : ''; - replacement = `${match}`; + const src = require(`twemoji/assets/svg/${filename}.svg`); + replacement = `${match}`; rend = i + match.length; // If the matched character was followed by VS15 (for selecting text presentation), skip it. if (str.codePointAt(rend) === 65038) { diff --git a/app/soapbox/middleware/sounds.js b/app/soapbox/middleware/sounds.js index a2fc7572f..6950e7618 100644 --- a/app/soapbox/middleware/sounds.js +++ b/app/soapbox/middleware/sounds.js @@ -1,8 +1,5 @@ 'use strict'; -import { join } from 'path'; -import { FE_SUBDIRECTORY } from 'soapbox/build_config'; - const createAudio = sources => { const audio = new Audio(); sources.forEach(({ type, src }) => { @@ -31,21 +28,21 @@ export default function soundsMiddleware() { const soundCache = { boop: createAudio([ { - src: join(FE_SUBDIRECTORY, '/sounds/boop.ogg'), + src: require('../../sounds/boop.ogg'), type: 'audio/ogg', }, { - src: join(FE_SUBDIRECTORY, '/sounds/boop.mp3'), + src: require('../../sounds/boop.mp3'), type: 'audio/mpeg', }, ]), chat: createAudio([ { - src: join(FE_SUBDIRECTORY, '/sounds/chat.oga'), + src: require('../../sounds/chat.oga'), type: 'audio/ogg', }, { - src: join(FE_SUBDIRECTORY, '/sounds/chat.mp3'), + src: require('../../sounds/chat.mp3'), type: 'audio/mpeg', }, ]), diff --git a/jest.config.js b/jest.config.js index d7b2fb44a..efe164457 100644 --- a/jest.config.js +++ b/jest.config.js @@ -31,4 +31,11 @@ module.exports = { '/app', ], 'testEnvironment': 'jsdom', + 'moduleNameMapper': { + '^.+.(css|styl|less|sass|scss|png|jpg|svg|ttf|woff|woff2)$': 'jest-transform-stub', + }, + 'transform': { + '\\.[jt]sx?$': 'babel-jest', + '.+\\.(css|styl|less|sass|scss|png|jpg|svg|ttf|woff|woff2)$': 'jest-transform-stub', + }, }; diff --git a/package.json b/package.json index 018088f2d..419294299 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,7 @@ "intl-messageformat-parser": "^6.0.0", "intl-pluralrules": "^1.3.0", "is-nan": "^1.2.1", + "jest-transform-stub": "^2.0.0", "jsdoc": "~3.6.7", "lodash": "^4.7.11", "mark-loader": "^0.1.6", diff --git a/webpack/configuration.js b/webpack/configuration.js index 0089469c4..960e25e60 100644 --- a/webpack/configuration.js +++ b/webpack/configuration.js @@ -12,7 +12,6 @@ const settings = { test_root_path: `${FE_BUILD_DIR}-test`, cache_path: 'tmp/cache', resolved_paths: [], - static_assets_extensions: [ '.jpg', '.jpeg', '.png', '.tiff', '.ico', '.svg', '.gif', '.eot', '.otf', '.ttf', '.woff', '.woff2', '.mp3', '.ogg', '.oga' ], extensions: [ '.mjs', '.js', '.sass', '.scss', '.css', '.module.sass', '.module.scss', '.module.css', '.png', '.svg', '.gif', '.jpeg', '.jpg' ], }; diff --git a/webpack/production.js b/webpack/production.js index 9968a2ca9..cccfc4163 100644 --- a/webpack/production.js +++ b/webpack/production.js @@ -25,37 +25,33 @@ module.exports = merge(sharedConfig, { new OfflinePlugin({ caches: { main: [':rest:'], - additional: [':externals:'], + additional: [ + 'packs/emoji/1f602-*.svg', // used for emoji picker dropdown + 'packs/images/32-*.png', // used in emoji-mart + + // Default emoji reacts + 'packs/emoji/1f44d-*.svg', // Thumbs up + 'packs/emoji/2764-*.svg', // Heart + 'packs/emoji/1f606-*.svg', // Laughing + 'packs/emoji/1f62e-*.svg', // Surprised + 'packs/emoji/1f622-*.svg', // Crying + 'packs/emoji/1f629-*.svg', // Weary + 'packs/emoji/1f621-*.svg', // Angry (Spinster) + ], optional: [ '**/locale_*.js', // don't fetch every locale; the user only needs one '**/*_polyfills-*.js', // the user may not need polyfills '**/*.chunk.js', // only cache chunks when needed '**/*.woff2', // the user may have system-fonts enabled - // images/audio can be cached on-demand + // images can be cached on-demand '**/*.png', - '**/*.jpg', - '**/*.jpeg', '**/*.svg', - '**/*.mp3', - '**/*.ogg', ], }, - externals: [ - '/emoji/1f602.svg', // used for emoji picker dropdown - '/emoji/sheet_13.png', // used in emoji-mart - - // Default emoji reacts - '/emoji/1f44d.svg', // Thumbs up - '/emoji/2764.svg', // Heart - '/emoji/1f606.svg', // Laughing - '/emoji/1f62e.svg', // Surprised - '/emoji/1f622.svg', // Crying - '/emoji/1f629.svg', // Weary - '/emoji/1f621.svg', // Angry (Spinster) - ], excludes: [ '**/*.gz', '**/*.map', + '**/*.LICENSE.txt', 'stats.json', 'report.html', 'instance/**/*', @@ -66,15 +62,20 @@ module.exports = merge(sharedConfig, { '**/*.woff', // Sounds return a 206 causing sw.js to crash // https://stackoverflow.com/a/66335638 - 'sounds/**/*', - // Don't cache index.html + '**/*.ogg', + '**/*.oga', + '**/*.mp3', + // Don't serve index.html + // https://github.com/bromite/bromite/issues/1294 'index.html', + '404.html', ], // ServiceWorker: { // entry: join(__dirname, '../app/soapbox/service_worker/entry.js'), // cacheName: 'soapbox', // minify: true, // }, + safeToUseOptionalCaches: true, }), ], }); diff --git a/webpack/rules/assets.js b/webpack/rules/assets.js new file mode 100644 index 000000000..2c6fb3f0d --- /dev/null +++ b/webpack/rules/assets.js @@ -0,0 +1,50 @@ +// Asset modules +// https://webpack.js.org/guides/asset-modules/ + +const { resolve } = require('path'); + +// These are processed in reverse-order +// We use the name 'packs' instead of 'assets' for legacy reasons +module.exports = [{ + test: /\.(png|svg)/, + type: 'asset/resource', + include: [ + resolve('app', 'images'), + resolve('node_modules', 'emoji-datasource'), + ], + generator: { + filename: 'packs/images/[name]-[contenthash:8][ext]', + }, +}, { + test: /\.(ttf|eot|svg|woff|woff2)/, + type: 'asset/resource', + include: [ + resolve('app', 'fonts'), + resolve('node_modules', 'fork-awesome'), + resolve('node_modules', '@fontsource'), + ], + generator: { + filename: 'packs/fonts/[name]-[contenthash:8][ext]', + }, +}, { + test: /\.(ogg|oga|mp3)/, + type: 'asset/resource', + include: resolve('app', 'sounds'), + generator: { + filename: 'packs/sounds/[name]-[contenthash:8][ext]', + }, +}, { + test: /\.svg$/, + type: 'asset/resource', + include: resolve('node_modules', 'twemoji'), + generator: { + filename: 'packs/emoji/[name]-[contenthash:8][ext]', + }, +}, { + test: /\.svg$/, + type: 'asset/resource', + include: resolve('node_modules', 'cryptocurrency-icons'), + generator: { + filename: 'packs/images/crypto/[name]-[contenthash:8][ext]', + }, +}]; diff --git a/webpack/rules/file.js b/webpack/rules/file.js deleted file mode 100644 index d23a0a977..000000000 --- a/webpack/rules/file.js +++ /dev/null @@ -1,20 +0,0 @@ -const { join } = require('path'); -const { settings } = require('../configuration'); - -module.exports = { - test: new RegExp(`(${settings.static_assets_extensions.join('|')})$`, 'i'), - use: [ - { - loader: 'file-loader', - options: { - name(file) { - if (file.includes(settings.source_path)) { - return 'packs/media/[path][name]-[contenthash].[ext]'; - } - return 'packs/media/[folder]/[name]-[contenthash:8].[ext]'; - }, - context: join(settings.source_path), - }, - }, - ], -}; diff --git a/webpack/rules/index.js b/webpack/rules/index.js index 91a4abd19..d3290659e 100644 --- a/webpack/rules/index.js +++ b/webpack/rules/index.js @@ -3,14 +3,14 @@ const git = require('./babel-git'); const gitRefresh = require('./git-refresh'); const buildConfig = require('./babel-build-config'); const css = require('./css'); -const file = require('./file'); +const assets = require('./assets'); const nodeModules = require('./node_modules'); // Webpack loaders are processed in reverse order // https://webpack.js.org/concepts/loaders/#loader-features // Lastly, process static files using file loader module.exports = [ - file, + ...assets, css, nodeModules, babel, diff --git a/webpack/shared.js b/webpack/shared.js index c0bbad931..0c4bd856e 100644 --- a/webpack/shared.js +++ b/webpack/shared.js @@ -30,10 +30,9 @@ const makeHtmlConfig = (params = {}) => { }; module.exports = { - entry: Object.assign( - { application: resolve('app/application.js') }, - { styles: resolve(join(settings.source_path, 'styles/application.scss')) }, - ), + entry: { + application: resolve('app/application.js'), + }, output: { filename: 'packs/js/[name]-[chunkhash].js', @@ -65,7 +64,7 @@ module.exports = { }, module: { - rules: Object.keys(rules).map(key => rules[key]), + rules, }, plugins: [ @@ -89,15 +88,6 @@ module.exports = { new HtmlWebpackHarddiskPlugin(), new CopyPlugin({ patterns: [{ - from: join(__dirname, '../node_modules/twemoji/assets/svg'), - to: join(output.path, 'emoji'), - }, { - from: join(__dirname, '../node_modules/emoji-datasource/img/twitter/sheets/32.png'), - to: join(output.path, 'emoji/sheet_13.png'), - }, { - from: join(__dirname, '../app/sounds'), - to: join(output.path, 'sounds'), - }, { from: join(__dirname, '../app/instance'), to: join(output.path, 'instance'), }], diff --git a/yarn.lock b/yarn.lock index 4876a41de..9e75304e7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7239,6 +7239,11 @@ jest-snapshot@^27.1.0: pretty-format "^27.1.0" semver "^7.3.2" +jest-transform-stub@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/jest-transform-stub/-/jest-transform-stub-2.0.0.tgz#19018b0851f7568972147a5d60074b55f0225a7d" + integrity sha512-lspHaCRx/mBbnm3h4uMMS3R5aZzMwyNpNIJLXj4cEsV0mIUtS4IjYJLSoyjRCtnxb6RIGJ4NL2quZzfIeNhbkg== + jest-util@^27.0.0: version "27.0.6" resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-27.0.6.tgz#e8e04eec159de2f4d5f57f795df9cdc091e50297"