about summary refs log tree commit diff
path: root/config/webpack
diff options
context:
space:
mode:
Diffstat (limited to 'config/webpack')
-rw-r--r--config/webpack/configuration.js54
-rw-r--r--config/webpack/generateLocalePacks.js102
-rw-r--r--config/webpack/rules/babel.js1
-rw-r--r--config/webpack/rules/css.js3
-rw-r--r--config/webpack/shared.js76
-rw-r--r--config/webpack/translationRunner.js27
6 files changed, 191 insertions, 72 deletions
diff --git a/config/webpack/configuration.js b/config/webpack/configuration.js
index 25b6b7abd..55ee06c0c 100644
--- a/config/webpack/configuration.js
+++ b/config/webpack/configuration.js
@@ -1,15 +1,58 @@
 // Common configuration for webpacker loaded from config/webpacker.yml
 
-const { resolve } = require('path');
+const { basename, dirname, extname, join, resolve } = require('path');
 const { env } = require('process');
 const { load } = require('js-yaml');
-const { readFileSync } = require('fs');
+const { lstatSync, readFileSync } = require('fs');
+const glob = require('glob');
 
 const configPath = resolve('config', 'webpacker.yml');
 const settings = load(readFileSync(configPath), 'utf8')[env.RAILS_ENV || env.NODE_ENV];
+const flavourFiles = glob.sync('app/javascript/flavours/*/theme.yml');
+const skinFiles = glob.sync('app/javascript/skins/*/*');
+const flavours = {};
 
-const themePath = resolve('config', 'themes.yml');
-const themes = load(readFileSync(themePath), 'utf8');
+const core = function () {
+  const coreFile = resolve('app', 'javascript', 'core', 'theme.yml');
+  const data = load(readFileSync(coreFile), 'utf8');
+  if (!data.pack_directory) {
+    data.pack_directory = dirname(coreFile);
+  }
+  return data.pack ? data : {};
+}();
+
+flavourFiles.forEach((flavourFile) => {
+  const data = load(readFileSync(flavourFile), 'utf8');
+  data.name = basename(dirname(flavourFile));
+  data.skin = {};
+  if (!data.pack_directory) {
+    data.pack_directory = dirname(flavourFile);
+  }
+  if (data.locales) {
+    data.locales = join(dirname(flavourFile), data.locales);
+  }
+  if (data.pack && typeof data.pack === 'object') {
+    flavours[data.name] = data;
+  }
+});
+
+skinFiles.forEach((skinFile) => {
+  let skin = basename(skinFile);
+  const name = basename(dirname(skinFile));
+  if (!flavours[name]) {
+    return;
+  }
+  const data = flavours[name].skin;
+  if (lstatSync(skinFile).isDirectory()) {
+    data[skin] = {};
+    const skinPacks = glob.sync(join(skinFile, '*.{css,scss}'));
+    skinPacks.forEach((pack) => {
+      data[skin][basename(pack, extname(pack))] = pack;
+    });
+  } else if ((skin = skin.match(/^(.*)\.s?css$/i))) {
+    data[skin[1]] = { common: skinFile };
+  }
+});
 
 const output = {
   path: resolve('public', settings.public_output_path),
@@ -18,7 +61,8 @@ const output = {
 
 module.exports = {
   settings,
-  themes,
+  core,
+  flavours,
   env: {
     NODE_ENV: env.NODE_ENV,
     PUBLIC_OUTPUT_PATH: settings.public_output_path,
diff --git a/config/webpack/generateLocalePacks.js b/config/webpack/generateLocalePacks.js
index b71cf2ade..b1b818159 100644
--- a/config/webpack/generateLocalePacks.js
+++ b/config/webpack/generateLocalePacks.js
@@ -1,52 +1,74 @@
+// A message from upstream:
+// ========================
 // To avoid adding a lot of boilerplate, locale packs are
 // automatically generated here. These are written into the tmp/
 // directory and then used to generate locale_en.js, locale_fr.js, etc.
 
-const fs = require('fs');
-const path = require('path');
+// Glitch note:
+// ============
+// This code has been entirely rewritten to support glitch flavours.
+// However, the underlying process is exactly the same.
+
+const { existsSync, readdirSync, writeFileSync } = require('fs');
+const { join, resolve } = require('path');
 const rimraf = require('rimraf');
 const mkdirp = require('mkdirp');
+const { flavours } = require('./configuration.js');
+
+module.exports = Object.keys(flavours).reduce(function (map, entry) {
+  const flavour = flavours[entry];
+  if (!flavour.locales) {
+    return map;
+  }
+  const locales = readdirSync(flavour.locales).filter(filename => {
+    return /\.json$/.test(filename) &&
+      !/defaultMessages/.test(filename) &&
+      !/whitelist/.test(filename);
+  }).map(filename => filename.replace(/\.json$/, ''));
+
+  let inherited_locales_path = null;
+  if (flavour.inherit_locales && flavours[flavour.inherit_locales]?.locales) {
+    inherited_locales_path = flavours[flavour.inherit_locales]?.locales;
+  }
+
+  const outPath = resolve('tmp', 'locales', entry);
+
+  rimraf.sync(outPath);
+  mkdirp.sync(outPath);
 
-const localesJsonPath = path.join(__dirname, '../../app/javascript/mastodon/locales');
-const locales = fs.readdirSync(localesJsonPath).filter(filename => {
-  return /\.json$/.test(filename) &&
-    !/defaultMessages/.test(filename) &&
-    !/whitelist/.test(filename);
-}).map(filename => filename.replace(/\.json$/, ''));
-
-const outPath = path.join(__dirname, '../../tmp/packs');
-
-rimraf.sync(outPath);
-mkdirp.sync(outPath);
-
-const outPaths = [];
-
-locales.forEach(locale => {
-  const localePath = path.join(outPath, `locale_${locale}.js`);
-  const baseLocale = locale.split('-')[0]; // e.g. 'zh-TW' -> 'zh'
-  const localeDataPath = [
-    // first try react-intl
-    `../../node_modules/react-intl/locale-data/${baseLocale}.js`,
-    // then check locales/locale-data
-    `../../app/javascript/mastodon/locales/locale-data/${baseLocale}.js`,
-    // fall back to English (this is what react-intl does anyway)
-    '../../node_modules/react-intl/locale-data/en.js',
-  ].filter(filename => fs.existsSync(path.join(outPath, filename)))
-    .map(filename => filename.replace(/..\/..\/node_modules\//, ''))[0];
-
-  const localeContent = `//
-// locale_${locale}.js
+  locales.forEach(function (locale) {
+    const localePath = join(outPath, `${locale}.js`);
+    const baseLocale = locale.split('-')[0]; // e.g. 'zh-TW' -> 'zh'
+    const localeDataPath = [
+      // first try react-intl
+      `node_modules/react-intl/locale-data/${baseLocale}.js`,
+      // then check locales/locale-data
+      `app/javascript/locales/locale-data/${baseLocale}.js`,
+      // fall back to English (this is what react-intl does anyway)
+      'node_modules/react-intl/locale-data/en.js',
+    ].filter(
+      filename => existsSync(filename)
+    ).map(
+      filename => filename.replace(/(?:node_modules|app\/javascript)\//, '')
+    )[0];
+    const localeContent = `//
+// locales/${entry}/${locale}.js
 // automatically generated by generateLocalePacks.js
 //
-import messages from '../../app/javascript/mastodon/locales/${locale}.json';
-import localeData from ${JSON.stringify(localeDataPath)};
-import { setLocale } from '../../app/javascript/mastodon/locales';
-setLocale({messages, localeData});
-`;
-  fs.writeFileSync(localePath, localeContent, 'utf8');
-  outPaths.push(localePath);
-});
 
-module.exports = outPaths;
+${inherited_locales_path ? `import inherited from '../../../${inherited_locales_path}/${locale}.json';` : ''}
+import messages from '../../../${flavour.locales}/${locale}.json';
+import localeData from '${localeDataPath}';
+import { setLocale } from 'locales';
 
+setLocale({
+  localeData,
+  ${inherited_locales_path ? 'messages: Object.assign({}, inherited, messages)' : 'messages'},
+});
+`;
+    writeFileSync(localePath, localeContent, 'utf8');
+    map[`locales/${entry}/${locale}`] = localePath;
+  });
 
+  return map;
+}, {});
diff --git a/config/webpack/rules/babel.js b/config/webpack/rules/babel.js
index ba1aff93a..8b6205a5c 100644
--- a/config/webpack/rules/babel.js
+++ b/config/webpack/rules/babel.js
@@ -12,6 +12,7 @@ module.exports = {
     {
       loader: 'babel-loader',
       options: {
+        sourceRoot: 'app/javascript',
         cacheDirectory: join(settings.cache_path, 'babel-loader'),
         cacheCompression: env.NODE_ENV === 'production',
         compact: env.NODE_ENV === 'production',
diff --git a/config/webpack/rules/css.js b/config/webpack/rules/css.js
index bc1f42c13..6ecfb3164 100644
--- a/config/webpack/rules/css.js
+++ b/config/webpack/rules/css.js
@@ -20,6 +20,9 @@ module.exports = {
     {
       loader: 'sass-loader',
       options: {
+        sassOptions: {
+          includePaths: ['app/javascript'],
+        },
         implementation: require('sass'),
         sourceMap: true,
       },
diff --git a/config/webpack/shared.js b/config/webpack/shared.js
index 78f660cfc..405858d0c 100644
--- a/config/webpack/shared.js
+++ b/config/webpack/shared.js
@@ -5,33 +5,57 @@ const { basename, dirname, join, relative, resolve } = require('path');
 const { sync } = require('glob');
 const MiniCssExtractPlugin = require('mini-css-extract-plugin');
 const AssetsManifestPlugin = require('webpack-assets-manifest');
-const extname = require('path-complete-extname');
-const { env, settings, themes, output } = require('./configuration');
+const { env, settings, core, flavours, output } = require('./configuration.js');
 const rules = require('./rules');
-const localePackPaths = require('./generateLocalePacks');
+const localePacks = require('./generateLocalePacks');
+
+function reducePacks (data, into = {}) {
+  if (!data.pack) return into;
+
+  for (const entry in data.pack) {
+    const pack = data.pack[entry];
+    if (!pack) continue;
+
+    let packFiles = [];
+    if (typeof pack === 'string')
+      packFiles = [pack];
+    else if (Array.isArray(pack))
+      packFiles = pack;
+    else
+      packFiles = [pack.filename];
+
+    if (packFiles) {
+      into[data.name ? `flavours/${data.name}/${entry}` : `core/${entry}`] = packFiles.map(packFile => resolve(data.pack_directory, packFile));
+    }
+  }
+
+  if (!data.name) return into;
+
+  for (const skinName in data.skin) {
+    const skin = data.skin[skinName];
+    if (!skin) continue;
+
+    for (const entry in skin) {
+      const packFile = skin[entry];
+      if (!packFile) continue;
+
+      into[`skins/${data.name}/${skinName}/${entry}`] = resolve(packFile);
+    }
+  }
+
+  return into;
+}
+
+const entries = Object.assign(
+  { locales: resolve('app', 'javascript', 'locales') },
+  localePacks,
+  reducePacks(core),
+  Object.values(flavours).reduce((map, data) => reducePacks(data, map), {})
+);
 
-const extensionGlob = `**/*{${settings.extensions.join(',')}}*`;
-const entryPath = join(settings.source_path, settings.source_entry_path);
-const packPaths = sync(join(entryPath, extensionGlob));
 
 module.exports = {
-  entry: Object.assign(
-    packPaths.reduce((map, entry) => {
-      const localMap = map;
-      const namespace = relative(join(entryPath), dirname(entry));
-      localMap[join(namespace, basename(entry, extname(entry)))] = resolve(entry);
-      return localMap;
-    }, {}),
-    localePackPaths.reduce((map, entry) => {
-      const localMap = map;
-      localMap[basename(entry, extname(entry, extname(entry)))] = resolve(entry);
-      return localMap;
-    }, {}),
-    Object.keys(themes).reduce((themePaths, name) => {
-      themePaths[name] = resolve(join(settings.source_path, themes[name]));
-      return themePaths;
-    }, {}),
-  ),
+  entry: entries,
 
   output: {
     filename: 'js/[name]-[chunkhash].js',
@@ -44,7 +68,7 @@ module.exports = {
 
   optimization: {
     runtimeChunk: {
-      name: 'common',
+      name: 'locales',
     },
     splitChunks: {
       cacheGroups: {
@@ -52,7 +76,9 @@ module.exports = {
         vendors: false,
         common: {
           name: 'common',
-          chunks: 'all',
+          chunks (chunk) {
+            return !(chunk.name in entries);
+          },
           minChunks: 2,
           minSize: 0,
           test: /^(?!.*[\\/]node_modules[\\/]react-intl[\\/]).+$/,
diff --git a/config/webpack/translationRunner.js b/config/webpack/translationRunner.js
index 38050baf8..c7f84cc7e 100644
--- a/config/webpack/translationRunner.js
+++ b/config/webpack/translationRunner.js
@@ -1,11 +1,12 @@
 const fs = require('fs');
 const path = require('path');
-const { default: manageTranslations } = require('react-intl-translations-manager');
+const { default: manageTranslations, readMessageFiles } = require('react-intl-translations-manager');
 
 const RFC5646_REGEXP = /^[a-z]{2,3}(?:-(?:x|[A-Za-z]{2,4}))*$/;
 
 const rootDirectory = path.resolve(__dirname, '..', '..');
-const translationsDirectory = path.resolve(rootDirectory, 'app', 'javascript', 'mastodon', 'locales');
+const externalDefaultMessages = path.resolve(rootDirectory, 'app', 'javascript', 'mastodon', 'locales', 'defaultMessages.json');
+const translationsDirectory = path.resolve(rootDirectory, 'app', 'javascript', 'flavours', 'glitch', 'locales');
 const messagesDirectory = path.resolve(rootDirectory, 'build', 'messages');
 const availableLanguages = fs.readdirSync(translationsDirectory).reduce((languages, filename) => {
   const basename = path.basename(filename, '.json');
@@ -86,6 +87,25 @@ validateLanguages(languages, [
   !argv.force && testAvailability,
 ].filter(Boolean));
 
+// Override `provideExtractedMessages` to ignore translation strings provided upstream already
+const provideExtractedMessages = () => {
+  const extractedMessages = readMessageFiles(messagesDirectory);
+  const originalExtractedMessages = JSON.parse(fs.readFileSync(externalDefaultMessages, 'utf8'));
+  const originalKeys = new Set();
+
+  originalExtractedMessages.forEach(file => {
+    file.descriptors.forEach(descriptor => {
+      originalKeys.add(descriptor.id)
+    });
+  });
+
+  extractedMessages.forEach(file => {
+    file.descriptors = file.descriptors.filter((descriptor) => !originalKeys.has(descriptor.id));
+  });
+
+  return extractedMessages.filter((file) => file.descriptors.length > 0);
+};
+
 // manage translations
 manageTranslations({
   messagesDirectory,
@@ -96,4 +116,7 @@ manageTranslations({
   jsonOptions: {
     trailingNewline: true,
   },
+  overrideCoreMethods: {
+    provideExtractedMessages,
+  },
 });