diff --git a/Dockerfile b/Dockerfile index fdbd4c0a..62d348c4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,7 +26,7 @@ COPY server . RUN cp .env.sample .env COPY --from=client-builder /app/build public -COPY --from=client-builder /app/build/index.html views +COPY --from=client-builder /app/build/index.html views/index.ejs VOLUME /app/public/user-avatars VOLUME /app/public/project-background-images diff --git a/client/config-overrides.js b/client/config-overrides.js new file mode 100644 index 00000000..53ef3908 --- /dev/null +++ b/client/config-overrides.js @@ -0,0 +1,56 @@ +const fs = require('fs'); +const path = require('path'); + +const BASE_URL_PLACEHOLDER = 'BASE_URL_PLACEHOLDER'; + +const replaceInFile = (file, search, replace) => { + fs.readFile(file, 'utf8', (readError, data) => { + if (readError) { + throw new Error(`${readError}`); + } + const res = data.replaceAll(search, replace); + fs.writeFile(file, res, 'utf8', (writeError) => { + if (writeError) { + throw new Error(`${writeError}`); + } + }); + }); +}; + +const replaceBaseUrl = (compiler) => { + compiler.hooks.assetEmitted.tap('ReplaceBaseUrlPlaceholder', (file, info) => { + if (info.content.indexOf(BASE_URL_PLACEHOLDER) >= 0) { + if (/\.css$/.exec(info.targetPath)) { + // For CSS 'url(...)' import we can use relative import + const relPath = path.relative(path.dirname(info.targetPath), info.outputPath); + replaceInFile(info.targetPath, BASE_URL_PLACEHOLDER, `${relPath}/`); + } else if (/\.js$/.exec(info.targetPath)) { + // For JS 'import ... from "some-asset"' we can get the variable injected in the window object + // eslint-disable-next-line no-template-curly-in-string + replaceInFile(info.targetPath, `"${BASE_URL_PLACEHOLDER}"`, '`${window.BASE_URL}/`'); + } else if (/index\.html$/.exec(info.targetPath)) { + // For the main html file, we set a placeholder for sails to inject the correct value as runtime + replaceInFile(info.targetPath, BASE_URL_PLACEHOLDER, '<%= BASE_URL %>'); + } + } + }); +}; + +module.exports = function override(config, env) { + if (env === 'production') { + const plugins = config.plugins.map((plugin) => { + if (plugin.constructor.name === 'InterpolateHtmlPlugin') { + const newPlugin = plugin; + newPlugin.replacements.PUBLIC_URL = BASE_URL_PLACEHOLDER; + return newPlugin; + } + return plugin; + }); + return { + ...config, + output: { ...config.output, publicPath: BASE_URL_PLACEHOLDER }, + plugins: [...plugins, { apply: replaceBaseUrl }], + }; + } + return config; +}; diff --git a/client/package-lock.json b/client/package-lock.json index 915cfd50..f69a43cd 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -63,6 +63,7 @@ "eslint-plugin-react-hooks": "^4.6.0", "jest-enzyme": "^7.1.2", "prettier": "2.7.1", + "react-app-rewired": "^2.2.1", "react-test-renderer": "^17.0.2" } }, @@ -18665,6 +18666,30 @@ "node": ">=14" } }, + "node_modules/react-app-rewired": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/react-app-rewired/-/react-app-rewired-2.2.1.tgz", + "integrity": "sha512-uFQWTErXeLDrMzOJHKp0h8P1z0LV9HzPGsJ6adOtGlA/B9WfT6Shh4j2tLTTGlXOfiVx6w6iWpp7SOC5pvk+gA==", + "dev": true, + "dependencies": { + "semver": "^5.6.0" + }, + "bin": { + "react-app-rewired": "bin/index.js" + }, + "peerDependencies": { + "react-scripts": ">=2.1.3" + } + }, + "node_modules/react-app-rewired/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, "node_modules/react-beautiful-dnd": { "version": "13.1.0", "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz", @@ -38717,6 +38742,23 @@ "whatwg-fetch": "^3.6.2" } }, + "react-app-rewired": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/react-app-rewired/-/react-app-rewired-2.2.1.tgz", + "integrity": "sha512-uFQWTErXeLDrMzOJHKp0h8P1z0LV9HzPGsJ6adOtGlA/B9WfT6Shh4j2tLTTGlXOfiVx6w6iWpp7SOC5pvk+gA==", + "dev": true, + "requires": { + "semver": "^5.6.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, "react-beautiful-dnd": { "version": "13.1.0", "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz", diff --git a/client/package.json b/client/package.json index f4f05ae7..d562336a 100755 --- a/client/package.json +++ b/client/package.json @@ -2,11 +2,11 @@ "name": "planka-client", "private": true, "scripts": { - "build": "react-scripts build", + "build": "react-app-rewired build", "eject": "react-scripts eject", - "lint": "eslint --ext js,jsx src", - "start": "react-scripts start", - "test": "react-scripts test" + "lint": "eslint --ext js,jsx src config-overrides.js", + "start": "react-app-rewired start", + "test": "react-app-rewired test" }, "browserslist": { "production": [ @@ -120,6 +120,7 @@ "eslint-plugin-react-hooks": "^4.6.0", "jest-enzyme": "^7.1.2", "prettier": "2.7.1", + "react-app-rewired": "^2.2.1", "react-test-renderer": "^17.0.2" } } diff --git a/client/public/index.html b/client/public/index.html index e8955e7e..fc7327a7 100755 --- a/client/public/index.html +++ b/client/public/index.html @@ -26,6 +26,7 @@ --> Planka +
diff --git a/client/src/api/socket.js b/client/src/api/socket.js index 2902661b..fa9fc443 100755 --- a/client/src/api/socket.js +++ b/client/src/api/socket.js @@ -5,7 +5,7 @@ import Config from '../constants/Config'; const io = sailsIOClient(socketIOClient); -io.sails.url = Config.SERVER_BASE_URL; +io.sails.url = Config.SERVER_HOST_NAME; io.sails.autoConnect = false; io.sails.reconnection = true; io.sails.useCORSRouteToGetCookie = false; @@ -13,6 +13,7 @@ io.sails.environment = process.env.NODE_ENV; const { socket } = io; +socket.path = `${Config.BASE_PATH}/socket.io`; socket.connect = socket._connect; // eslint-disable-line no-underscore-dangle ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].forEach((method) => { diff --git a/client/src/constants/Config.js b/client/src/constants/Config.js index 28fb3747..3035ebbf 100755 --- a/client/src/constants/Config.js +++ b/client/src/constants/Config.js @@ -1,6 +1,11 @@ +const { BASE_URL } = window; +const BASE_PATH = BASE_URL.replace(/^.*\/\/[^/]*(.*)[^?#]*.*$/, '$1'); + const SERVER_BASE_URL = process.env.REACT_APP_SERVER_BASE_URL || - (process.env.NODE_ENV === 'production' ? '' : 'http://localhost:1337'); + (process.env.NODE_ENV === 'production' ? BASE_URL : 'http://localhost:1337'); + +const SERVER_HOST_NAME = SERVER_BASE_URL.replace(/^(.*\/\/[^/?#]*).*$/, '$1'); const ACCESS_TOKEN_KEY = 'accessToken'; const ACCESS_TOKEN_VERSION_KEY = 'accessTokenVersion'; @@ -10,7 +15,9 @@ const POSITION_GAP = 65535; const ACTIVITIES_LIMIT = 50; export default { + BASE_PATH, SERVER_BASE_URL, + SERVER_HOST_NAME, ACCESS_TOKEN_KEY, ACCESS_TOKEN_VERSION_KEY, ACCESS_TOKEN_VERSION, diff --git a/client/src/constants/Paths.js b/client/src/constants/Paths.js index 1278d3cd..6f0d666a 100755 --- a/client/src/constants/Paths.js +++ b/client/src/constants/Paths.js @@ -1,8 +1,10 @@ -const ROOT = '/'; -const LOGIN = '/login'; -const PROJECTS = '/projects/:id'; -const BOARDS = '/boards/:id'; -const CARDS = '/cards/:id'; +import Config from './Config'; + +const ROOT = `${Config.BASE_PATH}/`; +const LOGIN = `${Config.BASE_PATH}/login`; +const PROJECTS = `${Config.BASE_PATH}/projects/:id`; +const BOARDS = `${Config.BASE_PATH}/boards/:id`; +const CARDS = `${Config.BASE_PATH}/cards/:id`; export default { ROOT, diff --git a/server/config/views.js b/server/config/views.js index 55ec7950..557ebf29 100644 --- a/server/config/views.js +++ b/server/config/views.js @@ -24,7 +24,7 @@ module.exports.views = { * */ - extension: 'html', + extension: 'ejs', /** * @@ -36,4 +36,5 @@ module.exports.views = { */ layout: false, + locals: { BASE_URL: process.env.BASE_URL }, };