├── .gitignore ├── README.md ├── _docs ├── create-your-first-snippet.md └── index.md ├── app ├── Helpers │ ├── Arr.php │ ├── Helper.php │ └── index.php ├── Hooks │ ├── Handlers │ │ ├── AdminMenuHandler.php │ │ └── CodeHandler.php │ ├── hooks.php │ └── index.php ├── Http │ ├── Controllers │ │ ├── SettingsController.php │ │ ├── SnippetsController.php │ │ └── index.php │ ├── index.php │ └── routes.php ├── Model │ ├── Snippet.php │ └── index.php ├── Services │ ├── CodeRunner.php │ ├── FluentSnippetCondition.php │ ├── PhpValidator.php │ ├── Router.php │ ├── Trans.php │ ├── index.php │ └── mu.stub └── index.php ├── build.sh ├── dist ├── app.js ├── app.js.LICENSE.txt ├── images │ └── logo.png ├── index.php └── mix-manifest.json ├── easy-code-manager.php ├── i18n.node.js ├── index.php ├── language ├── easy-code-manager.pot └── index.php ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── readme.txt ├── reserved18n.json ├── src ├── App.vue ├── Bits │ ├── Errors.js │ ├── Pagination.vue │ ├── Rest.js │ ├── Storage.js │ ├── common.js │ ├── data_config.js │ └── event-bus.js ├── app.js ├── app.scss ├── components │ ├── About.vue │ ├── AdvancedConditions.vue │ ├── ConfigSettings.vue │ ├── CreateSnippet.vue │ ├── Dashboard.vue │ ├── ExportImport │ │ ├── ExportSnippets.vue │ │ ├── ImportExportChoice.vue │ │ └── ImportSnippets.vue │ ├── FsnipSafeModesWarning.vue │ ├── SnippetEditView.vue │ ├── _CodeEditor.vue │ ├── _SelectPlus.vue │ ├── _SnippetForm.vue │ ├── _TagCreator.vue │ ├── _WhereRun.vue │ └── richFilters │ │ ├── Elements │ │ ├── _AjaxSelector.vue │ │ ├── _OptionSelector.vue │ │ └── _RestSelector.vue │ │ ├── FilterContainer.vue │ │ ├── FilterItem.vue │ │ └── RichFilters.vue ├── images │ └── logo.png └── routes.js └── webpack.mix.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | svn 3 | build.php 4 | trans.php 5 | builds 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## FluentSnippets 🚀 2 | #### The High-Performance Code Snippet Plugin for WP 3 | 4 | ---- 5 | 6 | [**Website**](https://fluentsnippets.com) | [**Download From WordPress**](https://wordpress.org/plugins/easy-code-manager/) 7 | 8 | ### Features 9 | - File-based Snippets (0 Database Query) 🚀 10 | - Custom Code Snippets (PHP/HTML/CSS/JS) 11 | - Advanced Conditional Logic 12 | - Automatic Error Handling 13 | - Custom Shortcode 14 | - Stand-alone Mode (keep running your snippets in stand-alone mode. No lock-in, use it whenever you want.) 15 | 16 | ### Why do we build FluentSnippets? 17 | Long story short, we manage many websites for our products and these are mostly content sites, our content team sometimes needs to add custom functionality for content placement and dynamic content blocks and we need a code snippets plugin as we manage our WP files via Git. We tried to use almost all the existing code snippets plugins and could not choose a single one because of the design decision these plugin authors made. Please read the full post to understand why we had to build a better solution to use our own. 18 | 19 | ### Design of FluentSnippets 20 | ![Design of FluentSnippets](https://fluentsnippets.com/wp-content/uploads/2023/12/fluent-snippets-plugin-design.png) 21 | 22 | You see, the design is super simple and this is what it should be! FluentSnippets stores the code snippets in the flat file and uses code blocks in each snippet file to add metadata like a description, title, conditional logic, snippet type, and other things. We also parse these data once and cache these into index.php so we don’t have to parse these code blocks in every request. Then on runtime, it just includes those files to your selected action hook. In the whole process, FluentSnippets runs 0 database queries. In fact, we don’t have any Database query in the whole plugin runtime. 23 | 24 | With this native design, FluentSnippets is native, secure by design, and the most performant code snippets in this category. 25 | 26 | ### The code editor 27 | The code editor of FluentSnippets is simple. We have used codemirror javascript library to add the code editor. It does not have advanced features like auto-complete. 28 | ![Code Editor](https://fluentsnippets.com/wp-content/uploads/2023/12/snippet-explained-2048x1362.png) 29 | 30 | ### Future of FluentSnippets 31 | Every plugin we build, we aim to solve problems for businesses and add immerse values. We are releasing FluentSnippets with the same goal and vision. We will continue imrove, innovate add features the community wants. Few things we are going to add in the next few weeks in this website 32 | 33 | - Code Snippets Library powered by the community and plugin authors 34 | - Community Forum for Support and discussions 35 | - Improve the Code Editor to support auto-complete 36 | - Adding more snippet locations 37 | - Adding more conditional logics 38 | 39 | You are more than welcome to contribute to the code or the documentation. Or helping us by recommending this to your friends and community. Together, let’s make the WordPress more powerful and secure. 40 | 41 | ### How to build 42 | - `npm install` 43 | - `npx mix watch` - For development 44 | - `npx mix --production` - For Production 45 | 46 | ### Building the plugin for WP Repo 47 | - `sh build.sh --loco --node-build` // Build the plugin for WP repo with Loco Translate and Node build 48 | -------------------------------------------------------------------------------- /_docs/create-your-first-snippet.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Create Your First Snippet 3 | slug: create-snippet 4 | tagline: Create Your First Snippet with FluentSnippets 5 | sidebar: true 6 | prev: true 7 | next: true 8 | editLink: true 9 | pageClass: docs-create 10 | menu_order: 1 11 | --- 12 | 13 | Ready to add your first code snippet with FluentSnippets? Follow this guide to get started. 14 | 15 | ## Adding a new snippet 16 | Get started by logging in to your WordPress admin. In that area look for “FluentSnippets” in the admin left sidebar menu and navigate to it and you can see all your snippets. 17 | 18 | ![FluentSnippets Menu](https://fluentsnippets.com/wp-content/uploads/2023/12/all-snippets-screen.png) 19 | 20 | Click on the “New Snippet” button to create a new snippet. 21 | 22 | ![FluentSnippets New Snippet](https://fluentsnippets.com/wp-content/uploads/2023/12/snippet-explained.png) 23 | 24 | You will see a screen like the above screenshot. Now you can add your snippet code in the "Snippet Code" field. You can also add a title and description for your snippet. Don't forget to click on the "**Save Snippet**" button and then **activate** to save and run your snippet. 25 | 26 | ## Snippet Types 27 | 28 | FluentSnippets has four types of snippets. You can choose the snippet type from the snippet type selection. 29 | 30 | **Functions - PHP Snippet** 31 | 32 | This snippet is for all your PHP code where you need to execute in specific area like you would write in your theme's functions.php file. 33 | 34 | You can use this snippet type to create functions / class, hook into other actions and filters, and more. 35 | 36 | **Content - PHP + HTML Snippet Type** 37 | 38 | This snippet type is used to insert content to different places like header, footer, after post content, before post content, etc. You can write php / html / js / css code in this snippet type. 39 | 40 | You can also use this snippet type to create shortcodes. 41 | 42 | *Use Cases:* 43 | 44 | - Add Analytics code to the header/footer 45 | - Add banner before/after post content 46 | - Add content just after body tag 47 | - Creating dynamic shortcodes 48 | 49 | **CSS Snippet Type** 50 | 51 | You can use this snippet type to add custom CSS to your site. 52 | 53 | **JS Snippet Type** 54 | 55 | You can use this snippet type to add custom JS to your site. 56 | 57 | ## Snippet Location - Where to run 58 | 59 | You can choose where you want to run your snippet. Depending on the snippet type you will see different options. 60 | 61 | **Snippet Locations** 62 | 63 | | **Location Name** | **Description** | **Available In** | 64 | |-------------------------|-------------------------------------------------------------------------------|--------------------------| 65 | | Run Everywhere | The snippet will run everywhere both backend and fronent | Functions (php) | 66 | | Admin Only | The Snippet will only run on /wp-admin area | Functions (php), CSS, JS | 67 | | Frontend Only | The Snippet will run on frontend | Functions (php), JS, CSS | 68 | | Site Wide Header | It will include the snippet in the head tag of your site | content (PHP + HTML), JS | 69 | | Site Wide Footer | It will include the snippet in the footer of your site | content (PHP + HTML), JS | 70 | | Site Wide Body Open | It will include the snippet just after body tag opening | content (PHP + HTML) | 71 | | Before Content | Snippet will be included just before the single post/page/cpt's post content | content (PHP + HTML) | 72 | | After Content | Snippet will be included after the single post/page/cpt's post content | content (PHP + HTML) | 73 | | Shortcode | Create dynamic shortcode from your snippet and print anywhere you want | content (PHP + HTML) | 74 | | Both Backend & Frontend | Available in CSS type snippets to add custom css styles | Styles (CSS) | 75 | 76 | 77 | ## Advanced Conditional Logic 78 | 79 | The Advanced Conditional Logic allows you to create powerful rules to run the snippet based on a set of rules. 80 | 81 | You can create groups of conditions that will all have to be true for the snippet to run. “OR” rules to add multiple groups of such combination of conditional rules. 82 | 83 | ![FluentSnippets Advanced Conditional Logic](https://fluentsnippets.com/wp-content/uploads/2023/12/conditiona-group-fluent-snippets.png) 84 | 85 | The above screenshot shows an example of a conditional group that will run the snippet if the post type is post,page OR the page type is the homepage. 86 | 87 | ## Snippet Grouping - Virtual Folder 88 | 89 | You can group your snippets by creating a snippet group. You can create a snippet group from the snippet group section. The same grouped snippets will be shown together in the snippet list page. This works as a virtual folder for your snippets. 90 | 91 | ## Snippet Tags 92 | 93 | You can add multiple tags to your snippet for easily filter and find your snippets from all snippets page. 94 | -------------------------------------------------------------------------------- /_docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting Started with FluentSnippets 3 | slug: getting-started 4 | tagline: Getting started with FluentSnippets 5 | sidebar: true 6 | prev: false 7 | next: true 8 | editLink: true 9 | pageClass: docs-home 10 | menu_order: 1 11 | --- 12 | 13 | ## Installation 14 | To install FluentSnippets on your WordPress website, login to your WordPress Website, go to Plugins -> Add New and then Search for "FluentSnippets" 15 | 16 | Once you find the plugin, Click Install and then Activate. 17 | 18 | ## Configure FluentSnippets 19 | There has few different section which you can configure in under a minute. 20 | 21 | ### General Settings 22 | 23 | ![General Settings](https://fluentsnippets.com/wp-content/uploads/2023/12/fluent-snippets-general-settings.png) 24 | 25 | - **Auto Activate:** You may enable this if you want to activate new snippets automatically. If you disable this then you have to manually activate new snippets after creation. 26 | - **Auto Disable:** FluentSnippets handle snippet errors automatically. So once a snippet has an error, it will be disabled automatically. If you want to alter this behavior then you can disable this feature. 27 | 28 | ### Safe Mode Settings 29 | 30 | ![FluentSnippets Safe Mode](https://fluentsnippets.com/wp-content/uploads/2023/12/fluent-snippets-safe-mode.png) 31 | 32 | FluentSnippets may automatically disable a snippet on fatal error based your General Settings. There are still situations when you might get locked out by running a snippet that doesn’t throw a fatal error on runtime. 33 | These scenarios are very rare but in case you run into that or you may just want to temporarily disable all the snippets on your site you can use the safe mode. 34 | 35 | There has two-way, you can disable all the custom snippets on your site. 36 | 37 | **By Safe Mode URL: ** 38 | You can disable all the custom snippets on your site by visiting the Safe Mode URL. The Safe Mode URL is something like this: `https://your-site.com/index.php?fluent_snippets=1&snippet_secret=RANDOM__SECURE_STRING` 39 | The URL will be different for each site and you can find the URL in the Safe Mode Settings section. 40 | 41 | **By Safe Mode Constant:** 42 | You can also disable all the custom snippets on your site by defining a constant in your wp-config.php file. The constant is `FLUENT_SNIPPETS_SAFE_MODE` and you have to set the value to `true` to enable the safe mode. 43 | 44 | ```php 45 | define( 'FLUENT_SNIPPETS_SAFE_MODE', true ); 46 | ``` 47 | 48 | #### Standalone Mode 49 | 50 | ![FluentSnippets Standalone Mode](https://fluentsnippets.com/wp-content/uploads/2023/12/fluent-snippets-standalone-mode.png) 51 | 52 | FluentSnippets does not force you to keep installing this plugin all the time. You can disable or uninstall this plugin and still keep running your snippets as a stand-alone mode. 53 | 54 | When using standalone mode your scripts will be executed from mu-plugins file. 55 | 56 | 57 | -------------------------------------------------------------------------------- /app/Helpers/Arr.php: -------------------------------------------------------------------------------- 1 | 1) { 108 | $key = array_shift($keys); 109 | 110 | // If the key doesn't exist at this depth, we will just create an empty array 111 | // to hold the next value, allowing us to create the arrays to hold final 112 | // values at the correct depth. Then we'll keep digging into the array. 113 | if (!isset($array[$key]) || !is_array($array[$key])) { 114 | $array[$key] = []; 115 | } 116 | 117 | $array = &$array[$key]; 118 | } 119 | 120 | $array[array_shift($keys)] = $value; 121 | 122 | return $array; 123 | } 124 | 125 | /** 126 | * Get a subset of the items from the given array. 127 | * 128 | * @param array $array 129 | * @param array|string $keys 130 | * @return array 131 | */ 132 | public static function only($array, $keys) 133 | { 134 | $keys = (array)$keys; 135 | 136 | $results = []; 137 | 138 | foreach ($keys as $key) { 139 | if(!isset($array[$key])) { 140 | continue; 141 | } 142 | static::set($results, $key, static::get($array, $key)); 143 | } 144 | 145 | return $results; 146 | } 147 | 148 | /** 149 | * Get all of the given array except for a specified array of items. 150 | * 151 | * @param array $array 152 | * @param array|string $keys 153 | * @return array 154 | */ 155 | public static function except($array, $keys) 156 | { 157 | $keys = (array)$keys; 158 | 159 | static::forget($array, $keys); 160 | 161 | return $array; 162 | } 163 | 164 | /** 165 | * Remove one or many array items from a given array using "dot" notation. 166 | * 167 | * @param array $array 168 | * @param array|string $keys 169 | * @return void 170 | */ 171 | public static function forget(&$array, $keys) 172 | { 173 | $original = &$array; 174 | 175 | $keys = (array)$keys; 176 | 177 | if (count($keys) === 0) { 178 | return; 179 | } 180 | 181 | foreach ($keys as $key) { 182 | // if the exact key exists in the top-level, remove it 183 | if (static::exists($array, $key)) { 184 | unset($array[$key]); 185 | 186 | continue; 187 | } 188 | 189 | $parts = explode('.', $key); 190 | 191 | // clean up before each pass 192 | $array = &$original; 193 | 194 | while (count($parts) > 1) { 195 | $part = array_shift($parts); 196 | 197 | if (isset($array[$part]) && is_array($array[$part])) { 198 | $array = &$array[$part]; 199 | } else { 200 | continue 2; 201 | } 202 | } 203 | 204 | unset($array[array_shift($parts)]); 205 | } 206 | } 207 | 208 | /** 209 | * Return the first element in an array passing a given truth test. 210 | * 211 | * @param array $array 212 | * @param \Closure $callback 213 | * @param mixed $default 214 | * @return mixed 215 | */ 216 | public static function first($array, $callback, $default = null) 217 | { 218 | foreach ($array as $key => $value) { 219 | if (call_user_func($callback, $key, $value)) return $value; 220 | } 221 | 222 | return static::value($default); 223 | } 224 | 225 | /** 226 | * Determine whether the given value is array accessible. 227 | * 228 | * @param mixed $value 229 | * @return bool 230 | */ 231 | public static function accessible($value) 232 | { 233 | return is_array($value) || $value instanceof ArrayAccess; 234 | } 235 | 236 | /** 237 | * Determine if the given key exists in the provided array. 238 | * 239 | * @param \ArrayAccess|array $array 240 | * @param string|int $key 241 | * @return bool 242 | */ 243 | public static function exists($array, $key) 244 | { 245 | if ($array instanceof ArrayAccess) { 246 | return $array->offsetExists($key); 247 | } 248 | 249 | return array_key_exists($key, $array); 250 | } 251 | 252 | /** 253 | * Return the default value of the given value. 254 | * 255 | * @param mixed $value 256 | * @return mixed 257 | */ 258 | public static function value($value) 259 | { 260 | return $value instanceof Closure ? $value() : $value; 261 | } 262 | 263 | /** 264 | * Flatten a multi-dimensional associative array with dots. 265 | * 266 | * @param array $array 267 | * @param string $prepend 268 | * 269 | * @return array 270 | */ 271 | public static function dot($array, $prepend = '') 272 | { 273 | $results = []; 274 | 275 | foreach ($array as $key => $value) { 276 | if (is_array($value) && !empty($value)) { 277 | $results = array_merge($results, static::dot($value, $prepend . $key . '.')); 278 | } else { 279 | $results[$prepend . $key] = $value; 280 | } 281 | } 282 | 283 | return $results; 284 | } 285 | 286 | public static function isTrue($array, $key) 287 | { 288 | $value = self::get($array, $key); 289 | $isFalse = !$value || $value =='false' || $value =='0'; 290 | 291 | return !$isFalse; 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /app/Helpers/Helper.php: -------------------------------------------------------------------------------- 1 | validate(); 37 | if (is_wp_error($result)) { 38 | return $result; 39 | } 40 | return $validator->checkRunTimeError(); 41 | } 42 | 43 | return true; 44 | } 45 | 46 | public static function cacheSnippetIndex($fileName = '', $isForced = false, $extraArgs = []) 47 | { 48 | $data = [ 49 | 'published' => [], 50 | 'draft' => [], 51 | 'hooks' => [] 52 | ]; 53 | 54 | $previousConfig = self::getIndexedConfig(false); 55 | 56 | if (!$previousConfig || empty($previousConfig['meta'])) { 57 | $previousConfig = [ 58 | 'meta' => [ 59 | 'auto_disable' => 'yes', 60 | 'auto_publish' => 'no', 61 | 'remove_on_uninstall' => 'no', 62 | 'force_disabled' => 'no', 63 | 'legacy_status' => 'new', 64 | 'secret_key' => bin2hex(random_bytes(16)) 65 | ], 66 | 'error_files' => [] 67 | ]; 68 | } 69 | 70 | if (empty($previousConfig['meta']['secret_key'])) { 71 | $previousConfig['meta']['secret_key'] = bin2hex(random_bytes(16)); 72 | } 73 | 74 | $data['meta'] = [ 75 | 'secret_key' => $previousConfig['meta']['secret_key'], 76 | 'force_disabled' => $previousConfig['meta']['force_disabled'], 77 | 'cached_at' => date('Y-m-d H:i:s'), 78 | 'cached_version' => FLUENT_SNIPPETS_PLUGIN_VERSION, 79 | 'cashed_domain' => site_url(), 80 | 'legacy_status' => Arr::get($previousConfig['meta'], 'legacy_status'), 81 | 'auto_disable' => $previousConfig['meta']['auto_disable'], 82 | 'auto_publish' => $previousConfig['meta']['auto_publish'], 83 | 'remove_on_uninstall' => $previousConfig['meta']['remove_on_uninstall'] 84 | ]; 85 | 86 | if ($extraArgs) { 87 | $data['meta'] = wp_parse_args($extraArgs, $data['meta']); 88 | } 89 | 90 | $errorFiles = $previousConfig['error_files']; 91 | 92 | $metaKeys = [ 93 | 'name', 94 | 'description', 95 | 'type', 96 | 'status', 97 | 'tags', 98 | 'created_at', 99 | 'updated_at', 100 | 'run_at', 101 | 'priority', 102 | 'group', 103 | 'condition', 104 | 'load_as_file' 105 | ]; 106 | 107 | $snippets = (new \FluentSnippets\App\Model\Snippet())->get(); 108 | 109 | if ($snippets) { 110 | usort($snippets, function ($a, $b) { 111 | return $a['meta']['priority'] <=> $b['meta']['priority']; 112 | }); 113 | } 114 | 115 | foreach ($snippets as $snippet) { 116 | $meta = Arr::only($snippet['meta'], $metaKeys); 117 | $fileName = basename($snippet['file']); 118 | 119 | // remove new line from $meta['description'] and limit to it 101 chars 120 | $meta['description'] = substr(str_replace(PHP_EOL, ". ", $meta['description']), 0, 101); 121 | 122 | if ($snippet['status'] != 'published') { 123 | $snippet['status'] = 'draft'; 124 | } 125 | 126 | if (!is_numeric($meta['priority']) || $meta['priority'] < 1) { 127 | $meta['priority'] = 10; 128 | } 129 | 130 | $meta['priority'] = (int)$meta['priority']; 131 | $meta['file_name'] = $fileName; 132 | $meta['status'] = $snippet['status']; 133 | 134 | if ($meta['status'] == 'published') { 135 | $runningHook = self::getRunAtHook($meta); 136 | if (empty($data['hooks'][$runningHook])) { 137 | $data['hooks'][$runningHook] = []; 138 | } 139 | 140 | $data['hooks'][$runningHook][] = $fileName; 141 | } 142 | 143 | $data[$snippet['status']][$fileName] = $meta; 144 | } 145 | 146 | $data['error_files'] = $errorFiles; 147 | 148 | return self::saveIndexedConfig($data); 149 | } 150 | 151 | public static function getRunAtHook($meta) 152 | { 153 | $runAt = $meta['run_at']; 154 | switch ($runAt) { 155 | case 'before_content': 156 | case 'after_content': 157 | return 'the_content'; 158 | default: 159 | return $runAt; 160 | } 161 | } 162 | 163 | public static function saveIndexedConfig($data, $cacheFile = '') 164 | { 165 | if (!$cacheFile) { 166 | $cacheFile = self::getStorageDir() . '/index.php'; 167 | 168 | if (!is_file($cacheFile)) { 169 | wp_mkdir_p(dirname($cacheFile)); 170 | } 171 | } 172 | 173 | $code = << 'yes', 218 | 'auto_publish' => 'no', 219 | 'remove_on_uninstall' => 'no', 220 | 'legacy_status' => 'new', 221 | 'has_line_wrap' => 'no', 222 | 223 | ]; 224 | 225 | if (!$config) { 226 | return $defaults; 227 | } 228 | 229 | $settings = Arr::only($config['meta'], array_keys($defaults)); 230 | $settings = array_filter($settings); 231 | 232 | return wp_parse_args($settings, $defaults); 233 | } 234 | 235 | public static function getErrorFiles() 236 | { 237 | $config = self::getIndexedConfig(); 238 | 239 | if (!$config || empty($config['error_files'])) { 240 | return []; 241 | } 242 | 243 | return $config['error_files']; 244 | } 245 | 246 | public static function getSecretKey() 247 | { 248 | $config = self::getIndexedConfig(); 249 | return Arr::get($config, 'meta.secret_key'); 250 | } 251 | 252 | public static function enableStandAlone($isForced = false) 253 | { 254 | if (defined('FLUENT_SNIPPETS_RUNNING_MU_VERSION') && FLUENT_SNIPPETS_RUNNING_MU_VERSION == FLUENT_SNIPPETS_PLUGIN_VERSION && !$isForced) { 255 | return true; 256 | } 257 | 258 | $muDir = WPMU_PLUGIN_DIR; 259 | if (!is_dir($muDir)) { 260 | mkdir($muDir, 0755); 261 | } 262 | 263 | if (!is_dir($muDir)) { 264 | return new \WP_Error('failed', 'mu-plugins dir could not be created'); 265 | } 266 | 267 | file_put_contents( 268 | $muDir . '/fluent-snippets-mu.php', 269 | file_get_contents(FLUENT_SNIPPETS_PLUGIN_PATH . 'app/Services/mu.stub') 270 | ); 271 | 272 | if (!is_file($muDir . '/fluent-snippets-mu.php')) { 273 | return new \WP_Error('failed', 'file could not be moved to mu-plugins directory'); 274 | } 275 | 276 | return true; 277 | } 278 | 279 | public static function disableStandAlone() 280 | { 281 | $muDir = WPMU_PLUGIN_DIR; 282 | 283 | if (!is_file($muDir . '/fluent-snippets-mu.php')) { 284 | return true; 285 | } 286 | 287 | @unlink(WPMU_PLUGIN_DIR . '/fluent-snippets-mu.php'); 288 | 289 | return true; 290 | 291 | } 292 | 293 | public static function getUserRoles() 294 | { 295 | $roles = get_editable_roles(); 296 | 297 | $formattedRoles = []; 298 | 299 | foreach ($roles as $role => $data) { 300 | $formattedRoles[$role] = $data['name']; 301 | } 302 | 303 | return $formattedRoles; 304 | } 305 | 306 | public static function sanitizeMetaValue($value) 307 | { 308 | if (is_numeric($value)) { 309 | return $value; 310 | } 311 | 312 | if (!$value) { 313 | return $value; 314 | } 315 | 316 | if (str_contains($value, '*/')) { 317 | $value = str_replace('*/', '', $value); // we will not allow */ in meta values 318 | } 319 | 320 | return $value; 321 | } 322 | 323 | public static function handleDeactivate() 324 | { 325 | if (defined('FLUENT_SNIPPETS_RUNNING_MU_VERSION')) { 326 | self::enableStandAlone(true); 327 | } 328 | } 329 | 330 | public static function escCssJs($code) 331 | { 332 | $code = preg_replace('/]*>/', '', $code); 333 | $code = preg_replace('/<\/script>/', '', $code); 334 | // remove opening js tag and closing js tag maybe 132 | get($snippet, 'priority', 10)); 135 | } 136 | break; 137 | case 'css': 138 | $runAt = $this->get($snippet, 'run_at', 'wp_head'); 139 | if (($runAt == 'everywehere' && is_admin()) || $runAt == 'admin_head') { 140 | $runAt = 'admin_head'; 141 | } else { 142 | $runAt = 'wp_head'; 143 | } 144 | 145 | $loadUrl = ''; 146 | if ($this->get($snippet, 'load_as_file') == 'yes') { 147 | $cachedFile = str_replace('.php', '.css', $fileName); 148 | $loadUrl = $this->getCachedFileUrl($cachedFile); 149 | if ($loadUrl) { 150 | $runAt = ($runAt == 'admin_head') ? 'admin_enqueue_scripts' : 'wp_enqueue_scripts'; 151 | } 152 | } 153 | 154 | add_action($runAt, function () use ($file, $snippet, $conditionalClass, $loadUrl) { 155 | if (!$conditionalClass->evaluate($snippet['condition'])) { 156 | return; 157 | } 158 | 159 | if ($loadUrl) { 160 | $snippetScriptName = str_replace('.php', '', $snippet['file_name']); 161 | wp_enqueue_style('fluent_snippet_' . $snippetScriptName, $loadUrl, [], strtotime($snippet['updated_at'])); 162 | } else { 163 | $code = $this->parseBlock(file_get_contents($file), true); 164 | ?> 165 | 166 | get($snippet, 'priority', 10)); 170 | 171 | break; 172 | case 'php_content': 173 | $runAt = $snippet['run_at']; 174 | if (in_array($runAt, ['wp_footer', 'wp_head', 'wp_body_open'])) { 175 | add_action($runAt, function () use ($file, $snippet, $conditionalClass) { 176 | if (!$conditionalClass->evaluate($snippet['condition'])) { 177 | return; 178 | } 179 | require_once $file; 180 | }, $snippet['priority']); 181 | } 182 | if (isset($filterMaps[$runAt])) { 183 | $filter = $filterMaps[$runAt]; 184 | add_filter($filter['hook'], function ($content) use ($file, $snippet, $conditionalClass, $filter) { 185 | if (!empty($filter['is_single'])) { 186 | if (!is_singular() || !in_the_loop() || !is_main_query()) { 187 | return $content; 188 | } 189 | } 190 | 191 | if (!$conditionalClass->evaluate($snippet['condition'])) { 192 | return $content; 193 | } 194 | 195 | ob_start(); 196 | require_once $file; 197 | $result = ob_get_clean(); 198 | if ($result) { 199 | if ($filter['insert'] == 'before') { 200 | return $result . $content; 201 | } 202 | 203 | return $content . $result; 204 | } 205 | return $content; 206 | }, $this->get($snippet, 'priority', 10)); 207 | } 208 | default: 209 | break; 210 | } 211 | } 212 | 213 | if ($hasInvalidFiles) { 214 | do_action('fluent_snippets/rebuild_index', false, true); 215 | } 216 | 217 | do_action('fluent_snippets/after_run_snippets'); 218 | } 219 | 220 | 221 | private function get($array, $key, $default = null) 222 | { 223 | if (isset($array[$key])) { 224 | return $array[$key]; 225 | } 226 | 227 | return $default; 228 | } 229 | 230 | private function parseBlock($fileContent, $codeOnly = false) 231 | { 232 | // get content from // to // 233 | $fileContent = explode('// ', $fileContent); 234 | 235 | if (count($fileContent) < 2) { 236 | if ($codeOnly) { 237 | return ''; 238 | } 239 | return [null, null]; 240 | } 241 | 242 | $fileContent = explode('// ?>' . PHP_EOL, $fileContent[1]); 243 | $docBlock = $fileContent[0]; 244 | $code = $fileContent[1]; 245 | 246 | if ($codeOnly) { 247 | return $code; 248 | } 249 | 250 | $docBlock = explode('*', $docBlock); 251 | // Explode by : and get the key and value 252 | $docBlockArray = [ 253 | 'name' => '', 254 | 'status' => '', 255 | 'tags' => '', 256 | 'description' => '', 257 | 'type' => '', 258 | 'run_at' => '', 259 | 'group' => '' 260 | ]; 261 | 262 | foreach ($docBlock as $key => $value) { 263 | $value = trim($value); 264 | $arr = explode(':', $value); 265 | if (count($arr) < 2) { 266 | continue; 267 | } 268 | 269 | // get the first item from the array and remove it from $arr 270 | $key = array_shift($arr); 271 | $key = trim(str_replace('@', '', $key)); 272 | if (!$key) { 273 | continue; 274 | } 275 | $docBlockArray[$key] = trim(implode(':', $arr)); 276 | } 277 | 278 | return [$docBlockArray, $code]; 279 | } 280 | 281 | 282 | private function escCssJs($code) 283 | { 284 | $code = preg_replace('/]*>/', '', $code); 285 | $code = preg_replace('/<\/script>/', '', $code); 286 | // remove opening js tag and closing js tag maybe 117 | -------------------------------------------------------------------------------- /src/Bits/Errors.js: -------------------------------------------------------------------------------- 1 | // Error handling class 2 | // Usable in all components 3 | export default class Errors { 4 | constructor() { 5 | this.errors = {} 6 | } 7 | 8 | get(field) { 9 | if (this.errors[field]) { 10 | return this.errors[field] 11 | } 12 | } 13 | 14 | first(field) { 15 | if (this.errors[field]) { 16 | if (typeof this.errors[field] === 'string') { 17 | return this.errors[field]; 18 | } else { 19 | let keys = Object.keys(this.errors[field]); 20 | return keys.length ? this.errors[field][keys[0]] : ''; 21 | } 22 | } 23 | } 24 | 25 | has(field) { 26 | return !!this.errors[field] 27 | } 28 | 29 | record(errors) { 30 | this.errors = errors 31 | } 32 | 33 | clear(field) { 34 | if (field) { 35 | this.errors[field] = null 36 | } else { 37 | this.errors = {} 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Bits/Pagination.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 72 | -------------------------------------------------------------------------------- /src/Bits/Rest.js: -------------------------------------------------------------------------------- 1 | const request = function (method, route, data = {}) { 2 | const url = `${window.fluentSnippetAdmin.rest.url}/${route}`; 3 | 4 | const headers = {'X-WP-Nonce': window.fluentSnippetAdmin.rest.nonce}; 5 | 6 | if (['PUT', 'PATCH', 'DELETE'].indexOf(method.toUpperCase()) !== -1) { 7 | headers['X-HTTP-Method-Override'] = method; 8 | method = 'POST'; 9 | } 10 | 11 | data.query_timestamp = Date.now(); 12 | 13 | return new Promise((resolve, reject) => { 14 | window.jQuery.ajax({ 15 | url: url, 16 | type: method, 17 | data: data, 18 | headers: headers 19 | }) 20 | .then(response => resolve(response)) 21 | .fail(errors => reject(errors.responseJSON || errors.responseText)); 22 | }); 23 | } 24 | 25 | export default { 26 | get(route, data = {}) { 27 | return request('GET', route, data); 28 | }, 29 | post(route, data = {}) { 30 | return request('POST', route, data); 31 | }, 32 | delete(route, data = {}) { 33 | return request('DELETE', route, data); 34 | }, 35 | put(route, data = {}) { 36 | return request('PUT', route, data); 37 | }, 38 | patch(route, data = {}) { 39 | return request('PATCH', route, data); 40 | } 41 | }; 42 | 43 | jQuery(document).ajaxSuccess((event, xhr, settings) => { 44 | const nonce = xhr.getResponseHeader('X-WP-Nonce'); 45 | if (nonce) { 46 | window.fluentSnippetAdmin.rest_nonce = nonce; 47 | } 48 | }); 49 | -------------------------------------------------------------------------------- /src/Bits/Storage.js: -------------------------------------------------------------------------------- 1 | const generator = key => 'fsnip-' + key; 2 | 3 | export default class Storage { 4 | static get(key, defaultValue = '') { 5 | let value = localStorage.getItem(generator(key)); 6 | 7 | if (value && ['{', '['].indexOf(value[0]) !== -1) { 8 | value = JSON.parse(value); 9 | } 10 | 11 | if (!value) { 12 | return defaultValue; 13 | } 14 | 15 | return value; 16 | } 17 | 18 | static set(key, value) { 19 | if (typeof value === 'object') { 20 | value = JSON.stringify(value); 21 | } 22 | 23 | localStorage.setItem(generator(key), value); 24 | } 25 | 26 | static remove(key) { 27 | localStorage.removeItem(generator(key)); 28 | } 29 | 30 | static clear() { 31 | localStorage.clear(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Bits/common.js: -------------------------------------------------------------------------------- 1 | export const calculatePercent = function (currentValue, compareValue, strict = false) { 2 | if (currentValue == 0) { 3 | return -100; 4 | } 5 | 6 | if (!compareValue) { 7 | return ''; 8 | } 9 | 10 | let percent = (currentValue - compareValue) / compareValue * 100; 11 | 12 | if (strict) { 13 | return percent.toFixed(2); 14 | } 15 | 16 | return parseInt(percent); 17 | } 18 | -------------------------------------------------------------------------------- /src/Bits/data_config.js: -------------------------------------------------------------------------------- 1 | export const orderColumns = [ 2 | { 3 | label: 'Date', 4 | value: 'created_at' 5 | }, 6 | { 7 | label: 'Customer Name', 8 | value: 'customer_name' 9 | }, 10 | { 11 | label: 'Customer Email', 12 | value: 'customer_email' 13 | }, 14 | { 15 | label: 'Order Status', 16 | value: 'order_status' 17 | }, 18 | { 19 | label: 'Shipping Status', 20 | value: 'shipping_status' 21 | }, 22 | { 23 | label: 'Total Amount', 24 | value: 'item_total' 25 | }, 26 | { 27 | label: 'Items', 28 | value: 'item_count' 29 | }, 30 | { 31 | label: 'Shipping Method', 32 | value: 'shipping_method' 33 | } 34 | ]; 35 | 36 | export const sortingOrderColumns = [ 37 | { 38 | label: 'Order ID', 39 | value: 'id' 40 | }, 41 | { 42 | label: 'Total Amount', 43 | value: 'item_total' 44 | }, 45 | { 46 | label: 'Items', 47 | value: 'item_count' 48 | }, 49 | { 50 | label: 'Shipped at', 51 | value: 'shipped_at' 52 | }, 53 | { 54 | label: 'Customer ID', 55 | value: 'customer_id' 56 | }, 57 | { 58 | label: 'Order Status', 59 | value: 'order_status' 60 | }, 61 | { 62 | label: 'Shipping Status', 63 | value: 'shipping_status', 64 | }, 65 | { 66 | label: 'Created At', 67 | value: 'created_at' 68 | } 69 | ]; 70 | 71 | export const customerColumns = [ 72 | { 73 | label: 'Date', 74 | value: 'created_at' 75 | }, 76 | { 77 | label: 'Customer Name', 78 | value: 'customer_name' 79 | }, 80 | { 81 | label: 'Customer Email', 82 | value: 'customer_email' 83 | }, 84 | { 85 | label: 'Status', 86 | value: 'customer_status' 87 | }, 88 | { 89 | label: 'Orders', 90 | value: 'orders' 91 | }, 92 | { 93 | label: 'Total Amount', 94 | value: 'total_order_value' 95 | }, 96 | { 97 | label: 'City', 98 | value: 'city' 99 | }, 100 | { 101 | label: 'Zip Code', 102 | value: 'zip_code' 103 | }, 104 | { 105 | label: 'Billing Country', 106 | value: 'billing_country' 107 | } 108 | ]; 109 | 110 | export const sortingCustomerColumns = [ 111 | { 112 | label: 'Customer ID', 113 | value: 'id' 114 | }, 115 | { 116 | label: 'Total Amount', 117 | value: 'total_order_value' 118 | }, 119 | { 120 | label: 'Orders', 121 | value: 'total_order_count' 122 | }, 123 | { 124 | label: 'Updated At', 125 | value: 'updated_at' 126 | } 127 | ]; 128 | -------------------------------------------------------------------------------- /src/Bits/event-bus.js: -------------------------------------------------------------------------------- 1 | import mitt from 'mitt'; 2 | 3 | export default { 4 | install: (app, options) => { 5 | app.config.globalProperties.$eventBus = mitt(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import {createApp} from 'vue' 2 | import {createRouter, createWebHashHistory} from 'vue-router'; 3 | import {routes} from './routes'; 4 | import Rest from './Bits/Rest.js'; 5 | import {ElNotification, ElLoading, ElMessageBox} from 'element-plus' 6 | import Storage from '@/Bits/Storage'; 7 | import App from './App.vue'; 8 | import eventBus from './Bits/event-bus'; 9 | 10 | require('./app.scss'); 11 | 12 | const dayjs = require('dayjs'); 13 | const relativeTime = require('dayjs/plugin/relativeTime'); 14 | require('dayjs/plugin/utc'); 15 | require('dayjs/plugin/localizedFormat'); 16 | dayjs.extend(require('dayjs/plugin/utc')); 17 | dayjs.extend(require('dayjs/plugin/localizedFormat')); 18 | dayjs.extend(relativeTime) 19 | 20 | function convertToText(obj) { 21 | const string = []; 22 | if (typeof (obj) === 'object' && (obj.join === undefined)) { 23 | for (const prop in obj) { 24 | string.push(convertToText(obj[prop])); 25 | } 26 | } else if (typeof (obj) === 'object' && !(obj.join === undefined)) { 27 | for (const prop in obj) { 28 | string.push(convertToText(obj[prop])); 29 | } 30 | } else if (typeof (obj) === 'function') { 31 | 32 | } else if (typeof (obj) === 'string') { 33 | string.push(obj) 34 | } 35 | 36 | return string.join('
') 37 | } 38 | 39 | const app = createApp(App); 40 | app.use(ElLoading); 41 | 42 | app.config.globalProperties.appVars = window.fluentSnippetAdmin; 43 | 44 | app.mixin({ 45 | data() { 46 | return { 47 | Storage, 48 | is_rtl: false 49 | } 50 | }, 51 | methods: { 52 | $get: Rest.get, 53 | $post: Rest.post, 54 | $put: Rest.put, 55 | $del: Rest.delete, 56 | changeTitle(title) { 57 | jQuery('head title').text(title + ' - FluentSnippets'); 58 | }, 59 | $handleError(response) { 60 | let errorMessage = ''; 61 | if (typeof response === 'string') { 62 | errorMessage = response; 63 | } else if (response && response.message) { 64 | errorMessage = response.message; 65 | } else { 66 | errorMessage = convertToText(response); 67 | } 68 | if (!errorMessage) { 69 | errorMessage = 'Something is wrong!'; 70 | } 71 | this.$notify({ 72 | type: 'error', 73 | title: 'Error', 74 | message: errorMessage, 75 | dangerouslyUseHTMLString: true 76 | }); 77 | }, 78 | convertToText, 79 | $t(string) { 80 | return window.fluentSnippetAdmin.i18n[string] || string; 81 | }, 82 | relativeTimeFromUtc(utcDateTime) { 83 | if(!utcDateTime) { 84 | return ''; 85 | } 86 | const localDateTime = dayjs.utc(utcDateTime).local(); 87 | return localDateTime.fromNow(); 88 | }, 89 | getLangLabelName(lang) { 90 | switch (lang) { 91 | case 'php_content': 92 | return 'PHP + HTML'; 93 | default: 94 | return lang.toUpperCase(); 95 | } 96 | }, 97 | $storeLocalData(key, value) { 98 | this.Storage.set(key, value); 99 | }, 100 | $getLocalData(key, defaultValue = '') { 101 | return this.Storage.get(key, defaultValue); 102 | }, 103 | ucFirst(string) { 104 | if (!string) { 105 | return ''; 106 | } 107 | return string.charAt(0).toUpperCase() + string.slice(1); 108 | }, 109 | exportSnippets(snippets) { 110 | let selected = snippets.map(snippet => { 111 | // replace .php from the end 112 | return snippet.replace(/\.php$/, ''); 113 | }); 114 | 115 | if (selected.length === 0) { 116 | this.$message.error('Please select at least one snippet to export.'); 117 | return; 118 | } 119 | 120 | location.href = window.ajaxurl + '?' + jQuery.param({ 121 | action: 'fluent_snippets_export_snippets', 122 | snippets: selected, 123 | _nonce: window.fluentSnippetAdmin.nonce 124 | }); 125 | } 126 | }, 127 | watch: { 128 | $route(to, from) { 129 | const active = to.meta.active; 130 | if (!active) { 131 | return; 132 | } 133 | jQuery('.fsnip_menu_primary').removeClass('router-link-active'); 134 | jQuery('.fsnip_menu_primary.fsnip_menu_' + active).addClass('router-link-active'); 135 | } 136 | } 137 | }); 138 | 139 | app.config.globalProperties.$notify = ElNotification; 140 | app.config.globalProperties.$confirm = ElMessageBox.confirm; 141 | app.config.globalProperties.$prompt = ElMessageBox.prompt; 142 | 143 | app.use(eventBus); 144 | 145 | const router = createRouter({ 146 | routes, 147 | history: createWebHashHistory() 148 | }); 149 | 150 | window.fluentFrameworkApp = app.use(router).mount( 151 | '#fluent_snippets_app' 152 | ); 153 | -------------------------------------------------------------------------------- /src/components/AdvancedConditions.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 70 | -------------------------------------------------------------------------------- /src/components/ConfigSettings.vue: -------------------------------------------------------------------------------- 1 | 116 | 117 | 205 | -------------------------------------------------------------------------------- /src/components/CreateSnippet.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 151 | -------------------------------------------------------------------------------- /src/components/ExportImport/ExportSnippets.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 69 | -------------------------------------------------------------------------------- /src/components/ExportImport/ImportExportChoice.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 49 | -------------------------------------------------------------------------------- /src/components/ExportImport/ImportSnippets.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 142 | -------------------------------------------------------------------------------- /src/components/FsnipSafeModesWarning.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 49 | -------------------------------------------------------------------------------- /src/components/SnippetEditView.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 155 | -------------------------------------------------------------------------------- /src/components/_CodeEditor.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 172 | -------------------------------------------------------------------------------- /src/components/_SelectPlus.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 53 | -------------------------------------------------------------------------------- /src/components/_SnippetForm.vue: -------------------------------------------------------------------------------- 1 | 107 | 108 | 134 | -------------------------------------------------------------------------------- /src/components/_TagCreator.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 58 | -------------------------------------------------------------------------------- /src/components/_WhereRun.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 93 | -------------------------------------------------------------------------------- /src/components/richFilters/Elements/_AjaxSelector.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 101 | -------------------------------------------------------------------------------- /src/components/richFilters/Elements/_OptionSelector.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 141 | 142 | 164 | -------------------------------------------------------------------------------- /src/components/richFilters/Elements/_RestSelector.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 100 | -------------------------------------------------------------------------------- /src/components/richFilters/FilterContainer.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 65 | -------------------------------------------------------------------------------- /src/components/richFilters/FilterItem.vue: -------------------------------------------------------------------------------- 1 | 94 | 95 | 295 | -------------------------------------------------------------------------------- /src/components/richFilters/RichFilters.vue: -------------------------------------------------------------------------------- 1 | 50 | 140 | -------------------------------------------------------------------------------- /src/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WPManageNinja/easy-code-manager/8c4b8ae1399fe4c9f6c1f68b60c993ee77b2f21c/src/images/logo.png -------------------------------------------------------------------------------- /src/routes.js: -------------------------------------------------------------------------------- 1 | import Dashboard from './components/Dashboard.vue'; 2 | import SnippetEditView from './components/SnippetEditView.vue'; 3 | import CreateSnippet from './components/CreateSnippet.vue'; 4 | import Settings from './components/ConfigSettings.vue'; 5 | import About from "./components/About.vue"; 6 | 7 | export var routes = [ 8 | { 9 | path: '/', 10 | name: 'dashboard', 11 | component: Dashboard, 12 | meta: { 13 | active: 'dashboard' 14 | } 15 | }, 16 | { 17 | path: '/snippets/:snippet_name', 18 | name: 'edit_snippet', 19 | component: SnippetEditView, 20 | props: true, 21 | meta: { 22 | active: 'dashboard' 23 | } 24 | }, 25 | { 26 | path: '/create-new', 27 | name: 'create_snippet', 28 | component: CreateSnippet, 29 | meta: { 30 | active: 'dashboard' 31 | } 32 | }, 33 | { 34 | path: '/settings', 35 | name: 'settings', 36 | component: Settings, 37 | meta: { 38 | active: 'settings' 39 | } 40 | }, 41 | { 42 | path: '/about', 43 | name: 'about', 44 | component: About, 45 | meta: { 46 | active: 'about' 47 | } 48 | } 49 | ]; 50 | -------------------------------------------------------------------------------- /webpack.mix.js: -------------------------------------------------------------------------------- 1 | // webpack.mix.js 2 | let mix = require('laravel-mix'); 3 | const AutoImport = require('unplugin-auto-import/webpack').default; 4 | const { ElementPlusResolver } = require('unplugin-vue-components/resolvers'); 5 | const VueComponents = require('unplugin-vue-components/webpack').default; 6 | var path = require('path'); 7 | 8 | mix.webpackConfig({ 9 | module: { 10 | rules: [{ 11 | test: /\.mjs$/, 12 | resolve: { fullySpecified: false }, 13 | include: /node_modules/, 14 | type: "javascript/auto" 15 | }] 16 | }, 17 | plugins: [ 18 | AutoImport({ 19 | resolvers: [ElementPlusResolver()], 20 | }), 21 | VueComponents({ 22 | resolvers: [ElementPlusResolver()], 23 | directives: false 24 | }), 25 | ], 26 | resolve: { 27 | extensions: ['.js', '.vue', '.json'], 28 | alias: { 29 | '@': path.resolve(__dirname, 'src') 30 | } 31 | } 32 | }); 33 | 34 | mix.js('src/app.js', 'dist').vue({ version: 3 }) 35 | .copy('src/images', 'dist/images') 36 | .setPublicPath('dist'); 37 | --------------------------------------------------------------------------------