Compare commits

...

40 Commits
v1.4 ... master

Author SHA1 Message Date
Tony Air 2308506244 IMPR: SS5 test 2024-03-12 03:37:50 +02:00
Tony Air c74164d20b IMPR: Skip captcha caching 2024-03-11 20:08:26 +02:00
Tony Air d787b36857 FIX: JS file Path 2022-06-13 22:10:42 +02:00
Tony Air bd10c36a99 Webpack upd, Offline image placeholder 2022-05-21 23:42:33 +02:00
Tony Air b9c982bd08 IMPR: add change password URL. Ref: https://web.dev/change-password-url/ 2022-01-07 20:33:51 +02:00
Tony Air ec57485d4e IMPR: Maskable icon 2021-08-03 03:33:27 +02:00
Tony Air 3b6fe46699 FIX: expose issue 2021-03-29 14:48:47 +07:00
Tony Air bb9403844a IMPR: standalone display mode to display Android status bar 2021-01-13 22:28:47 +07:00
Tony Air fc8718140b IMPR: Offline enabled manifest.json option 2021-01-11 17:27:52 +07:00
Tony Air dcab3837dc Compatibility config file 2020-11-28 19:09:04 +07:00
Tony Air d2c610b3d1 Build system update 2020-11-28 18:38:07 +07:00
Tony Air 80b0acf887
FIX: correct scope for sub-directory websites 2020-07-30 22:29:18 +07:00
Tony Air ef4c30f729
Update SiteTree.php 2020-05-06 17:34:37 +07:00
Tony Air cc150c9f80
Update SiteTree.php 2020-05-04 11:57:25 +07:00
Tony Air 8329777989
Update composer.json 2020-04-10 10:08:10 +07:00
Tony Air 60582eba85 Allow custom service worker (sw.js) 2020-04-03 02:47:54 +07:00
Tony Air 4b6efcaa7c Povide SWVersion to templates 2020-04-03 02:44:18 +07:00
Tony Air 05e148c814 Reamme update 2020-04-02 19:13:49 +07:00
Tony Air 2887b7be91 Merge branch 'master' of https://github.com/a2nt/silverstripe-progressivewebapp 2020-04-02 10:11:00 +07:00
Tony Air e6eff33908 IMPR: Versioning 2020-04-02 10:10:36 +07:00
Tony Air 0ef7b8eb06
Update README.md 2020-04-02 07:23:45 +07:00
Tony Air db8bb88bbc IMPR: More debug logging 2020-04-02 07:07:24 +07:00
Tony Air 7eff45c737 FIX: composer.json 2020-04-02 06:49:12 +07:00
Tony Air 61853c53a5 Minor info update 2020-04-02 06:46:41 +07:00
Tony Air 3df26ac98f Offline Caching service worker 2020-04-02 06:41:49 +07:00
Tony Air 10e7cb5977 Initial updates 2020-04-02 02:42:13 +07:00
Tony Air e7c0f9b416
Update composer.json 2020-04-02 02:06:15 +07:00
Michel vd Steege 8ace11ef79
Update ServiceWorker.ss 2018-08-02 15:26:32 +02:00
Michel vd Steege 889e5def38
Update ServiceWorkerController.php 2018-08-02 08:46:31 +02:00
Michel vd Steege b63aaf90a8
Update ServiceWorker.ss 2018-07-29 12:19:25 +02:00
Michel vd Steege 5b93c34485
Update ServiceWorker.ss 2018-07-11 15:41:38 +02:00
Michel b435128ac0 Service worker 2018-07-08 10:55:43 +02:00
Michel vd Steege a747fb2362
Update ProgressiveWebAppSiteConfigExtension.php 2018-07-05 18:13:13 +02:00
Michel vd Steege 5027bbf88f
Update ServiceWorkerController.php 2018-07-05 18:12:59 +02:00
Michel vd Steege d9a1ea02b6
Update ManifestController.php 2018-07-05 18:12:47 +02:00
Michel vd Steege 8d72bc2316
Update config.yml 2018-07-05 18:12:25 +02:00
Michel vd Steege f20afe1f51
Update README.md 2018-07-05 18:04:16 +02:00
Michel vd Steege b35d563266
Update composer.json 2018-07-05 18:03:05 +02:00
Michel 5f7c53ba5f Service worker 2018-07-04 12:53:26 +02:00
Michel bde15f3cf2 Service worker js 2018-07-04 12:36:46 +02:00
38 changed files with 14317 additions and 220 deletions

View File

@ -1,6 +1,8 @@
# For more information about the properties used in this file,
# please see the EditorConfig documentation:
# http://editorconfig.org
# For more information about the properties used in
# this file, please see the EditorConfig documentation:
# http://editorconfig.org/
root = true
[*]
charset = utf-8
@ -10,8 +12,15 @@ indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[{*.yml,package.json,*.scss,*.js}]
indent_size = 2
[*.md]
trim_trailing_whitespace = false
# The indent size used in the package.json file cannot be changed:
[*.yml]
indent_size = 2
indent_style = space
[{.travis.yml,package.json}]
# The indent size used in the `package.json` file cannot be changed
# https://github.com/npm/npm/pull/3180#issuecomment-16336516
indent_size = 2
indent_style = space

1
.eslintignore Executable file
View File

@ -0,0 +1 @@
/client/src/thirdparty

262
.eslintrc Executable file
View File

@ -0,0 +1,262 @@
{
// http://eslint.org/docs/rules/
"extends": "eslint:recommended",
"settings": {
"react": {
"version": "detect"
}
},
"env": {
"browser": true, // browser global variables.
"node": true, // Node.js global variables and Node.js-specific rules.
"amd": true, // defines require() and define() as global variables as per the amd spec.
"mocha": false, // adds all of the Mocha testing global variables.
"jasmine": false, // adds all of the Jasmine testing global variables for version 1.3 and 2.0.
"phantomjs": false, // phantomjs global variables.
"jquery": true, // jquery global variables.
"prototypejs": false, // prototypejs global variables.
"shelljs": false, // shelljs global variables.
"es6": true
},
"globals": {
// e.g. "angular": true
},
"plugins": [
"react",
"import",
"jquery"
],
"parser": "babel-eslint",
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true,
"experimentalObjectRestSpread": true
}
},
"rules": {
////////// Possible Errors //////////
"no-comma-dangle": 0, // disallow trailing commas in object literals
"no-cond-assign": 0, // disallow assignment in conditional expressions
"no-console": 0, // disallow use of console (off by default in the node environment)
"no-constant-condition": 0, // disallow use of constant expressions in conditions
"no-control-regex": 0, // disallow control characters in regular expressions
"no-debugger": 0, // disallow use of debugger
"no-dupe-keys": 0, // disallow duplicate keys when creating object literals
"no-empty": 0, // disallow empty statements
"no-empty-class": 0, // disallow the use of empty character classes in regular expressions
"no-ex-assign": 0, // disallow assigning to the exception in a catch block
"no-extra-boolean-cast": 0, // disallow double-negation boolean casts in a boolean context
"no-extra-parens": 0, // disallow unnecessary parentheses (off by default)
"no-extra-semi": 0, // disallow unnecessary semicolons
"no-func-assign": 0, // disallow overwriting functions written as function declarations
"no-inner-declarations": 0, // disallow function or variable declarations in nested blocks
"no-invalid-regexp": 0, // disallow invalid regular expression strings in the RegExp constructor
"no-irregular-whitespace": 0, // disallow irregular whitespace outside of strings and comments
"no-negated-in-lhs": 0, // disallow negation of the left operand of an in expression
"no-obj-calls": 0, // disallow the use of object properties of the global object (Math and JSON) as functions
"no-regex-spaces": 0, // disallow multiple spaces in a regular expression literal
"no-reserved-keys": 0, // disallow reserved words being used as object literal keys (off by default)
"no-sparse-arrays": 0, // disallow sparse arrays
"no-unreachable": 0, // disallow unreachable statements after a return, throw, continue, or break statement
"use-isnan": 0, // disallow comparisons with the value NaN
"valid-jsdoc": 0, // Ensure JSDoc comments are valid (off by default)
"valid-typeof": 0, // Ensure that the results of typeof are compared against a valid string
////////// Best Practices //////////
"block-scoped-var": 0, // treat var statements as if they were block scoped (off by default)
"complexity": 0, // specify the maximum cyclomatic complexity allowed in a program (off by default)
"consistent-return": 0, // require return statements to either always or never specify values
"curly": 0, // specify curly brace conventions for all control statements
"default-case": 0, // require default case in switch statements (off by default)
"dot-notation": 0, // encourages use of dot notation whenever possible
"eqeqeq": 0, // require the use of === and !==
"guard-for-in": 0, // make sure for-in loops have an if statement (off by default)
"no-alert": 0, // disallow the use of alert, confirm, and prompt
"no-caller": 0, // disallow use of arguments.caller or arguments.callee
"no-div-regex": 0, // disallow division operators explicitly at beginning of regular expression (off by default)
"no-else-return": 0, // disallow else after a return in an if (off by default)
"no-empty-label": 0, // disallow use of labels for anything other then loops and switches
"no-eq-null": 0, // disallow comparisons to null without a type-checking operator (off by default)
"no-eval": 0, // disallow use of eval()
"no-extend-native": 0, // disallow adding to native types
"no-extra-bind": 0, // disallow unnecessary function binding
"no-fallthrough": 0, // disallow fallthrough of case statements
"no-floating-decimal": 0, // disallow the use of leading or trailing decimal points in numeric literals (off by default)
"no-implied-eval": 0, // disallow use of eval()-like methods
"no-iterator": 0, // disallow usage of __iterator__ property
"no-labels": 0, // disallow use of labeled statements
"no-lone-blocks": 0, // disallow unnecessary nested blocks
"no-loop-func": 0, // disallow creation of functions within loops
"no-multi-spaces": 0, // disallow use of multiple spaces
"no-multi-str": 0, // disallow use of multiline strings
"no-native-reassign": 0, // disallow reassignments of native objects
"no-new": 0, // disallow use of new operator when not part of the assignment or comparison
"no-new-func": 0, // disallow use of new operator for Function object
"no-new-wrappers": 0, // disallows creating new instances of String, Number, and Boolean
"no-octal": 0, // disallow use of octal literals
"no-octal-escape": 0, // disallow use of octal escape sequences in string literals, such as var foo = "Copyright \251";
"no-process-env": 0, // disallow use of process.env (off by default)
"no-proto": 0, // disallow usage of __proto__ property
"no-redeclare": 0, // disallow declaring the same variable more then once
"no-return-assign": 0, // disallow use of assignment in return statement
"no-script-url": 0, // disallow use of javascript: urls.
"no-self-compare": 0, // disallow comparisons where both sides are exactly the same (off by default)
"no-sequences": 0, // disallow use of comma operator
"no-unused-expressions": 0, // disallow usage of expressions in statement position
"no-void": 0, // disallow use of void operator (off by default)
"no-warning-comments": 0, // disallow usage of configurable warning terms in comments, e.g. TODO or FIXME (off by default)
"no-with": 0, // disallow use of the with statement
"radix": 0, // require use of the second argument for parseInt() (off by default)
"vars-on-top": 0, // requires to declare all vars on top of their containing scope (off by default)
"wrap-iife": 0, // require immediate function invocation to be wrapped in parentheses (off by default)
"yoda": 0, // require or disallow Yoda conditions
////////// Strict Mode //////////
"global-strict": 0, // (deprecated) require or disallow the "use strict" pragma in the global scope (off by default in the node environment)
"no-extra-strict": 0, // (deprecated) disallow unnecessary use of "use strict"; when already in strict mode
"strict": 0, // controls location of Use Strict Directives
////////// Variables //////////
"no-catch-shadow": 0, // disallow the catch clause parameter name being the same as a variable in the outer scope (off by default in the node environment)
"no-delete-var": 0, // disallow deletion of variables
"no-label-var": 0, // disallow labels that share a name with a variable
"no-shadow": 0, // disallow declaration of variables already declared in the outer scope
"no-shadow-restricted-names": 0, // disallow shadowing of names such as arguments
"no-undef": 0, // disallow use of undeclared variables unless mentioned in a /*global */ block
"no-undef-init": 0, // disallow use of undefined when initializing variables
"no-undefined": 0, // disallow use of undefined variable (off by default)
"no-unused-vars": 0, // disallow declaration of variables that are not used in the code
"no-use-before-define": 0, // disallow use of variables before they are defined
////////// Node.js //////////
"handle-callback-err": 0, // enforces error handling in callbacks (off by default) (on by default in the node environment)
"no-mixed-requires": 0, // disallow mixing regular variable and require declarations (off by default) (on by default in the node environment)
"no-new-require": 0, // disallow use of new operator with the require function (off by default) (on by default in the node environment)
"no-path-concat": 0, // disallow string concatenation with __dirname and __filename (off by default) (on by default in the node environment)
"no-process-exit": 0, // disallow process.exit() (on by default in the node environment)
"no-restricted-modules": 0, // restrict usage of specified node modules (off by default)
"no-sync": 0, // disallow use of synchronous methods (off by default)
////////// Stylistic Issues //////////
"brace-style": 0, // enforce one true brace style (off by default)
"camelcase": 0, // require camel case names
"comma-spacing": 0, // enforce spacing before and after comma
"comma-style": 0, // enforce one true comma style (off by default)
"consistent-this": 0, // enforces consistent naming when capturing the current execution context (off by default)
"eol-last": 0, // enforce newline at the end of file, with no multiple empty lines
"func-names": 0, // require function expressions to have a name (off by default)
"func-style": 0, // enforces use of function declarations or expressions (off by default)
"key-spacing": 0, // enforces spacing between keys and values in object literal properties
"max-nested-callbacks": 0, // specify the maximum depth callbacks can be nested (off by default)
"new-cap": 0, // require a capital letter for constructors
"new-parens": 0, // disallow the omission of parentheses when invoking a constructor with no arguments
"no-array-constructor": 0, // disallow use of the Array constructor
"no-inline-comments": 0, // disallow comments inline after code (off by default)
"no-lonely-if": 0, // disallow if as the only statement in an else block (off by default)
"no-mixed-spaces-and-tabs": 0, // disallow mixed spaces and tabs for indentation
"no-multiple-empty-lines": 0, // disallow multiple empty lines (off by default)
"no-nested-ternary": 0, // disallow nested ternary expressions (off by default)
"no-new-object": 0, // disallow use of the Object constructor
"no-space-before-semi": 0, // disallow space before semicolon
"no-spaced-func": 0, // disallow space between function identifier and application
"no-ternary": 0, // disallow the use of ternary operators (off by default)
"no-trailing-spaces": 0, // disallow trailing whitespace at the end of lines
"no-underscore-dangle": 0, // disallow dangling underscores in identifiers
"no-wrap-func": 0, // disallow wrapping of non-IIFE statements in parens
"one-var": 0, // allow just one var statement per function (off by default)
"operator-assignment": 0, // require assignment operator shorthand where possible or prohibit it entirely (off by default)
"padded-blocks": 0, // enforce padding within blocks (off by default)
"quote-props": 0, // require quotes around object literal property names (off by default)
"quotes": 0, // specify whether double or single quotes should be used
"semi": 0, // require or disallow use of semicolons instead of ASI
"sort-vars": 0, // sort variables within the same declaration block (off by default)
"space-after-function-name": 0, // require a space after function names (off by default)
"space-after-keywords": 0, // require a space after certain keywords (off by default)
"space-before-blocks": 0, // require or disallow space before blocks (off by default)
"space-in-brackets": 0, // require or disallow spaces inside brackets (off by default)
"space-in-parens": 0, // require or disallow spaces inside parentheses (off by default)
"space-infix-ops": 0, // require spaces around operators
"space-return-throw-case": 0, // require a space after return, throw, and case
"space-unary-ops": 0, // Require or disallow spaces before/after unary operators (words on by default, nonwords off by default)
"spaced-line-comment": 0, // require or disallow a space immediately following the // in a line comment (off by default)
"wrap-regex": 0, // require regex literals to be wrapped in parentheses (off by default)
////////// ECMAScript 6 //////////
"no-var": 0, // require let or const instead of var (off by default)
"generator-star": 0, // enforce the position of the * in generator functions (off by default)
////////// Legacy //////////
"max-depth": 0, // specify the maximum depth that blocks can be nested (off by default)
"max-len": 0, // specify the maximum length of a line in your program (off by default)
"max-params": 0, // limits the number of parameters that can be used in the function declaration. (off by default)
"max-statements": 0, // specify the maximum number of statement allowed in a function (off by default)
"no-bitwise": 0, // disallow use of bitwise operators (off by default)
"no-plusplus": 0, // disallow use of unary operators, ++ and -- (off by default)
//////// Extra //////////
"array-bracket-spacing": ["error", "never"],
"array-callback-return": "error",
"arrow-parens": ["error", "always"],
"arrow-spacing": ["error", { "before": true, "after": true }],
"comma-dangle": ["error", "always-multiline"],
"indent": ["error", 2, { "SwitchCase": 1 }],
"no-case-declarations": "error",
"no-confusing-arrow": "error",
"no-duplicate-imports": "error",
"no-param-reassign": "error",
"no-useless-escape": "error",
"object-curly-spacing": ["error", "always"],
"object-shorthand": ["error", "properties"],
"prefer-arrow-callback": "error",
"prefer-const": "error",
"prefer-template": "error",
"react/jsx-closing-bracket-location": "error",
"react/jsx-curly-spacing": ["error", "never", {"allowMultiline": true}],
"react/jsx-filename-extension": ["error", { "extensions": [".react.js", ".js", ".jsx"] }],
"react/jsx-no-duplicate-props": "error",
"react/jsx-no-bind": ["error", { "ignoreRefs": true, "allowArrowFunctions": true, "allowBind": false }],
"react/jsx-no-undef": "error",
"react/jsx-pascal-case": "error",
"react/jsx-tag-spacing": ["error", {"closingSlash": "never", "beforeSelfClosing": "always", "afterOpening": "never"}],
"react/jsx-uses-react": "error",
"react/jsx-uses-vars": "error",
"react/no-danger": "error",
"react/no-deprecated": "error",
"react/no-did-mount-set-state": "error",
"react/no-did-update-set-state": "error",
"react/no-direct-mutation-state": "error",
"react/no-is-mounted": "error",
"react/no-multi-comp": "error",
"react/prefer-es6-class": "error",
"react/prop-types": "error",
"react/require-render-return": "error",
"react/self-closing-comp": "error",
"react/sort-comp": "error",
"import/no-mutable-exports": "error",
"import/imports-first": "warn"
}
}

6
.gitignore vendored
View File

@ -1,3 +1,3 @@
/node_modules/
/**/*.js.map
/**/*.css.map
/node_modules
/yarn.lock
yarn-error\.log

2
.npmrc Executable file
View File

@ -0,0 +1,2 @@
registry=https://npm.pkg.github.com/a2nt
registry=https://registry.npmjs.org/

View File

@ -1,20 +1,33 @@
# SilverStripe Progressive Web App
Tools to add progressive web app functionality to your silverstripe website
And make it available offline
## Installation
```
composer require pixelspin/silverstripe-progressivewebapp
composer require a2nt/silverstripe-progressivewebapp
```
## Usage
Install the module, run dev/build and fill in the settings in the siteconfig
Create a symlink at the root of you website to vendor/pixelspin/silverstripe-progressivewebapp/service-worker.js
Place the link to the manifest file (<link rel="manifest" href="{$BaseHref}manifest.json">) in the head of your pages and add the color meta data as well (<meta name="theme-color" content="$SiteConfig.ManifestColor">)
Include the js (Requirements::javascript('pixelspin/silverstripe-progressivewebapp:resources/js/progressivewebapp.js');)
## Todo
- Add "add to homescreen" prompt
- Add offline support
- Create an you are offline page
- Improve documentation
- Install the module, run dev/build and fill in the settings in the siteconfig
- Add js to register the service worker (example can be found at client/src/app.js)
```
if ('serviceWorker' in navigator) {
var baseHref = (document.getElementsByTagName('base')[0] || {}).href;
var version = (document.querySelector('meta[name="swversion"]') || {})
.content;
if (baseHref) {
navigator.serviceWorker
.register(baseHref + 'sw.js?v=' + version)
.then(() => {
console.log('SW: Registered');
});
}
}
```
- Add the following tags to the head of your website
```
<meta name="theme-color" content="#000000" />
<link rel="manifest" href="{$BaseHref}manifest.json" />
<meta name="swversion" content="{$SWVersion}" />
```

1
_config.php Executable file
View File

@ -0,0 +1 @@
<?php

View File

@ -4,8 +4,10 @@ Name: progressivewebapp
SilverStripe\Control\Director:
rules:
'manifest.json': 'Pixelspin\ProgressiveWebApp\Controllers\ProgressiveWebAppController'
SilverStripe\SiteConfig\SiteConfig:
extensions:
- Pixelspin\ProgressiveWebApp\Extensions\ProgressiveWebAppSiteConfigExtension
'manifest.json': 'A2nt\ProgressiveWebApp\Controllers\ManifestController'
'sw.js/$Action': 'A2nt\ProgressiveWebApp\Controllers\ServiceWorkerController'
'.well-known/$Action!': 'A2nt\ProgressiveWebApp\Controllers\WellKnownController'
SilverStripe\CMS\Model\SiteTree:
extensions:
- A2nt\ProgressiveWebApp\Extensions\SiteTree

28
babel.config.json Normal file
View File

@ -0,0 +1,28 @@
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "6.10",
"browsers": "> 0.25%, not dead"
}
}
],
[
"@babel/preset-react",
{
"pragma": "dom", // default pragma is React.createElement (only in classic runtime)
"pragmaFrag": "DomFrag", // default is React.Fragment (only in classic runtime)
"throwIfNamespace": false, // defaults to true
"runtime": "classic" // defaults to classic
// "importSource": "custom-jsx-library" // defaults to react (only in automatic runtime)
}
]
],
"plugins": [
"@babel/plugin-syntax-top-level-await",
"@babel/plugin-proposal-object-rest-spread",
"@babel/plugin-syntax-jsx"
]
}

1
client/dist/js/app_app.js vendored Normal file
View File

@ -0,0 +1 @@
!function(){if("serviceWorker"in navigator){var e=(document.getElementsByTagName("base")[0]||{}).href,n=(document.querySelector('meta[name="swversion"]')||{}).content;e&&navigator.serviceWorker.register("".concat(e,"sw.js?v=").concat(n)).then((function(){console.log("SW: Registered")}))}}();

1
client/dist/js/app_sw.js vendored Normal file
View File

@ -0,0 +1 @@
!function(){var e={729:function(e){e.exports=function log(e){debug&&console.log(e)}},671:function(e){Cache.prototype.add||(Cache.prototype.add=function add(e){return this.addAll([e])}),Cache.prototype.addAll||(Cache.prototype.addAll=function addAll(e){var t=this;function NetworkError(e){this.name="NetworkError",this.code=19,this.message=e}return NetworkError.prototype=Object.create(Error.prototype),Promise.resolve().then((function(){if(arguments.length<1)throw new TypeError;return e=e.map((function(e){return e instanceof Request?e:String(e)})),Promise.all(e.map((function(e){"string"===typeof e&&(e=new Request(e));var t=new URL(e.url).protocol;if("http:"!==t&&"https:"!==t)throw new NetworkError("Invalid scheme");return fetch(e.clone())})))})).then((function(n){return Promise.all(n.map((function(n,r){return t.put(e[r],n)})))})).then((function(){}))}),CacheStorage.prototype.match||(CacheStorage.prototype.match=function match(e,t){var n=this;return this.keys().then((function(r){var o;return r.reduce((function(r,i){return r.then((function(){return o||n.open(i).then((function(n){return n.match(e,t)})).then((function(e){return o=e}))}))}),Promise.resolve())}))}),e.exports=self.caches}},t={};function __webpack_require__(n){var r=t[n];if(void 0!==r)return r.exports;var o=t[n]={exports:{}};return e[n](o,o.exports,__webpack_require__),o.exports}!function(){var e=__webpack_require__(729),t=__webpack_require__(671);if(debug&&(e("SW: debug is on"),e("SW: CACHE_NAME: ".concat(CACHE_NAME)),e("SW: appDomain: ".concat(appDomain)),e("SW: lang: ".concat(lang))),"string"!==typeof self.CACHE_NAME)throw new Error("Cache Name cannot be empty");self.addEventListener("fetch",(function(n){if("GET"===n.request.method){var r=new URL(n.request.url);if(r.pathname.indexOf("turnstile")>=0&&e("SW: skip captcha ".concat(n.request.url)),r.pathname.indexOf("admin")>=0||r.pathname.indexOf("Security")>=0||r.pathname.indexOf("dev")>=0)e("SW: skip admin ".concat(n.request.url));else{var o=n.request.clone(),i=n.request.clone();n.respondWith(fetch(o).then((function(e){var r=e.clone();return t.open(self.CACHE_NAME).then((function(e){var t=n.request.clone();e.put(t,r)})),e})).catch((function(n){return e("SW: fetch failed"),t.match(i)})))}}})),self.addEventListener("activate",(function(n){e("SW: activated: ".concat(version)),n.waitUntil(t.delete(self.CACHE_NAME))})),self.addEventListener("fetch",(function(e){var n=e.request;e.respondWith(t.match(n).then((function(e){return e||fetch(n).then((function(e){return e})).catch((function(){if(n.url.match(/\.(jpe?g|png|gif|svg)$/))return new Response('<svg role="img" aria-labelledby="offline-title" viewBox="0 0 400 225" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid slice"><title id="offline-title">Offline</title><path fill="rgba(145,145,145,0.5)" d="M0 0h400v225H0z" /><text fill="rgba(0,0,0,0.33)" font-family="Helvetica Neue,Arial,sans-serif" font-size="27" text-anchor="middle" x="200" y="113" dominant-baseline="central">offline</text></svg>',{headers:{"Content-Type":"image/svg+xml"}})}))})))})),self.addEventListener("install",(function(t){e("SW: installing version: ".concat(version))}))}()}();

30
client/dist/records.json vendored Normal file
View File

@ -0,0 +1,30 @@
{
"chunks": {
"byName": {
"app_app": 21,
"app_sw": 898
},
"bySource": {
"0 app_app": 21,
"0 app_sw": 898
},
"usedIds": [
21,
898
]
},
"modules": {
"byIdentifier": {
"./node_modules/.pnpm/babel-loader@8.2.5_qqaml4ljchhh37dxp4aoesgrby/node_modules/babel-loader/lib/index.js??ruleSet[1].rules[0].use!./client/src/js/lib/log.js": 729,
"./node_modules/.pnpm/babel-loader@8.2.5_qqaml4ljchhh37dxp4aoesgrby/node_modules/babel-loader/lib/index.js??ruleSet[1].rules[0].use!./client/src/js/types/app.js": 617,
"./node_modules/.pnpm/babel-loader@8.2.5_qqaml4ljchhh37dxp4aoesgrby/node_modules/babel-loader/lib/index.js??ruleSet[1].rules[0].use!./client/src/js/types/sw.js": 647,
"./node_modules/.pnpm/babel-loader@8.2.5_qqaml4ljchhh37dxp4aoesgrby/node_modules/babel-loader/lib/index.js??ruleSet[1].rules[0].use!./client/src/thirdparty/serviceworker-caches.js": 671
},
"usedIds": [
617,
647,
671,
729
]
}
}

38
client/dist/report.html vendored Normal file

File diff suppressed because one or more lines are too long

7
client/src/js/lib/log.js Normal file
View File

@ -0,0 +1,7 @@
const log = (msg) => {
if (debug) {
console.log(msg);
}
};
module.exports = log;

View File

@ -0,0 +1,13 @@
// Register service worker
if ('serviceWorker' in navigator) {
const baseHref = (document.getElementsByTagName('base')[0] || {}).href;
const version = (document.querySelector('meta[name="swversion"]') || {})
.content;
if (baseHref) {
navigator.serviceWorker
.register(`${baseHref}sw.js?v=${version}`)
.then(() => {
console.log('SW: Registered');
});
}
}

133
client/src/js/types/sw.js Normal file
View File

@ -0,0 +1,133 @@
// caches polyfill because it is not added to native yet!
var log = require('../lib/log');
var caches = require('../../thirdparty/serviceworker-caches');
if (debug) {
log('SW: debug is on');
log(`SW: CACHE_NAME: ${CACHE_NAME}`);
log(`SW: appDomain: ${appDomain}`);
log(`SW: lang: ${lang}`);
}
if (typeof self.CACHE_NAME !== 'string') {
throw new Error('Cache Name cannot be empty');
}
self.addEventListener('fetch', (event) => {
// skip non-get
if (event.request.method !== 'GET') {
return;
}
//Parse the url
const requestURL = new URL(event.request.url);
//Check for our own urls
/*if (requestURL.origin !== location.origin) {
log('SW: skip external ' + event.request.url);
return;
}*/
// skip captchas
if (
requestURL.pathname.indexOf('turnstile') >= 0
){
log(`SW: skip captcha ${event.request.url}`);
}
//Skip admin url's
if (
requestURL.pathname.indexOf('admin') >= 0 ||
requestURL.pathname.indexOf('Security') >= 0 ||
requestURL.pathname.indexOf('dev') >= 0
) {
log(`SW: skip admin ${event.request.url}`);
return;
}
//Test for images
/*if (/\.(jpg|jpeg|png|gif|webp)$/.test(requestURL.pathname)) {
log('SW: skip image ' + event.request.url);
//For now we skip images but change this later to maybe some caching and/or an offline fallback
return;
}*/
// Clone the request for fetch and cache
// A request is a stream and can be consumed only once.
const fetchRequest = event.request.clone(),
cacheRequest = event.request.clone();
// Respond with content from fetch or cache
event.respondWith(
// Try fetch
fetch(fetchRequest)
// when fetch is successful, we update the cache
.then((response) => {
// A response is a stream and can be consumed only once.
// Because we want the browser to consume the response,
// as well as cache to consume the response, we need to
// clone it so we have 2 streams
const responseToCache = response.clone();
// and update the cache
caches.open(self.CACHE_NAME).then((cache) => {
// Clone the request again to use it
// as the key for our cache
const cacheSaveRequest = event.request.clone();
cache.put(cacheSaveRequest, responseToCache);
});
// Return the response stream to be consumed by browser
return response;
})
// when fetch times out or fails
.catch((err) => {
log('SW: fetch failed');
// Return the promise which
// resolves on a match in cache for the current request
// or rejects if no matches are found
return caches.match(cacheRequest);
}),
);
});
// Now we need to clean up resources in the previous versions
// of Service Worker scripts
self.addEventListener('activate', (event) => {
log(`SW: activated: ${version}`);
// Destroy the cache
event.waitUntil(caches.delete(self.CACHE_NAME));
});
self.addEventListener('fetch', (event) => {
const request = event.request;
event.respondWith(
caches.match(request)
.then((response) => {
return response || fetch(request)
.then((response) => {
return response;
})
.catch(() => {
// Offline fallback image
if (request.url.match(/\.(jpe?g|png|gif|svg)$/)) {
return new Response(
'<svg role="img" aria-labelledby="offline-title" viewBox="0 0 400 225" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid slice"><title id="offline-title">Offline</title><path fill="rgba(145,145,145,0.5)" d="M0 0h400v225H0z" /><text fill="rgba(0,0,0,0.33)" font-family="Helvetica Neue,Arial,sans-serif" font-size="27" text-anchor="middle" x="200" y="113" dominant-baseline="central">offline</text></svg>',
{
headers: {
'Content-Type': 'image/svg+xml',
},
}
);
}
});
})
);
});
self.addEventListener('install', (e) => {
log(`SW: installing version: ${version}`);
});

View File

@ -0,0 +1,259 @@
/*
Copyright 2014 Google Inc. All Rights Reserved.
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.
*/
// While overkill for this specific sample in which there is only one cache,
// this is one best practice that can be followed in general to keep track of
// multiple caches used by a given service worker, and keep them all versioned.
// It maps a shorthand identifier for a cache to a specific, versioned cache name.
// Note that since global state is discarded in between service worker restarts, these
// variables will be reinitialized each time the service worker handles an event, and you
// should not attempt to change their values inside an event handler. (Treat them as constants.)
// If at any point you want to force pages that use this service worker to start using a fresh
// cache, then increment the CACHE_VERSION value. It will kick off the service worker update
// flow and the old cache(s) will be purged as part of the activate event handler when the
// updated service worker is activated.
var CACHE_VERSION = 1;
var CURRENT_CACHES = {
'offline-analytics': `offline-analytics-v${ CACHE_VERSION}`,
};
var idbDatabase;
var IDB_VERSION = 1;
var STOP_RETRYING_AFTER = 86400000; // One day, in milliseconds.
var STORE_NAME = 'urls';
// This is basic boilerplate for interacting with IndexedDB. Adapted from
// https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB
function openDatabaseAndReplayRequests() {
var indexedDBOpenRequest = indexedDB.open('offline-analytics', IDB_VERSION);
// This top-level error handler will be invoked any time there's an IndexedDB-related error.
indexedDBOpenRequest.onerror = function(error) {
console.error('IndexedDB error:', error);
};
// This should only execute if there's a need to create a new database for the given IDB_VERSION.
indexedDBOpenRequest.onupgradeneeded = function() {
this.result.createObjectStore(STORE_NAME, { keyPath: 'url' });
};
// This will execute each time the database is opened.
indexedDBOpenRequest.onsuccess = function() {
idbDatabase = this.result;
replayAnalyticsRequests();
};
}
// Helper method to get the object store that we care about.
function getObjectStore(storeName, mode) {
return idbDatabase.transaction(storeName, mode).objectStore(storeName);
}
function replayAnalyticsRequests() {
var savedRequests = [];
getObjectStore(STORE_NAME).openCursor().onsuccess = function(event) {
// See https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB#Using_a_cursor
var cursor = event.target.result;
if (cursor) {
// Keep moving the cursor forward and collecting saved requests.
savedRequests.push(cursor.value);
cursor.continue();
} else {
// At this point, we have all the saved requests.
console.log(
'About to replay %d saved Google Analytics requests...',
savedRequests.length,
);
savedRequests.forEach((savedRequest) => {
var queueTime = Date.now() - savedRequest.timestamp;
if (queueTime > STOP_RETRYING_AFTER) {
getObjectStore(STORE_NAME, 'readwrite').delete(savedRequest.url);
console.log(
' Request has been queued for %d milliseconds. ' +
'No longer attempting to replay.',
queueTime,
);
} else {
// The qt= URL parameter specifies the time delta in between right now, and when the
// /collect request was initially intended to be sent. See
// https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#qt
var requestUrl = `${savedRequest.url }&qt=${ queueTime}`;
console.log(' Replaying', requestUrl);
fetch(requestUrl)
.then((response) => {
if (response.status < 400) {
// If sending the /collect request was successful, then remove it from the IndexedDB.
getObjectStore(STORE_NAME, 'readwrite').delete(
savedRequest.url,
);
console.log(' Replaying succeeded.');
} else {
// This will be triggered if, e.g., Google Analytics returns a HTTP 50x response.
// The request will be replayed the next time the service worker starts up.
console.error(' Replaying failed:', response);
}
})
.catch((error) => {
// This will be triggered if the network is still down. The request will be replayed again
// the next time the service worker starts up.
console.error(' Replaying failed:', error);
});
}
});
}
};
}
// Open the IndexedDB and check for requests to replay each time the service worker starts up.
// Since the service worker is terminated fairly frequently, it should start up again for most
// page navigations. It also might start up if it's used in a background sync or a push
// notification context.
openDatabaseAndReplayRequests();
self.addEventListener('activate', (event) => {
// Delete all caches that aren't named in CURRENT_CACHES.
// While there is only one cache in this example, the same logic will handle the case where
// there are multiple versioned caches.
var expectedCacheNames = Object.keys(CURRENT_CACHES).map((key) => {
return CURRENT_CACHES[key];
});
event.waitUntil(
// `caches` refers to the global CacheStorage object, and is defined at
// http://slightlyoff.github.io/ServiceWorker/spec/service_worker/#self-caches
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (expectedCacheNames.indexOf(cacheName) === -1) {
// If this cache name isn't present in the array of "expected" cache names, then delete it.
console.log('Deleting out of date cache:', cacheName);
return caches.delete(cacheName);
}
}),
);
}),
);
});
// This sample illustrates an aggressive approach to caching, in which every valid response is
// cached and every request is first checked against the cache.
// This may not be an appropriate approach if your web application makes requests for
// arbitrary URLs as part of its normal operation (e.g. a RSS client or a news aggregator),
// as the cache could end up containing large responses that might not end up ever being accessed.
// Other approaches, like selectively caching based on response headers or only caching
// responses served from a specific domain, might be more appropriate for those use cases.
self.addEventListener('fetch', (event) => {
console.log('Handling fetch event for', event.request.url);
event.respondWith(
caches.open(CURRENT_CACHES['offline-analytics']).then((cache) => {
return cache
.match(event.request)
.then((response) => {
if (response) {
// If there is an entry in the cache for event.request, then response will be defined
// and we can just return it.
console.log(' Found response in cache:', response);
return response;
}
// Otherwise, if there is no entry in the cache for event.request, response will be
// undefined, and we need to fetch() the resource.
console.log(
' No response for %s found in cache. ' +
'About to fetch from network...',
event.request.url,
);
// We call .clone() on the request since we might use it in the call to cache.put() later on.
// Both fetch() and cache.put() "consume" the request, so we need to make a copy.
// (see https://fetch.spec.whatwg.org/#dom-request-clone)
return fetch(event.request.clone())
.then((response) => {
console.log(
' Response for %s from network is: %O',
event.request.url,
response,
);
// Optional: add in extra conditions here, e.g. response.type == 'basic' to only cache
// responses from the same domain. See https://fetch.spec.whatwg.org/#concept-response-type
if (response.status < 400) {
// This avoids caching responses that we know are errors (i.e. HTTP status code of 4xx or 5xx).
// One limitation is that, for non-CORS requests, we get back a filtered opaque response
// (https://fetch.spec.whatwg.org/#concept-filtered-response-opaque) which will always have a
// .status of 0, regardless of whether the underlying HTTP call was successful. Since we're
// blindly caching those opaque responses, we run the risk of caching a transient error response.
//
// We need to call .clone() on the response object to save a copy of it to the cache.
// (https://fetch.spec.whatwg.org/#dom-request-clone)
cache.put(event.request, response.clone());
} else if (response.status >= 500) {
// If this is a Google Analytics ping then we want to retry it if a HTTP 5xx response
// was returned, just like we'd retry it if the network was down.
checkForAnalyticsRequest(event.request.url);
}
// Return the original response object, which will be used to fulfill the resource request.
return response;
})
.catch((error) => {
// The catch() will be triggered for network failures. Let's see if it was a request for
// a Google Analytics ping, and save it to be retried if it was.
checkForAnalyticsRequest(event.request.url);
throw error;
});
})
.catch((error) => {
// This catch() will handle exceptions that arise from the match() or fetch() operations.
// Note that a HTTP error response (e.g. 404) will NOT trigger an exception.
// It will return a normal response object that has the appropriate error code set.
throw error;
});
}),
);
});
function checkForAnalyticsRequest(requestUrl) {
// Construct a URL object (https://developer.mozilla.org/en-US/docs/Web/API/URL.URL)
// to make it easier to check the various components without dealing with string parsing.
var url = new URL(requestUrl);
if (
(url.hostname === 'www.google-analytics.com' ||
url.hostname === 'ssl.google-analytics.com') &&
url.pathname === '/collect'
) {
console.log(
' Storing Google Analytics request in IndexedDB ' +
'to be replayed later.',
);
saveAnalyticsRequest(requestUrl);
}
}
function saveAnalyticsRequest(requestUrl) {
getObjectStore(STORE_NAME, 'readwrite').add({
url: requestUrl,
timestamp: Date.now(),
});
}

View File

@ -0,0 +1,93 @@
if (!Cache.prototype.add) {
Cache.prototype.add = function add(request) {
return this.addAll([request]);
};
}
if (!Cache.prototype.addAll) {
Cache.prototype.addAll = function addAll(requests) {
var cache = this;
// Since DOMExceptions are not constructable:
function NetworkError(message) {
this.name = 'NetworkError';
this.code = 19;
this.message = message;
}
NetworkError.prototype = Object.create(Error.prototype);
return Promise.resolve()
.then(function() {
if (arguments.length < 1) throw new TypeError();
// Simulate sequence<(Request or USVString)> binding:
var sequence = [];
requests = requests.map((request) => {
if (request instanceof Request) {
return request;
} else {
return String(request); // may throw TypeError
}
});
return Promise.all(
requests.map((request) => {
if (typeof request === 'string') {
request = new Request(request);
}
var scheme = new URL(request.url).protocol;
if (scheme !== 'http:' && scheme !== 'https:') {
throw new NetworkError('Invalid scheme');
}
return fetch(request.clone());
}),
);
})
.then((responses) => {
// TODO: check that requests don't overwrite one another
// (don't think this is possible to polyfill due to opaque responses)
return Promise.all(
responses.map((response, i) => {
return cache.put(requests[i], response);
}),
);
})
.then(() => {
return undefined;
});
};
}
if (!CacheStorage.prototype.match) {
// This is probably vulnerable to race conditions (removing caches etc)
CacheStorage.prototype.match = function match(request, opts) {
var caches = this;
return this.keys().then((cacheNames) => {
var match;
return cacheNames.reduce((chain, cacheName) => {
return chain.then(() => {
return (
match ||
caches
.open(cacheName)
.then((cache) => {
return cache.match(request, opts);
})
.then((response) => {
match = response;
return match;
})
);
});
}, Promise.resolve());
});
};
}
module.exports = self.caches;

View File

@ -1,6 +1,6 @@
{
"name": "pixelspin/silverstripe-progressivewebapp",
"description": "Tools to add progressive web app functionality to your silverstripe website",
"name": "a2nt/silverstripe-progressivewebapp",
"description": "Tools to add offline caching and the other progressive web app functionality to your silverstripe website",
"type": "silverstripe-vendormodule",
"keywords": [
"silverstripe",
@ -8,33 +8,29 @@
"app"
],
"license": "BSD-3-Clause",
"authors": [{
"name": "Michel van der Steege",
"email": "michelsteege@hotmail.com"
"authors": [
{
"name": "Tony Air",
"email": "tony@twma.pro"
}],
"minimum-stability": "dev",
"prefer-stable": true,
"require": {
"silverstripe/cms": "^4.0@dev",
"silverstripe/vendor-plugin": "^1.0",
"silverware/colorpicker": "^1.0"
"require":
{
"silverstripe/cms": "*"
},
"require-dev": {
"phpunit/phpunit": "^5.7",
"squizlabs/php_codesniffer": "^3.0"
},
"extra": {
"extra":
{
"installer-name": "silverstripe-progressivewebapp",
"branch-alias": {
"branch-alias":
{
"dev-master": "2.x-dev"
},
"expose": [
"resources"
]
}
},
"autoload": {
"psr-4": {
"Pixelspin\\ProgressiveWebApp\\": "src/"
"autoload":
{
"psr-4":
{
"A2nt\\ProgressiveWebApp\\": "src/"
}
}
}

272
eslint.config.json Executable file
View File

@ -0,0 +1,272 @@
{
// http://eslint.org/docs/rules/
"extends": "eslint:recommended",
"settings": {
"react": {
"version": "detect"
}
},
"env": {
"browser": true, // browser global variables.
"node": true, // Node.js global variables and Node.js-specific rules.
"amd": true, // defines require() and define() as global variables as per the amd spec.
"mocha": false, // adds all of the Mocha testing global variables.
"jasmine": false, // adds all of the Jasmine testing global variables for version 1.3 and 2.0.
"phantomjs": false, // phantomjs global variables.
"jquery": true, // jquery global variables.
"prototypejs": false, // prototypejs global variables.
"shelljs": false, // shelljs global variables.
"es6": true
},
"globals": {
// e.g. "angular": true
},
"plugins": ["react", "import", "jquery"],
"parser": "@babel/eslint-parser",
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true,
"experimentalObjectRestSpread": true
}
},
"rules": {
////////// Possible Errors //////////
"no-comma-dangle": 0, // disallow trailing commas in object literals
"no-cond-assign": 0, // disallow assignment in conditional expressions
"no-console": 0, // disallow use of console (off by default in the node environment)
"no-constant-condition": 0, // disallow use of constant expressions in conditions
"no-control-regex": 0, // disallow control characters in regular expressions
"no-debugger": 0, // disallow use of debugger
"no-dupe-keys": 0, // disallow duplicate keys when creating object literals
"no-empty": 0, // disallow empty statements
"no-empty-class": 0, // disallow the use of empty character classes in regular expressions
"no-ex-assign": 0, // disallow assigning to the exception in a catch block
"no-extra-boolean-cast": 0, // disallow double-negation boolean casts in a boolean context
"no-extra-parens": 0, // disallow unnecessary parentheses (off by default)
"no-extra-semi": 0, // disallow unnecessary semicolons
"no-func-assign": 0, // disallow overwriting functions written as function declarations
"no-inner-declarations": 0, // disallow function or variable declarations in nested blocks
"no-invalid-regexp": 0, // disallow invalid regular expression strings in the RegExp constructor
"no-irregular-whitespace": 0, // disallow irregular whitespace outside of strings and comments
"no-negated-in-lhs": 0, // disallow negation of the left operand of an in expression
"no-obj-calls": 0, // disallow the use of object properties of the global object (Math and JSON) as functions
"no-regex-spaces": 0, // disallow multiple spaces in a regular expression literal
"no-reserved-keys": 0, // disallow reserved words being used as object literal keys (off by default)
"no-sparse-arrays": 0, // disallow sparse arrays
"no-unreachable": 0, // disallow unreachable statements after a return, throw, continue, or break statement
"use-isnan": 0, // disallow comparisons with the value NaN
"valid-jsdoc": 0, // Ensure JSDoc comments are valid (off by default)
"valid-typeof": 0, // Ensure that the results of typeof are compared against a valid string
////////// Best Practices //////////
"block-scoped-var": 0, // treat var statements as if they were block scoped (off by default)
"complexity": 0, // specify the maximum cyclomatic complexity allowed in a program (off by default)
"consistent-return": 0, // require return statements to either always or never specify values
"curly": 0, // specify curly brace conventions for all control statements
"default-case": 0, // require default case in switch statements (off by default)
"dot-notation": 0, // encourages use of dot notation whenever possible
"eqeqeq": 0, // require the use of === and !==
"guard-for-in": 0, // make sure for-in loops have an if statement (off by default)
"no-alert": 0, // disallow the use of alert, confirm, and prompt
"no-caller": 0, // disallow use of arguments.caller or arguments.callee
"no-div-regex": 0, // disallow division operators explicitly at beginning of regular expression (off by default)
"no-else-return": 0, // disallow else after a return in an if (off by default)
"no-empty-label": 0, // disallow use of labels for anything other then loops and switches
"no-eq-null": 0, // disallow comparisons to null without a type-checking operator (off by default)
"no-eval": 0, // disallow use of eval()
"no-extend-native": 0, // disallow adding to native types
"no-extra-bind": 0, // disallow unnecessary function binding
"no-fallthrough": 0, // disallow fallthrough of case statements
"no-floating-decimal": 0, // disallow the use of leading or trailing decimal points in numeric literals (off by default)
"no-implied-eval": 0, // disallow use of eval()-like methods
"no-iterator": 0, // disallow usage of __iterator__ property
"no-labels": 0, // disallow use of labeled statements
"no-lone-blocks": 0, // disallow unnecessary nested blocks
"no-loop-func": 0, // disallow creation of functions within loops
"no-multi-spaces": 0, // disallow use of multiple spaces
"no-multi-str": 0, // disallow use of multiline strings
"no-native-reassign": 0, // disallow reassignments of native objects
"no-new": 0, // disallow use of new operator when not part of the assignment or comparison
"no-new-func": 0, // disallow use of new operator for Function object
"no-new-wrappers": 0, // disallows creating new instances of String, Number, and Boolean
"no-octal": 0, // disallow use of octal literals
"no-octal-escape": 0, // disallow use of octal escape sequences in string literals, such as var foo = "Copyright \251";
"no-process-env": 0, // disallow use of process.env (off by default)
"no-proto": 0, // disallow usage of __proto__ property
"no-redeclare": 0, // disallow declaring the same variable more then once
"no-return-assign": 0, // disallow use of assignment in return statement
"no-script-url": 0, // disallow use of javascript: urls.
"no-self-compare": 0, // disallow comparisons where both sides are exactly the same (off by default)
"no-sequences": 0, // disallow use of comma operator
"no-unused-expressions": 0, // disallow usage of expressions in statement position
"no-void": 0, // disallow use of void operator (off by default)
"no-warning-comments": 0, // disallow usage of configurable warning terms in comments, e.g. TODO or FIXME (off by default)
"no-with": 0, // disallow use of the with statement
"radix": 0, // require use of the second argument for parseInt() (off by default)
"vars-on-top": 0, // requires to declare all vars on top of their containing scope (off by default)
"wrap-iife": 0, // require immediate function invocation to be wrapped in parentheses (off by default)
"yoda": 0, // require or disallow Yoda conditions
////////// Strict Mode //////////
"global-strict": 0, // (deprecated) require or disallow the "use strict" pragma in the global scope (off by default in the node environment)
"no-extra-strict": 0, // (deprecated) disallow unnecessary use of "use strict"; when already in strict mode
"strict": 0, // controls location of Use Strict Directives
////////// Variables //////////
"no-catch-shadow": 0, // disallow the catch clause parameter name being the same as a variable in the outer scope (off by default in the node environment)
"no-delete-var": 0, // disallow deletion of variables
"no-label-var": 0, // disallow labels that share a name with a variable
"no-shadow": 0, // disallow declaration of variables already declared in the outer scope
"no-shadow-restricted-names": 0, // disallow shadowing of names such as arguments
"no-undef": 0, // disallow use of undeclared variables unless mentioned in a /*global */ block
"no-undef-init": 0, // disallow use of undefined when initializing variables
"no-undefined": 0, // disallow use of undefined variable (off by default)
"no-unused-vars": 0, // disallow declaration of variables that are not used in the code
"no-use-before-define": 0, // disallow use of variables before they are defined
////////// Node.js //////////
"handle-callback-err": 0, // enforces error handling in callbacks (off by default) (on by default in the node environment)
"no-mixed-requires": 0, // disallow mixing regular variable and require declarations (off by default) (on by default in the node environment)
"no-new-require": 0, // disallow use of new operator with the require function (off by default) (on by default in the node environment)
"no-path-concat": 0, // disallow string concatenation with __dirname and __filename (off by default) (on by default in the node environment)
"no-process-exit": 0, // disallow process.exit() (on by default in the node environment)
"no-restricted-modules": 0, // restrict usage of specified node modules (off by default)
"no-sync": 0, // disallow use of synchronous methods (off by default)
////////// Stylistic Issues //////////
"brace-style": 0, // enforce one true brace style (off by default)
"camelcase": 0, // require camel case names
"comma-spacing": 0, // enforce spacing before and after comma
"comma-style": 0, // enforce one true comma style (off by default)
"consistent-this": 0, // enforces consistent naming when capturing the current execution context (off by default)
"eol-last": 0, // enforce newline at the end of file, with no multiple empty lines
"func-names": 0, // require function expressions to have a name (off by default)
"func-style": 0, // enforces use of function declarations or expressions (off by default)
"key-spacing": 0, // enforces spacing between keys and values in object literal properties
"max-nested-callbacks": 0, // specify the maximum depth callbacks can be nested (off by default)
"new-cap": 0, // require a capital letter for constructors
"new-parens": 0, // disallow the omission of parentheses when invoking a constructor with no arguments
"no-array-constructor": 0, // disallow use of the Array constructor
"no-inline-comments": 0, // disallow comments inline after code (off by default)
"no-lonely-if": 0, // disallow if as the only statement in an else block (off by default)
"no-mixed-spaces-and-tabs": 0, // disallow mixed spaces and tabs for indentation
"no-multiple-empty-lines": 0, // disallow multiple empty lines (off by default)
"no-nested-ternary": 0, // disallow nested ternary expressions (off by default)
"no-new-object": 0, // disallow use of the Object constructor
"no-space-before-semi": 0, // disallow space before semicolon
"no-spaced-func": 0, // disallow space between function identifier and application
"no-ternary": 0, // disallow the use of ternary operators (off by default)
"no-trailing-spaces": 0, // disallow trailing whitespace at the end of lines
"no-underscore-dangle": 0, // disallow dangling underscores in identifiers
"no-wrap-func": 0, // disallow wrapping of non-IIFE statements in parens
"one-var": 0, // allow just one var statement per function (off by default)
"operator-assignment": 0, // require assignment operator shorthand where possible or prohibit it entirely (off by default)
"padded-blocks": 0, // enforce padding within blocks (off by default)
"quote-props": 0, // require quotes around object literal property names (off by default)
"quotes": 0, // specify whether double or single quotes should be used
"semi": 0, // require or disallow use of semicolons instead of ASI
"sort-vars": 0, // sort variables within the same declaration block (off by default)
"space-after-function-name": 0, // require a space after function names (off by default)
"space-after-keywords": 0, // require a space after certain keywords (off by default)
"space-before-blocks": 0, // require or disallow space before blocks (off by default)
"space-in-brackets": 0, // require or disallow spaces inside brackets (off by default)
"space-in-parens": 0, // require or disallow spaces inside parentheses (off by default)
"space-infix-ops": 0, // require spaces around operators
"space-return-throw-case": 0, // require a space after return, throw, and case
"space-unary-ops": 0, // Require or disallow spaces before/after unary operators (words on by default, nonwords off by default)
"spaced-line-comment": 0, // require or disallow a space immediately following the // in a line comment (off by default)
"wrap-regex": 0, // require regex literals to be wrapped in parentheses (off by default)
////////// ECMAScript 6 //////////
"no-var": 0, // require let or const instead of var (off by default)
"generator-star": 0, // enforce the position of the * in generator functions (off by default)
////////// Legacy //////////
"max-depth": 0, // specify the maximum depth that blocks can be nested (off by default)
"max-len": 0, // specify the maximum length of a line in your program (off by default)
"max-params": 0, // limits the number of parameters that can be used in the function declaration. (off by default)
"max-statements": 0, // specify the maximum number of statement allowed in a function (off by default)
"no-bitwise": 0, // disallow use of bitwise operators (off by default)
"no-plusplus": 0, // disallow use of unary operators, ++ and -- (off by default)
//////// Extra //////////
"array-bracket-spacing": ["error", "never"],
"array-callback-return": "error",
"arrow-parens": ["error", "always"],
"arrow-spacing": ["error", { "before": true, "after": true }],
"comma-dangle": ["error", "always-multiline"],
"indent": ["error", 2, { "SwitchCase": 1 }],
"no-case-declarations": "error",
"no-confusing-arrow": "error",
"no-duplicate-imports": "error",
"no-param-reassign": "error",
"no-useless-escape": "error",
"object-curly-spacing": ["error", "always"],
"object-shorthand": ["error", "properties"],
"prefer-arrow-callback": "error",
"prefer-const": "error",
"prefer-template": "error",
"react/jsx-closing-bracket-location": "error",
"react/jsx-curly-spacing": [
"error",
"never",
{ "allowMultiline": true }
],
"react/jsx-filename-extension": [
"error",
{ "extensions": [".react.js", ".js", ".jsx"] }
],
"react/jsx-no-duplicate-props": "error",
"react/jsx-no-bind": [
"error",
{
"ignoreRefs": true,
"allowArrowFunctions": true,
"allowBind": false
}
],
"react/jsx-no-undef": "error",
"react/jsx-pascal-case": "error",
"react/jsx-tag-spacing": [
"error",
{
"closingSlash": "never",
"beforeSelfClosing": "always",
"afterOpening": "never"
}
],
"react/jsx-uses-react": "error",
"react/jsx-uses-vars": "error",
"react/no-danger": "error",
"react/no-deprecated": "error",
"react/no-did-mount-set-state": "error",
"react/no-did-update-set-state": "error",
"react/no-direct-mutation-state": "error",
"react/no-is-mounted": "error",
"react/no-multi-comp": "error",
"react/prefer-es6-class": "error",
"react/prop-types": "error",
"react/require-render-return": "error",
"react/self-closing-comp": "error",
"react/sort-comp": "error",
"import/no-mutable-exports": "error",
"import/imports-first": "warn"
}
}

232
package.json Executable file
View File

@ -0,0 +1,232 @@
{
"name": "ss-webpack-boilerplate",
"version": "5.0.0",
"description": "Lets you create SilverStripe faster",
"author": "Tony Air <tony@twma.pro>",
"license": "MIT",
"private": false,
"repository": {
"type": "git",
"url": "git+https://github.com/a2nt/silverstripe-webpack"
},
"engines": {
"yarn": ">= 1.0.0"
},
"scripts": {
"start": "cross-env NODE_ENV=development webpack-dev-server --config webpack.config.serve.js",
"dash": "cross-env NODE_ENV=development webpack-dashboard -- webpack-dev-server --config webpack.config.serve.js",
"prebuild": "yarn lint:fix && yarn lint:check && rimraf ./client/dist",
"build": "cross-env NODE_ENV=production webpack --progress --stats-all",
"lint:fix": "eslint './client/src/**/*.js' -c eslint.config.json --fix",
"lint:js": "eslint './client/src/**/*.js' -c eslint.config.json",
"lint:scss": "sass-lint ./client/src/**/*.scss -c sass-lint.yml -v",
"lint:check": "yarn lint:js && yarn lint:scss",
"prunecaches": "rimraf ./node_modules/.cache/",
"postinstall": "npm run prunecaches",
"postuninstall": "npm run prunecaches",
"preinstall": "npx only-allow pnpm"
},
"resolutions": {
"colors": "1.4.0"
},
"browserslist": [
"defaults",
"ie>=11"
],
"dependencies": {
"@a2nt/meta-lightbox-js": "^4.2.2",
"@a2nt/ss-bootstrap-ui-webpack-boilerplate-react": "^4.6.1",
"@angular/common": "^13.3.5",
"@angular/core": "^13.3.5",
"@apollo/client": "^3.6.2",
"@jsanahuja/instagramfeed": "github:jsanahuja/instagramfeed",
"@popperjs/core": "^2.11.5",
"@turf/clone": "^6.5.0",
"@turf/clusters-dbscan": "^6.5.0",
"@turf/clusters-kmeans": "^6.5.0",
"@turf/distance": "^6.5.0",
"@turf/helpers": "^6.5.0",
"@turf/invariant": "^6.5.0",
"@turf/meta": "^6.5.0",
"aos": "^2.3.4",
"apollo3-cache-persist": "^0.14.0",
"balanced-match": "^2.0.0",
"bootbox": "^5.5.3",
"bootstrap": "^5.1.3",
"brace-expansion": "^2.0.1",
"charming": "^3.0.2",
"density-clustering": "^1.3.0",
"eslint-scope": "^7.1.1",
"fast-deep-equal": "^3.1.3",
"font-awesome": "^4.7.0",
"graphql": "^16.4.0",
"hammerjs": "^2.0.8",
"inputmask": "^5.0.7",
"kdbush": "^3.0.0",
"keyboardjs": "^2.6.4",
"localforage": "^1.10.0",
"localforage-cordovasqlitedriver": "^1.8.0",
"lodash.debounce": "^4.0.8",
"lodash.throttle": "^4.1.1",
"mapbox-gl": "^2.8.2",
"material-design-color": "^2.3.2",
"minimatch": "^5.0.1",
"moment": "^2.29.3",
"react": "^18.1.0",
"react-dom": "^18.1.0",
"react-easy-swipe": "^0.0.22",
"react-tiny-oembed": "^1.1.0",
"redaxios": "^0.5.0",
"rxjs": "^7.5.5",
"select2": "^4.0.13",
"setimmediate": "^1.0.5",
"skmeans": "^0.11.3",
"supercluster": "^7.1.5",
"vanilla-calendar": "^1.0.30",
"vanillajs-datepicker": "^1.2.0",
"youtube-embed": "^1.0.0"
},
"devDependencies": {
"@a2nt/image-sprite-webpack-plugin": "^0.2.5",
"@babel/core": "^7.17.10",
"@babel/eslint-parser": "^7.17.0",
"@babel/plugin-proposal-class-properties": "^7.16.7",
"@babel/plugin-proposal-object-rest-spread": "^7.17.3",
"@babel/plugin-syntax-jsx": "^7.16.7",
"@babel/plugin-syntax-top-level-await": "^7.14.5",
"@babel/plugin-transform-react-jsx": "^7.17.3",
"@babel/plugin-transform-runtime": "^7.17.10",
"@babel/plugin-transform-typescript": "^7.16.8",
"@babel/preset-env": "^7.17.10",
"@babel/preset-react": "^7.16.7",
"@babel/runtime": "^7.17.9",
"@googlemaps/markerclusterer": "*",
"@sucrase/webpack-loader": "^2.0.0",
"@ungap/global-this": "^0.4.4",
"@wry/context": "^0.6.1",
"@wry/equality": "^0.5.2",
"@wry/trie": "^0.3.1",
"animate.css": "^4.1.1",
"ansi-html": "^0.0.9",
"ansi-html-community": "^0.0.8",
"ansi-regex": "^6.0.1",
"autoprefixer": "^10.4.7",
"babel-loader": "^8.2.5",
"classnames": "^2.3.1",
"copy-webpack-plugin": "^10.2.4",
"croppie": "^2.6.5",
"cross-env": "^7.0.3",
"css-loader": "^6.7.1",
"css-minimizer-webpack-plugin": "^3.4.1",
"eslint": "^8.14.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jquery": "^1.5.1",
"eslint-plugin-react": "^7.29.4",
"events": "^3.3.0",
"exif-js": "^2.3.0",
"exports-loader": "^3.1.0",
"fast-equals": "^3.0.2",
"fast-json-stable-stringify": "^2.1.0",
"fast-levenshtein": "^3.0.0",
"fastest-levenshtein": "^1.0.12",
"favicons": "^6.2.2",
"favicons-webpack-plugin": "^5.0.2",
"file-loader": "^6.2.0",
"graphql-tag": "^2.12.6",
"hoist-non-react-statics": "^3.3.2",
"html-dom-parser": "^1.2.0",
"html-entities": "^2.3.3",
"html-loader": "^3.1.0",
"html-react-parser": "^1.4.12",
"html-webpack-plugin": "^5.5.0",
"img-optimize-loader": "^1.0.7",
"js-yaml": "^4.1.0",
"loglevel": "^1.8.0",
"mini-css-extract-plugin": "^2.6.0",
"msw": "^0.39.2",
"node-fetch": "^3.2.4",
"object-assign": "^4.1.1",
"optimism": "^0.16.1",
"optimize-css-assets-webpack-plugin": "^6.0.1",
"postcss-loader": "^6.2.1",
"prop-types": "^15.8.1",
"punycode": "^2.1.1",
"querystring": "^0.2.1",
"raw-loader": "^4.0.2",
"react-hot-loader": "^4.13.0",
"react-is": "^18.1.0",
"react-lifecycles-compat": "^3.0.4",
"regenerator-runtime": "^0.13.9",
"resolve-url-loader": "^5.0.0",
"rimraf": "^3.0.2",
"routie": "0.0.1",
"sass": "*",
"sass-lint": "^1.13.1",
"sass-lint-auto-fix": "^0.21.2",
"sass-lint-fix": "^1.12.1",
"sass-loader": "^12.6.0",
"scheduler": "^0.22.0",
"shallowequal": "^1.1.0",
"strip-ansi": "^7.0.1",
"style-loader": "^3.3.1",
"sucrase": "^3.21.0",
"svg-url-loader": "^7.1.1",
"symbol-observable": "^4.0.0",
"terser-webpack-plugin": "^5.3.1",
"ts-invariant": "^0.9.4",
"tslib": "^2.4.0",
"url": "^0.11.0",
"url-loader": "^4.1.1",
"webpack": "^5.72.0",
"webpack-bundle-analyzer": "^4.5.0",
"webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.8.1",
"webpack-manifest-plugin": "^5.0.0",
"webpack-merge": "^5.8.0",
"yarn": "^1.22.18",
"zen-observable": "^0.8.15"
},
"stylelint": {
"rules": {
"block-no-empty": null,
"color-no-invalid-hex": true,
"comment-empty-line-before": [
"always",
{
"ignore": [
"stylelint-commands",
"after-comment"
]
}
],
"declaration-colon-space-after": "always",
"indentation": [
4,
{
"except": [
"value"
]
}
],
"max-empty-lines": 2,
"rule-empty-line-before": [
"always",
{
"except": [
"first-nested"
],
"ignore": [
"after-comment"
]
}
],
"unit-whitelist": [
"em",
"rem",
"%",
"s",
"px"
]
}
}
}

11654
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +0,0 @@
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js');
}

173
sass-lint.yml Executable file
View File

@ -0,0 +1,173 @@
# sass-lint config to match the AirBNB style guide
files:
include: 'app/client/src/**/*.scss'
ignore:
- 'app/client/src/thirdparty/*'
options:
formatter: stylish
merge-default-rules: false
rules:
# Warnings
# Things that require actual refactoring are marked as warnings
class-name-format:
- 1
- convention: hyphenatedbem
placeholder-name-format:
- 1
- convention: hyphenatedlowercase
nesting-depth:
- 1
- max-depth: 3
no-ids: 0
no-important: 0
no-misspelled-properties:
- 1
- extra-properties:
- '-moz-border-radius-topleft'
- '-moz-border-radius-topright'
- '-moz-border-radius-bottomleft'
- '-moz-border-radius-bottomright'
variable-name-format:
- 1
- allow-leading-underscore: true
convention: hyphenatedlowercase
no-extends: 1
# Warnings: these things are preferential rather than mandatory
no-css-comments: 1
# Errors
# Things that can be easily fixed are marked as errors
indentation:
- 2
- size: 4
final-newline:
- 2
- include: true
no-trailing-whitespace: 2
border-zero:
- 2
- convention: '0'
brace-style:
- 2
- allow-single-line: true
clean-import-paths:
- 2
- filename-extension: false
- leading-underscore: true
no-debug: 2
no-empty-rulesets: 2
no-invalid-hex: 2
no-mergeable-selectors: 2
# no-qualifying-elements:
# - 1
# - allow-element-with-attribute: false
# allow-element-with-class: false
# allow-element-with-id: false
no-trailing-zero: 2
no-url-protocols: 2
quotes:
- 2
- style: double
space-after-bang:
- 2
- include: false
space-after-colon:
- 2
- include: true
space-after-comma:
- 2
- include: true
space-before-bang:
- 2
- include: true
space-before-brace:
- 2
- include: true
space-before-colon: 2
space-between-parens:
- 2
- include: false
trailing-semicolon: 2
url-quotes: 2
zero-unit: 2
single-line-per-selector: 2
one-declaration-per-line: 2
empty-line-between-blocks:
- 2
- ignore-single-line-rulesets: true
# Missing rules
# There are no sass-lint rules for the following AirBNB style items, but thess
# - Put comments on their own line
# - Put property delcarations before mixins
# Disabled rules
# These are other rules that we may wish to consider using in the future
# They are not part of the AirBNB CSS standard but they would introduce some strictness
# bem-depth: 0
# variable-for-property: 0
# no-transition-all: 0
# hex-length:
# - 1
# - style: short
# hex-notation:
# - 1
# - style: lowercase
# property-units:
# - 1
# - global:
# - ch
# - em
# - ex
# - rem
# - cm
# - in
# - mm
# - pc
# - pt
# - px
# - q
# - vh
# - vw
# - vmin
# - vmax
# - deg
# - grad
# - rad
# - turn
# - ms
# - s
# - Hz
# - kHz
# - dpi
# - dpcm
# - dppx
# - '%'
# per-property: {}
# force-attribute-nesting: 1
# force-element-nesting: 1
# force-pseudo-nesting: 1
# function-name-format:
# - 1
# - allow-leading-underscore: true
# convention: hyphenatedlowercase
# no-color-literals: 1
# no-duplicate-properties: 1
# mixin-name-format:
# - 1
# - allow-leading-underscore: true
# convention: hyphenatedlowercase
# shorthand-values:
# - 1
# - allowed-shorthands:
# - 1
# - 2
# - 3
# leading-zero:
# - 1
# - include: false
# no-vendor-prefixes:
# - 1
# - additional-identifiers: []
# excluded-identifiers: []
# placeholder-in-extend: 1
# no-color-keywords: 2

View File

@ -1 +0,0 @@
self.addEventListener('fetch', (event) => {});

View File

@ -0,0 +1,123 @@
<?php
namespace A2nt\ProgressiveWebApp\Controllers;
use SilverStripe\Control\Controller;
use SilverStripe\Control\Director;
use SilverStripe\SiteConfig\SiteConfig;
use Site\Pages\HomePage;
class ManifestController extends Controller
{
private static $gcm_sender_id;
private static $background = '#000000';
/**
* @var array
*/
private static $allowed_actions = [
'index',
];
/**
* Default controller action for the manifest.json file
*
* @return mixed
*/
public function index($url)
{
$cfg = $this->config();
$cfg_site = SiteConfig::current_site_config();
$baseURL = Director::absoluteBaseURL();
$icons_path = self::join_links($baseURL, RESOURCES_DIR, 'app', 'client', 'dist', 'icons');
$title = $cfg_site->getField('Title');
$desc = $cfg_site->getField('Description');
$desc = $desc ?: $title;
$manifestContent = [
'lang' => 'en',
'dir' => 'ltr',
'url' => $baseURL,
'name' => $title,
'short_name' => $title,
'description' => $desc,
'offline_enabled' => true,
'start_url' => Director::baseURL(),
'scope' => Director::baseURL(),
'permissions' => [
'gcm',
],
'display' => 'standalone',
'background_color' => $cfg->get('background'),
'theme_color' => $cfg->get('background'),
'orientation' => 'portrait-primary',
'serviceworker' => [
'src' => 'sw.js?v='.ServiceWorkerController::Version(),
'scope' => '/',
'use_cache' => true,
],
'icons' => [
[
'src' => self::join_links($icons_path, 'favicon-16x16.png'),
'sizes' => '16x16',
'type' => 'image/png',
],
[
'src' => self::join_links($icons_path, 'favicon-32x32.png'),
'sizes' => '32x32',
'type' => 'image/png',
],
[
'src' => self::join_links($icons_path, 'android-chrome-48x48.png'),
'sizes' => '48x48',
'type' => 'image/png',
],[
'src' => self::join_links($icons_path, 'android-chrome-72x72.png'),
'sizes' => '72x72',
'type' => 'image/png',
], [
'src' => self::join_links($icons_path, 'android-chrome-96x96.png'),
'sizes' => '96x96',
'type' => 'image/png',
], [
'src' => self::join_links($icons_path, 'android-chrome-144x144.png'),
'sizes' => '144x144',
'type' => 'image/png',
],
[
'src' => self::join_links($icons_path, 'android-chrome-192x192.png'),
'sizes' => '192x192',
'type' => 'image/png',
],
[
'src' => self::join_links($icons_path, 'android-chrome-256x256.png'),
'sizes' => '256x256',
'type' => 'image/png',
],
[
'src' => self::join_links($icons_path, 'android-chrome-384x384.png'),
'sizes' => '384x384',
'type' => 'image/png',
],
[
'src' => self::join_links($icons_path, 'android-chrome-512x512.png'),
'sizes' => '512x512',
'type' => 'image/png',
'purpose' => 'any maskable',
],
]
];
$gcm_sender_id = $cfg->get('gcm_sender_id');
if ($gcm_sender_id) {
$manifestContent['gcm_sender_id'] = $gcm_sender_id;
$manifestContent['gcm_user_visible_only'] = true;
}
$this->getResponse()->addHeader('Content-Type', 'application/manifest+json; charset="utf-8"');
return json_encode($manifestContent);
}
}

View File

@ -1,100 +0,0 @@
<?php
namespace Pixelspin\ProgressiveWebApp\Controllers;
use SilverStripe\Control\Controller;
use SilverStripe\SiteConfig\SiteConfig;
class ProgressiveWebAppController extends Controller {
/**
* @var array
*/
private static $allowed_actions = [
'index'
];
/**
* Default controller action for the manifest.json file
*
* @return mixed
*/
public function index($url) {
$config = SiteConfig::current_site_config();
$manifestContent = [];
$manifestContent['start_url'] = '/';
if($config->ManifestName){
$manifestContent['name'] = $config->ManifestName;
}
if($config->ManifestShortName){
$manifestContent['short_name'] = $config->ManifestShortName;
}
if($config->ManifestDescription){
$manifestContent['description'] = $config->ManifestDescription;
}
if($config->ManifestColor){
$manifestContent['background_color'] = $config->ManifestColor;
$manifestContent['theme_color'] = $config->ManifestColor;
}
if($config->ManifestOrientation){
$manifestContent['orientation'] = $config->ManifestOrientation;
}
if($config->ManifestDisplay){
$manifestContent['display'] = $config->ManifestDisplay;
}
$logo = $config->ManifestLogo();
if($logo && $logo->exists()){
$mime = $logo->getMimeType();
$manifestContent['icons'] = [
[
'src' => $logo->Fill(48,48)->Link(),
'sizes' => '48x48',
'type' => $mime
],
[
'src' => $logo->Fill(72,72)->Link(),
'sizes' => '72x72',
'type' => $mime
],
[
'src' => $logo->Fill(96,96)->Link(),
'sizes' => '96x96',
'type' => $mime
],
[
'src' => $logo->Fill(144,144)->Link(),
'sizes' => '144x144',
'type' => $mime
],
[
'src' => $logo->Fill(168,168)->Link(),
'sizes' => '168x168',
'type' => $mime
],
[
'src' => $logo->Fill(192,192)->Link(),
'sizes' => '192x192',
'type' => $mime
],
[
'src' => $logo->Fill(256,256)->Link(),
'sizes' => '256x256',
'type' => $mime
],
[
'src' => $logo->Fill(512,512)->Link(),
'sizes' => '512x512',
'type' => $mime
]
];
}
$this->getResponse()->addHeader('Content-Type', 'application/manifest+json; charset="utf-8"');
return json_encode($manifestContent);
}
}

View File

@ -0,0 +1,108 @@
<?php
namespace A2nt\ProgressiveWebApp\Controllers;
use SilverStripe\Control\Controller;
use SilverStripe\Control\Director;
use SilverStripe\ORM\ArrayList;
use SilverStripe\View\ArrayData;
use SilverStripe\Core\ClassInfo;
use MichelSteege\ProgressiveWebApp\Interfaces\ServiceWorkerCacheProvider;
class ServiceWorkerController extends Controller {
/**
* @var array
*/
private static $allowed_actions = [
'index',
];
/**
* @config
*/
private static $debug_mode = false;
private static $version = '1';
private static $custom_sw_path;
/**
* Default controller action for the service-worker.js file
*
* @return mixed
*/
public function index($req) {
$resp = $this->getResponse();
$script = file_get_contents(self::getScriptPath());
if($req->param('Action') === 'cachequeue') {
return json_encode([
'urls' => [
self::join_links(self::BaseUrl(),'app','client','dist', 'js', 'app.js')
]
]);
}
$resp->addHeader('Content-Type', 'application/javascript; charset="utf-8"');
return $this->customise([
'Script' => $script,
])->renderWith('ServiceWorker');
}
private static function getScriptPath()
{
$custom_path = self::config()->get('custom_sw_path');
return $custom_path ? $custom_path : join(DIRECTORY_SEPARATOR, [
__DIR__,
'..',
'..',
'client',
'dist',
'js',
'app_sw.js',
]);
}
/**
* Base URL
* @return varchar
*/
public static function BaseUrl() {
return Director::absoluteBaseURL();
}
/**
* Debug mode
* @return bool
*/
public static function DebugMode() {
if(Director::isDev()){
return true;
}
return self::config()->get('debug_mode');
}
public static function Version() {
return self::config()->get('version').filemtime(self::getScriptPath());
}
/**
* A list with file to cache in the install event
* @return ArrayList
*/
public static function CacheOnInstall() {
$paths = [];
foreach(ClassInfo::implementorsOf(ServiceWorkerCacheProvider::class) as $class){
foreach($class::getServiceWorkerCachedPaths() as $path){
$paths[] = $path;
}
}
$list = new ArrayList();
foreach($paths as $path){
$list->push(new ArrayData([
'Path' => $path
]));
}
return $list;
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace A2nt\ProgressiveWebApp\Controllers;
use SilverStripe\Control\Controller;
use SilverStripe\Control\Director;
use SilverStripe\Security\Security;
class WellKnownController extends Controller
{
private static $allowed_actions = [
'index',
];
public function index()
{
$req = $this->getRequest();
$action = $req->param('Action');
switch($action) {
case 'change-password':
return $this->changepassword();
default:
return $this->httpError(404, 'Not found');
}
}
public function changepassword()
{
return $this->redirect(
Director::absoluteURL(
Security::singleton()->Link('changepassword')
), 303
);
}
}

View File

@ -1,68 +0,0 @@
<?php
namespace Pixelspin\ProgressiveWebApp\Extensions;
use SilverStripe\ORM\DataExtension;
use SilverWare\Colorpicker\ORM\FieldType\DBColor;
use SilverStripe\Assets\Image;
use SilverStripe\Forms\FieldList;
use SilverStripe\AssetAdmin\Forms\UploadField;
use SilverStripe\Forms\TextField;
use SilverStripe\Forms\DropdownField;
use SilverWare\Colorpicker\Forms\ColorField;
class ProgressiveWebAppSiteConfigExtension extends DataExtension {
private static $db = [
'ManifestName' => 'Varchar',
'ManifestShortName' => 'Varchar',
'ManifestDescription' => 'Varchar(255)',
'ManifestColor' => DBColor::class,
'ManifestOrientation' => 'Varchar',
'ManifestDisplay' => 'Varchar'
];
private static $displays = [
'fullscreen',
'standalone',
'minimal-ui',
'browser'
];
private static $orientations = [
'any',
'natural',
'landscape',
'landscape-primary',
'landscape-secondary',
'portrait',
'portrait-primary',
'portrait-secondary'
];
private static $has_one = [
'ManifestLogo' => Image::class
];
public function onAfterWrite() {
parent::onAfterWrite();
$manifestLogo = $this->owner->ManifestLogo();
if ($manifestLogo && $manifestLogo->exists()) {
$manifestLogo->doPublish();
}
}
public function updateCMSFields(FieldList $fields) {
$fields->addFieldToTab('Root.ProgressiveWebApp', TextField::create('ManifestName', 'Name')->setDescription('Application name'));
$fields->addFieldToTab('Root.ProgressiveWebApp', TextField::create('ManifestShortName', 'Short name')->setDescription('Short human-readable name for the application try to keep it at a maximum of 12 characters'));
$fields->addFieldToTab('Root.ProgressiveWebApp', TextField::create('ManifestDescription', 'Description')->setDescription('Short description about the app'));
$fields->addFieldToTab('Root.ProgressiveWebApp', ColorField::create('ManifestColor', 'Color')->setDescription('Color used for the splash screen and/or icon'));
$fields->addFieldToTab('Root.ProgressiveWebApp', DropdownField::create('ManifestOrientation', 'Orientation', array_combine(self::$orientations, self::$orientations))->setDescription('App orientation'));
$fields->addFieldToTab('Root.ProgressiveWebApp', DropdownField::create('ManifestDisplay', 'Display', array_combine(self::$displays, self::$displays))->setDescription('Display mode of the app'));
$fields->addFieldToTab('Root.ProgressiveWebApp', UploadField::create('ManifestLogo', 'Logo')->setDescription('This image must be square and at least 512x512px'));
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace A2nt\ProgressiveWebApp\Extensions;
use SilverStripe\Forms\CheckboxField;
use SilverStripe\Forms\FieldList;
use SilverStripe\ORM\DataExtension;
class SiteTree extends DataExtension
{
private static $db = [
'AvailableOffline' => 'Boolean(1)',
];
public function updateSettingsFields(FieldList $fields)
{
parent::updateCMSFields($fields);
$fields->addFieldsToTab('Root.Settings', [
CheckboxField::create('AvailableOffline', 'Make page available offline'),
]);
}
}

View File

@ -0,0 +1,9 @@
<?php
namespace A2nt\ProgressiveWebApp\Interfaces;
interface ServiceWorkerCacheProvider {
public static function getServiceWorkerCachedPaths();
}

View File

@ -0,0 +1,23 @@
<?php
namespace A2nt\ProgressiveWebApp\Templates;
use A2nt\ProgressiveWebApp\Controllers\ServiceWorkerController;
use SilverStripe\View\TemplateGlobalProvider;
class ServiceWorkerTemplateProvider implements TemplateGlobalProvider
{
public static function get_template_global_variables(): array
{
return [
'SWVersion' => 'swVersion',
];
}
public static function swVersion()
{
if(class_exists(ServiceWorkerController::class)) {
return ServiceWorkerController::Version();
}
}
}

View File

@ -0,0 +1,8 @@
'use strict';
var version = '{$Version}',
appDomain = '{$BaseUrl}',
lang = 'en',
debug = <% if $DebugMode %>true<% else %>false<% end_if %>,
CACHE_NAME = 'sw' + version;
$Script.RAW;

196
webpack.config.common.js Executable file
View File

@ -0,0 +1,196 @@
/*
* Common Environment
*/
const INDEX_NAME = '';
const YML_PATH = 'webpack.yml';
const CONF_VAR = 'A2nt\\CMSNiceties\\Templates\\WebpackTemplateProvider';
const path = require('path');
const fs = require('fs');
const yaml = require('js-yaml');
const webpack = require('webpack');
/*
* Load webpack configuration from webpack.yml
*/
const yml = yaml.load(
fs.readFileSync(path.join(__dirname, YML_PATH), 'utf8'),
);
const conf = yml[CONF_VAR];
let themes = [];
// add themes
if (conf.THEMESDIR) {
const themeDir = conf.THEMESDIR;
const dir = path.resolve(__dirname, themeDir);
if (fs.existsSync(dir)) {
fs.readdirSync(dir).forEach((file) => {
filePath = path.join(themeDir, file);
const stat = fs.statSync(filePath);
if (stat && stat.isDirectory()) {
themes.push(filePath);
}
});
}
}
/* Setup Entries */
const includes = {};
const modules = [
path.resolve(__dirname, conf.APPDIR, conf.SRC),
path.resolve(__dirname, conf.APPDIR, conf.SRC, 'js'),
path.resolve(__dirname, conf.APPDIR, conf.SRC, 'scss'),
path.resolve(__dirname, conf.APPDIR, conf.SRC, 'img'),
path.resolve(__dirname, conf.APPDIR, conf.SRC, 'thirdparty'),
path.resolve(__dirname, 'node_modules'),
path.resolve(__dirname),
path.resolve(__dirname, 'public'),
];
const _addAppFiles = (theme) => {
const dirPath = './' + theme;
let themeName = path.basename(theme);
if (themeName == '.') {
themeName = 'app';
}
if (fs.existsSync(path.join(dirPath, conf.SRC, 'js', INDEX_NAME + '.js'))) {
includes[`${themeName}`] = path.join(dirPath, conf.SRC, 'js', INDEX_NAME + '.js');
} else if (
fs.existsSync(path.join(dirPath, conf.SRC, 'scss', INDEX_NAME + '.scss'))
) {
includes[`${themeName}`] = path.join(
dirPath,
conf.SRC,
'scss',
INDEX_NAME + '.scss',
);
}
modules.push(path.join(dirPath, conf.SRC, 'js'));
modules.push(path.join(dirPath, conf.SRC, 'scss'));
modules.push(path.join(dirPath, conf.SRC, 'img'));
modules.push(path.join(dirPath, conf.SRC, 'thirdparty'));
const _getAllFilesFromFolder = function (dir, includeSubFolders = true) {
const dirPath = path.resolve(__dirname, dir);
let results = [];
fs.readdirSync(dirPath).forEach((file) => {
if (file.charAt(0) === '_') {
return;
}
const filePath = path.join(dirPath, file);
const stat = fs.statSync(filePath);
if (stat && stat.isDirectory() && includeSubFolders) {
results = results.concat(
_getAllFilesFromFolder(filePath, includeSubFolders),
);
} else {
results.push(filePath);
}
});
return results;
};
// add page specific scripts
const typesJSPath = path.join(theme, conf.TYPESJS);
if (fs.existsSync(typesJSPath)) {
const pageScripts = _getAllFilesFromFolder(typesJSPath, true);
pageScripts.forEach((file) => {
includes[`${themeName}_${path.basename(file, '.js')}`] = file;
});
}
// add page specific scss
const typesSCSSPath = path.join(theme, conf.TYPESSCSS);
if (fs.existsSync(typesSCSSPath)) {
const scssIncludes = _getAllFilesFromFolder(typesSCSSPath, true);
scssIncludes.forEach((file) => {
includes[`${themeName}_${path.basename(file, '.scss')}`] = file;
});
}
};
_addAppFiles(conf.APPDIR);
// add themes
themes.forEach((theme) => {
_addAppFiles(theme);
});
const UIInfo = require('./node_modules/@a2nt/ss-bootstrap-ui-webpack-boilerplate-react/package.json');
const UIVERSION = JSON.stringify(UIInfo.version);
const UIMetaInfo = require('./node_modules/@a2nt/meta-lightbox-js/package.json');
const NODE_ENV = conf.NODE_ENV || process.env.NODE_ENV;
const COMPRESS = NODE_ENV === 'production' ? true : false;
const IP = process.env.IP || conf.HOSTNAME;
const PORT = process.env.PORT || conf.PORT;
console.log('NODE_ENV: ' + NODE_ENV);
console.log('COMPRESS: ' + COMPRESS);
console.log('WebP images: ' + conf['webp']);
console.log('GRAPHQL_API_KEY: ' + conf['GRAPHQL_API_KEY']);
const JSVARS = {
NODE_ENV: JSON.stringify(NODE_ENV),
UINAME: JSON.stringify(UIInfo.name),
UIVERSION: UIVERSION,
UIAUTHOR: JSON.stringify(UIInfo.author),
UIMetaNAME: JSON.stringify(UIMetaInfo.name),
UIMetaVersion: JSON.stringify(UIMetaInfo.version),
GRAPHQL_API_KEY: JSON.stringify(conf['GRAPHQL_API_KEY']),
SWVERSION: JSON.stringify(`sw-${new Date().getTime()}`),
BASE_HREF: JSON.stringify(''),
};
const provides = {};
const externals = {};
const aliases = {};
if (!conf['JQUERY']) {
provides['react'] = 'React';
provides['react-dom'] = 'ReactDOM';
externals['react'] = 'React';
externals['react-dom'] = 'ReactDOM';
} else {
provides['$'] = 'jquery';
provides['jQuery'] = 'jquery';
externals['jquery'] = 'jQuery';
aliases['window.jQuery'] = require.resolve('jquery');
aliases['$'] = require.resolve('jquery');
aliases['jquery'] = require.resolve('jquery');
aliases['jQuery'] = require.resolve('jquery');
}
module.exports = {
PROVIDES: provides,
JSVARS: JSVARS,
configuration: conf,
themes: themes,
webpack: {
entry: includes,
externals: externals,
resolve: {
modules: modules,
extensions: ['.tsx', '.ts', '.js'],
alias: aliases,
fallback: {
path: false,
},
},
experiments: {
topLevelAwait: true,
},
},
};

357
webpack.config.js Executable file
View File

@ -0,0 +1,357 @@
/*
* Production assets generation
*/
const common = require('./webpack.config.common.js');
const conf = common.configuration;
const webpack = require('webpack');
const {
merge,
} = require('webpack-merge');
const fs = require('fs');
const path = require('path');
const FaviconsWebpackPlugin = require('favicons-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
//const ImageSpritePlugin = require('@a2nt/image-sprite-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const NODE_ENV = conf.NODE_ENV || process.env.NODE_ENV;
const COMPRESS = NODE_ENV === 'production' ? true : false;
const IP = process.env.IP || conf.HOSTNAME;
const PORT = process.env.PORT || conf.PORT;
const plugins = [
new webpack.ProvidePlugin(common['PROVIDES']),
new webpack.DefinePlugin(common['JSVARS']),
new webpack.LoaderOptionsPlugin({
minimize: COMPRESS,
debug: !COMPRESS,
}),
new MiniCssExtractPlugin({
experimentalUseImportModule: false,
filename: 'css/[name].css',
//allChunks: true,
}),
];
const indexPath = path.join(__dirname, conf.APPDIR, conf.SRC, 'index.html');
if (fs.existsSync(indexPath)) {
plugins.push(
new HtmlWebpackPlugin({
publicPath: '',
template: path.join(conf.APPDIR, conf.SRC, 'index.html'),
templateParameters: {
NODE_ENV: NODE_ENV,
GRAPHQL_URL: conf['GRAPHQL_URL'],
STATIC_URL: conf['STATIC_URL'],
REACT_SCRIPTS: NODE_ENV === 'production' ?
'<script crossorigin src="https://unpkg.com/react@17/umd/react.production.min.js"></script><script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>' : '<script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script><script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>',
},
xhtml: true,
}),
);
}
const faviconPath = path.join(__dirname, conf.APPDIR, conf.SRC, 'favicon.png');
if (fs.existsSync(faviconPath)) {
plugins.push(
new FaviconsWebpackPlugin({
title: 'Webpack App',
logo: faviconPath,
prefix: '/icons/',
emitStats: false,
persistentCache: true,
inject: false,
statsFilename: path.join(
conf.APPDIR,
conf.DIST,
'icons',
'iconstats.json',
),
icons: {
android: true,
appleIcon: true,
appleStartup: true,
coast: true,
favicons: true,
firefox: true,
opengraph: true,
twitter: true,
yandex: true,
windows: true,
},
}),
);
}
// add themes favicons
common.themes.forEach((theme) => {
const faviconPath = path.join(__dirname, theme, conf.SRC, 'favicon.png');
if (fs.existsSync(faviconPath)) {
plugins.push(
new FaviconsWebpackPlugin({
title: 'Webpack App',
logo: faviconPath,
prefix: '/' + theme + '-icons/',
emitStats: false,
persistentCache: true,
inject: false,
statsFilename: path.join(
conf.APPDIR,
conf.DIST,
theme + '-icons',
'iconstats.json',
),
icons: {
android: true,
appleIcon: true,
appleStartup: true,
coast: true,
favicons: true,
firefox: true,
opengraph: true,
twitter: true,
yandex: true,
windows: true,
},
}),
);
}
});
const minimizers = [];
minimizers.push(
new TerserPlugin({
terserOptions: {
module: false,
parse: {
// we want terser to parse ecma 8 code. However, we don't want it
// to apply any minfication steps that turns valid ecma 5 code
// into invalid ecma 5 code. This is why the 'compress' and 'output'
// sections only apply transformations that are ecma 5 safe
// https://github.com/facebook/create-react-app/pull/4234
ecma: 8,
},
compress: {
ecma: 6,
warnings: false,
// Disabled because of an issue with Uglify breaking seemingly valid code:
// https://github.com/facebook/create-react-app/issues/2376
// Pending further investigation:
// https://github.com/mishoo/UglifyJS2/issues/2011
comparisons: false,
},
keep_fnames: true,
keep_classnames: true,
mangle: {
safari10: true,
keep_fnames: true,
keep_classnames: true,
reserved: ['$', 'jQuery', 'jquery'],
},
output: {
ecma: 5,
comments: false,
// Turned on because emoji and regex is not minified properly using default
// https://github.com/facebook/create-react-app/issues/2488
ascii_only: true,
},
},
// Use multi-process parallel running to improve the build speed
// Default number of concurrent runs: os.cpus().length - 1
parallel: true,
})
);
if (conf['PROCESS_CSS']) {
minimizers.push(
new CssMinimizerPlugin({
parallel: true,
minimizerOptions: [{
preset: [
'default',
{
discardComments: {
removeAll: true,
},
zindex: true,
cssDeclarationSorter: true,
reduceIdents: false,
mergeIdents: true,
mergeRules: true,
mergeLonghand: true,
discardUnused: true,
discardOverridden: true,
discardDuplicates: true,
},
],
},],
minify: [
CssMinimizerPlugin.cssnanoMinify,
//CssMinimizerPlugin.cleanCssMinify,
],
})
);
}
if (COMPRESS) {
plugins.push(require('autoprefixer'));
/*plugins.push(
new ImageSpritePlugin({
exclude: /exclude|original|default-|icons|sprite|svg|logo|favicon/,
commentOrigin: false,
compress: COMPRESS,
extensions: ['png'],
indent: '',
log: true,
//outputPath: path.join(__dirname, conf.APPDIR, conf.DIST),
outputFilename: 'img/sprite-[hash].png',
padding: 0,
}),
);*/
}
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer')
.BundleAnalyzerPlugin;
plugins.push(
new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: false,
}),
);
const cfg = merge(common.webpack, {
mode: NODE_ENV,
cache: {
type: 'filesystem',
},
recordsPath: path.join(__dirname, conf.APPDIR, conf.DIST, 'records.json'),
optimization: {
//removeAvailableModules: false,
//realContentHash: false,
splitChunks: {
name: 'vendor',
minChunks: 2,
},
concatenateModules: true, //ModuleConcatenationPlugin
minimizer: minimizers,
},
output: {
publicPath: path.join(conf.APPDIR, conf.DIST) + '/',
path: path.join(__dirname, conf.APPDIR, conf.DIST) + '/',
filename: path.join('js', '[name].js'),
},
module: {
rules: [{
test: /\.(js|ts)x?$/,
//exclude: /node_modules/,
use: {
loader: 'babel-loader', //'@sucrase/webpack-loader',
options: {
//transforms: ['jsx']
presets: [
'@babel/preset-env',
'@babel/react',
{
plugins: [
'@babel/plugin-proposal-class-properties',
],
},
], //Preset used for env setup
plugins: [
'@babel/plugin-transform-typescript',
'@babel/transform-react-jsx',
],
cacheDirectory: true,
cacheCompression: true,
},
},
},
{
test: /\.s?css$/,
use: [{
loader: MiniCssExtractPlugin.loader,
options: {
publicPath: '../',
},
},
{
loader: 'css-loader',
options: {
sourceMap: true,
},
},
{
loader: 'sass-loader',
options: {
sourceMap: true,
},
},
],
},
{
test: /fontawesome([^.]+).(ttf|otf|eot|woff(2)?)(\?[a-z0-9]+)?$/,
type: 'asset/resource',
},
{
test: /\.(ttf|otf|eot|woff(2)?)$/,
type: 'asset/resource',
}, {
test: /\.(png|webp|jpg|jpeg|gif|svg)$/,
type: 'javascript/auto',
use: [
{
loader: 'img-optimize-loader',
options: {
name: '[name].[ext]',
outputPath: 'img/',
publicPath: '../img/',
compress: {
// This will take more time and get smaller images.
mode: 'low', // 'lossless', 'high', 'low'
disableOnDevelopment: true,
webp: conf['webp'],
// loseless compression for png
optipng: {
optimizationLevel: 4,
},
// lossy compression for png. This will generate smaller file than optipng.
pngquant: {
quality: [0.2, 0.8],
},
// Compression for svg.
svgo: true,
// Compression for gif.
gifsicle: {
optimizationLevel: 3,
},
// Compression for jpg.
mozjpeg: {
progressive: true,
quality: 60,
},
},
inline: {
limit: 1,
},
},
},],
},],
},
plugins: plugins,
});
console.log(cfg);
module.exports = cfg;

141
webpack.config.serve.js Executable file
View File

@ -0,0 +1,141 @@
/*
* Development assets generation
*/
const common = require('./webpack.config.common.js');
const conf = common.configuration;
const path = require('path');
const fs = require('fs');
//const autoprefixer = require('autoprefixer');
const webpack = require('webpack');
const {
merge,
} = require('webpack-merge');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const IP = process.env.IP || conf.HOSTNAME;
const PORT = process.env.PORT || conf.PORT;
const NODE_ENV = 'development'; //conf.NODE_ENV || process.env.NODE_ENV;
const COMPRESS = NODE_ENV === 'production' ? true : false;
const plugins = [
new webpack.ProvidePlugin(common['PROVIDES']),
new webpack.DefinePlugin(common['JSVARS']),
//new webpack.HotModuleReplacementPlugin(),
new MiniCssExtractPlugin(),
];
const indexPath = path.join(__dirname, conf.APPDIR, conf.SRC, 'index.html');
if (fs.existsSync(indexPath)) {
plugins.push(
new HtmlWebpackPlugin({
publicPath: '',
template: path.join(conf.APPDIR, conf.SRC, 'index.html'),
templateParameters: {
NODE_ENV: NODE_ENV,
GRAPHQL_URL: conf['GRAPHQL_URL'],
STATIC_URL: conf['STATIC_URL'],
REACT_SCRIPTS: NODE_ENV === 'production' ?
'<script crossorigin src="https://unpkg.com/react@17/umd/react.production.min.js"></script><script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>' : '<script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script><script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>',
},
}),
);
}
const config = merge(common.webpack, {
mode: 'development',
entry: {
/*hot: [
'react-hot-loader/patch',
'webpack-dev-server/?https://' + conf.HOSTNAME + ':' + conf.PORT,
'webpack/hot/only-dev-server',
],*/
},
output: {
path: path.join(__dirname),
filename: '[name].js',
// necessary for HMR to know where to load the hot update chunks
publicPath: `http${conf['HTTPS'] ? 's' : ''}://${conf['HOSTNAME']}:${
conf.PORT
}/`,
},
module: {
rules: [{
test: /\.(js|ts)x?$/,
//exclude: /node_modules/,
use: {
loader: 'babel-loader', //'@sucrase/webpack-loader',
options: {
//transforms: ['jsx']
presets: [
'@babel/preset-env',
'@babel/react',
{
plugins: [
'@babel/plugin-proposal-class-properties',
],
},
], //Preset used for env setup
plugins: [
'@babel/transform-react-jsx',
'@babel/plugin-transform-typescript',
],
cacheDirectory: true,
cacheCompression: true,
},
},
},
{
test: /\.s?css$/,
use: [{
loader: 'style-loader', //MiniCssExtractPlugin.loader,
},
{
loader: 'css-loader',
options: {
sourceMap: true,
},
},
{
loader: 'sass-loader',
options: {
sourceMap: true,
},
}, ],
},
{
test: /fontawesome([^.]+).(ttf|otf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/,
type: 'asset/resource',
},
{
test: /\.(gif|png|jpg|jpeg|ttf|otf|eot|svg|webp|woff(2)?)$/,
type: 'asset/resource',
}, ],
},
plugins: plugins,
devServer: {
host: IP,
port: PORT,
historyApiFallback: false,
static: path.resolve(__dirname, conf['APPDIR'], conf['SRC']),
https: conf['HTTPS'],
hot: false,
//injectClient: conf['injectClient'],
headers: {
'Access-Control-Allow-Origin': '*',
'Referrer-Policy': 'unsafe-url',
'service-worker-allowed': '/',
},
},
});
module.exports = config;

24
webpack.yml Executable file
View File

@ -0,0 +1,24 @@
# Name: webapp-webpack
# that's important to place this file into /app/_config/webpack.yml
# with all configuration variables presented
# Cuz WebPack compiling script use it to set configuration
A2nt\CMSNiceties\Templates\WebpackTemplateProvider:
APPDIR: './'
THEMESDIR: './'
HOSTNAME: 127.0.0.1
PORT: 3000
SRC: client/src
DIST: client/dist
TYPESJS: client/src/js/types
TYPESSCSS: client/src/scss/types
webp: false
NODE_ENV: production #production,development
HTTPS: true
injectClient: true
GRAPHQL_URL: '/graphql'
GRAPHQL_API_KEY: 'hGz2hB26Sse2ageAsLD6xUD1WPPfZJyI5IfI7o'
PROCESS_CSS: true # Deep CSS minification
absolute_path: false
JQUERY: false # We don't use jQuery, otherwise it's aliassed and being loaded externaly
#STATIC_URL: 'http://127.0.0.1'