diff --git a/client/build/make_replacements.mjs b/client/build/make_replacements.mjs
new file mode 100644
index 0000000000..038cc37da0
--- /dev/null
+++ b/client/build/make_replacements.mjs
@@ -0,0 +1,23 @@
+// Copyright 2024 The Outline Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import replace from 'replace-in-file';
+
+export async function makeReplacements(replacements) {
+ let results = [];
+
+ for (const replacement of replacements) {
+ results = [...results, ...(await replace(replacement))];
+ }
+}
diff --git a/client/package.json b/client/package.json
index a1d6cdc737..816500fa43 100644
--- a/client/package.json
+++ b/client/package.json
@@ -95,6 +95,7 @@
"eslint-plugin-import": "^2.26.0",
"esm": "^3.2.25",
"file-loader": "^6.2.0",
+ "folder-hash": "^4.0.4",
"html-webpack-plugin": "^5.1.0",
"husky": "^1.3.1",
"i18n-strings-files": "^2.0.0",
diff --git a/client/src/cordova/dev.action.mjs b/client/src/cordova/dev.action.mjs
new file mode 100644
index 0000000000..9cce62cd7e
--- /dev/null
+++ b/client/src/cordova/dev.action.mjs
@@ -0,0 +1,142 @@
+// Copyright 2022 The Outline Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import os from 'os';
+import path from 'path';
+import url from 'url';
+
+import {createReloadServer} from '@outline/infrastructure/build/create_reload_server.mjs';
+import {getRootDir} from '@outline/infrastructure/build/get_root_dir.mjs';
+import {runAction} from '@outline/infrastructure/build/run_action.mjs';
+import {spawnStream} from '@outline/infrastructure/build/spawn_stream.mjs';
+import {hashElement} from 'folder-hash';
+import * as fs from 'fs-extra';
+import minimist from 'minimist';
+
+import {makeReplacements} from '../../build/make_replacements.mjs';
+
+const OUTPUT_PATH = 'output/build/client/macos';
+const OUTLINE_APP_PATH = 'Debug/Outline.app';
+const OUTLINE_APP_WWW_PATH = 'Contents/Resources/www';
+const RELOAD_SERVER_PORT = 35729;
+
+const getUIHash = async () => {
+ const hashResult = await hashElement(
+ path.join(getRootDir(), 'client/src/www'),
+ {
+ files: {include: ['**/*.ts', '**/*.html', '**/*.css', '**/*.js']},
+ }
+ );
+
+ return hashResult.hash;
+};
+
+/**
+ * @description Runs the cordova app in development mode.
+ *
+ * @param {string[]} parameters
+ */
+export async function main(...givenParameters) {
+ const {
+ _: [platform = 'macos'],
+ buildMode = 'release',
+ sentryDsn = 'https://public@sentry.example.com/1',
+ versionName = '0.0.0-dev',
+ } = minimist(givenParameters);
+
+ if (platform !== 'macos') {
+ throw new Error('This action currently only works for the MacOS platform.');
+ }
+
+ if (buildMode !== 'release') {
+ throw new Error('MacOS only renders properly in the release build mode.');
+ }
+
+ if (os.platform() !== 'darwin') {
+ throw new Error('You must be on MacOS to develop for MacOS.');
+ }
+
+ const parameters = [
+ 'macos',
+ '--buildMode=release',
+ `--sentryDsn=${sentryDsn}`,
+ `--versionName=${versionName}`,
+ ];
+
+ await runAction('client/src/www/build', ...parameters);
+ await runAction('client/go/build', ...parameters);
+ await runAction('client/src/cordova/setup', ...parameters);
+
+ await makeReplacements([
+ {
+ files: path.join(
+ getRootDir(),
+ 'client/platforms/osx/**/index_cordova.html'
+ ),
+ from: '',
+ to: `
+
+ `,
+ },
+ ]);
+
+ await spawnStream(
+ 'xcodebuild',
+ '-scheme',
+ 'Outline',
+ '-workspace',
+ path.join(getRootDir(), 'client/src/cordova/apple/macos.xcworkspace'),
+ `SYMROOT=${path.join(getRootDir(), OUTPUT_PATH)}`
+ );
+
+ await spawnStream(
+ 'open',
+ path.join(getRootDir(), OUTPUT_PATH, OUTLINE_APP_PATH)
+ );
+
+ let previousUIHashResult = await getUIHash();
+
+ console.log(`Starting reload server @ port ${RELOAD_SERVER_PORT}...`);
+ createReloadServer(async () => {
+ const currentUIHashResult = await getUIHash();
+
+ if (previousUIHashResult === currentUIHashResult) {
+ return false;
+ }
+
+ previousUIHashResult = currentUIHashResult;
+
+ await runAction('client/src/www/build', ...parameters);
+
+ await fs.copy(
+ path.join(getRootDir(), 'client/www'),
+ path.join(OUTPUT_PATH, OUTLINE_APP_PATH, OUTLINE_APP_WWW_PATH)
+ );
+
+ return true;
+ }).listen(RELOAD_SERVER_PORT);
+}
+
+if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {
+ await main(...process.argv.slice(2));
+}
diff --git a/client/src/cordova/setup.action.mjs b/client/src/cordova/setup.action.mjs
index 3e4c390f95..9d7770c06b 100644
--- a/client/src/cordova/setup.action.mjs
+++ b/client/src/cordova/setup.action.mjs
@@ -21,10 +21,10 @@ import {runAction} from '@outline/infrastructure/build/run_action.mjs';
import {spawnStream} from '@outline/infrastructure/build/spawn_stream.mjs';
import chalk from 'chalk';
import cordovaLib from 'cordova-lib';
-import replace from 'replace-in-file';
import rmfr from 'rmfr';
import {getBuildParameters} from '../../build/get_build_parameters.mjs';
+import {makeReplacements} from '../../build/make_replacements.mjs';
const {cordova} = cordovaLib;
const WORKING_CORDOVA_OSX_COMMIT = '07e62a53aa6a8a828fd988bc9e884c38c3495a67';
@@ -37,17 +37,20 @@ const WORKING_CORDOVA_OSX_COMMIT = '07e62a53aa6a8a828fd988bc9e884c38c3495a67';
* @param {string[]} parameters
*/
export async function main(...parameters) {
- const {platform, buildMode, verbose, buildNumber, versionName} = getBuildParameters(parameters);
+ const {platform, buildMode, verbose, buildNumber, versionName} =
+ getBuildParameters(parameters);
await runAction('client/src/www/build', ...parameters);
await runAction('client/go/build', ...parameters);
-
- const CORDOVA_PROJECT_DIR = path.resolve(getRootDir(), 'client');
+
+ const CORDOVA_PROJECT_DIR = path.resolve(getRootDir(), 'client');
await rmfr(path.resolve(CORDOVA_PROJECT_DIR, 'platforms'));
await rmfr(path.resolve(CORDOVA_PROJECT_DIR, 'plugins'));
if (verbose) {
- cordova.on('verbose', message => console.debug(`[cordova:verbose] ${message}`));
+ cordova.on('verbose', message =>
+ console.debug(`[cordova:verbose] ${message}`)
+ );
}
// this is so cordova doesn't complain about not being in a cordova project
@@ -57,7 +60,9 @@ export async function main(...parameters) {
case 'android' + 'debug':
return androidDebug(verbose);
case 'android' + 'release':
- console.warn('NOTE: You must open the Outline.zip file after building to upload to the Play Store.');
+ console.warn(
+ 'NOTE: You must open the Outline.zip file after building to upload to the Play Store.'
+ );
return androidRelease(versionName, buildNumber, verbose);
case 'ios' + 'debug':
case 'maccatalyst' + 'debug':
@@ -86,14 +91,6 @@ async function androidDebug(verbose) {
});
}
-async function makeReplacements(replacements) {
- let results = [];
-
- for (const replacement of replacements) {
- results = [...results, ...(await replace(replacement))];
- }
-}
-
async function androidRelease(versionName, buildNumber, verbose) {
await cordova.prepare({
platforms: ['android'],
@@ -101,8 +98,20 @@ async function androidRelease(versionName, buildNumber, verbose) {
verbose,
});
- const manifestXmlGlob = path.join(getRootDir(), 'platforms', 'android', '**', 'AndroidManifest.xml');
- const configXmlGlob = path.join(getRootDir(), 'platforms', 'android', '**', 'config.xml');
+ const manifestXmlGlob = path.join(
+ getRootDir(),
+ 'platforms',
+ 'android',
+ '**',
+ 'AndroidManifest.xml'
+ );
+ const configXmlGlob = path.join(
+ getRootDir(),
+ 'platforms',
+ 'android',
+ '**',
+ 'config.xml'
+ );
await makeReplacements([
{
@@ -130,7 +139,9 @@ async function androidRelease(versionName, buildNumber, verbose) {
async function appleIosDebug(verbose) {
if (os.platform() !== 'darwin') {
- throw new Error('Building an Apple binary requires xcodebuild and can only be done on MacOS');
+ throw new Error(
+ 'Building an Apple binary requires xcodebuild and can only be done on MacOS'
+ );
}
await cordova.prepare({
@@ -140,19 +151,32 @@ async function appleIosDebug(verbose) {
});
// TODO(daniellacosse): move this to a cordova hook
- await spawnStream('rsync', '-avc', 'src/cordova/apple/xcode/ios/', 'platforms/ios/');
+ await spawnStream(
+ 'rsync',
+ '-avc',
+ 'src/cordova/apple/xcode/ios/',
+ 'platforms/ios/'
+ );
}
async function appleMacOsDebug(verbose) {
if (os.platform() !== 'darwin') {
- throw new Error('Building an Apple binary requires xcodebuild and can only be done on MacOS');
+ throw new Error(
+ 'Building an Apple binary requires xcodebuild and can only be done on MacOS'
+ );
}
console.warn(
- chalk.yellow('Debug mode on the MacOS client is currently broken. Try running with `--buildMode=release` instead.')
+ chalk.yellow(
+ 'Debug mode on the MacOS client is currently broken. Try running with `--buildMode=release` instead.'
+ )
);
- await cordova.platform('add', [`github:apache/cordova-osx#${WORKING_CORDOVA_OSX_COMMIT}`], {save: false});
+ await cordova.platform(
+ 'add',
+ [`github:apache/cordova-osx#${WORKING_CORDOVA_OSX_COMMIT}`],
+ {save: false}
+ );
await cordova.prepare({
platforms: ['osx'],
@@ -161,7 +185,12 @@ async function appleMacOsDebug(verbose) {
});
// TODO(daniellacosse): move this to a cordova hook
- await spawnStream('rsync', '-avc', 'src/cordova/apple/xcode/macos/', 'platforms/osx/');
+ await spawnStream(
+ 'rsync',
+ '-avc',
+ 'src/cordova/apple/xcode/macos/',
+ 'platforms/osx/'
+ );
}
async function setAppleVersion(platform, versionName, buildNumber) {
@@ -181,7 +210,9 @@ async function setAppleVersion(platform, versionName, buildNumber) {
async function appleIosRelease(version, buildNumber, verbose) {
if (os.platform() !== 'darwin') {
- throw new Error('Building an Apple binary requires xcodebuild and can only be done on MacOS');
+ throw new Error(
+ 'Building an Apple binary requires xcodebuild and can only be done on MacOS'
+ );
}
await cordova.prepare({
@@ -191,17 +222,28 @@ async function appleIosRelease(version, buildNumber, verbose) {
});
// TODO(daniellacosse): move this to a cordova hook
- await spawnStream('rsync', '-avc', 'src/cordova/apple/xcode/ios/', 'platforms/ios/');
+ await spawnStream(
+ 'rsync',
+ '-avc',
+ 'src/cordova/apple/xcode/ios/',
+ 'platforms/ios/'
+ );
await setAppleVersion('ios', version, buildNumber);
}
async function appleMacOsRelease(version, buildNumber, verbose) {
if (os.platform() !== 'darwin') {
- throw new Error('Building an Apple binary requires xcodebuild and can only be done on MacOS');
+ throw new Error(
+ 'Building an Apple binary requires xcodebuild and can only be done on MacOS'
+ );
}
- await cordova.platform('add', [`github:apache/cordova-osx#${WORKING_CORDOVA_OSX_COMMIT}`], {save: false});
+ await cordova.platform(
+ 'add',
+ [`github:apache/cordova-osx#${WORKING_CORDOVA_OSX_COMMIT}`],
+ {save: false}
+ );
await cordova.prepare({
platforms: ['osx'],
@@ -210,7 +252,12 @@ async function appleMacOsRelease(version, buildNumber, verbose) {
});
// TODO(daniellacosse): move this to a cordova hook
- await spawnStream('rsync', '-avc', 'src/cordova/apple/xcode/macos/', 'platforms/osx/');
+ await spawnStream(
+ 'rsync',
+ '-avc',
+ 'src/cordova/apple/xcode/macos/',
+ 'platforms/osx/'
+ );
await setAppleVersion('osx', version, buildNumber);
}
diff --git a/client/src/www/index_cordova.html b/client/src/www/index_cordova.html
index 6e2181e4a2..1ca43c19a7 100644
--- a/client/src/www/index_cordova.html
+++ b/client/src/www/index_cordova.html
@@ -26,5 +26,4 @@
-