diff --git a/Core/core.php b/Core/core.php index ef7e264..09464d9 100644 --- a/Core/core.php +++ b/Core/core.php @@ -10,7 +10,7 @@ if (is_file($autoLoad)) { require_once $autoLoad; } -const WEBBASE_VERSION = "2.3.0"; +const WEBBASE_VERSION = "2.3.1"; spl_autoload_extensions(".php"); spl_autoload_register(function ($class) { diff --git a/README.md b/README.md index cee3457..3d60f23 100644 --- a/README.md +++ b/README.md @@ -305,7 +305,17 @@ php cli.php routes remove 1 php cli.php routes enable 1 php cli.php routes disable 1 php cli.php routes add /some/path static /static/test.html -php.cli.php routes modify 1 '/regex(/.*)?' dynamic '\\Documents\\Test' +php cli.php routes modify 1 '/regex(/.*)?' dynamic '\\Documents\\Test' +``` + +### Frontend commands +```bash +# Frontend commands +php cli.php frontend build +php cli.php frontend ls +php cli.php frontend add +php cli.php frontend rm +php cli.php frontend dev ``` ## Anything more? diff --git a/cli.php b/cli.php index 3312d3b..25ade96 100644 --- a/cli.php +++ b/cli.php @@ -50,16 +50,6 @@ if ($database->getProperty("isDocker", false) && !is_file("/.dockerenv")) { } } -if ($database->getProperty("isDocker", false) && !is_file("/.dockerenv")) { - printLine("Detected docker environment in config, running docker exec..."); - if (count($argv) < 3 || $argv[1] !== "db" || !in_array($argv[2], ["shell", "import", "export"])) { - $containerName = $dockerYaml["services"]["php"]["container_name"]; - $command = array_merge(["docker", "exec", "-it", $containerName, "php"], $argv); - $proc = proc_open($command, [1 => STDOUT, 2 => STDERR], $pipes); - exit(proc_close($proc)); - } -} - function connectSQL(): ?SQL { global $context; $sql = $context->initSQL(); @@ -662,40 +652,180 @@ function onImpersonate($argv): void { echo "session=" . $session->getUUID() . PHP_EOL; } -$argv = $_SERVER['argv']; -if (count($argv) < 2) { - _exit("Usage: cli.php [options...]"); +function onFrontend(array $argv): void { + if (count($argv) < 3) { + _exit("Usage: cli.php frontend [options...]"); + } + + $reactRoot = realpath(WEBROOT . "/react/"); + if (!$reactRoot) { + _exit("React root directory not found!"); + } + + $action = $argv[2] ?? null; + if ($action === "build") { + $proc = proc_open(["yarn", "run", "build"], [1 => STDOUT, 2 => STDERR], $pipes, $reactRoot); + exit(proc_close($proc)); + } else if ($action === "add") { + if (count($argv) < 4) { + _exit("Usage: cli.php frontend add "); + } + + $moduleName = strtolower($argv[3]); + if (!preg_match("/[a-z0-9_-]/", $moduleName)) { + _exit("Module name should only be [a-zA-Z0-9_-]"); + } + + $templatePath = implode(DIRECTORY_SEPARATOR, [$reactRoot, "_tmpl"]); + $modulePath = implode(DIRECTORY_SEPARATOR, [$reactRoot, $moduleName]); + if (file_exists($modulePath)) { + _exit("File or module does already exist: " . $modulePath); + } + + $rootPackageJsonPath = implode(DIRECTORY_SEPARATOR, [$reactRoot, "package.json"]); + $rootPackageJson = @json_decode(@file_get_contents($rootPackageJsonPath), true); + if (!$rootPackageJson) { + _exit("Unable to read root package.json"); + } + + $reactVersion = $rootPackageJson["dependencies"]["react"]; + if (!array_key_exists($moduleName, $rootPackageJson["targets"])) { + $rootPackageJson["targets"][$moduleName] = [ + "source" => "./$moduleName/src/index.js", + "distDir" => "./dist/$moduleName" + ]; + file_put_contents($rootPackageJsonPath, json_encode($rootPackageJson, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES)); + } + + mkdir($modulePath, 0775, true); + $placeHolders = [ + "MODULE_NAME" => $moduleName, + "REACT_VERSION" => $reactVersion + ]; + + $it = new RecursiveDirectoryIterator($templatePath); + foreach (new RecursiveIteratorIterator($it) as $file) { + $fileName = $file->getFilename(); + $relDir = substr($file->getPath(), strlen($templatePath) + 1); + $targetFile = implode(DIRECTORY_SEPARATOR, [$modulePath, $relDir, $fileName]); + if ($file->isFile()) { + $contents = file_get_contents($file); + foreach ($placeHolders as $key => $value) { + $contents = str_replace("{{{$key}}}", $value, $contents); + } + $directory = dirname($targetFile); + if (!is_dir($directory)) { + mkdir($directory, 0775, true); + } + + file_put_contents($targetFile, $contents); + } + } + + printLine("Successfully added react module: $moduleName"); + printLine("Run `php cli.php frontend build` to create a production build or"); + printLine("run `php cli.php frontend dev $moduleName` to start a dev-server with your module"); + } else if ($action === "dev") { + if (count($argv) < 4) { + _exit("Usage: cli.php frontend add "); + } + + $moduleName = strtolower($argv[3]); + $proc = proc_open(["yarn", "workspace", $moduleName, "run", "dev"], [1 => STDOUT, 2 => STDERR], $pipes, $reactRoot); + exit(proc_close($proc)); + } else if ($action === "rm") { + if (count($argv) < 4) { + _exit("Usage: cli.php frontend add "); + } + + $moduleName = strtolower($argv[3]); + if (!preg_match("/[a-z0-9_-]/", $moduleName)) { + _exit("Module name should only be [a-zA-Z0-9_-]"); + } + + $modulePath = implode(DIRECTORY_SEPARATOR, [$reactRoot, $moduleName]); + if (!is_dir($modulePath)) { + _exit("Module not found: $modulePath"); + } + + $rootPackageJsonPath = implode(DIRECTORY_SEPARATOR, [$reactRoot, "package.json"]); + $rootPackageJson = @json_decode(@file_get_contents($rootPackageJsonPath), true); + if (!$rootPackageJson) { + _exit("Unable to read root package.json"); + } + + if (array_key_exists($moduleName, $rootPackageJson["targets"])) { + unset($rootPackageJson["targets"][$moduleName]); + file_put_contents($rootPackageJsonPath, json_encode($rootPackageJson, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES)); + } + + $input = strtolower(trim(readline("Do you want to disable the module only and keep the files? [Y|n]: "))); + if ($input === "n") { + rrmdir($modulePath); + printLine("Disabled and deleted module: $moduleName"); + } else { + printLine("Disabled module: $moduleName"); + } + } else if ($action === "ls") { + printLine("Current available modules:"); + foreach (glob(implode(DIRECTORY_SEPARATOR, [$reactRoot, "*"]), GLOB_ONLYDIR) as $directory) { + if (basename($directory) === "_tmpl") { + continue; + } + + $packageJson = realpath(implode(DIRECTORY_SEPARATOR, [$directory, "package.json"])); + if ($packageJson) { + $packageJsonContents = @json_decode(@file_get_contents($packageJson), true); + if (!$packageJsonContents) { + printLine("$directory: Unable to read package.json"); + } else { + $packageName = $packageJsonContents["name"]; + $packageVersion = $packageJsonContents["version"]; + printLine("- $packageName version: $packageVersion"); + } + } + } + } else { + _exit("Usage: cli.php frontend [options...]"); + } } -$command = $argv[1]; -switch ($command) { - case 'help': - printHelp($argv); - exit; - case 'db': - handleDatabase($argv); - break; - case 'routes': - onRoutes($argv); - break; - case 'maintenance': - onMaintenance($argv); - break; - case 'test': - onTest($argv); - break; - case 'mail': - onMail($argv); - break; - case 'settings': - onSettings($argv); - break; - case 'impersonate': - onImpersonate($argv); - break; - default: +$argv = $_SERVER['argv']; +$registeredCommands = [ + "help" => ["handler" => "printHelp"], + "db" => ["handler" => "handleDatabase", "requiresDocker" => ["shell", "import", "export"]], + "routes" => ["handler" => "onRoutes"], + "maintenance" => ["handler" => "onMaintenance"], + "test" => ["handler" => "onTest"], + "mail" => ["handler" => "onMail"], + "settings" => ["handler" => "onSettings"], + "impersonate" => ["handler" => "onImpersonate"], + "frontend" => ["handler" => "onFrontend"], +]; + + +if (count($argv) < 2) { + _exit("Usage: cli.php <" . implode("|", array_keys($registeredCommands)) . "> [options...]"); +} else { + $command = $argv[1]; + if (array_key_exists($command, $registeredCommands)) { + + if ($database->getProperty("isDocker", false) && !is_file("/.dockerenv")) { + $requiresDocker = in_array($argv[2] ?? null, $registeredCommands[$command]["requiresDocker"] ?? []); + if ($requiresDocker) { + printLine("Detected docker environment in config, running docker exec..."); + $containerName = $dockerYaml["services"]["php"]["container_name"]; + $command = array_merge(["docker", "exec", "-it", $containerName, "php"], $argv); + $proc = proc_open($command, [1 => STDOUT, 2 => STDERR], $pipes); + exit(proc_close($proc)); + } + } + + call_user_func($registeredCommands[$command]["handler"], $argv); + } else { printLine("Unknown command '$command'"); printLine(); printHelp($argv); exit; -} \ No newline at end of file + } +} diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile index 7cfda89..1f66f21 100644 --- a/docker/php/Dockerfile +++ b/docker/php/Dockerfile @@ -5,7 +5,7 @@ WORKDIR "/application" ARG ADDITIONAL_PACKAGES ARG ADDITIONAL_SCRIPT -RUN mkdir -p /application/core/Configuration /var/www/.gnupg && \ +RUN mkdir -p /application/Core/Configuration /var/www/.gnupg && \ chown -R www-data:www-data /application /var/www/ && \ chmod 700 /var/www/.gnupg diff --git a/react/_tmpl/config-overrides.js b/react/_tmpl/config-overrides.js new file mode 100644 index 0000000..4595006 --- /dev/null +++ b/react/_tmpl/config-overrides.js @@ -0,0 +1,27 @@ +const fs = require('fs'); +const path = require('path'); +const { + override, + removeModuleScopePlugin, + babelInclude, + addWebpackModuleRule, +} = require('customize-cra'); + +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); +const addMiniCssExtractPlugin = config => { + config.plugins.push(new MiniCssExtractPlugin()); + return config; +} + +module.exports = override( + removeModuleScopePlugin(), + addMiniCssExtractPlugin, + addWebpackModuleRule({ + test: /\.css$/, + use: [ MiniCssExtractPlugin.loader, 'css-loader' ] + }), + babelInclude([ + path.resolve(path.join(__dirname, 'src')), + fs.realpathSync(path.join(__dirname, '../shared')), + ]), +); \ No newline at end of file diff --git a/react/_tmpl/package.json b/react/_tmpl/package.json new file mode 100644 index 0000000..74789ac --- /dev/null +++ b/react/_tmpl/package.json @@ -0,0 +1,28 @@ +{ + "name": "{{MODULE_NAME}}", + "version": "1.0.0", + "dependencies": { + "shared": "link:../shared", + "react": "{{REACT_VERSION}}" + }, + "scripts": { + "dev": "react-app-rewired start" + }, + "author": "", + "license": "ISC", + "description": "", + "devDependencies": {}, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "proxy": "http://localhost" +} diff --git a/react/_tmpl/src/App.jsx b/react/_tmpl/src/App.jsx new file mode 100644 index 0000000..3745770 --- /dev/null +++ b/react/_tmpl/src/App.jsx @@ -0,0 +1,9 @@ +import {useMemo} from "react"; +import API from "shared/api"; + +export default function App() { + + const api = useMemo(() => new API(), []); + + return <> +} \ No newline at end of file diff --git a/react/_tmpl/src/index.js b/react/_tmpl/src/index.js new file mode 100644 index 0000000..003df4d --- /dev/null +++ b/react/_tmpl/src/index.js @@ -0,0 +1,9 @@ +import React from "react"; +import {createRoot} from "react-dom/client"; +import App from "./App"; +import {LocaleProvider} from "shared/locale"; + +const root = createRoot(document.getElementById('{{MODULE_NAME}}')); +root.render( + +); diff --git a/react/package.json b/react/package.json index 3ea199a..a16298c 100644 --- a/react/package.json +++ b/react/package.json @@ -1,66 +1,66 @@ { - "name": "web-base-frontend", - "version": "1.0.0", - "description": "", - "private": true, - "targets": { - "admin-panel": { - "source": "./admin-panel/src/index.js", - "distDir": "./dist/admin-panel" - } - }, - "workspaces": [ - "admin-panel" - ], - "scripts": { - "build": "parcel build", - "clean": "rm -rfd .parcel-cache dist/*" - }, - "author": "", - "license": "ISC", - "devDependencies": { - "@babel/core": "^7.20.5", - "@babel/plugin-transform-react-jsx": "^7.19.0", - "customize-cra": "^1.0.0", - "parcel": "^2.8.0", - "react-app-rewired": "^2.2.1", - "react-scripts": "^5.0.1" - }, - "@parcel/bundler-default": { - "minBundles": 1, - "minBundleSize": 3000, - "maxParallelRequests": 1 - }, - "dependencies": { - "@emotion/react": "^11.10.5", - "@emotion/styled": "^11.10.5", - "@material-ui/core": "^4.12.4", - "@material-ui/icons": "^4.11.3", - "@material-ui/lab": "^4.0.0-alpha.61", - "@mui/icons-material": "^5.11.0", - "@mui/material": "^5.11.3", - "chart.js": "^4.0.1", - "clsx": "^1.2.1", - "date-fns": "^2.29.3", - "material-ui-color-picker": "^3.5.1", - "mini-css-extract-plugin": "^2.7.1", - "react": "^18.2.0", - "react-chartjs-2": "^5.0.1", - "react-collapse": "^5.1.1", - "react-dom": "^18.2.0", - "react-router-dom": "^6.6.2", - "sprintf-js": "^1.1.2" - }, - "browserslist": { - "production": [ - ">0.2%", - "not dead", - "not op_mini all" + "name": "web-base-frontend", + "version": "1.0.0", + "description": "", + "private": true, + "targets": { + "admin-panel": { + "source": "./admin-panel/src/index.js", + "distDir": "./dist/admin-panel" + } + }, + "workspaces": [ + "admin-panel" ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] - } -} + "scripts": { + "build": "parcel build", + "clean": "rm -rfd .parcel-cache dist/*" + }, + "author": "", + "license": "ISC", + "devDependencies": { + "@babel/core": "^7.20.5", + "@babel/plugin-transform-react-jsx": "^7.19.0", + "customize-cra": "^1.0.0", + "parcel": "^2.8.0", + "react-app-rewired": "^2.2.1", + "react-scripts": "^5.0.1" + }, + "@parcel/bundler-default": { + "minBundles": 1, + "minBundleSize": 3000, + "maxParallelRequests": 1 + }, + "dependencies": { + "@emotion/react": "^11.10.5", + "@emotion/styled": "^11.10.5", + "@material-ui/core": "^4.12.4", + "@material-ui/icons": "^4.11.3", + "@material-ui/lab": "^4.0.0-alpha.61", + "@mui/icons-material": "^5.11.0", + "@mui/material": "^5.11.3", + "chart.js": "^4.0.1", + "clsx": "^1.2.1", + "date-fns": "^2.29.3", + "material-ui-color-picker": "^3.5.1", + "mini-css-extract-plugin": "^2.7.1", + "react": "^18.2.0", + "react-chartjs-2": "^5.0.1", + "react-collapse": "^5.1.1", + "react-dom": "^18.2.0", + "react-router-dom": "^6.6.2", + "sprintf-js": "^1.1.2" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} \ No newline at end of file diff --git a/react/yarn.lock b/react/yarn.lock index a782add..35de9c8 100644 --- a/react/yarn.lock +++ b/react/yarn.lock @@ -2487,10 +2487,10 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.6.tgz#cee20bd55e68a1720bdab363ecf0c821ded4cd45" integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw== -"@remix-run/router@1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.0.3.tgz#953b88c20ea00d0eddaffdc1b115c08474aa295d" - integrity sha512-ceuyTSs7PZ/tQqi19YZNBc5X7kj1f8p+4DIyrcIYFY9h+hd1OKm4RqtiWldR9eGEvIiJfsqwM4BsuCtRIuEw6Q== +"@remix-run/router@1.15.3": + version "1.15.3" + resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.15.3.tgz#d2509048d69dbb72d5389a14945339f1430b2d3c" + integrity sha512-Oy8rmScVrVxWZVOpEF57ovlnhpZ8CCPlnIIumVcV9nFdiSIrus99+Lw78ekXyGvVDlIsFJbSfmSovJUhCWYV3w== "@rollup/plugin-babel@^5.2.0": version "5.3.1" @@ -3838,9 +3838,9 @@ caniuse-api@^3.0.0: lodash.uniq "^4.5.0" caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001400, caniuse-lite@^1.0.30001426: - version "1.0.30001435" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001435.tgz#502c93dbd2f493bee73a408fe98e98fb1dad10b2" - integrity sha512-kdCkUTjR+v4YAJelyiDTqiu82BDr4W4CP5sgTA0ZBmqn30XfS2ZghPLMowik9TPhS+psWJiUNxsqLyurDbmutA== + version "1.0.30001600" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001600.tgz" + integrity sha512-+2S9/2JFhYmYaDpZvo0lKkfvuKIglrx68MwOBqMGHhQsNkLjB5xtc/TGoEPs+MxjSyN/72qer2g97nzR641mOQ== case-sensitive-paths-webpack-plugin@^2.4.0: version "2.4.0" @@ -8584,20 +8584,20 @@ react-refresh@^0.9.0: resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.9.0.tgz#71863337adc3e5c2f8a6bfddd12ae3bfe32aafbf" integrity sha512-Gvzk7OZpiqKSkxsQvO/mbTN1poglhmAV7gR/DdIrRrSMXraRQQlfikRJOr3Nb9GTMPC5kof948Zy6jJZIFtDvQ== -react-router-dom@^6.4.3: - version "6.4.3" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.4.3.tgz#70093b5f65f85f1df9e5d4182eb7ff3a08299275" - integrity sha512-MiaYQU8CwVCaOfJdYvt84KQNjT78VF0TJrA17SIQgNHRvLnXDJO6qsFqq8F/zzB1BWZjCFIrQpu4QxcshitziQ== +react-router-dom@^6.6.2: + version "6.22.3" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.22.3.tgz#9781415667fd1361a475146c5826d9f16752a691" + integrity sha512-7ZILI7HjcE+p31oQvwbokjk6OA/bnFxrhJ19n82Ex9Ph8fNAq+Hm/7KchpMGlTgWhUxRHMMCut+vEtNpWpowKw== dependencies: - "@remix-run/router" "1.0.3" - react-router "6.4.3" + "@remix-run/router" "1.15.3" + react-router "6.22.3" -react-router@6.4.3: - version "6.4.3" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.4.3.tgz#9ed3ee4d6e95889e9b075a5d63e29acc7def0d49" - integrity sha512-BT6DoGn6aV1FVP5yfODMOiieakp3z46P1Fk0RNzJMACzE7C339sFuHebfvWtnB4pzBvXXkHP2vscJzWRuUjTtA== +react-router@6.22.3: + version "6.22.3" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.22.3.tgz#9d9142f35e08be08c736a2082db5f0c9540a885e" + integrity sha512-dr2eb3Mj5zK2YISHK++foM9w4eBnO23eKnZEDs7c880P6oKbrjz/Svg9+nxqtHQK+oMW4OtjZca0RqPglXxguQ== dependencies: - "@remix-run/router" "1.0.3" + "@remix-run/router" "1.15.3" react-scripts@^5.0.1: version "5.0.1"