├── .babelrc
├── .editorconfig
├── .eslintrc
├── .gitignore
├── .prettierignore
├── .prettierrc
├── .stylelintrc
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── config
├── babel.legacy.config.js
├── webpack.dev.config.js
└── webpack.prod.config.js
├── package-lock.json
├── package.json
├── plato-node.js
├── plato
├── @types
│ ├── global.d.ts
│ ├── index.d.ts
│ └── route.d.ts
├── commands
│ ├── build-critical.ts
│ ├── build-javascript.ts
│ ├── build.ts
│ ├── develop-watchers.ts
│ └── develop.ts
├── core
│ ├── buildHTML.ts
│ ├── createPages.ts
│ ├── saveData.ts
│ └── updateRoutes.ts
├── index.ts
├── tsconfig.json
└── utils
│ ├── reporter.ts
│ └── routes.ts
├── postcss.config.cjs
├── reports
├── plain-report.txt
└── stats.json
├── shared
└── templates
│ ├── about.art
│ ├── filters
│ └── index.js
│ ├── homepage.art
│ ├── layout.art
│ ├── notfound.art
│ ├── partials
│ ├── footer.art
│ ├── header.art
│ ├── responsive-image.art
│ └── srcset.art
│ └── svgs
│ └── instagram.art
├── site-config.js
├── size-plugin.json
└── src
├── .htaccess
├── assets
├── favicon
│ ├── android-chrome-192x192.png
│ ├── android-chrome-256x256.png
│ ├── apple-touch-icon.png
│ ├── browserconfig.xml
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── favicon.ico
│ ├── mstile-150x150.png
│ ├── safari-pinned-tab.svg
│ └── site.webmanifest
├── fonts
│ ├── UntitledSans-Regular.woff
│ └── UntitledSans-Regular.woff2
├── images
│ ├── image.gif
│ └── test.jpg
└── svgs
│ ├── instagram.svg
│ └── twitter.svg
├── css
├── app.scss
├── base
│ ├── _base.scss
│ ├── _typography.scss
│ ├── _ui-defs.scss
│ ├── _ui-resets.scss
│ ├── mixins
│ │ ├── _aspect_ratio.scss
│ │ ├── _font_style.scss
│ │ ├── _mixins.scss
│ │ └── _responsive_font.scss
│ ├── utilities
│ │ ├── _u-align.scss
│ │ ├── _u-grab.scss
│ │ └── _u-overlay.scss
│ └── vars
│ │ ├── _colors.scss
│ │ ├── _fonts.scss
│ │ ├── _icons.scss
│ │ ├── _media-queries.scss
│ │ ├── _sizes.scss
│ │ └── _transitions.scss
└── components
│ ├── _footer.scss
│ ├── _header.scss
│ └── _page.scss
├── data
├── 404.json
├── about.json
└── index.json
└── scripts
├── abstract
├── base.js
├── component.js
└── page.js
├── app
├── App.js
├── Cache.js
├── actions.js
├── constants.js
├── reducers.js
└── selectors.js
├── components
└── header
│ ├── Header.js
│ ├── actions.js
│ ├── constants.js
│ └── reducers.js
├── constants
├── config.js
└── langs.js
├── index.js
├── layout
├── Layout.js
├── Prefetch.js
├── Prevent.js
├── actions.js
├── constants.js
└── reducers.js
├── pages
├── about
│ └── About.js
├── homepage
│ └── Homepage.js
└── notfound
│ └── Notfound.js
├── reducers
└── index.js
├── router.js
├── store
├── globalStore.js
├── index.js
├── store.js
└── storeWatcher.js
└── utils
├── cleanURL.js
├── is.js
├── loadImages.js
├── loadScript.js
├── misc.js
├── offset.js
├── scrollPrevent.js
├── uniqueId.js
└── url.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "@babel/env",
5 | {
6 | "targets": {
7 | // https://jamie.build/last-2-versions
8 | // "browsers": [">0.25%", "not op_mini all", "not ie 11"]
9 | // https://web.dev/serve-modern-code-to-modern-browsers/
10 | // https://philipwalton.com/articles/deploying-es2015-code-in-production-today/
11 | "esmodules": true
12 | },
13 | // "debug": true,
14 | "useBuiltIns": "usage",
15 | "bugfixes": true,
16 | "corejs": 3
17 | }
18 | ]
19 | ],
20 | "plugins": ["@babel/plugin-transform-runtime", "minify-dead-code-elimination"]
21 | }
22 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_size = 2
7 | indent_style = tab
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["google", "prettier"],
3 | "parser": "@babel/eslint-parser",
4 | "rules": {
5 | "global-require": 0,
6 | "curly": [2, "multi-line"],
7 | "no-mixed-spaces-and-tabs": [2, "smart-tabs"],
8 | "key-spacing": 0,
9 | "indent": [2, "tab", { "SwitchCase": 1 }],
10 | "strict": [2, "global"],
11 | "quotes": [2, "single"],
12 | "quote-props": [2, "as-needed"],
13 | "max-len": [2, 200],
14 | "no-use-before-define": [0],
15 | "no-negated-condition": [0],
16 | "no-lonely-if": [0],
17 | "no-debugger": [0],
18 | "padded-blocks": [0, "always"],
19 | "spaced-comment": [1],
20 | "space-before-blocks": [0],
21 | "space-in-parens": [0],
22 | "require-jsdoc": [0],
23 | "no-undef": [0],
24 | "no-tabs": [0],
25 | "valid-jsdoc": [1]
26 | },
27 | "env": {
28 | "es6": true,
29 | "node": true,
30 | "browser": true
31 | },
32 | "parserOptions": {
33 | "requireConfigFile": false,
34 | "babelOptions": {
35 | "babelrc": false,
36 | "configFile": false,
37 | // your babel options
38 | "presets": ["@babel/preset-env"]
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # OS
2 | .DS_Store
3 | *.pyc
4 | *.swp
5 | *~
6 | *.zip
7 |
8 | # installation
9 | node_modules
10 | vendor
11 |
12 | npm-debug.log.*
13 | *.map
14 | *.log
15 | =======
16 | node_modules
17 | .DS_Store
18 | npm-debug.log.*
19 |
20 | build
21 | public
22 | public/assets/js
23 | public/assets/css
24 | plato-dist
25 | .plato
26 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | src/scripts/vendors/
2 | node_modules
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "bracketSpacing": true,
3 | "semi": true,
4 | "singleQuote": true,
5 | "printWidth": 120,
6 | "trailingComma": "es5",
7 | "useTabs": false
8 | }
9 |
--------------------------------------------------------------------------------
/.stylelintrc:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "at-rule-empty-line-before": [ "always", {
4 | except: [
5 | "blockless-after-same-name-blockless",
6 | "first-nested",
7 | ],
8 | ignore: ["after-comment"],
9 | } ],
10 | "at-rule-name-case": "lower",
11 | "at-rule-name-space-after": "always-single-line",
12 | "at-rule-semicolon-newline-after": "always",
13 | "block-closing-brace-empty-line-before": "never",
14 | "block-closing-brace-newline-after": "always",
15 | "block-closing-brace-newline-before": "always-multi-line",
16 | "block-closing-brace-space-before": "always-single-line",
17 | "block-no-empty": true,
18 | "block-opening-brace-newline-after": "always-multi-line",
19 | "block-opening-brace-space-after": "always-single-line",
20 | "block-opening-brace-space-before": "always",
21 | "color-hex-case": "lower",
22 | "color-hex-length": "short",
23 | "color-no-invalid-hex": true,
24 | "comment-empty-line-before": [ "always", {
25 | except: ["first-nested"],
26 | ignore: ["stylelint-commands"],
27 | } ],
28 | "comment-no-empty": true,
29 | "comment-whitespace-inside": "always",
30 | "custom-property-empty-line-before": [ "always", {
31 | except: [
32 | "after-custom-property",
33 | "first-nested",
34 | ],
35 | ignore: [
36 | "after-comment",
37 | "inside-single-line-block",
38 | ],
39 | } ],
40 | "declaration-bang-space-after": "never",
41 | "declaration-bang-space-before": "always",
42 | "declaration-block-no-duplicate-properties": [ true, {
43 | ignore: ["consecutive-duplicates-with-different-values"],
44 | } ],
45 | "declaration-block-no-redundant-longhand-properties": true,
46 | "declaration-block-no-shorthand-property-overrides": true,
47 | "declaration-block-semicolon-newline-after": "always-multi-line",
48 | "declaration-block-semicolon-space-after": "always-single-line",
49 | "declaration-block-semicolon-space-before": "never",
50 | "declaration-block-single-line-max-declarations": 1,
51 | "declaration-block-trailing-semicolon": "always",
52 | "declaration-colon-newline-after": "always-multi-line",
53 | "declaration-colon-space-after": "always-single-line",
54 | "declaration-colon-space-before": "never",
55 | "declaration-empty-line-before": [ "always", {
56 | except: [
57 | "after-declaration",
58 | "first-nested",
59 | ],
60 | ignore: [
61 | "after-comment",
62 | "inside-single-line-block",
63 | ],
64 | } ],
65 | "font-family-no-duplicate-names": true,
66 | "function-calc-no-unspaced-operator": true,
67 | "function-comma-newline-after": "always-multi-line",
68 | "function-comma-space-after": "always-single-line",
69 | "function-comma-space-before": "never",
70 | "function-linear-gradient-no-nonstandard-direction": true,
71 | "function-max-empty-lines": 0,
72 | "function-name-case": "lower",
73 | "function-parentheses-newline-inside": "always-multi-line",
74 | "function-parentheses-space-inside": "never-single-line",
75 | "function-whitespace-after": "always",
76 | "indentation": 2,
77 | "keyframe-declaration-no-important": true,
78 | "length-zero-no-unit": true,
79 | "max-empty-lines": 1,
80 | "media-feature-colon-space-after": "always",
81 | "media-feature-colon-space-before": "never",
82 | "media-feature-name-case": "lower",
83 | "media-feature-name-no-unknown": true,
84 | "media-feature-parentheses-space-inside": "never",
85 | "media-feature-range-operator-space-after": "always",
86 | "media-feature-range-operator-space-before": "always",
87 | "media-query-list-comma-newline-after": "always-multi-line",
88 | "media-query-list-comma-space-after": "always-single-line",
89 | "media-query-list-comma-space-before": "never",
90 | "no-empty-source": true,
91 | "no-eol-whitespace": true,
92 | "no-extra-semicolons": true,
93 | "no-invalid-double-slash-comments": true,
94 | "no-missing-end-of-source-newline": true,
95 | "number-leading-zero": "always",
96 | "number-no-trailing-zeros": true,
97 | "property-case": "lower",
98 | "property-no-unknown": true,
99 | "rule-empty-line-before": [ "always-multi-line", {
100 | except: ["first-nested"],
101 | ignore: ["after-comment"],
102 | } ],
103 | "selector-attribute-brackets-space-inside": "never",
104 | "selector-attribute-operator-space-after": "never",
105 | "selector-attribute-operator-space-before": "never",
106 | "selector-combinator-space-after": "always",
107 | "selector-combinator-space-before": "always",
108 | "selector-descendant-combinator-no-non-space": true,
109 | "selector-list-comma-newline-after": "always",
110 | "selector-list-comma-space-before": "never",
111 | "selector-max-empty-lines": 0,
112 | "selector-pseudo-class-case": "lower",
113 | "selector-pseudo-class-no-unknown": true,
114 | "selector-pseudo-class-parentheses-space-inside": "never",
115 | "selector-pseudo-element-case": "lower",
116 | "selector-pseudo-element-colon-notation": "double",
117 | "selector-pseudo-element-no-unknown": true,
118 | "selector-type-case": "lower",
119 | "selector-type-no-unknown": true,
120 | "shorthand-property-no-redundant-values": true,
121 | "string-no-newline": true,
122 | "unit-case": "lower",
123 | "unit-no-unknown": true,
124 | "value-list-comma-newline-after": "always-multi-line",
125 | "value-list-comma-space-after": "always-single-line",
126 | "value-list-comma-space-before": "never",
127 | "value-list-max-empty-lines": 0,
128 | },
129 | }
130 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.formatOnSave": true,
3 | "javascript.format.enable": true,
4 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Tim ROUSSILHE
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Plato: Starter
2 |
3 | - **Creator:** Timothee Roussilhe
4 | - **Twitter:** [@timroussilhe](https://twitter.com/TimRoussilhe)
5 |
6 | 
7 |
8 | ---
9 |
10 | ## CES Naming Convention
11 |
12 | Component classes should be named following a CES naming convention: component\_\_element--state.
13 |
14 | - Component: A standalone entity that is meaningful on its own.
15 | - Element: A part of the component that we want to style.
16 | - State: The state of the element.
17 | - Should NOT be modified by javascript once instantiated.
18 | - If a state IS modified by javascript, it doesn't need to follow CES (ex. toggling `is-active`)
19 | - Should be positive (ex. `--has-wrapping` instead of `--without-wrapping`)
20 | Unlike BEM, `__element` should be at most one level below the component. This avoids deep nesting but keeps an important level of specificity.
21 | It's fine to not use the top-level component name if we are styling a nuclear component by itself like a `button`. Specific Component scope className would then be used to add more style that's needed in the context of the parent component.
22 | For utility classes that embody some logic, we use a simple pattern of the utility description separated by a dash, no need to always attach this to the component as a modifier if it's something global: `justify-start` or `font-style-title`.
23 | Example:
24 |
25 | ```
26 |
27 |
28 |

29 |
30 |
31 |
ChitChat
32 |
You have a new message!
33 |
34 |
35 | ```
36 |
37 | ## Component State
38 |
39 | Each state comes with a simple local state management.
40 | The state is not attached to any update or render methods but you can pass a callback method when using `setState`.
41 |
42 | ```
43 | // Declare initial states in constructor
44 | this.state = {
45 | canUpdate: false,
46 | isInit: false,
47 | isAnimating: false,
48 | isShown: false,
49 | }
50 |
51 | // Update State
52 | this.setState({ isAnimating: false})
53 |
54 | // you can also pass a callback or render the component
55 | this.setState({ isAnimating: true }, () => this.onIsAnimatingUpdate()))
56 |
57 | // when you call setState, after that the state been
58 |
59 | // Read the state
60 | if ( this.state.canUpdate ) this.onUpdate()
61 |
62 | ```
63 |
--------------------------------------------------------------------------------
/config/babel.legacy.config.js:
--------------------------------------------------------------------------------
1 | export const babelLegacyLoaderRules = {
2 | test: /\.js$/,
3 | exclude: /node_modules/,
4 |
5 | loader: 'babel-loader',
6 | options: {
7 | presets: [
8 | [
9 | '@babel/preset-env',
10 | {
11 | useBuiltIns: 'usage',
12 | targets: {
13 | esmodules: false,
14 | },
15 | corejs: 3,
16 | },
17 | ],
18 | ],
19 | },
20 | };
21 |
--------------------------------------------------------------------------------
/config/webpack.dev.config.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import { fileURLToPath } from 'url';
3 | import webpack from 'webpack';
4 | import SizePlugin from 'size-plugin';
5 |
6 | const __filename = fileURLToPath(import.meta.url);
7 | const __dirname = path.dirname(__filename);
8 |
9 | const appEntryPoint = path.join(__dirname, '../src/scripts/index.js');
10 | const outputPath = path.join(__dirname, '../public/assets/js/');
11 | const filename = 'bundle.js';
12 | const entryPoints = appEntryPoint;
13 |
14 | export default {
15 | mode: 'development',
16 | entry: entryPoints,
17 |
18 | // if multiple outputs, use [name] and it will use the name of the entry point, and loop through them
19 | output: {
20 | path: outputPath,
21 | filename: filename,
22 | publicPath: '/assets/js/',
23 | chunkFilename: '[name].bundle.js',
24 | },
25 |
26 | optimization: {
27 | emitOnErrors: true,
28 | },
29 |
30 | plugins: [new SizePlugin(), new webpack.HotModuleReplacementPlugin()],
31 |
32 | // i. e. through the resolve.alias option
33 | // will be included in the bundle, no need to add and load vendor
34 | resolve: {
35 | extensions: ['.js', '.json', '.art', '.html'],
36 | modules: ['src/scripts/', 'src/scripts/vendors/', '.plato/', 'shared/', 'public/assets/', 'node_modules'],
37 | },
38 |
39 | module: {
40 | rules: [
41 | // copy fonts for us, this is needed to avoid webpack renaing the font via css-loader
42 | // https://webpack.js.org/guides/asset-modules/
43 | {
44 | test: /\.(woff(2)?|ttf|eot)$/,
45 | type: 'asset/resource',
46 | generator: {
47 | filename: './../fonts/[name][ext]',
48 | },
49 | },
50 | {
51 | test: /\.js$/,
52 | exclude: /node_modules/,
53 | use: {
54 | loader: 'babel-loader',
55 | },
56 | },
57 | { test: /\.art$/, use: 'art-template-loader' },
58 | {
59 | test: /\.(scss|css)$/,
60 | use: [
61 | {
62 | loader: 'style-loader',
63 | },
64 | {
65 | loader: 'css-loader',
66 | },
67 | {
68 | loader: 'postcss-loader',
69 | options: {
70 | postcssOptions: {
71 | ident: 'postcss',
72 | plugins: () => [require('autoprefixer')],
73 | },
74 | },
75 | },
76 | {
77 | loader: 'sass-loader',
78 | options: {
79 | sourceMap: true,
80 | },
81 | },
82 | ],
83 | },
84 | ],
85 | },
86 |
87 | stats: {
88 | // Nice colored output
89 | colors: true,
90 | },
91 |
92 | // Create Sourcemaps for the bundle
93 | devtool: 'eval-source-map',
94 | };
95 |
--------------------------------------------------------------------------------
/config/webpack.prod.config.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import { fileURLToPath } from 'url';
3 | import webpack from 'webpack';
4 | import TerserPlugin from 'terser-webpack-plugin';
5 | import MiniCssExtractPlugin from 'mini-css-extract-plugin';
6 | import CssMinimizerPlugin from 'css-minimizer-webpack-plugin';
7 |
8 | import { WebpackManifestPlugin } from 'webpack-manifest-plugin';
9 |
10 | import { WebpackBundleSizeAnalyzerPlugin } from 'webpack-bundle-size-analyzer';
11 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
12 |
13 | const __filename = fileURLToPath(import.meta.url);
14 | const __dirname = path.dirname(__filename);
15 | const appEntryPoint = path.join(__dirname, '../src/scripts/index.js');
16 | const outputPath = path.join(__dirname, '../build/assets/');
17 | const reportPath = path.join(__dirname, '../reports/plain-report.txt');
18 |
19 | import { babelLegacyLoaderRules } from './babel.legacy.config.js';
20 | const entryPoints = appEntryPoint;
21 |
22 | export default (env) => {
23 | const envPlugins = [];
24 | if (env && env.bundleSize)
25 | envPlugins.push(
26 | new BundleAnalyzerPlugin({
27 | analyzerMode: 'static',
28 | generateStatsFile: 'true',
29 | })
30 | );
31 |
32 | if (env !== 'legacy') {
33 | envPlugins.push(new WebpackBundleSizeAnalyzerPlugin(reportPath));
34 | envPlugins.push(
35 | new WebpackManifestPlugin({
36 | publicPath: '/assets/',
37 | })
38 | );
39 | }
40 |
41 | return {
42 | mode: 'production',
43 | entry: entryPoints,
44 |
45 | // if multiple outputs, use [name] and it will use the name of the entry point, and loop through them
46 | output: {
47 | path: outputPath,
48 | filename: env !== 'legacy' ? 'js/[name].[contenthash].js' : 'js/legacy.js',
49 | chunkFilename: env !== 'legacy' ? 'js/[name].[contenthash].js' : 'js/[name].[contenthash]-legacy.js',
50 | publicPath: '/assets/',
51 | },
52 |
53 | optimization: {
54 | emitOnErrors: false,
55 | concatenateModules: true,
56 | minimizer: [
57 | new TerserPlugin({
58 | terserOptions: {
59 | compress: {
60 | drop_console: true,
61 | dead_code: true,
62 | },
63 | keep_classnames: false,
64 | keep_fnames: false,
65 | output: {
66 | comments: false,
67 | },
68 | },
69 | }),
70 | new CssMinimizerPlugin({
71 | minimizerOptions: {
72 | preset: [
73 | 'default',
74 | {
75 | discardComments: { removeAll: true },
76 | },
77 | ],
78 | },
79 | }),
80 | ],
81 | moduleIds: 'deterministic',
82 | },
83 |
84 | plugins: [
85 | new MiniCssExtractPlugin({
86 | // Options similar to the same options in webpackOptions.output
87 | // both options are optional
88 | filename: 'css/[name].[contenthash].css',
89 | }),
90 | new webpack.optimize.ModuleConcatenationPlugin(),
91 | ...envPlugins,
92 | ],
93 | resolve: {
94 | extensions: ['.js', '.json', '.art', '.html'],
95 | modules: ['src/scripts/', 'src/scripts/vendors/', 'shared/', 'public/assets/', 'node_modules'],
96 | },
97 |
98 | module: {
99 | rules: [
100 | // copy fonts for us, this is needed to avoid webpack renaing the font via css-loader
101 | // https://webpack.js.org/guides/asset-modules/
102 | {
103 | test: /\.(woff(2)?|ttf|eot)$/,
104 | type: 'asset/resource',
105 | generator: {
106 | filename: './fonts/[name][ext]',
107 | },
108 | },
109 | ...(env !== 'legacy'
110 | ? [
111 | {
112 | test: /\.js?$/,
113 | exclude: /node_modules/,
114 | use: 'babel-loader',
115 | },
116 | ]
117 | : [babelLegacyLoaderRules]),
118 | { test: /\.art$/, use: 'art-template-loader' },
119 | {
120 | test: /\.scss$/,
121 | use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'sass-loader'],
122 | },
123 | ],
124 | },
125 |
126 | stats: {
127 | // Nice colored output
128 | colors: true,
129 | },
130 |
131 | devtool: false,
132 | };
133 | };
134 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "plato",
3 | "description": "Plato Boilerplate",
4 | "version": "0.0.2",
5 | "private": true,
6 | "author": "Tim Roussilhe",
7 | "contributors": [
8 | "Tim Roussilhe"
9 | ],
10 | "type": "module",
11 | "scripts": {
12 | "start": "npm run build-plato && npm run dev",
13 | "clean-plato": "rm -rf plato-dist/* ",
14 | "dev-plato": "npm run clean-plato && tsc -w --project ./plato/tsconfig.json",
15 | "build-plato": "npm run clean-plato && tsc --project ./plato/tsconfig.json",
16 | "dev": "node plato-dist develop -v",
17 | "develop": "node plato-dist develop -v",
18 | "serve": "webpack-dev-server --config ./config/webpack.dev.config.js",
19 | "build": "npm run build-plato && node plato-dist build -v",
20 | "build-and-test": "node plato-dist build -v -o",
21 | "bundleSizeAnalyser": "webpack --config ./config/webpack.prod.config.js --env bundleSize",
22 | "prettier:check": "prettier --config ./.prettierrc --check src/**/**.{js,scss}",
23 | "prettier:write": "prettier --config ./.prettierrc --write src/**/**.{js,scss}"
24 | },
25 | "devDependencies": {
26 | "@babel/core": "^7.18.10",
27 | "@babel/eslint-parser": "^7.18.9",
28 | "@babel/plugin-syntax-dynamic-import": "^7.2.0",
29 | "@babel/plugin-transform-runtime": "^7.18.10",
30 | "@babel/preset-env": "^7.18.10",
31 | "@types/fs-extra": "^9.0.13",
32 | "@types/node": "^18.7.2",
33 | "@types/node-fetch": "^2.6.2",
34 | "art-template-loader": "^1.4.3",
35 | "autoprefixer": "^10.4.8",
36 | "babel-loader": "^8.2.5",
37 | "babel-plugin-minify-dead-code-elimination": "^0.5.2",
38 | "boxen": "^4.1.0",
39 | "commander": "^2.19.0",
40 | "connect": "^3.7.0",
41 | "convert-hrtime": "^2.0.0",
42 | "critical": "^5.1.1",
43 | "css-loader": "^6.7.1",
44 | "css-minimizer-webpack-plugin": "^4.0.0",
45 | "eslint": "^8.21.0",
46 | "eslint-config-google": "^0.14.0",
47 | "fs-extra": "^10.1.0",
48 | "gsap": "^3.10.4",
49 | "html-minifier": "^3.5.21",
50 | "mini-css-extract-plugin": "^2.6.1",
51 | "node-fetch": "^2.3.0",
52 | "opn": "^5.4.0",
53 | "page": "^1.11.6",
54 | "postcss-loader": "^7.0.1",
55 | "prettier": "^2.7.1",
56 | "sass": "^1.54.0",
57 | "sass-loader": "^13.0.2",
58 | "serve-static": "^1.14.1",
59 | "size-plugin": "2.0.2",
60 | "style-loader": "^3.3.1",
61 | "terminal-link": "^2.0.0",
62 | "terser-webpack-plugin": "^5.3.3",
63 | "webpack": "^5.74.0",
64 | "webpack-bundle-analyzer": "^4.5.0",
65 | "webpack-bundle-size-analyzer": "^3.1.0",
66 | "webpack-cli": "^4.10.0",
67 | "webpack-dev-server": "^4.9.3",
68 | "webpack-manifest-plugin": "^5.0.0"
69 | },
70 | "dependencies": {
71 | "@babel/runtime": "^7.18.9",
72 | "art-template": "^4.13.2",
73 | "core-js": "^3.24.1",
74 | "dotenv": "^16.3.1",
75 | "eslint-config-prettier": "^6.3.0",
76 | "fast-average-color-node": "^2.4.0",
77 | "html-minifier-terser": "^7.2.0"
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/plato-node.js:
--------------------------------------------------------------------------------
1 | import fetch from 'node-fetch';
2 |
3 | export const getStaticPagesProps = async () => {
4 | const res = await fetch('https://pokeapi.co/api/v2/pokemon/?limit=6');
5 | const posts = await res.json();
6 |
7 | const pages = posts.results.map((pokemon, i) => {
8 | return {
9 | id: pokemon.name,
10 | url: pokemon.name,
11 | template: 'about',
12 | data: pokemon,
13 | };
14 | });
15 |
16 | return pages;
17 | };
18 |
19 | export const createGlobalData = () => {
20 | return new Promise((resolve, reject) => {
21 | resolve({
22 | globals: {
23 | array: [1, 2, 3, 4, 5],
24 | },
25 | });
26 | });
27 | };
28 |
--------------------------------------------------------------------------------
/plato/@types/global.d.ts:
--------------------------------------------------------------------------------
1 | // global.d.ts
2 | declare module globalThis {
3 | var appRoot: string;
4 | var srcPath: string;
5 | var siteDir: string;
6 | var routeDest: string;
7 | }
8 |
--------------------------------------------------------------------------------
/plato/@types/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'opn';
2 | declare module 'critical';
3 | declare module 'convert-hrtime';
4 | declare module 'html-minifier';
5 |
--------------------------------------------------------------------------------
/plato/@types/route.d.ts:
--------------------------------------------------------------------------------
1 | export interface Route {
2 | url: string;
3 | id: string;
4 | template: string;
5 | json: string;
6 | // dynamic filename (useful for 404)
7 | fileName?: string;
8 | data?: object;
9 | // data source for static routes
10 | dataSource?: string;
11 | }
12 |
13 | export interface Routes {
14 | routes: Array;
15 | }
16 |
--------------------------------------------------------------------------------
/plato/commands/build-critical.ts:
--------------------------------------------------------------------------------
1 | import { generate } from 'critical';
2 | import { minify } from 'html-minifier-terser';
3 | import fse from 'fs-extra';
4 |
5 | export default (filename: string, dest: string) => {
6 | return new Promise(async (resolve, reject) => {
7 | let criticalHTML = '';
8 | try {
9 | const { css, html, uncritical } = await generate({
10 | inline: true,
11 | base: 'build/',
12 | src: filename,
13 | width: 1600,
14 | height: 900,
15 | });
16 |
17 | criticalHTML = html;
18 | } catch (err) {
19 | console.log('caught error from criticalHTML: ', err);
20 | }
21 |
22 | // You now have critical-path CSS as well as the modified HTML.
23 | // Works with and without target specified.
24 | // You now have critical-path CSS
25 | // Works with and without dest specified
26 | minify(criticalHTML, {
27 | removeComments: true,
28 | // collapseWhitespace: true,
29 | conservativeCollapse: true,
30 | })
31 | .then((html) => {
32 | fse.writeFileSync(dest, html, {
33 | encoding: 'utf8',
34 | flag: 'w',
35 | });
36 | resolve();
37 | })
38 | .catch((err) => {
39 | // sometime the render of critical will come out to early / truncated and will error out,
40 | // so in this case we want to just write the original file
41 | console.log('skip write FILE BUG', err);
42 | resolve();
43 | // reject(err);
44 | });
45 | });
46 | };
47 |
--------------------------------------------------------------------------------
/plato/commands/build-javascript.ts:
--------------------------------------------------------------------------------
1 | import webpack from 'webpack';
2 | import webpackConfig from '../../config/webpack.prod.config.js';
3 |
4 | export default async (env: string) => {
5 | return new Promise((resolve, reject) => {
6 | webpack(webpackConfig(env)).run((err, stats) => {
7 | if (err) {
8 | console.error(err.stack || err);
9 | reject(err);
10 | return;
11 | }
12 |
13 | if (stats) {
14 | const info = stats.toJson();
15 |
16 | if (stats.hasErrors()) {
17 | console.error(info.errors);
18 | reject(err);
19 | return;
20 | }
21 |
22 | if (stats.hasWarnings()) {
23 | console.warn(info.warnings);
24 | }
25 | }
26 |
27 | resolve();
28 | });
29 | });
30 | };
31 |
--------------------------------------------------------------------------------
/plato/commands/build.ts:
--------------------------------------------------------------------------------
1 | import fse from 'fs-extra';
2 | import path from 'path';
3 | import opn from 'opn';
4 | import connect from 'connect';
5 | import serveStatic from 'serve-static';
6 |
7 | import { Routes, Route } from '../@types/route.js';
8 |
9 | import reporter from '../utils/reporter.js';
10 | import { saveRemoteDataFromSource } from '../core/saveData.js';
11 | import { updateRoutes } from '../core/updateRoutes.js';
12 | import { createPages } from '../core/createPages.js';
13 | import buildHTML from '../core/buildHTML.js';
14 | import buildProductionBundle from './build-javascript.js';
15 | import buildCritical from './build-critical.js';
16 |
17 | import config from './../../site-config.js';
18 |
19 | export default async function build(verbose = false, open = false) {
20 | reporter.verbose = verbose;
21 |
22 | // Grab static routes from config
23 | const staticRoutes = config.staticRoutes as Route[];
24 |
25 | let globalActivity = reporter.activity('Plato Build', '🤔');
26 | globalActivity.start();
27 |
28 | /**
29 | * Clean repo
30 | * Empty directories and move source files
31 | */
32 | let activity = reporter.activity('Cleaning Repo', '🧽');
33 | activity.start();
34 |
35 | // clear destination folder
36 | fse.emptyDirSync(global.siteDir);
37 |
38 | // remove real_routes files
39 | fse.emptyDirSync(global.routeDest);
40 |
41 | // copy assets folder
42 | fse.copySync(path.resolve(global.srcPath, './.htaccess'), path.resolve(global.siteDir, './.htaccess'));
43 | fse.copySync(path.resolve(global.srcPath, './assets'), path.resolve(global.siteDir, './assets'));
44 | fse.copySync(path.resolve(global.srcPath, './data'), path.resolve(global.siteDir, './data'));
45 |
46 | activity.end();
47 |
48 | /**
49 | * Build dynamic routes file
50 | * And save remote date from the static route file
51 | */
52 | activity = reporter.activity('Build Routes and save remote Data from Static routes', '🛣️');
53 | activity.start();
54 |
55 | // add static routes to final_routes
56 | // save remote endpoint for static routes
57 | try {
58 | await updateRoutes(staticRoutes);
59 | } catch (err) {
60 | reporter.failure('Error during updating routes: ', err as string);
61 | }
62 |
63 | // save remote endpoint for static routes
64 | try {
65 | for (const route of staticRoutes) {
66 | if (route.dataSource) await saveRemoteDataFromSource(route.dataSource, route.json, global.siteDir + '/data/');
67 | }
68 | } catch (err) {
69 | reporter.failure('Error during saving static file: ', err as string);
70 | }
71 | activity.end();
72 |
73 | /**
74 | * Get Pages data from Plato Node API getStaticPagesProps method
75 | */
76 | activity = reporter.activity('Create Pages from Plato API', '🤖');
77 | activity.start(true);
78 |
79 | let pagesProps: Route[] = [];
80 | try {
81 | const { getStaticPagesProps } = await import('./../../plato-node.js');
82 | if (getStaticPagesProps) {
83 | pagesProps = await getStaticPagesProps();
84 | }
85 | } catch (err) {
86 | reporter.info('Error during getStaticPagesProps call');
87 | }
88 |
89 | /**
90 | * Create Pages from Plato Node API getStaticPagesProps method
91 | */
92 | let finalRoutes: Routes = { routes: [] };
93 | try {
94 | if (pagesProps && pagesProps.length > 0) {
95 | finalRoutes = await createPages(pagesProps, global.siteDir);
96 | }
97 | } catch (err) {
98 | reporter.error('Error during page creation ' + err);
99 | }
100 |
101 | // check if node API is used and if so, check if createGlobalData is used
102 | let globalData = {};
103 | try {
104 | const { createGlobalData } = await import('./../../plato-node.js');
105 |
106 | if (createGlobalData) {
107 | globalData = await createGlobalData();
108 | }
109 | } catch (err) {
110 | console.log('No API found: ' + err);
111 | }
112 |
113 | // on plato complete Hook
114 | try {
115 | const { onPageCreatedHook } = await import('./../../plato-node.js');
116 |
117 | if (onPageCreatedHook) {
118 | await onPageCreatedHook();
119 | }
120 | } catch (err) {
121 | console.log('Error during onPageCreatedHook: ' + err);
122 | }
123 | activity.end();
124 |
125 | /**
126 | * Build production javascript using webpack
127 | */
128 | activity = reporter.activity('Build Javascript', '📁');
129 | activity.start();
130 | // Build Javascript and CSS Production Bundle Return the manifest with files and
131 | // their hashed path.
132 | await buildProductionBundle('').catch((err) => {
133 | reporter.failure('Generating JavaScript bundles failed', err as string);
134 | });
135 |
136 | activity.end();
137 |
138 | /**
139 | * Build legacy javascript using webpack
140 | */
141 | activity = reporter.activity('Build Legacy Javascript', '👴');
142 | activity.start();
143 | await buildProductionBundle('legacy').catch((err) => {
144 | reporter.failure('Generating Legacy JavaScript bundles failed', err as string);
145 | });
146 |
147 | activity.end();
148 |
149 | activity = reporter.activity('Build HTML', '💻');
150 | activity.start();
151 |
152 | // manifest created by webpack prod build
153 | const files: any[] = [];
154 | const manifestFile = fse.readJsonSync('./build/assets/manifest.json');
155 | try {
156 | for (let page of finalRoutes.routes) {
157 | const filename = await buildHTML(page, manifestFile, 'production', global.siteDir, globalData);
158 | files.push(filename);
159 | }
160 | } catch (err) {
161 | reporter.failure('Error during page generation: ', err as string);
162 | }
163 | activity.end();
164 |
165 | activity = reporter.activity('Build Critical CSS and minify HTML', '🎨');
166 | activity.start();
167 |
168 | function startNewJob(): Promise {
169 | const file = files.pop(); // NOTE: mutates file array
170 | if (!file) {
171 | // no more new jobs to process (might still be jobs currently in process)
172 | return Promise.resolve();
173 | }
174 | const dest = path.join(global.siteDir, file);
175 | return buildCritical(file, dest)
176 | .then(() => {
177 | // Then call to see if there are more jobs to process
178 | reporter.info(`Critical Built : ${file}`);
179 | return startNewJob();
180 | })
181 | .catch((err) => {
182 | reporter.failure('Error during Critical generation: ', err);
183 | });
184 | }
185 | // how many jobs do we want to handle in parallel?
186 | // Below, 3:
187 | await Promise.all([startNewJob(), startNewJob(), startNewJob()]).catch((err) =>
188 | reporter.failure('Error during Critical generations: ', err)
189 | );
190 |
191 | activity.end();
192 | globalActivity.end();
193 |
194 | // open HTTP server serving our local files
195 | if (open) {
196 | const port = 8080;
197 | connect()
198 | .use(serveStatic(global.siteDir))
199 | .listen(port, function () {
200 | reporter.displayUrl(`Server running on ${port}`, 'http://localhost:' + port);
201 | opn('http://localhost:' + port);
202 | });
203 | }
204 | }
205 |
--------------------------------------------------------------------------------
/plato/commands/develop-watchers.ts:
--------------------------------------------------------------------------------
1 | import fse from 'fs-extra';
2 | import chokidar from 'chokidar';
3 |
4 | import buildHTML from '../core/buildHTML.js';
5 | import { Route } from '../@types/route.js';
6 | import { getRoutesByTemplatePath } from '../utils/routes.js';
7 |
8 | import reporter from '../utils/reporter.js';
9 |
10 | function copyAssetToPublicFolder(path: string) {
11 | const publicPath = path.replace('src/', 'public/');
12 | fse
13 | .copy(path, publicPath)
14 | .then(() => reporter.log('📗 File Copied: ' + path))
15 | .catch((err: string) => {
16 | reporter.failure('Error during asset copy : ', err);
17 | });
18 | }
19 |
20 | export default function createWatchers(routes: Route[], globalData: object) {
21 | /**
22 | * Add Watchers
23 | * Watcher for assets
24 | * Watcher for templates change
25 | */
26 | // Initialize watcher for template files
27 | let watcher = chokidar.watch('./shared/templates/', {
28 | ignored: /(^|[\/\\])\../,
29 | persistent: true,
30 | });
31 | watcher
32 | .on('add', (path) => reporter.log(`File ${path} has been added`))
33 | .on('change', (path) => {
34 | const activeRoutes = getRoutesByTemplatePath(routes, path);
35 | if (activeRoutes !== null) {
36 | for (let page of activeRoutes) {
37 | buildHTML(page, null, 'development', global.siteDir, globalData).catch(console.error);
38 | }
39 | } else {
40 | // change from a partial => rebuild everything
41 | // TODO : check if change is not happening in an unlink template
42 | for (let page of routes) {
43 | buildHTML(page, null, 'development', global.siteDir, globalData).catch((err) => {
44 | reporter.log(`Error: ${err}`);
45 | });
46 | }
47 | }
48 | reporter.log(`File ${path} has been changed`);
49 | });
50 |
51 | watcher.add('./shared/partials/');
52 |
53 | // Initialize watcher for assets files
54 | let watcherAsset = chokidar.watch('./src/assets/', {
55 | ignored: /(^|[\/\\])\../,
56 | persistent: true,
57 | ignoreInitial: true,
58 | });
59 |
60 | watcherAsset.on('add', (path) => copyAssetToPublicFolder(path));
61 | watcherAsset.on('change', (path) => copyAssetToPublicFolder(path));
62 | }
63 |
--------------------------------------------------------------------------------
/plato/commands/develop.ts:
--------------------------------------------------------------------------------
1 | import fse from 'fs-extra';
2 | import path from 'path';
3 | import webpack from 'webpack';
4 | import WebpackDevServer from 'webpack-dev-server';
5 | import opn from 'opn';
6 |
7 | import { Routes, Route } from '../@types/route.js';
8 |
9 | import config from '../../site-config.js';
10 | import webpackConfig from '../../config/webpack.dev.config.js';
11 |
12 | import buildHTML from '../core/buildHTML.js';
13 | import { createPages } from '../core/createPages.js';
14 | import { saveRemoteDataFromSource } from '../core/saveData.js';
15 | import { updateRoutes } from '../core/updateRoutes.js';
16 | import createWatchers from './develop-watchers.js';
17 |
18 | import reporter from '../utils/reporter.js';
19 | const port = 9090;
20 |
21 | export default async function develop(verbose: boolean = false, open: boolean = false) {
22 | reporter.verbose = verbose;
23 |
24 | // Grab static routes
25 | const staticRoutes = config.staticRoutes as Route[];
26 |
27 | let globalActivity = reporter.activity('Plato Develop', '🤔');
28 | globalActivity.start(true);
29 |
30 | /**
31 | * Clean repo
32 | * Empty directories and move source files
33 | */
34 | let activity = reporter.activity('Cleaning Repo', '🧽');
35 | activity.start();
36 |
37 | // clear destination folder
38 | fse.emptyDirSync(global.siteDir);
39 |
40 | // remove real_routes files
41 | fse.emptyDirSync(global.routeDest);
42 |
43 | // copy assets folder
44 | fse.copySync(path.resolve(global.srcPath, './.htaccess'), path.resolve(global.siteDir, './.htaccess'));
45 | fse.copySync(path.resolve(global.srcPath, './assets'), path.resolve(global.siteDir, './assets'));
46 | fse.copySync(path.resolve(global.srcPath, './data'), path.resolve(global.siteDir, './data'));
47 |
48 | activity.end();
49 |
50 | /**
51 | * Build dynamic routes file
52 | * And save remote date from the static route file
53 | */
54 | activity = reporter.activity('Build Routes and save remote Data from Static routes', '🛣️');
55 | activity.start();
56 |
57 | // add static routes to final_routes
58 | // save remote endpoint for static routes
59 | try {
60 | await updateRoutes(staticRoutes);
61 | } catch (err) {
62 | reporter.failure('Error during updating routes: ', err as string);
63 | }
64 |
65 | // save remote endpoint for static routes
66 | try {
67 | for (const route of staticRoutes) {
68 | if (route.dataSource) await saveRemoteDataFromSource(route.dataSource, route.json, global.siteDir + '/data/');
69 | }
70 | } catch (err) {
71 | reporter.failure('Error during saving static file: ', err as string);
72 | }
73 | activity.end();
74 |
75 | /**
76 | * Get Pages data from Plato Node API getStaticPagesProps method
77 | */
78 | activity = reporter.activity('Create Pages from Plato API', '🤖');
79 | activity.start(true);
80 |
81 | let pagesProps: Route[] = [];
82 | try {
83 | const { getStaticPagesProps } = await import('./../../plato-node.js');
84 | if (getStaticPagesProps) {
85 | //TODO: add validation on user data
86 | pagesProps = await getStaticPagesProps();
87 | }
88 | } catch (err) {
89 | reporter.failure('Error during getStaticPagesProps call', err as string);
90 | }
91 |
92 | /**
93 | * Create Pages from pagesProps
94 | */
95 | let finalRoutes: Routes = { routes: [] };
96 | if (pagesProps && pagesProps.length > 0) {
97 | try {
98 | finalRoutes = await createPages(pagesProps, global.siteDir);
99 | } catch (err) {
100 | reporter.failure('Error during page creation ', err as string);
101 | }
102 | }
103 |
104 | // on plato complete Hook
105 | try {
106 | const { onPageCreatedHook } = await import('./../../plato-node.js');
107 |
108 | if (onPageCreatedHook) {
109 | await onPageCreatedHook();
110 | }
111 | } catch (err) {
112 | console.log('Error during onPageCreatedHook: ' + err);
113 | }
114 |
115 | // check if node API is used and if so, check if createGlobalData is used
116 | let globalData = {};
117 | try {
118 | const { createGlobalData } = await import('./../../plato-node.js');
119 | if (createGlobalData) {
120 | globalData = await createGlobalData();
121 | }
122 | } catch (err) {
123 | reporter.failure('Error createGlobalData ', err as string);
124 | }
125 | activity.end();
126 |
127 | /**
128 | * Build static HTML
129 | */
130 | activity = reporter.activity('Build Static site', '🚀');
131 | activity.start();
132 |
133 | try {
134 | for (let page of finalRoutes.routes) {
135 | await buildHTML(page, null, 'development', global.siteDir, globalData);
136 | }
137 | } catch (err) {
138 | reporter.failure('Error during page generation: ', err as string);
139 | }
140 | activity.end();
141 |
142 | /**
143 | * Create file watchers to rebuild HTML when templates changes
144 | */
145 | createWatchers(finalRoutes.routes, globalData);
146 |
147 | //TODO: provide dynamicRewrite support via API
148 | let dynamicRewrite: any[] = [];
149 | // dynamicRewrite.push({ from: '/furniture/*', to: '/furniture/' });
150 |
151 | const options = {
152 | port,
153 | hot: true,
154 | headers: {
155 | 'Cache-Control': 'max-age=0',
156 | },
157 | host: 'localhost',
158 | allowedHosts: 'all',
159 | historyApiFallback: {
160 | rewrites: dynamicRewrite,
161 | },
162 | static: {
163 | directory: 'public',
164 | watch: true,
165 | serveIndex: true,
166 | },
167 | };
168 |
169 | const compiler = webpack(webpackConfig);
170 | const server = new WebpackDevServer(options, compiler);
171 | globalActivity.end();
172 |
173 | const runServer = async () => {
174 | await server.start();
175 | reporter.displayUrl('Development server started', 'http://localhost:' + port);
176 | if (open) opn('http://localhost:' + port);
177 | };
178 |
179 | runServer();
180 | }
181 |
--------------------------------------------------------------------------------
/plato/core/buildHTML.ts:
--------------------------------------------------------------------------------
1 | import fse from 'fs-extra';
2 | import path from 'path';
3 | import template from 'art-template';
4 |
5 | import reporter from '../utils/reporter.js';
6 | import config from '../../site-config.js';
7 | import { Route } from '../@types/route.js';
8 |
9 | // Set ART FILTERS
10 | import { filters } from '../../shared/templates/filters/index.js';
11 | for (let [key, value] of Object.entries(filters)) {
12 | // @ts-expect-error
13 | template.defaults.imports[key] = value;
14 | }
15 |
16 | // A function that returns a promise to resolve into the data //fetched from the API or an error
17 | let artTemplatePromise = (templatePath: string, data: {}) => {
18 | return new Promise((resolve, reject) => {
19 | try {
20 | let htmlArt = template(templatePath, data);
21 | resolve(htmlArt);
22 | } catch (ex) {
23 | reject(ex);
24 | }
25 | });
26 | };
27 |
28 | function writeFileWithDirectory(dirPath: string, contents: string) {
29 | return new Promise((resolve, reject) => {
30 | // return value is a Promise resolving to the first directory created
31 | fse
32 | .mkdirp(path.dirname(dirPath))
33 | .then(() => {
34 | fse.writeFileSync(dirPath, contents);
35 | resolve();
36 | })
37 | .catch((err) => reject(err));
38 | });
39 | }
40 |
41 | export default (
42 | page: Route,
43 | manifest: string | null,
44 | mode = 'development',
45 | siteDir: string,
46 | globalData: { serverData?: {} }
47 | ) => {
48 | return new Promise((resolve, reject) => {
49 | let destPath: string = '';
50 | // this will be provided to critical
51 | let source: string;
52 |
53 | // needed to genere 404.html
54 | let fileName = page.fileName || 'index.html';
55 | if (page.id === 'index' || page.id === '404') {
56 | destPath = path.join(siteDir, '/');
57 | source = fileName;
58 | } else {
59 | try {
60 | destPath = path.join(siteDir, page.url, '/');
61 | source = `${page.url}/${fileName}`;
62 | } catch (err) {
63 | reject(err);
64 | }
65 | }
66 |
67 | fse.mkdirs(destPath).catch((err: string) => {
68 | reject(err);
69 | });
70 |
71 | let data = {};
72 | if (page.json) {
73 | try {
74 | data = fse.readJsonSync(path.resolve(siteDir, './data/', page.json));
75 | } catch (err) {
76 | reject(err);
77 | }
78 | }
79 |
80 | const templatePath = path.resolve(global.appRoot, `./shared/templates/${page.template}.art`);
81 | const exists = fse.existsSync(templatePath);
82 | if (!exists) reject(new Error('Template file does not exists'));
83 |
84 | artTemplatePromise(templatePath, {
85 | data,
86 | globalData,
87 | })
88 | .then((html) => {
89 | // TODO: remove globals we don't need in JS
90 | // adding serverData for the first render here
91 | globalData.serverData = data;
92 | artTemplatePromise(path.resolve('./shared/templates/layout.art'), {
93 | html,
94 | config,
95 | data,
96 | mode,
97 | manifest,
98 | globals: globalData,
99 | globalDataString: JSON.stringify(globalData),
100 | location: page.id,
101 | type: page.template,
102 | })
103 | .then((html) => {
104 | writeFileWithDirectory(path.resolve(destPath, fileName), html)
105 | .then(() => {
106 | reporter.info(`Page Built : ${destPath}${fileName}`);
107 | resolve(source);
108 | })
109 | .catch((err) => {
110 | reject(err);
111 | });
112 | })
113 | .catch((err) => reject(err));
114 | })
115 | .catch((err) => reject(err));
116 | });
117 | };
118 |
--------------------------------------------------------------------------------
/plato/core/createPages.ts:
--------------------------------------------------------------------------------
1 | import { saveRemoteData } from './saveData.js';
2 | import { updateRoutes } from './updateRoutes.js';
3 | import { Routes, Route } from '../@types/route.js';
4 |
5 | /**
6 | * Create a page.
7 | * This method let you create a page from the plato-node.js file
8 | * A page, is an item of the routes Object
9 | * A page is composed of the following element:
10 | * - index ( used to match the route later etc...)
11 | * - url ( the slug of the page )
12 | * - template id ( template name to be match )
13 | * - data *optional ( the path of remote data )
14 | * - json ( the name of the json local data )
15 | */
16 | export const createPages = (pagesProps: Route[], siteDir: string) => {
17 | return new Promise(async (resolve, reject) => {
18 | // create JSON Files
19 | pagesProps.forEach(({ data, id }) => {
20 | saveRemoteData(JSON.stringify(data), id + '.json', siteDir + '/data/');
21 | });
22 |
23 | const routes = pagesProps.map(({ id, url, template }): Route => {
24 | return {
25 | id,
26 | url,
27 | template,
28 | json: id + '.json',
29 | };
30 | });
31 |
32 | try {
33 | const activeRoutes = await updateRoutes(routes);
34 | resolve(activeRoutes);
35 | } catch (error) {
36 | reject(error);
37 | }
38 | });
39 | };
40 |
--------------------------------------------------------------------------------
/plato/core/saveData.ts:
--------------------------------------------------------------------------------
1 | import fse from 'fs-extra';
2 | import path from 'path';
3 | import fetch from 'node-fetch';
4 | import reporter from '../utils/reporter.js';
5 |
6 | const saveFile = (destinationPath: string, data: string) => {
7 | fse.writeFileSync(destinationPath, data, 'utf8');
8 | };
9 |
10 | export const saveRemoteData = (data: string, fileName: string, siteDir: string) => {
11 | const destinationPath = path.join(siteDir, fileName);
12 | return saveFile(destinationPath, data);
13 | };
14 |
15 | export const saveRemoteDataFromSource = (source: string, fileName: string, siteDir: string) => {
16 | return new Promise((resolve, reject) => {
17 | const start = process.hrtime();
18 |
19 | fetch(source, {})
20 | .then((res) => res.json())
21 | .then((body) => {
22 | const destinationPath = path.join(siteDir, fileName);
23 | saveFile(destinationPath, JSON.stringify(body));
24 | const end = process.hrtime(start);
25 | reporter.info(`Saved remote data ${fileName} in ${end[1] / 1000000}ms`);
26 | resolve(fileName);
27 | })
28 | .catch((error) => {
29 | reject(error);
30 | });
31 | });
32 | };
33 |
--------------------------------------------------------------------------------
/plato/core/updateRoutes.ts:
--------------------------------------------------------------------------------
1 | import fse from 'fs-extra';
2 | import path from 'path';
3 | import { Routes, Route } from '../@types/route.js';
4 |
5 | // create main route JSON file
6 | // will create the route
7 | // or add to it if it already exists to deal with static + dynamic routes
8 | export const updateRoutes = (routes: Route[]) => {
9 | return new Promise((resolve, reject) => {
10 | const destinationPath = path.resolve(global.routeDest, './routes.json');
11 | const exists = fse.existsSync(destinationPath);
12 |
13 | if (exists) {
14 | fse.readFile(destinationPath).then((data: Buffer) => {
15 | let obj: Routes = { routes: [] };
16 | try {
17 | obj = JSON.parse(data.toString()); // now it's an object
18 | } catch (e) {
19 | reject(e);
20 | }
21 |
22 | obj.routes = [...obj.routes, ...routes]; // add some data
23 | const json = JSON.stringify(obj); // convert it back to json
24 | fse
25 | .writeFile(destinationPath, json, 'utf8') // write it back
26 | .then(() => resolve(obj))
27 | .catch((err) => reject(err));
28 | });
29 | } else {
30 | const routesObject = {
31 | routes,
32 | };
33 |
34 | fse.mkdirSync(path.dirname(destinationPath), { recursive: true });
35 | const json = JSON.stringify(routesObject);
36 |
37 | try {
38 | fse.writeFileSync(destinationPath, json, 'utf8');
39 | resolve(routesObject);
40 | } catch (error) {
41 | reject();
42 | }
43 | }
44 | });
45 | };
46 |
--------------------------------------------------------------------------------
/plato/index.ts:
--------------------------------------------------------------------------------
1 | // We import the config here to avoid order issue
2 | // In Node modules are hoisted before
3 | // but dotenv needs to be preloaded
4 | // https://stackoverflow.com/a/63541230
5 | import 'dotenv/config';
6 |
7 | import program from 'commander';
8 | import develop from './commands/develop.js';
9 | import build from './commands/build.js';
10 | import path from 'path';
11 | import { fileURLToPath } from 'url';
12 |
13 | const __filename = fileURLToPath(import.meta.url);
14 | const __dirname = path.dirname(__filename);
15 |
16 | program
17 | .command('develop')
18 | .option('-v, --verbose', 'output extra debugging')
19 | .option('-o, --open', 'open dev')
20 | .description('Develop website')
21 | // create global path data that will be used across our scripts
22 | .action(() => {
23 | global.appRoot = path.resolve(__dirname + './../');
24 | global.srcPath = path.resolve(global.appRoot, './src/');
25 | global.siteDir = path.resolve(global.appRoot, './public/');
26 | global.routeDest = path.resolve(global.appRoot, './.plato/');
27 | })
28 | .action((cmdObj: any) => {
29 | develop(cmdObj.verbose, cmdObj.open);
30 | });
31 |
32 | program
33 | .command('build')
34 | .option('-v, --verbose', 'output extra debugging')
35 | .option('-o, --open', 'open local http server to QA build')
36 | // create global path data that will be used across our scripts
37 | .action(() => {
38 | global.appRoot = path.resolve(__dirname + './../');
39 | global.srcPath = path.resolve(global.appRoot, './src/');
40 | global.siteDir = path.resolve(global.appRoot, './build/');
41 | global.routeDest = path.resolve(global.appRoot, './.plato/');
42 | })
43 | .action((cmdObj: any) => {
44 | build(cmdObj.verbose, cmdObj.open);
45 | });
46 |
47 | program.parse(process.argv);
48 |
--------------------------------------------------------------------------------
/plato/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig to read more about this file */
4 |
5 | /* Projects */
6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
12 |
13 | /* Language and Environment */
14 | "target": "es2020" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
16 | // "jsx": "preserve", /* Specify what JSX code is generated. */
17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
26 |
27 | /* Modules */
28 | "module": "ES2020" /* Specify what module code is generated. */,
29 | // "rootDir": "./", /* Specify the root folder within your source files. */
30 | "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */,
31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
34 | "typeRoots": ["./@types/"] /* Specify multiple folders that act like './node_modules/@types'. */,
35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */
36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
38 | // "resolveJsonModule": true, /* Enable importing .json files. */
39 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */
40 |
41 | /* JavaScript Support */
42 | // "allowJs": true /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */,
43 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
44 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
45 |
46 | /* Emit */
47 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
48 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */
49 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
50 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
51 | // "outFile": "./" /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */,
52 | "outDir": "./../plato-dist/" /* Specify an output folder for all emitted files. */,
53 | // "removeComments": true, /* Disable emitting comments. */
54 | // "noEmit": true, /* Disable emitting files from a compilation. */
55 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
56 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
57 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
58 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
60 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
61 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
62 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
63 | // "newLine": "crlf", /* Set the newline character for emitting files. */
64 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
65 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
66 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
67 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
68 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
69 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
70 |
71 | /* Interop Constraints */
72 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
73 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
74 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
75 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
76 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
77 |
78 | /* Type Checking */
79 | "strict": true /* Enable all strict type-checking options. */,
80 | "noImplicitAny": false /* Enable error reporting for expressions and declarations with an implied 'any' type. */,
81 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
82 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
83 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
84 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
85 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
86 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
87 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
88 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
89 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
90 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
91 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
92 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
93 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
94 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
95 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
96 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
97 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
98 |
99 | /* Completeness */
100 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
101 | "skipLibCheck": true /* Skip type checking all .d.ts files. */
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/plato/utils/reporter.ts:
--------------------------------------------------------------------------------
1 | import convertHrtime from 'convert-hrtime';
2 | import chalk from 'chalk';
3 | import terminalLink from 'terminal-link';
4 | import boxen from 'boxen';
5 | const { BorderStyle } = boxen;
6 |
7 | const log = console.log;
8 |
9 | const defaultColors = {
10 | activityStart: chalk.magentaBright,
11 | log: chalk.white,
12 | info: chalk.grey,
13 | warn: chalk.yellow,
14 | error: chalk.red,
15 | success: chalk.green,
16 | bigError: chalk.bold.red,
17 | bigSucces: chalk.bold.blue,
18 | };
19 |
20 | class Reporter {
21 | verbose = false;
22 | colors = defaultColors;
23 |
24 | updateColors(colors: {}) {
25 | this.colors = Object.assign(colors, defaultColors);
26 | }
27 |
28 | log(message: string) {
29 | log(this.colors.log(message));
30 | }
31 |
32 | info(message: string) {
33 | // info is only printed if verbose is precised
34 | this.verbose && log(this.colors.info(message));
35 | }
36 |
37 | warn(message: string) {
38 | log(this.colors.warn(message));
39 | }
40 |
41 | error(message: string, exit = false) {
42 | log(this.colors.error(message));
43 | if (exit) process.exit(1);
44 | }
45 |
46 | failure(message: string, error: string) {
47 | log(this.colors.bigError(message));
48 | this.error(error);
49 | process.exit(1);
50 | }
51 |
52 | success(message: string) {
53 | log(this.colors.success(message));
54 | }
55 |
56 | displayUrl = function (message: string, url: string) {
57 | log(boxen(`${message} \n ${terminalLink(url, url)}`, { padding: 1, margin: 0, borderStyle: BorderStyle.Double }));
58 | };
59 |
60 | activity(activityName: string, activityEmoji: string) {
61 | return Activity(activityName, activityEmoji, this);
62 | }
63 | }
64 |
65 | function Activity(activityName: string, activityEmoji = '', reporter: Reporter) {
66 | let startTime;
67 | return {
68 | start: (verbose = false) => {
69 | startTime = process.hrtime();
70 | verbose && log(reporter.colors.activityStart(`starting ${activityEmoji} ${activityName}`));
71 | },
72 | update: (verbose = false) => {
73 | const elapsedTime = () => {
74 | let elapsed = process.hrtime(startTime);
75 | return `${convertHrtime(elapsed)['seconds'].toFixed(3)} s`;
76 | };
77 | verbose &&
78 | log(reporter.colors.info(`update ${activityEmoji}`), reporter.colors.log(`${activityName} - ${elapsedTime()}`));
79 | },
80 | end: () => {
81 | const elapsedTime = () => {
82 | let elapsed = process.hrtime(startTime);
83 | return `${convertHrtime(elapsed)['seconds'].toFixed(3)} s`;
84 | };
85 | log(
86 | reporter.colors.success(`success ${activityEmoji}`),
87 | reporter.colors.log(`${activityName} - ${elapsedTime()}`)
88 | );
89 | },
90 | };
91 | }
92 |
93 | const reporter = new Reporter();
94 | export default reporter;
95 |
--------------------------------------------------------------------------------
/plato/utils/routes.ts:
--------------------------------------------------------------------------------
1 | import { Route } from '../@types/route.js';
2 |
3 | export function getRoutesByTemplatePath(routes: Route[], templatePath: string): Route[] | null {
4 | const testRegexp = templatePath.split('/').slice(-1);
5 | const name = testRegexp[0].substring(0, testRegexp[0].lastIndexOf('.')) || testRegexp[0];
6 | let activeRoutes: Route[] = [];
7 |
8 | for (let route of routes) {
9 | if (route.template === name) activeRoutes.push(route);
10 | }
11 |
12 | if (activeRoutes.length > 0) return activeRoutes;
13 | return null;
14 | }
15 |
--------------------------------------------------------------------------------
/postcss.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [require('autoprefixer')],
3 | };
4 |
--------------------------------------------------------------------------------
/reports/plain-report.txt:
--------------------------------------------------------------------------------
1 | gsap: 278.86 KB (48.6%)
2 | core-js: 139.01 KB (24.2%)
3 | page: 31.01 KB (5.40%)
4 | css-loader: 2.91 KB (0.507%)
5 | art-template: 2.35 KB (0.409%)
6 | @babel/runtime: 269 B (0.0458%)
7 | : 119.63 KB (20.8%)
8 |
--------------------------------------------------------------------------------
/shared/templates/about.art:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{if data.content}}
5 |
Dynamic content: {{ data.content.title}}
6 | {{/if}}
7 | ABOUT PAGE Static content
8 |
9 |
10 |
--------------------------------------------------------------------------------
/shared/templates/filters/index.js:
--------------------------------------------------------------------------------
1 | // Reminders: FILTERS don’t seem to work when using with Set, just as output…
2 | // The idea is then to move the conditional logic to the "back-end" side
3 | export const filters = {
4 | round: function (value) {
5 | return Math.round(value);
6 | },
7 | round2decimals: function (value) {
8 | return Math.round(value * 100) / 100;
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/shared/templates/homepage.art:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Dynamic content: {{ data.content.title }}
4 | INDEX RELOAD Static content
5 | {{set test = 5.4}}
6 | test filter: {{test|round }}
7 | {{ include './svgs/instagram.art' }}
8 |
9 |
--------------------------------------------------------------------------------
/shared/templates/layout.art:
--------------------------------------------------------------------------------
1 | {{ include './partials/header.art' }}
2 |
3 |
4 | {{@ html}}
5 |
6 |
7 | {{ include './partials/footer.art' }}
--------------------------------------------------------------------------------
/shared/templates/notfound.art:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Not the page you are looking for
4 |
5 |
--------------------------------------------------------------------------------
/shared/templates/partials/footer.art:
--------------------------------------------------------------------------------
1 |
2 | {{ if globalDataString }}
3 |
6 | {{/if}}
7 |
8 | {{ if manifest }}
9 |
10 |
11 |
12 |
13 |
14 | {{else}}
15 |
16 | {{/if}}
17 |